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