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