sui_test_transaction_builder/
lib.rs

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