Skip to main content

iota_test_transaction_builder/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::path::PathBuf;
6
7use iota_genesis_builder::validator_info::GenesisValidatorMetadata;
8use iota_move_build::{BuildConfig, CompiledPackage};
9use iota_sdk::{
10    rpc_types::{
11        IotaObjectDataOptions, IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponse,
12        get_new_package_obj_from_response,
13    },
14    wallet_context::WalletContext,
15};
16use iota_sdk_crypto::Signer;
17use iota_sdk_transaction_builder::PTBArgumentList;
18use iota_sdk_types::{
19    Address, Identifier, Input, ObjectId, ObjectReference, Owner, ProgrammableTransaction,
20    SharedObjectReference, StructTag, Transaction, TransactionDigest, TransactionKind, TypeTag,
21    UserSignature, Version,
22    crypto::{BitmapUnit, MultisigAggregatedSignature, MultisigCommittee, SimpleSignature},
23};
24use iota_types::{
25    crypto::AccountPrivateKey,
26    transaction::{
27        CallArg, DEFAULT_VALIDATOR_GAS_PRICE, TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE,
28        TEST_ONLY_GAS_UNIT_FOR_TRANSFER, TransactionAPI, TransactionEnvelope,
29    },
30    utils::to_sender_signed_transaction,
31};
32use rand::RngExt;
33
34pub struct TestTransactionBuilder {
35    test_data: TestTransactionData,
36    sender: Address,
37    gas_object: ObjectReference,
38    gas_price: u64,
39    gas_budget: Option<u64>,
40    nonce: Option<u64>,
41}
42
43impl TestTransactionBuilder {
44    pub fn new(sender: Address, gas_object: ObjectReference, gas_price: u64) -> Self {
45        Self {
46            test_data: TestTransactionData::Empty,
47            sender,
48            gas_object,
49            gas_price,
50            gas_budget: None,
51            nonce: None,
52        }
53    }
54
55    /// Inject a random unused pure input so that two otherwise-identical
56    /// transactions build to distinct digests.
57    ///
58    /// Use this for workloads that repeatedly submit logically identical
59    /// transactions (same sender, gas object and arguments) and must avoid
60    /// colliding on an already-executed digest.
61    pub fn ensure_unique(mut self) -> Self {
62        self.nonce = Some(rand::rng().random());
63        self
64    }
65
66    pub fn sender(&self) -> Address {
67        self.sender
68    }
69
70    pub fn gas_object(&self) -> ObjectReference {
71        self.gas_object
72    }
73
74    // Use `with_type_args` below to provide type args if any
75    pub fn move_call(
76        mut self,
77        package_id: ObjectId,
78        module: &str,
79        function: &str,
80        args: Vec<CallArg>,
81    ) -> Self {
82        assert!(matches!(self.test_data, TestTransactionData::Empty));
83        self.test_data = TestTransactionData::Move(MoveData {
84            package_id,
85            module: Identifier::new(module).unwrap(),
86            function: Identifier::new(function).unwrap(),
87            args,
88            type_args: vec![],
89        });
90        self
91    }
92
93    pub fn with_type_args(mut self, type_args: Vec<TypeTag>) -> Self {
94        if let TestTransactionData::Move(data) = &mut self.test_data {
95            assert!(data.type_args.is_empty());
96            data.type_args = type_args;
97        } else {
98            panic!("Cannot set type args for non-move call");
99        }
100        self
101    }
102
103    pub fn with_gas_budget(mut self, gas_budget: u64) -> Self {
104        self.gas_budget = Some(gas_budget);
105        self
106    }
107
108    pub fn call_counter_create(self, package_id: ObjectId) -> Self {
109        self.move_call(package_id, "counter", "create", vec![])
110    }
111
112    pub fn call_counter_increment(
113        self,
114        package_id: ObjectId,
115        counter_id: ObjectId,
116        counter_initial_shared_version: Version,
117    ) -> Self {
118        self.move_call(
119            package_id,
120            "counter",
121            "increment",
122            vec![CallArg::Shared(SharedObjectReference::new(
123                counter_id,
124                counter_initial_shared_version,
125                true,
126            ))],
127        )
128    }
129
130    pub fn call_counter_read(
131        self,
132        package_id: ObjectId,
133        counter_id: ObjectId,
134        counter_initial_shared_version: Version,
135    ) -> Self {
136        self.move_call(
137            package_id,
138            "counter",
139            "value",
140            vec![CallArg::Shared(SharedObjectReference::new(
141                counter_id,
142                counter_initial_shared_version,
143                false,
144            ))],
145        )
146    }
147
148    pub fn call_counter_delete(
149        self,
150        package_id: ObjectId,
151        counter_id: ObjectId,
152        counter_initial_shared_version: Version,
153    ) -> Self {
154        self.move_call(
155            package_id,
156            "counter",
157            "delete",
158            vec![CallArg::Shared(SharedObjectReference::new(
159                counter_id,
160                counter_initial_shared_version,
161                true,
162            ))],
163        )
164    }
165
166    pub fn call_nft_create(self, package_id: ObjectId) -> Self {
167        self.move_call(
168            package_id,
169            "testnet_nft",
170            "mint_to_sender",
171            vec![
172                CallArg::pure(&"example_nft_name"),
173                CallArg::pure(&"example_nft_description"),
174                CallArg::pure(&"https://iota.org/_nuxt/img/iota-logo.8d3c44e.svg"),
175            ],
176        )
177    }
178
179    pub fn call_nft_delete(self, package_id: ObjectId, nft_to_delete: ObjectReference) -> Self {
180        self.move_call(
181            package_id,
182            "testnet_nft",
183            "burn",
184            vec![CallArg::ImmutableOrOwned(nft_to_delete)],
185        )
186    }
187
188    pub fn call_staking(self, stake_coin: ObjectReference, validator: Address) -> Self {
189        self.move_call(
190            ObjectId::SYSTEM,
191            Identifier::IOTA_SYSTEM_MODULE.as_str(),
192            "request_add_stake",
193            vec![
194                CallArg::IOTA_SYSTEM_MUTABLE,
195                CallArg::ImmutableOrOwned(stake_coin),
196                CallArg::pure(&validator),
197            ],
198        )
199    }
200
201    pub fn call_unstaking(self, staked_iota: ObjectReference) -> Self {
202        self.move_call(
203            ObjectId::SYSTEM,
204            Identifier::IOTA_SYSTEM_MODULE.as_str(),
205            "request_withdraw_stake",
206            vec![
207                CallArg::IOTA_SYSTEM_MUTABLE,
208                CallArg::ImmutableOrOwned(staked_iota),
209            ],
210        )
211    }
212
213    pub fn call_emit_random(
214        self,
215        package_id: ObjectId,
216        randomness_initial_shared_version: Version,
217    ) -> Self {
218        self.move_call(
219            package_id,
220            "random",
221            "new",
222            vec![CallArg::Shared(SharedObjectReference::new(
223                ObjectId::RANDOMNESS_STATE,
224                randomness_initial_shared_version,
225                false,
226            ))],
227        )
228    }
229
230    pub fn call_request_add_validator(self) -> Self {
231        self.move_call(
232            ObjectId::SYSTEM,
233            Identifier::IOTA_SYSTEM_MODULE.as_str(),
234            "request_add_validator",
235            vec![CallArg::IOTA_SYSTEM_MUTABLE],
236        )
237    }
238
239    pub fn call_request_add_validator_candidate(
240        self,
241        validator: &GenesisValidatorMetadata,
242    ) -> Self {
243        self.move_call(
244            ObjectId::SYSTEM,
245            Identifier::IOTA_SYSTEM_MODULE.as_str(),
246            "request_add_validator_candidate",
247            vec![
248                CallArg::IOTA_SYSTEM_MUTABLE,
249                CallArg::pure(&validator.authority_public_key),
250                CallArg::pure(&validator.network_public_key),
251                CallArg::pure(&validator.protocol_public_key),
252                CallArg::pure(&validator.proof_of_possession),
253                CallArg::pure(&validator.name),
254                CallArg::pure(&validator.description),
255                CallArg::pure(&validator.image_url),
256                CallArg::pure(&validator.project_url),
257                CallArg::pure(&validator.network_address),
258                CallArg::pure(&validator.p2p_address),
259                CallArg::pure(&validator.primary_address),
260                CallArg::pure(&DEFAULT_VALIDATOR_GAS_PRICE), // gas_price
261                CallArg::pure(&0u64),                        // commission_rate
262            ],
263        )
264    }
265
266    pub fn call_request_remove_validator(self) -> Self {
267        self.move_call(
268            ObjectId::SYSTEM,
269            Identifier::IOTA_SYSTEM_MODULE.as_str(),
270            "request_remove_validator",
271            vec![CallArg::IOTA_SYSTEM_MUTABLE],
272        )
273    }
274
275    pub fn transfer(mut self, object: ObjectReference, recipient: Address) -> Self {
276        self.test_data = TestTransactionData::Transfer(TransferData { object, recipient });
277        self
278    }
279
280    pub fn transfer_iota(mut self, amount: Option<u64>, recipient: Address) -> Self {
281        self.test_data = TestTransactionData::TransferIota(TransferIotaData { amount, recipient });
282        self
283    }
284
285    pub fn split_coin(mut self, coin: ObjectReference, amounts: Vec<u64>) -> Self {
286        self.test_data = TestTransactionData::SplitCoin(SplitCoinData { coin, amounts });
287        self
288    }
289
290    pub fn publish(mut self, path: PathBuf) -> Self {
291        assert!(matches!(self.test_data, TestTransactionData::Empty));
292        self.test_data = TestTransactionData::Publish(PublishData::Source(path, false));
293        self
294    }
295
296    pub fn publish_with_deps(mut self, path: PathBuf) -> Self {
297        assert!(matches!(self.test_data, TestTransactionData::Empty));
298        self.test_data = TestTransactionData::Publish(PublishData::Source(path, true));
299        self
300    }
301
302    pub fn publish_with_data(mut self, data: PublishData) -> Self {
303        assert!(matches!(self.test_data, TestTransactionData::Empty));
304        self.test_data = TestTransactionData::Publish(data);
305        self
306    }
307
308    pub fn publish_examples(self, subpath: &'static str) -> Self {
309        let path = if let Ok(p) = std::env::var("MOVE_EXAMPLES_DIR") {
310            let mut path = PathBuf::from(p);
311            path.extend([subpath]);
312            path
313        } else {
314            let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
315            path.extend(["..", "..", "examples", "move", subpath]);
316            path
317        };
318        self.publish(path)
319    }
320
321    pub fn programmable(mut self, programmable: ProgrammableTransaction) -> Self {
322        self.test_data = TestTransactionData::Programmable(programmable);
323        self
324    }
325
326    pub fn build(self) -> Transaction {
327        let nonce = self.nonce;
328        let mut data = self.build_inner();
329        if let Some(nonce) = nonce {
330            // A trailing pure input that no command references leaves execution
331            // unchanged but alters the serialized transaction, and hence its
332            // digest.
333            if let TransactionKind::Programmable(pt) = data.kind_mut() {
334                pt.inputs.push(Input::Pure(nonce.to_le_bytes().to_vec()));
335            }
336        }
337        data
338    }
339
340    fn build_inner(self) -> Transaction {
341        match self.test_data {
342            TestTransactionData::Move(data) => Transaction::new_move_call(
343                self.sender,
344                data.package_id,
345                data.module,
346                data.function,
347                data.type_args,
348                self.gas_object,
349                data.args,
350                self.gas_budget
351                    .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE),
352                self.gas_price,
353            )
354            .unwrap(),
355            TestTransactionData::Transfer(data) => Transaction::new_transfer(
356                data.recipient,
357                data.object,
358                self.sender,
359                self.gas_object,
360                self.gas_budget
361                    .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER),
362                self.gas_price,
363            ),
364            TestTransactionData::TransferIota(data) => Transaction::new_transfer_iota(
365                data.recipient,
366                self.sender,
367                data.amount,
368                self.gas_object,
369                self.gas_budget
370                    .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER),
371                self.gas_price,
372            ),
373            TestTransactionData::SplitCoin(data) => Transaction::new_split_coin(
374                self.sender,
375                data.coin,
376                data.amounts,
377                self.gas_object,
378                self.gas_budget
379                    .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER),
380                self.gas_price,
381            ),
382            TestTransactionData::Publish(data) => {
383                let (all_module_bytes, dependencies) = match data {
384                    PublishData::Source(path, with_unpublished_deps) => {
385                        let compiled_package = BuildConfig::new_for_testing().build(&path).unwrap();
386                        let all_module_bytes =
387                            compiled_package.get_package_bytes(with_unpublished_deps);
388                        let dependencies = compiled_package.get_dependency_storage_package_ids();
389                        (all_module_bytes, dependencies)
390                    }
391                    PublishData::ModuleBytes(bytecode) => (bytecode, vec![]),
392                    PublishData::CompiledPackage(compiled_package) => {
393                        let all_module_bytes = compiled_package.get_package_bytes(false);
394                        let dependencies = compiled_package.get_dependency_storage_package_ids();
395                        (all_module_bytes, dependencies)
396                    }
397                };
398
399                Transaction::new_module(
400                    self.sender,
401                    self.gas_object,
402                    all_module_bytes,
403                    dependencies,
404                    self.gas_budget.unwrap_or(
405                        self.gas_price * TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE,
406                    ),
407                    self.gas_price,
408                )
409            }
410            TestTransactionData::Programmable(pt) => Transaction::new_programmable(
411                self.sender,
412                vec![self.gas_object],
413                pt,
414                self.gas_budget
415                    .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE),
416                self.gas_price,
417            ),
418            TestTransactionData::Empty => {
419                panic!("Cannot build empty transaction");
420            }
421        }
422    }
423
424    pub fn build_and_sign(self, signer: &impl Signer<SimpleSignature>) -> TransactionEnvelope {
425        TransactionEnvelope::from_data_and_signer(self.build(), vec![signer])
426    }
427
428    pub fn build_and_sign_multisig(
429        self,
430        multisig_pk: MultisigCommittee,
431        signers: &[&dyn Signer<SimpleSignature>],
432        bitmap: BitmapUnit,
433    ) -> TransactionEnvelope {
434        let data = self.build();
435        let digest = data.signing_digest();
436        let signatures = signers.iter().map(|s| s.sign(&digest).into()).collect();
437        let multisig = UserSignature::Multisig(MultisigAggregatedSignature::new_unchecked(
438            signatures,
439            bitmap,
440            multisig_pk,
441        ));
442
443        TransactionEnvelope::from_user_sig_data(data, vec![multisig])
444    }
445}
446
447#[expect(clippy::large_enum_variant)]
448enum TestTransactionData {
449    Move(MoveData),
450    Transfer(TransferData),
451    TransferIota(TransferIotaData),
452    SplitCoin(SplitCoinData),
453    Publish(PublishData),
454    Programmable(ProgrammableTransaction),
455    Empty,
456}
457
458struct MoveData {
459    package_id: ObjectId,
460    module: Identifier,
461    function: Identifier,
462    args: Vec<CallArg>,
463    type_args: Vec<TypeTag>,
464}
465
466#[expect(clippy::large_enum_variant)]
467pub enum PublishData {
468    /// Path to source code directory and with_unpublished_deps.
469    /// with_unpublished_deps indicates whether to publish unpublished
470    /// dependencies in the same transaction or not.
471    Source(PathBuf, bool),
472    ModuleBytes(Vec<Vec<u8>>),
473    CompiledPackage(CompiledPackage),
474}
475
476struct TransferData {
477    object: ObjectReference,
478    recipient: Address,
479}
480
481struct TransferIotaData {
482    amount: Option<u64>,
483    recipient: Address,
484}
485
486struct SplitCoinData {
487    coin: ObjectReference,
488    amounts: Vec<u64>,
489}
490
491/// A helper function to make Transactions with controlled accounts in
492/// WalletContext. Particularly, the wallet needs to own gas objects for
493/// transactions. However, if this function is called multiple times without any
494/// "sync" actions on gas object management, txns may fail and objects may be
495/// locked.
496///
497/// The param is called `max_txn_num` because it does not always return the
498/// exact same amount of Transactions, for example when there are not enough gas
499/// objects controlled by the WalletContext. Caller should rely on the return
500/// value to check the count.
501pub async fn batch_make_transfer_transactions(
502    context: &WalletContext,
503    max_txn_num: usize,
504) -> Vec<TransactionEnvelope> {
505    let recipient = Address::random();
506    let result = context.get_all_accounts_and_gas_objects().await;
507    let accounts_and_objs = result.unwrap();
508    let mut res = Vec::with_capacity(max_txn_num);
509
510    let gas_price = context.get_reference_gas_price().await.unwrap();
511    for (address, objs) in accounts_and_objs {
512        for obj in objs {
513            if res.len() >= max_txn_num {
514                return res;
515            }
516            let tx = Transaction::new_transfer_iota(
517                recipient,
518                address,
519                Some(2),
520                obj,
521                gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER,
522                gas_price,
523            );
524            let tx = context.sign_transaction(&tx);
525            res.push(tx);
526        }
527    }
528    res
529}
530
531pub async fn make_transfer_iota_transaction(
532    context: &WalletContext,
533    recipient: Option<Address>,
534    amount: Option<u64>,
535) -> TransactionEnvelope {
536    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
537    let gas_price = context.get_reference_gas_price().await.unwrap();
538    context.sign_transaction(
539        &TestTransactionBuilder::new(sender, gas_object, gas_price)
540            .transfer_iota(amount, recipient.unwrap_or(sender))
541            .build(),
542    )
543}
544
545pub async fn make_staking_transaction(
546    context: &WalletContext,
547    validator_address: Address,
548) -> TransactionEnvelope {
549    let accounts_and_objs = context.get_all_accounts_and_gas_objects().await.unwrap();
550    let sender = accounts_and_objs[0].0;
551    let gas_object = accounts_and_objs[0].1[0];
552    let stake_object = accounts_and_objs[0].1[1];
553    let gas_price = context.get_reference_gas_price().await.unwrap();
554    context.sign_transaction(
555        &TestTransactionBuilder::new(sender, gas_object, gas_price)
556            .call_staking(stake_object, validator_address)
557            .build(),
558    )
559}
560
561pub async fn make_publish_transaction(
562    context: &WalletContext,
563    path: PathBuf,
564) -> TransactionEnvelope {
565    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
566    let gas_price = context.get_reference_gas_price().await.unwrap();
567    context.sign_transaction(
568        &TestTransactionBuilder::new(sender, gas_object, gas_price)
569            .publish(path)
570            .build(),
571    )
572}
573
574pub async fn make_publish_transaction_with_deps(
575    context: &WalletContext,
576    path: PathBuf,
577) -> TransactionEnvelope {
578    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
579    let gas_price = context.get_reference_gas_price().await.unwrap();
580    context.sign_transaction(
581        &TestTransactionBuilder::new(sender, gas_object, gas_price)
582            .publish_with_deps(path)
583            .build(),
584    )
585}
586
587pub async fn publish_package(context: &WalletContext, path: PathBuf) -> ObjectReference {
588    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
589    let gas_price = context.get_reference_gas_price().await.unwrap();
590    let txn = context.sign_transaction(
591        &TestTransactionBuilder::new(sender, gas_object, gas_price)
592            .publish(path)
593            .build(),
594    );
595    let resp = context.execute_transaction_must_succeed(txn).await;
596    get_new_package_obj_from_response(&resp).unwrap()
597}
598
599/// Executes a transaction to publish the `basics` package and returns the
600/// package object ref.
601pub async fn publish_basics_package(context: &WalletContext) -> ObjectReference {
602    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
603    let gas_price = context.get_reference_gas_price().await.unwrap();
604    let txn = context.sign_transaction(
605        &TestTransactionBuilder::new(sender, gas_object, gas_price)
606            .publish_examples("basics")
607            .build(),
608    );
609    let resp = context.execute_transaction_must_succeed(txn).await;
610    get_new_package_obj_from_response(&resp).unwrap()
611}
612
613/// Executes a transaction to publish the `basics` package and another one to
614/// create a counter. Returns the package object ref and the counter object ref.
615pub async fn publish_basics_package_and_make_counter(
616    context: &WalletContext,
617) -> (ObjectReference, ObjectReference) {
618    let package_ref = publish_basics_package(context).await;
619    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
620    let gas_price = context.get_reference_gas_price().await.unwrap();
621    let counter_creation_txn = context.sign_transaction(
622        &TestTransactionBuilder::new(sender, gas_object, gas_price)
623            .call_counter_create(package_ref.object_id)
624            .build(),
625    );
626    let resp = context
627        .execute_transaction_must_succeed(counter_creation_txn)
628        .await;
629    let counter_ref = resp
630        .effects
631        .unwrap()
632        .created()
633        .iter()
634        .find(|obj_ref| matches!(obj_ref.owner, Owner::Shared(_)))
635        .unwrap()
636        .reference;
637    (package_ref, counter_ref)
638}
639
640/// Executes a transaction to increment a counter object.
641/// Must be called after calling `publish_basics_package_and_make_counter`.
642pub async fn increment_counter(
643    context: &WalletContext,
644    sender: Address,
645    gas_object_id: Option<ObjectId>,
646    package_id: ObjectId,
647    counter_id: ObjectId,
648    initial_shared_version: Version,
649) -> IotaTransactionBlockResponse {
650    let gas_object = if let Some(gas_object_id) = gas_object_id {
651        context.get_object_ref(gas_object_id).await.unwrap()
652    } else {
653        context
654            .get_one_gas_object_owned_by_address(sender)
655            .await
656            .unwrap()
657            .unwrap()
658    };
659    let rgp = context.get_reference_gas_price().await.unwrap();
660    let txn = context.sign_transaction(
661        &TestTransactionBuilder::new(sender, gas_object, rgp)
662            .call_counter_increment(package_id, counter_id, initial_shared_version)
663            .build(),
664    );
665    context.execute_transaction_must_succeed(txn).await
666}
667
668/// Executes a transaction that generates a new random u128 using Random and
669/// emits it as an event.
670pub async fn emit_new_random_u128(
671    context: &WalletContext,
672    package_id: ObjectId,
673) -> IotaTransactionBlockResponse {
674    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
675    let rgp = context.get_reference_gas_price().await.unwrap();
676    let random_call_arg = randomness_state_call_arg(context).await;
677
678    let txn = context.sign_transaction(
679        &TestTransactionBuilder::new(sender, gas_object, rgp)
680            .move_call(package_id, "random", "new", vec![random_call_arg])
681            .build(),
682    );
683    context.execute_transaction_must_succeed(txn).await
684}
685
686/// Builds the `CallArg` for reading the shared randomness state object.
687pub async fn randomness_state_call_arg(context: &WalletContext) -> CallArg {
688    let client = context.get_client().await.unwrap();
689    let random_obj = client
690        .read_api()
691        .get_object_with_options(
692            ObjectId::RANDOMNESS_STATE,
693            IotaObjectDataOptions::new().with_owner(),
694        )
695        .await
696        .unwrap()
697        .into_object()
698        .unwrap();
699    let random_obj_owner = random_obj
700        .owner
701        .expect("Expect Randomness object to have an owner");
702
703    let Owner::Shared(initial_shared_version) = random_obj_owner else {
704        panic!("Expect Randomness to be shared object")
705    };
706    CallArg::Shared(SharedObjectReference::new(
707        ObjectId::RANDOMNESS_STATE,
708        initial_shared_version,
709        false,
710    ))
711}
712
713/// Executes a transaction to publish the specified examples package and returns
714/// the package id and the digest of the transaction.
715pub async fn publish_example_package(
716    context: &WalletContext,
717    example_subpath: &'static str,
718    sender_key: &AccountPrivateKey,
719    sender: Address,
720    gas: ObjectReference,
721) -> (ObjectId, TransactionDigest) {
722    let gas_price = context.get_reference_gas_price().await.unwrap();
723    let tx = to_sender_signed_transaction(
724        TestTransactionBuilder::new(sender, gas, gas_price)
725            .publish_examples(example_subpath)
726            .build(),
727        sender_key,
728    );
729
730    let resp = context.execute_transaction_must_succeed(tx).await;
731    let package_id = get_new_package_obj_from_response(&resp).unwrap().object_id;
732    (package_id, resp.digest)
733}
734
735/// Executes a transaction to publish the `nft` package and returns the package
736/// id, id of the gas object used, and the digest of the transaction.
737pub async fn publish_nfts_package(
738    context: &WalletContext,
739) -> (ObjectId, ObjectId, TransactionDigest) {
740    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
741    let gas_id = gas_object.object_id;
742    let gas_price = context.get_reference_gas_price().await.unwrap();
743    let txn = context.sign_transaction(
744        &TestTransactionBuilder::new(sender, gas_object, gas_price)
745            .publish_examples("nft")
746            .build(),
747    );
748    let resp = context.execute_transaction_must_succeed(txn).await;
749    let package_id = get_new_package_obj_from_response(&resp).unwrap().object_id;
750    (package_id, gas_id, resp.digest)
751}
752
753/// Executes a transaction to publish the `simple_warrior` package and returns
754/// the package id and the digest of the transaction.
755pub async fn publish_simple_warrior_package(
756    context: &WalletContext,
757    sender_key: &AccountPrivateKey,
758    sender: Address,
759    gas: ObjectReference,
760) -> (ObjectId, TransactionDigest) {
761    publish_example_package(context, "simple_warrior", sender_key, sender, gas).await
762}
763
764/// Pre-requisite: `publish_nfts_package` must be called before this function.
765/// Executes a transaction to create an NFT and returns the sender address, the
766/// object id of the NFT, and the digest of the transaction.
767pub async fn create_nft(
768    context: &WalletContext,
769    package_id: ObjectId,
770) -> (Address, ObjectId, TransactionDigest) {
771    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
772    let rgp = context.get_reference_gas_price().await.unwrap();
773
774    let txn = context.sign_transaction(
775        &TestTransactionBuilder::new(sender, gas_object, rgp)
776            .call_nft_create(package_id)
777            .build(),
778    );
779    let resp = context.execute_transaction_must_succeed(txn).await;
780
781    let object_id = resp
782        .effects
783        .as_ref()
784        .unwrap()
785        .created()
786        .first()
787        .unwrap()
788        .reference
789        .object_id;
790
791    (sender, object_id, resp.digest)
792}
793
794/// Executes a transaction to delete the given NFT.
795pub async fn delete_nft(
796    context: &WalletContext,
797    sender: Address,
798    package_id: ObjectId,
799    nft_to_delete: ObjectReference,
800) -> IotaTransactionBlockResponse {
801    let gas = context
802        .get_one_gas_object_owned_by_address(sender)
803        .await
804        .unwrap()
805        .unwrap_or_else(|| panic!("Expect {sender} to have at least one gas object"));
806    let rgp = context.get_reference_gas_price().await.unwrap();
807    let txn = context.sign_transaction(
808        &TestTransactionBuilder::new(sender, gas, rgp)
809            .call_nft_delete(package_id, nft_to_delete)
810            .build(),
811    );
812    context.execute_transaction_must_succeed(txn).await
813}
814
815/// Fetch one IOTA coin owned by `sender` to use as an explicit gas coin.
816///
817/// Without an explicit gas coin,
818/// [`TransactionBuilder::finish`](iota_sdk_transaction_builder::TransactionBuilder::finish)
819/// auto-adds every IOTA coin the sender owns as gas inputs and merges the leftover into
820/// one output coin, breaking tests that observe the sender's coin count.
821pub async fn select_gas_coin(
822    grpc_client: &iota_grpc_client::GrpcClient,
823    sender: Address,
824) -> ObjectId {
825    let gas_coin = grpc_client
826        .owned_objects(sender)
827        .object_type(StructTag::new_gas_coin())
828        .page_size(1)
829        .collect(Some(1))
830        .await
831        .expect("failed to fetch gas coin")
832        .into_inner()
833        .into_iter()
834        .next()
835        .expect("sender has no gas coin");
836    *gas_coin
837        .object_reference()
838        .expect("gas coin missing object reference")
839        .object_id()
840}
841
842/// Build a Move-call transaction ready to be signed, paying gas from a single
843/// coin picked with [`select_gas_coin`].
844///
845/// `args` is anything the builder accepts as an argument list: a tuple of
846/// mixed argument types (`ObjectId` for owned objects, `SharedMut(id)` for
847/// shared mutable objects, `u64`/`Address` and other pure values), or an
848/// array/`Vec` of a single argument type.
849pub async fn move_call_tx<A: PTBArgumentList>(
850    grpc_client: &iota_grpc_client::GrpcClient,
851    sender: Address,
852    package_id: ObjectId,
853    module: &str,
854    function: &str,
855    args: A,
856    gas_budget: u64,
857) -> Transaction {
858    let mut builder = grpc_client.transaction_builder(sender);
859
860    builder
861        .move_call(package_id, module, function)
862        .arguments(args);
863
864    builder.gas(vec![select_gas_coin(grpc_client, sender).await]);
865    builder.gas_budget(gas_budget);
866
867    builder
868        .finish()
869        .await
870        .expect("failed to construct move call transaction")
871}
872
873/// Build a transaction splitting `coin_to_split` into `num_coins` coins of
874/// equal value, ready to be signed. The original coin keeps the remainder.
875///
876/// `gas_coin` must differ from `coin_to_split`; when `None`, the builder
877/// selects gas automatically from the sender's IOTA coins.
878pub async fn split_coin_equal_tx(
879    grpc_client: &iota_grpc_client::GrpcClient,
880    sender: Address,
881    coin_to_split: ObjectId,
882    num_coins: u64,
883    gas_coin: Option<ObjectId>,
884    gas_budget: u64,
885) -> Transaction {
886    let coin_object = grpc_client
887        .objects([coin_to_split])
888        .await
889        .expect("failed to fetch coin")
890        .into_inner()
891        .into_iter()
892        .next()
893        .expect("no result for the requested coin")
894        .expect("coin not found")
895        .object()
896        .expect("invalid coin object");
897    let coin_balance = iota_sdk_types::Coin::try_from_object(&coin_object)
898        .expect("object is not a coin")
899        .balance();
900
901    // Create `num_coins - 1` new coins of equal value; the original keeps the
902    // remainder.
903    let amount_per_split = coin_balance / num_coins;
904    let split_amounts: Vec<u64> = vec![amount_per_split; (num_coins - 1) as usize];
905
906    let mut builder = grpc_client.transaction_builder(sender);
907
908    // Split off the new coin and transfer it back to the sender; an untransferred
909    // `Coin` would be an unused PTB value (coins have no `drop`) and the
910    // transaction would be rejected.
911    let new_coin = builder.split_coins(coin_to_split, split_amounts).result();
912    builder.transfer_objects(sender, [new_coin]);
913
914    if let Some(gas) = gas_coin {
915        builder.gas([gas]);
916    }
917    builder.gas_budget(gas_budget);
918
919    builder
920        .finish()
921        .await
922        .expect("failed to construct split coin transaction")
923}
924
925#[cfg(test)]
926mod tests {
927    use iota_types::base_types::{dbg_addr, random_object_ref};
928
929    use super::*;
930
931    #[test]
932    fn ensure_unique_changes_digest() {
933        let sender = dbg_addr(1);
934        let recipient = dbg_addr(2);
935        let gas = random_object_ref();
936        let build =
937            || TestTransactionBuilder::new(sender, gas, 1000).transfer_iota(Some(1), recipient);
938
939        // Identical inputs build to the same digest.
940        assert_eq!(build().build().digest(), build().build().digest());
941
942        let base = build().build();
943        let unique = build().ensure_unique().build();
944
945        // ensure_unique() perturbs the digest by appending one trailing pure
946        // input that no command references.
947        assert_ne!(base.digest(), unique.digest());
948        match (base.kind(), unique.kind()) {
949            (TransactionKind::Programmable(base), TransactionKind::Programmable(unique)) => {
950                assert_eq!(unique.inputs.len(), base.inputs.len() + 1);
951                assert!(matches!(unique.inputs.last(), Some(Input::Pure(_))));
952            }
953            _ => panic!("expected programmable transactions"),
954        }
955
956        // Two independent unique builds differ from each other.
957        assert_ne!(
958            build().ensure_unique().build().digest(),
959            build().ensure_unique().build().digest()
960        );
961    }
962}