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