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