sui_test_transaction_builder/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use move_core_types::ident_str;
5use move_core_types::u256::U256;
6use shared_crypto::intent::{Intent, IntentMessage};
7use std::path::PathBuf;
8use sui_genesis_builder::validator_info::GenesisValidatorMetadata;
9use sui_move_build::{BuildConfig, CompiledPackage};
10use sui_rpc_api::client::ExecutedTransaction;
11use sui_sdk::wallet_context::WalletContext;
12use sui_types::balance::Balance;
13use sui_types::base_types::{FullObjectRef, ObjectID, ObjectRef, SequenceNumber, SuiAddress};
14use sui_types::committee::EpochId;
15use sui_types::crypto::{AccountKeyPair, Signature, Signer, get_key_pair};
16use sui_types::digests::ChainIdentifier;
17use sui_types::digests::TransactionDigest;
18use sui_types::effects::TransactionEffectsAPI;
19use sui_types::gas_coin::GAS;
20use sui_types::multisig::{BitmapUnit, MultiSig, MultiSigPublicKey};
21use sui_types::multisig_legacy::{MultiSigLegacy, MultiSigPublicKeyLegacy};
22use sui_types::object::Owner;
23use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
24use sui_types::signature::GenericSignature;
25use sui_types::sui_system_state::SUI_SYSTEM_MODULE_NAME;
26use sui_types::transaction::{
27    Argument, CallArg, DEFAULT_VALIDATOR_GAS_PRICE, FundsWithdrawalArg, ObjectArg,
28    SharedObjectMutability, TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE,
29    TEST_ONLY_GAS_UNIT_FOR_TRANSFER, Transaction, TransactionData,
30};
31use sui_types::{Identifier, SUI_FRAMEWORK_PACKAGE_ID, SUI_RANDOMNESS_STATE_OBJECT_ID};
32use sui_types::{SUI_SYSTEM_PACKAGE_ID, TypeTag};
33
34#[derive(Clone)]
35pub enum FundSource {
36    Coin(ObjectRef),
37    AddressFund {
38        /// If None, it will be set the same as the total transfer amount.
39        reservation: Option<u64>,
40    },
41    ObjectFund {
42        /// The ID of the object_balance package from examples.
43        /// We need this package to test object balance withdrawals.
44        package_id: ObjectID,
45        /// The object to withdraw funds from.
46        withdraw_object: ObjectFundObject,
47    },
48}
49
50#[derive(Clone)]
51pub enum ObjectFundObject {
52    Owned(ObjectRef),
53    Shared(ObjectID, SequenceNumber /* init shared version */),
54}
55
56/// Configuration for paying gas from address balance instead of a coin object.
57#[derive(Clone)]
58pub struct AddressBalanceGasConfig {
59    pub chain_identifier: ChainIdentifier,
60    pub current_epoch: EpochId,
61    pub nonce: u32,
62}
63
64impl FundSource {
65    pub fn coin(coin: ObjectRef) -> Self {
66        Self::Coin(coin)
67    }
68
69    pub fn address_fund() -> Self {
70        Self::AddressFund { reservation: None }
71    }
72
73    pub fn address_fund_with_reservation(reservation: u64) -> Self {
74        Self::AddressFund {
75            reservation: Some(reservation),
76        }
77    }
78
79    pub fn object_fund_owned(package_id: ObjectID, withdraw_object: ObjectRef) -> Self {
80        Self::ObjectFund {
81            package_id,
82            withdraw_object: ObjectFundObject::Owned(withdraw_object),
83        }
84    }
85
86    pub fn object_fund_shared(
87        package_id: ObjectID,
88        withdraw_object_id: ObjectID,
89        initial_shared_version: SequenceNumber,
90    ) -> Self {
91        Self::ObjectFund {
92            package_id,
93            withdraw_object: ObjectFundObject::Shared(withdraw_object_id, initial_shared_version),
94        }
95    }
96}
97
98pub struct TestTransactionBuilder {
99    ptb_builder: ProgrammableTransactionBuilder,
100    sender: SuiAddress,
101    gas_objects: Vec<ObjectRef>,
102    gas_price: u64,
103    gas_budget: Option<u64>,
104    address_balance_gas: Option<AddressBalanceGasConfig>,
105}
106
107impl TestTransactionBuilder {
108    pub fn new(sender: SuiAddress, gas_object: ObjectRef, gas_price: u64) -> Self {
109        Self::new_impl(sender, vec![gas_object], gas_price)
110    }
111
112    pub fn new_with_gas_objects(
113        sender: SuiAddress,
114        gas_objects: Vec<ObjectRef>,
115        gas_price: u64,
116    ) -> Self {
117        Self::new_impl(sender, gas_objects, gas_price)
118    }
119
120    fn new_impl(sender: SuiAddress, gas_objects: Vec<ObjectRef>, gas_price: u64) -> Self {
121        Self {
122            ptb_builder: ProgrammableTransactionBuilder::new(),
123            sender,
124            gas_objects,
125            gas_price,
126            gas_budget: None,
127            address_balance_gas: None,
128        }
129    }
130
131    pub fn new_with_address_balance_gas(
132        sender: SuiAddress,
133        gas_price: u64,
134        chain_identifier: ChainIdentifier,
135        current_epoch: EpochId,
136        nonce: u32,
137    ) -> Self {
138        Self::new_impl(sender, vec![], gas_price).with_address_balance_gas(
139            chain_identifier,
140            current_epoch,
141            nonce,
142        )
143    }
144
145    pub fn sender(&self) -> SuiAddress {
146        self.sender
147    }
148
149    pub fn gas_object(&self) -> Option<ObjectRef> {
150        assert!(
151            self.gas_objects.len() <= 1,
152            "gas_object() called but multiple gas objects are set; use gas_objects() instead"
153        );
154        self.gas_objects.first().copied()
155    }
156
157    pub fn ptb_builder_mut(&mut self) -> &mut ProgrammableTransactionBuilder {
158        &mut self.ptb_builder
159    }
160
161    pub fn move_call(
162        self,
163        package_id: ObjectID,
164        module: &'static str,
165        function: &'static str,
166        args: Vec<CallArg>,
167    ) -> Self {
168        self.move_call_with_type_args(package_id, module, function, vec![], args)
169    }
170
171    pub fn move_call_with_type_args(
172        mut self,
173        package_id: ObjectID,
174        module: &'static str,
175        function: &'static str,
176        type_args: Vec<TypeTag>,
177        args: Vec<CallArg>,
178    ) -> Self {
179        self.ptb_builder
180            .move_call(
181                package_id,
182                ident_str!(module).to_owned(),
183                ident_str!(function).to_owned(),
184                type_args,
185                args,
186            )
187            .unwrap();
188
189        self
190    }
191
192    pub fn with_gas_budget(mut self, gas_budget: u64) -> Self {
193        self.gas_budget = Some(gas_budget);
194        self
195    }
196
197    pub fn with_address_balance_gas(
198        mut self,
199        chain_identifier: ChainIdentifier,
200        current_epoch: EpochId,
201        nonce: u32,
202    ) -> Self {
203        self.address_balance_gas = Some(AddressBalanceGasConfig {
204            chain_identifier,
205            current_epoch,
206            nonce,
207        });
208        self
209    }
210
211    pub fn call_counter_create(self, package_id: ObjectID) -> Self {
212        self.move_call(package_id, "counter", "create", vec![])
213    }
214
215    pub fn call_counter_increment(
216        self,
217        package_id: ObjectID,
218        counter_id: ObjectID,
219        counter_initial_shared_version: SequenceNumber,
220    ) -> Self {
221        self.move_call(
222            package_id,
223            "counter",
224            "increment",
225            vec![CallArg::Object(ObjectArg::SharedObject {
226                id: counter_id,
227                initial_shared_version: counter_initial_shared_version,
228                mutability: SharedObjectMutability::Mutable,
229            })],
230        )
231    }
232
233    pub fn call_counter_read(
234        self,
235        package_id: ObjectID,
236        counter_id: ObjectID,
237        counter_initial_shared_version: SequenceNumber,
238    ) -> Self {
239        self.move_call(
240            package_id,
241            "counter",
242            "value",
243            vec![CallArg::Object(ObjectArg::SharedObject {
244                id: counter_id,
245                initial_shared_version: counter_initial_shared_version,
246                mutability: SharedObjectMutability::Immutable,
247            })],
248        )
249    }
250
251    pub fn call_counter_delete(
252        self,
253        package_id: ObjectID,
254        counter_id: ObjectID,
255        counter_initial_shared_version: SequenceNumber,
256    ) -> Self {
257        self.move_call(
258            package_id,
259            "counter",
260            "delete",
261            vec![CallArg::Object(ObjectArg::SharedObject {
262                id: counter_id,
263                initial_shared_version: counter_initial_shared_version,
264                mutability: SharedObjectMutability::Mutable,
265            })],
266        )
267    }
268
269    pub fn call_nft_create(self, package_id: ObjectID) -> Self {
270        self.move_call(
271            package_id,
272            "testnet_nft",
273            "mint_to_sender",
274            vec![
275                CallArg::Pure(bcs::to_bytes("example_nft_name").unwrap()),
276                CallArg::Pure(bcs::to_bytes("example_nft_description").unwrap()),
277                CallArg::Pure(
278                    bcs::to_bytes("https://sui.io/_nuxt/img/sui-logo.8d3c44e.svg").unwrap(),
279                ),
280            ],
281        )
282    }
283
284    pub fn call_nft_delete(self, package_id: ObjectID, nft_to_delete: ObjectRef) -> Self {
285        self.move_call(
286            package_id,
287            "testnet_nft",
288            "burn",
289            vec![CallArg::Object(ObjectArg::ImmOrOwnedObject(nft_to_delete))],
290        )
291    }
292
293    pub fn call_staking(self, stake_coin: ObjectRef, validator: SuiAddress) -> Self {
294        self.move_call(
295            SUI_SYSTEM_PACKAGE_ID,
296            SUI_SYSTEM_MODULE_NAME.as_str(),
297            "request_add_stake",
298            vec![
299                CallArg::SUI_SYSTEM_MUT,
300                CallArg::Object(ObjectArg::ImmOrOwnedObject(stake_coin)),
301                CallArg::Pure(bcs::to_bytes(&validator).unwrap()),
302            ],
303        )
304    }
305
306    pub fn call_emit_random(
307        self,
308        package_id: ObjectID,
309        randomness_initial_shared_version: SequenceNumber,
310    ) -> Self {
311        self.move_call(
312            package_id,
313            "random",
314            "new",
315            vec![CallArg::Object(ObjectArg::SharedObject {
316                id: SUI_RANDOMNESS_STATE_OBJECT_ID,
317                initial_shared_version: randomness_initial_shared_version,
318                mutability: SharedObjectMutability::Immutable,
319            })],
320        )
321    }
322
323    pub fn call_request_add_validator(self) -> Self {
324        self.move_call(
325            SUI_SYSTEM_PACKAGE_ID,
326            SUI_SYSTEM_MODULE_NAME.as_str(),
327            "request_add_validator",
328            vec![CallArg::SUI_SYSTEM_MUT],
329        )
330    }
331
332    pub fn call_request_add_validator_candidate(
333        self,
334        validator: &GenesisValidatorMetadata,
335    ) -> Self {
336        self.move_call(
337            SUI_SYSTEM_PACKAGE_ID,
338            SUI_SYSTEM_MODULE_NAME.as_str(),
339            "request_add_validator_candidate",
340            vec![
341                CallArg::SUI_SYSTEM_MUT,
342                CallArg::Pure(bcs::to_bytes(&validator.protocol_public_key).unwrap()),
343                CallArg::Pure(bcs::to_bytes(&validator.network_public_key).unwrap()),
344                CallArg::Pure(bcs::to_bytes(&validator.worker_public_key).unwrap()),
345                CallArg::Pure(bcs::to_bytes(&validator.proof_of_possession).unwrap()),
346                CallArg::Pure(bcs::to_bytes(validator.name.as_bytes()).unwrap()),
347                CallArg::Pure(bcs::to_bytes(validator.description.as_bytes()).unwrap()),
348                CallArg::Pure(bcs::to_bytes(validator.image_url.as_bytes()).unwrap()),
349                CallArg::Pure(bcs::to_bytes(validator.project_url.as_bytes()).unwrap()),
350                CallArg::Pure(bcs::to_bytes(&validator.network_address).unwrap()),
351                CallArg::Pure(bcs::to_bytes(&validator.p2p_address).unwrap()),
352                CallArg::Pure(bcs::to_bytes(&validator.primary_address).unwrap()),
353                CallArg::Pure(bcs::to_bytes(&validator.worker_address).unwrap()),
354                CallArg::Pure(bcs::to_bytes(&DEFAULT_VALIDATOR_GAS_PRICE).unwrap()), // gas_price
355                CallArg::Pure(bcs::to_bytes(&0u64).unwrap()), // commission_rate
356            ],
357        )
358    }
359
360    pub fn call_request_remove_validator(self) -> Self {
361        self.move_call(
362            SUI_SYSTEM_PACKAGE_ID,
363            SUI_SYSTEM_MODULE_NAME.as_str(),
364            "request_remove_validator",
365            vec![CallArg::SUI_SYSTEM_MUT],
366        )
367    }
368
369    pub fn call_object_create_party(self, package_id: ObjectID) -> Self {
370        let sender = self.sender;
371        self.move_call(
372            package_id,
373            "object_basics",
374            "create_party",
375            vec![
376                CallArg::Pure(bcs::to_bytes(&1000u64).unwrap()),
377                CallArg::Pure(bcs::to_bytes(&sender).unwrap()),
378            ],
379        )
380    }
381
382    pub fn call_object_party_transfer_single_owner(
383        self,
384        package_id: ObjectID,
385        object_arg: ObjectArg,
386        recipient: SuiAddress,
387    ) -> Self {
388        self.move_call(
389            package_id,
390            "object_basics",
391            "party_transfer_single_owner",
392            vec![
393                CallArg::Object(object_arg),
394                CallArg::Pure(bcs::to_bytes(&recipient).unwrap()),
395            ],
396        )
397    }
398
399    pub fn call_object_delete(self, package_id: ObjectID, object_arg: ObjectArg) -> Self {
400        self.move_call(
401            package_id,
402            "object_basics",
403            "delete",
404            vec![CallArg::Object(object_arg)],
405        )
406    }
407
408    pub fn transfer(mut self, object: FullObjectRef, recipient: SuiAddress) -> Self {
409        self.ptb_builder.transfer_object(recipient, object).unwrap();
410        self
411    }
412
413    pub fn transfer_sui(mut self, amount: Option<u64>, recipient: SuiAddress) -> Self {
414        self.ptb_builder.transfer_sui(recipient, amount);
415        self
416    }
417
418    pub fn transfer_sui_to_address_balance(
419        self,
420        source: FundSource,
421        amounts_and_recipients: Vec<(u64, SuiAddress)>,
422    ) -> Self {
423        self.transfer_funds_to_address_balance(source, amounts_and_recipients, GAS::type_tag())
424    }
425
426    pub fn transfer_funds_to_address_balance(
427        mut self,
428        fund_source: FundSource,
429        amounts_and_recipients: Vec<(u64, SuiAddress)>,
430        type_arg: TypeTag,
431    ) -> Self {
432        fn send_funds(
433            builder: &mut ProgrammableTransactionBuilder,
434            balance: Argument,
435            recipient: SuiAddress,
436            type_arg: TypeTag,
437        ) {
438            let recipient_arg = builder.pure(recipient).unwrap();
439            builder.programmable_move_call(
440                SUI_FRAMEWORK_PACKAGE_ID,
441                Identifier::new("balance").unwrap(),
442                Identifier::new("send_funds").unwrap(),
443                vec![type_arg],
444                vec![balance, recipient_arg],
445            );
446        }
447
448        let total_amount = amounts_and_recipients
449            .iter()
450            .map(|(amount, _)| *amount)
451            .sum::<u64>();
452        match fund_source {
453            FundSource::Coin(coin) => {
454                let source = if self.gas_objects.first() == Some(&coin) {
455                    Argument::GasCoin
456                } else {
457                    self.ptb_builder
458                        .obj(ObjectArg::ImmOrOwnedObject(coin))
459                        .unwrap()
460                };
461                for (amount, recipient) in amounts_and_recipients {
462                    let amount_arg = self.ptb_builder.pure(amount).unwrap();
463                    let coin = self.ptb_builder.programmable_move_call(
464                        SUI_FRAMEWORK_PACKAGE_ID,
465                        Identifier::new("coin").unwrap(),
466                        Identifier::new("split").unwrap(),
467                        vec![type_arg.clone()],
468                        vec![source, amount_arg],
469                    );
470                    let balance = self.ptb_builder.programmable_move_call(
471                        SUI_FRAMEWORK_PACKAGE_ID,
472                        Identifier::new("coin").unwrap(),
473                        Identifier::new("into_balance").unwrap(),
474                        vec![type_arg.clone()],
475                        vec![coin],
476                    );
477                    send_funds(&mut self.ptb_builder, balance, recipient, type_arg.clone());
478                }
479            }
480            FundSource::AddressFund { reservation } => {
481                let reservation = reservation.unwrap_or(total_amount);
482                let source = self
483                    .ptb_builder
484                    .funds_withdrawal(FundsWithdrawalArg::balance_from_sender(
485                        reservation,
486                        type_arg.clone(),
487                    ))
488                    .unwrap();
489                for (amount, recipient) in amounts_and_recipients {
490                    let amount_arg = self.ptb_builder.pure(U256::from(amount)).unwrap();
491                    let split = self.ptb_builder.programmable_move_call(
492                        SUI_FRAMEWORK_PACKAGE_ID,
493                        Identifier::new("funds_accumulator").unwrap(),
494                        Identifier::new("withdrawal_split").unwrap(),
495                        vec![Balance::type_tag(type_arg.clone())],
496                        vec![source, amount_arg],
497                    );
498                    let balance = self.ptb_builder.programmable_move_call(
499                        SUI_FRAMEWORK_PACKAGE_ID,
500                        Identifier::new("balance").unwrap(),
501                        Identifier::new("redeem_funds").unwrap(),
502                        vec![type_arg.clone()],
503                        vec![split],
504                    );
505                    send_funds(&mut self.ptb_builder, balance, recipient, type_arg.clone());
506                }
507            }
508            FundSource::ObjectFund {
509                package_id,
510                withdraw_object,
511            } => {
512                let source = match withdraw_object {
513                    ObjectFundObject::Owned(object) => self
514                        .ptb_builder
515                        .obj(ObjectArg::ImmOrOwnedObject(object))
516                        .unwrap(),
517                    ObjectFundObject::Shared(object_id, initial_shared_version) => self
518                        .ptb_builder
519                        .obj(ObjectArg::SharedObject {
520                            id: object_id,
521                            initial_shared_version,
522                            mutability: SharedObjectMutability::Mutable,
523                        })
524                        .unwrap(),
525                };
526                for (amount, recipient) in amounts_and_recipients {
527                    let amount_arg = self.ptb_builder.pure(amount).unwrap();
528                    let balance = self.ptb_builder.programmable_move_call(
529                        package_id,
530                        Identifier::new("object_balance").unwrap(),
531                        Identifier::new("withdraw_funds").unwrap(),
532                        vec![type_arg.clone()],
533                        vec![source, amount_arg],
534                    );
535                    send_funds(&mut self.ptb_builder, balance, recipient, type_arg.clone());
536                }
537            }
538        }
539
540        self
541    }
542
543    pub fn split_coin(mut self, coin: ObjectRef, amounts: Vec<u64>) -> Self {
544        self.ptb_builder.split_coin(self.sender, coin, amounts);
545        self
546    }
547
548    pub fn merge_coins(mut self, target: ObjectRef, coins: Vec<ObjectRef>) -> Self {
549        self.ptb_builder.merge_coins(target, coins).unwrap();
550        self
551    }
552
553    pub fn publish(self, path: PathBuf) -> Self {
554        if cfg!(msim) {
555            panic!("In simtests, you must use publish_async() instead");
556        }
557        self.publish_with_data(PublishData::Source(path, false))
558    }
559
560    pub async fn publish_async(self, path: PathBuf) -> Self {
561        self.publish_with_data_async(PublishData::Source(path, false))
562            .await
563    }
564
565    pub fn publish_with_deps(self, path: PathBuf) -> Self {
566        self.publish_with_data(PublishData::Source(path, true))
567    }
568
569    pub fn publish_with_data(mut self, data: PublishData) -> Self {
570        let (all_module_bytes, dependencies) = match data {
571            PublishData::Source(path, with_unpublished_deps) => {
572                let mut config = BuildConfig::new_for_testing();
573                config.config.set_unpublished_deps_to_zero = with_unpublished_deps;
574                let compiled_package = config.build(&path).unwrap();
575                let all_module_bytes = compiled_package.get_package_bytes(with_unpublished_deps);
576                let dependencies = compiled_package.get_dependency_storage_package_ids();
577                (all_module_bytes, dependencies)
578            }
579            PublishData::ModuleBytes(bytecode) => (bytecode, vec![]),
580            PublishData::CompiledPackage(compiled_package) => {
581                let all_module_bytes = compiled_package.get_package_bytes(false);
582                let dependencies = compiled_package.get_dependency_storage_package_ids();
583                (all_module_bytes, dependencies)
584            }
585        };
586
587        let upgrade_cap = self
588            .ptb_builder
589            .publish_upgradeable(all_module_bytes, dependencies);
590        self.ptb_builder.transfer_arg(self.sender, upgrade_cap);
591
592        self
593    }
594
595    pub async fn publish_with_data_async(mut self, data: PublishData) -> Self {
596        let (all_module_bytes, dependencies) = match data {
597            PublishData::Source(path, with_unpublished_deps) => {
598                let compiled_package = BuildConfig::new_for_testing()
599                    .build_async(&path)
600                    .await
601                    .unwrap();
602                let all_module_bytes = compiled_package.get_package_bytes(with_unpublished_deps);
603                let dependencies = compiled_package.get_dependency_storage_package_ids();
604                (all_module_bytes, dependencies)
605            }
606            PublishData::ModuleBytes(bytecode) => (bytecode, vec![]),
607            PublishData::CompiledPackage(compiled_package) => {
608                let all_module_bytes = compiled_package.get_package_bytes(false);
609                let dependencies = compiled_package.get_dependency_storage_package_ids();
610                (all_module_bytes, dependencies)
611            }
612        };
613
614        let upgrade_cap = self
615            .ptb_builder
616            .publish_upgradeable(all_module_bytes, dependencies);
617        self.ptb_builder.transfer_arg(self.sender, upgrade_cap);
618
619        self
620    }
621
622    pub async fn publish_examples(self, subpath: &'static str) -> Self {
623        let path: PathBuf = if let Ok(p) = std::env::var("MOVE_EXAMPLES_DIR") {
624            let mut path = PathBuf::from(p);
625            path.extend([subpath]);
626            path
627        } else {
628            let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
629            path.extend(["..", "..", "examples", "move", subpath]);
630            path
631        };
632        self.publish_async(path).await
633    }
634
635    pub fn build(self) -> TransactionData {
636        let pt = self.ptb_builder.finish();
637        let gas_budget = self
638            .gas_budget
639            .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE);
640
641        if let Some(ab_gas) = self.address_balance_gas {
642            TransactionData::new_programmable_with_address_balance_gas(
643                self.sender,
644                pt,
645                gas_budget,
646                self.gas_price,
647                ab_gas.chain_identifier,
648                ab_gas.current_epoch,
649                ab_gas.nonce,
650            )
651        } else {
652            assert!(
653                !self.gas_objects.is_empty(),
654                "gas_objects required when not using address_balance_gas"
655            );
656            TransactionData::new_programmable(
657                self.sender,
658                self.gas_objects,
659                pt,
660                gas_budget,
661                self.gas_price,
662            )
663        }
664    }
665
666    pub fn build_and_sign(self, signer: &dyn Signer<Signature>) -> Transaction {
667        Transaction::from_data_and_signer(self.build(), vec![signer])
668    }
669
670    pub fn build_and_sign_multisig(
671        self,
672        multisig_pk: MultiSigPublicKey,
673        signers: &[&dyn Signer<Signature>],
674        bitmap: BitmapUnit,
675    ) -> Transaction {
676        let data = self.build();
677        let intent_msg = IntentMessage::new(Intent::sui_transaction(), data.clone());
678
679        let mut signatures = Vec::with_capacity(signers.len());
680        for signer in signers {
681            signatures.push(
682                GenericSignature::from(Signature::new_secure(&intent_msg, *signer))
683                    .to_compressed()
684                    .unwrap(),
685            );
686        }
687
688        let multisig =
689            GenericSignature::MultiSig(MultiSig::insecure_new(signatures, bitmap, multisig_pk));
690
691        Transaction::from_generic_sig_data(data, vec![multisig])
692    }
693
694    pub fn build_and_sign_multisig_legacy(
695        self,
696        multisig_pk: MultiSigPublicKeyLegacy,
697        signers: &[&dyn Signer<Signature>],
698    ) -> Transaction {
699        let data = self.build();
700        let intent = Intent::sui_transaction();
701        let intent_msg = IntentMessage::new(intent.clone(), data.clone());
702
703        let mut signatures = Vec::with_capacity(signers.len());
704        for signer in signers {
705            signatures.push(Signature::new_secure(&intent_msg, *signer).into());
706        }
707
708        let multisig = GenericSignature::MultiSigLegacy(
709            MultiSigLegacy::combine(signatures, multisig_pk).unwrap(),
710        );
711
712        Transaction::from_generic_sig_data(data, vec![multisig])
713    }
714}
715
716#[allow(clippy::large_enum_variant)]
717pub enum PublishData {
718    /// Path to source code directory and with_unpublished_deps.
719    /// with_unpublished_deps indicates whether to publish unpublished dependencies in the same transaction or not.
720    Source(PathBuf, bool),
721    ModuleBytes(Vec<Vec<u8>>),
722    CompiledPackage(CompiledPackage),
723}
724
725// TODO: Cleanup the following floating functions.
726
727/// A helper function to make Transactions with controlled accounts in WalletContext.
728/// Particularly, the wallet needs to own gas objects for transactions.
729/// However, if this function is called multiple times without any "sync" actions
730/// on gas object management, txns may fail and objects may be locked.
731///
732/// The param is called `max_txn_num` because it does not always return the exact
733/// same amount of Transactions, for example when there are not enough gas objects
734/// controlled by the WalletContext. Caller should rely on the return value to
735/// check the count.
736pub async fn batch_make_transfer_transactions(
737    context: &WalletContext,
738    max_txn_num: usize,
739) -> Vec<Transaction> {
740    let recipient = get_key_pair::<AccountKeyPair>().0;
741    let result = context.get_all_accounts_and_gas_objects().await;
742    let accounts_and_objs = result.unwrap();
743    let mut res = Vec::with_capacity(max_txn_num);
744
745    let gas_price = context.get_reference_gas_price().await.unwrap();
746    for (address, objs) in accounts_and_objs {
747        for obj in objs {
748            if res.len() >= max_txn_num {
749                return res;
750            }
751            let data = TransactionData::new_transfer_sui(
752                recipient,
753                address,
754                Some(2),
755                obj,
756                gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER,
757                gas_price,
758            );
759            let tx = context.sign_transaction(&data).await;
760            res.push(tx);
761        }
762    }
763    res
764}
765
766pub async fn make_transfer_sui_transaction(
767    context: &WalletContext,
768    recipient: Option<SuiAddress>,
769    amount: Option<u64>,
770) -> Transaction {
771    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
772    let gas_price = context.get_reference_gas_price().await.unwrap();
773    context
774        .sign_transaction(
775            &TestTransactionBuilder::new(sender, gas_object, gas_price)
776                .transfer_sui(amount, recipient.unwrap_or(sender))
777                .build(),
778        )
779        .await
780}
781
782pub async fn make_transfer_sui_address_balance_transaction(
783    context: &WalletContext,
784    recipient: Option<SuiAddress>,
785    amount: u64,
786) -> Transaction {
787    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
788    let gas_price = context.get_reference_gas_price().await.unwrap();
789    context
790        .sign_transaction(
791            &TestTransactionBuilder::new(sender, gas_object, gas_price)
792                .transfer_sui_to_address_balance(
793                    FundSource::Coin(gas_object),
794                    vec![(amount, recipient.unwrap_or(sender))],
795                )
796                .build(),
797        )
798        .await
799}
800
801pub async fn make_staking_transaction(
802    context: &WalletContext,
803    validator_address: SuiAddress,
804) -> Transaction {
805    let accounts_and_objs = context.get_all_accounts_and_gas_objects().await.unwrap();
806    let sender = accounts_and_objs[0].0;
807    let gas_object = accounts_and_objs[0].1[0];
808    let stake_object = accounts_and_objs[0].1[1];
809    let gas_price = context.get_reference_gas_price().await.unwrap();
810    context
811        .sign_transaction(
812            &TestTransactionBuilder::new(sender, gas_object, gas_price)
813                .call_staking(stake_object, validator_address)
814                .build(),
815        )
816        .await
817}
818
819pub async fn make_publish_transaction(context: &WalletContext, path: PathBuf) -> Transaction {
820    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
821    let gas_price = context.get_reference_gas_price().await.unwrap();
822    context
823        .sign_transaction(
824            &TestTransactionBuilder::new(sender, gas_object, gas_price)
825                .publish_async(path)
826                .await
827                .build(),
828        )
829        .await
830}
831
832pub async fn make_publish_transaction_with_deps(
833    context: &WalletContext,
834    path: PathBuf,
835) -> Transaction {
836    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
837    let gas_price = context.get_reference_gas_price().await.unwrap();
838    context
839        .sign_transaction(
840            &TestTransactionBuilder::new(sender, gas_object, gas_price)
841                .publish_with_deps(path)
842                .build(),
843        )
844        .await
845}
846
847pub async fn publish_package(context: &WalletContext, path: PathBuf) -> ObjectRef {
848    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
849    let gas_price = context.get_reference_gas_price().await.unwrap();
850    let txn = context
851        .sign_transaction(
852            &TestTransactionBuilder::new(sender, gas_object, gas_price)
853                .publish_async(path)
854                .await
855                .build(),
856        )
857        .await;
858    let resp = context.execute_transaction_must_succeed(txn).await;
859    resp.get_new_package_obj().unwrap()
860}
861
862/// Executes a transaction to publish the `basics` package and returns the package object ref.
863pub async fn publish_basics_package(context: &WalletContext) -> ObjectRef {
864    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
865    let gas_price = context.get_reference_gas_price().await.unwrap();
866    let txn = context
867        .sign_transaction(
868            &TestTransactionBuilder::new(sender, gas_object, gas_price)
869                .publish_examples("basics")
870                .await
871                .build(),
872        )
873        .await;
874    let resp = context.execute_transaction_must_succeed(txn).await;
875    resp.get_new_package_obj().unwrap()
876}
877
878/// Executes a transaction to publish the `basics` package and another one to create a counter.
879/// Returns the package object ref and the counter object ref.
880pub async fn publish_basics_package_and_make_counter(
881    context: &WalletContext,
882) -> (ObjectRef, ObjectRef) {
883    let package_ref = publish_basics_package(context).await;
884    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
885    let gas_price = context.get_reference_gas_price().await.unwrap();
886    let counter_creation_txn = context
887        .sign_transaction(
888            &TestTransactionBuilder::new(sender, gas_object, gas_price)
889                .call_counter_create(package_ref.0)
890                .build(),
891        )
892        .await;
893    let resp = context
894        .execute_transaction_must_succeed(counter_creation_txn)
895        .await;
896    let counter_ref = resp
897        .effects
898        .created()
899        .iter()
900        .find(|obj_ref| matches!(obj_ref.1, Owner::Shared { .. }))
901        .unwrap()
902        .0;
903    (package_ref, counter_ref)
904}
905
906/// Executes a transaction to publish the `basics` package and another one to create a party
907/// object. Returns the package object ref and the counter object ref.
908pub async fn publish_basics_package_and_make_party_object(
909    context: &WalletContext,
910) -> (ObjectRef, ObjectRef) {
911    let package_ref = publish_basics_package(context).await;
912    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
913    let gas_price = context.get_reference_gas_price().await.unwrap();
914    let object_creation_txn = context
915        .sign_transaction(
916            &TestTransactionBuilder::new(sender, gas_object, gas_price)
917                .call_object_create_party(package_ref.0)
918                .build(),
919        )
920        .await;
921    let resp = context
922        .execute_transaction_must_succeed(object_creation_txn)
923        .await;
924    let object_ref = resp
925        .effects
926        .created()
927        .iter()
928        .find(|obj_ref| matches!(obj_ref.1, Owner::ConsensusAddressOwner { .. }))
929        .unwrap()
930        .0;
931    (package_ref, object_ref)
932}
933
934/// Executes a transaction to increment a counter object.
935/// Must be called after calling `publish_basics_package_and_make_counter`.
936pub async fn increment_counter(
937    context: &WalletContext,
938    sender: SuiAddress,
939    gas_object_id: Option<ObjectID>,
940    package_id: ObjectID,
941    counter_id: ObjectID,
942    initial_shared_version: SequenceNumber,
943) -> ExecutedTransaction {
944    let gas_object = if let Some(gas_object_id) = gas_object_id {
945        context.get_object_ref(gas_object_id).await.unwrap()
946    } else {
947        context
948            .get_one_gas_object_owned_by_address(sender)
949            .await
950            .unwrap()
951            .unwrap()
952    };
953    let rgp = context.get_reference_gas_price().await.unwrap();
954    let txn = context
955        .sign_transaction(
956            &TestTransactionBuilder::new(sender, gas_object, rgp)
957                .call_counter_increment(package_id, counter_id, initial_shared_version)
958                .build(),
959        )
960        .await;
961    context.execute_transaction_must_succeed(txn).await
962}
963
964/// Executes a transaction that generates a new random u128 using Random and emits it as an event.
965pub async fn emit_new_random_u128(
966    context: &WalletContext,
967    package_id: ObjectID,
968) -> ExecutedTransaction {
969    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
970    let rgp = context.get_reference_gas_price().await.unwrap();
971
972    let mut client = context.grpc_client().unwrap();
973    let random_obj = client
974        .get_object(SUI_RANDOMNESS_STATE_OBJECT_ID)
975        .await
976        .unwrap();
977    let random_obj_owner = random_obj.owner().to_owned();
978
979    let Owner::Shared {
980        initial_shared_version,
981    } = random_obj_owner
982    else {
983        panic!("Expect Randomness to be shared object")
984    };
985    let random_call_arg = CallArg::Object(ObjectArg::SharedObject {
986        id: SUI_RANDOMNESS_STATE_OBJECT_ID,
987        initial_shared_version,
988        mutability: SharedObjectMutability::Immutable,
989    });
990
991    let txn = context
992        .sign_transaction(
993            &TestTransactionBuilder::new(sender, gas_object, rgp)
994                .move_call(package_id, "random", "new", vec![random_call_arg])
995                .build(),
996        )
997        .await;
998    context.execute_transaction_must_succeed(txn).await
999}
1000
1001/// Executes a transaction to publish the `nfts` package and returns the package id, id of the gas
1002/// object used, and the digest of the transaction.
1003pub async fn publish_nfts_package(
1004    context: &WalletContext,
1005) -> (ObjectID, ObjectID, TransactionDigest) {
1006    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
1007    let gas_id = gas_object.0;
1008    let gas_price = context.get_reference_gas_price().await.unwrap();
1009    let txn = context
1010        .sign_transaction(
1011            &TestTransactionBuilder::new(sender, gas_object, gas_price)
1012                .publish_examples("nft")
1013                .await
1014                .build(),
1015        )
1016        .await;
1017    let resp = context.execute_transaction_must_succeed(txn).await;
1018    let package_id = resp.get_new_package_obj().unwrap().0;
1019    (package_id, gas_id, resp.transaction.digest())
1020}
1021
1022/// Pre-requisite: `publish_nfts_package` must be called before this function.  Executes a
1023/// transaction to create an NFT and returns the sender address, the object id of the NFT, and the
1024/// digest of the transaction.
1025pub async fn create_nft(
1026    context: &WalletContext,
1027    package_id: ObjectID,
1028) -> (SuiAddress, ObjectID, TransactionDigest) {
1029    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
1030    let rgp = context.get_reference_gas_price().await.unwrap();
1031
1032    let txn = context
1033        .sign_transaction(
1034            &TestTransactionBuilder::new(sender, gas_object, rgp)
1035                .call_nft_create(package_id)
1036                .build(),
1037        )
1038        .await;
1039    let resp = context.execute_transaction_must_succeed(txn).await;
1040
1041    let object_id = resp.effects.created().first().unwrap().0.0;
1042
1043    (sender, object_id, resp.transaction.digest())
1044}
1045
1046/// Executes a transaction to delete the given NFT.
1047pub async fn delete_nft(
1048    context: &WalletContext,
1049    sender: SuiAddress,
1050    package_id: ObjectID,
1051    nft_to_delete: ObjectRef,
1052) -> ExecutedTransaction {
1053    let gas = context
1054        .get_one_gas_object_owned_by_address(sender)
1055        .await
1056        .unwrap()
1057        .unwrap_or_else(|| panic!("Expect {sender} to have at least one gas object"));
1058    let rgp = context.get_reference_gas_price().await.unwrap();
1059    let txn = context
1060        .sign_transaction(
1061            &TestTransactionBuilder::new(sender, gas, rgp)
1062                .call_nft_delete(package_id, nft_to_delete)
1063                .build(),
1064        )
1065        .await;
1066    context.execute_transaction_must_succeed(txn).await
1067}