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