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