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
402enum TestTransactionData {
403    Move(MoveData),
404    Transfer(TransferData),
405    TransferIota(TransferIotaData),
406    Publish(PublishData),
407    Programmable(ProgrammableTransaction),
408    Empty,
409}
410
411struct MoveData {
412    package_id: ObjectID,
413    module: &'static str,
414    function: &'static str,
415    args: Vec<CallArg>,
416    type_args: Vec<TypeTag>,
417}
418
419pub enum PublishData {
420    /// Path to source code directory and with_unpublished_deps.
421    /// with_unpublished_deps indicates whether to publish unpublished
422    /// dependencies in the same transaction or not.
423    Source(PathBuf, bool),
424    ModuleBytes(Vec<Vec<u8>>),
425    CompiledPackage(CompiledPackage),
426}
427
428struct TransferData {
429    object: ObjectRef,
430    recipient: IotaAddress,
431}
432
433struct TransferIotaData {
434    amount: Option<u64>,
435    recipient: IotaAddress,
436}
437
438/// A helper function to make Transactions with controlled accounts in
439/// WalletContext. Particularly, the wallet needs to own gas objects for
440/// transactions. However, if this function is called multiple times without any
441/// "sync" actions on gas object management, txns may fail and objects may be
442/// locked.
443///
444/// The param is called `max_txn_num` because it does not always return the
445/// exact same amount of Transactions, for example when there are not enough gas
446/// objects controlled by the WalletContext. Caller should rely on the return
447/// value to check the count.
448pub async fn batch_make_transfer_transactions(
449    context: &WalletContext,
450    max_txn_num: usize,
451) -> Vec<Transaction> {
452    let recipient = get_key_pair::<AccountKeyPair>().0;
453    let result = context.get_all_accounts_and_gas_objects().await;
454    let accounts_and_objs = result.unwrap();
455    let mut res = Vec::with_capacity(max_txn_num);
456
457    let gas_price = context.get_reference_gas_price().await.unwrap();
458    for (address, objs) in accounts_and_objs {
459        for obj in objs {
460            if res.len() >= max_txn_num {
461                return res;
462            }
463            let data = TransactionData::new_transfer_iota(
464                recipient,
465                address,
466                Some(2),
467                obj,
468                gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER,
469                gas_price,
470            );
471            let tx = context.sign_transaction(&data);
472            res.push(tx);
473        }
474    }
475    res
476}
477
478pub async fn make_transfer_iota_transaction(
479    context: &WalletContext,
480    recipient: Option<IotaAddress>,
481    amount: Option<u64>,
482) -> Transaction {
483    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
484    let gas_price = context.get_reference_gas_price().await.unwrap();
485    context.sign_transaction(
486        &TestTransactionBuilder::new(sender, gas_object, gas_price)
487            .transfer_iota(amount, recipient.unwrap_or(sender))
488            .build(),
489    )
490}
491
492pub async fn make_staking_transaction(
493    context: &WalletContext,
494    validator_address: IotaAddress,
495) -> Transaction {
496    let accounts_and_objs = context.get_all_accounts_and_gas_objects().await.unwrap();
497    let sender = accounts_and_objs[0].0;
498    let gas_object = accounts_and_objs[0].1[0];
499    let stake_object = accounts_and_objs[0].1[1];
500    let gas_price = context.get_reference_gas_price().await.unwrap();
501    context.sign_transaction(
502        &TestTransactionBuilder::new(sender, gas_object, gas_price)
503            .call_staking(stake_object, validator_address)
504            .build(),
505    )
506}
507
508pub async fn make_publish_transaction(context: &WalletContext, path: PathBuf) -> Transaction {
509    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
510    let gas_price = context.get_reference_gas_price().await.unwrap();
511    context.sign_transaction(
512        &TestTransactionBuilder::new(sender, gas_object, gas_price)
513            .publish(path)
514            .build(),
515    )
516}
517
518pub async fn make_publish_transaction_with_deps(
519    context: &WalletContext,
520    path: PathBuf,
521) -> Transaction {
522    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
523    let gas_price = context.get_reference_gas_price().await.unwrap();
524    context.sign_transaction(
525        &TestTransactionBuilder::new(sender, gas_object, gas_price)
526            .publish_with_deps(path)
527            .build(),
528    )
529}
530
531pub async fn publish_package(context: &WalletContext, path: PathBuf) -> ObjectRef {
532    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
533    let gas_price = context.get_reference_gas_price().await.unwrap();
534    let txn = context.sign_transaction(
535        &TestTransactionBuilder::new(sender, gas_object, gas_price)
536            .publish(path)
537            .build(),
538    );
539    let resp = context.execute_transaction_must_succeed(txn).await;
540    get_new_package_obj_from_response(&resp).unwrap()
541}
542
543/// Executes a transaction to publish the `basics` package and returns the
544/// package object ref.
545pub async fn publish_basics_package(context: &WalletContext) -> ObjectRef {
546    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
547    let gas_price = context.get_reference_gas_price().await.unwrap();
548    let txn = context.sign_transaction(
549        &TestTransactionBuilder::new(sender, gas_object, gas_price)
550            .publish_examples("basics")
551            .build(),
552    );
553    let resp = context.execute_transaction_must_succeed(txn).await;
554    get_new_package_obj_from_response(&resp).unwrap()
555}
556
557/// Executes a transaction to publish the `basics` package and another one to
558/// create a counter. Returns the package object ref and the counter object ref.
559pub async fn publish_basics_package_and_make_counter(
560    context: &WalletContext,
561) -> (ObjectRef, ObjectRef) {
562    let package_ref = publish_basics_package(context).await;
563    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
564    let gas_price = context.get_reference_gas_price().await.unwrap();
565    let counter_creation_txn = context.sign_transaction(
566        &TestTransactionBuilder::new(sender, gas_object, gas_price)
567            .call_counter_create(package_ref.0)
568            .build(),
569    );
570    let resp = context
571        .execute_transaction_must_succeed(counter_creation_txn)
572        .await;
573    let counter_ref = resp
574        .effects
575        .unwrap()
576        .created()
577        .iter()
578        .find(|obj_ref| matches!(obj_ref.owner, Owner::Shared { .. }))
579        .unwrap()
580        .reference
581        .to_object_ref();
582    (package_ref, counter_ref)
583}
584
585/// Executes a transaction to increment a counter object.
586/// Must be called after calling `publish_basics_package_and_make_counter`.
587pub async fn increment_counter(
588    context: &WalletContext,
589    sender: IotaAddress,
590    gas_object_id: Option<ObjectID>,
591    package_id: ObjectID,
592    counter_id: ObjectID,
593    initial_shared_version: SequenceNumber,
594) -> IotaTransactionBlockResponse {
595    let gas_object = if let Some(gas_object_id) = gas_object_id {
596        context.get_object_ref(gas_object_id).await.unwrap()
597    } else {
598        context
599            .get_one_gas_object_owned_by_address(sender)
600            .await
601            .unwrap()
602            .unwrap()
603    };
604    let rgp = context.get_reference_gas_price().await.unwrap();
605    let txn = context.sign_transaction(
606        &TestTransactionBuilder::new(sender, gas_object, rgp)
607            .call_counter_increment(package_id, counter_id, initial_shared_version)
608            .build(),
609    );
610    context.execute_transaction_must_succeed(txn).await
611}
612
613/// Executes a transaction that generates a new random u128 using Random and
614/// emits it as an event.
615pub async fn emit_new_random_u128(
616    context: &WalletContext,
617    package_id: ObjectID,
618) -> IotaTransactionBlockResponse {
619    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
620    let rgp = context.get_reference_gas_price().await.unwrap();
621
622    let client = context.get_client().await.unwrap();
623    let random_obj = client
624        .read_api()
625        .get_object_with_options(
626            IOTA_RANDOMNESS_STATE_OBJECT_ID,
627            IotaObjectDataOptions::new().with_owner(),
628        )
629        .await
630        .unwrap()
631        .into_object()
632        .unwrap();
633    let random_obj_owner = random_obj
634        .owner
635        .expect("Expect Randomness object to have an owner");
636
637    let Owner::Shared {
638        initial_shared_version,
639    } = random_obj_owner
640    else {
641        panic!("Expect Randomness to be shared object")
642    };
643    let random_call_arg = CallArg::Object(ObjectArg::SharedObject {
644        id: IOTA_RANDOMNESS_STATE_OBJECT_ID,
645        initial_shared_version,
646        mutable: false,
647    });
648
649    let txn = context.sign_transaction(
650        &TestTransactionBuilder::new(sender, gas_object, rgp)
651            .move_call(package_id, "random", "new", vec![random_call_arg])
652            .build(),
653    );
654    context.execute_transaction_must_succeed(txn).await
655}
656
657/// Executes a transaction to publish the `nfts` package and returns the package
658/// id, id of the gas object used, and the digest of the transaction.
659pub async fn publish_nfts_package(
660    context: &WalletContext,
661) -> (ObjectID, ObjectID, TransactionDigest) {
662    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
663    let gas_id = gas_object.0;
664    let gas_price = context.get_reference_gas_price().await.unwrap();
665    let txn = context.sign_transaction(
666        &TestTransactionBuilder::new(sender, gas_object, gas_price)
667            .publish_examples("nft")
668            .build(),
669    );
670    let resp = context.execute_transaction_must_succeed(txn).await;
671    let package_id = get_new_package_obj_from_response(&resp).unwrap().0;
672    (package_id, gas_id, resp.digest)
673}
674
675/// Pre-requisite: `publish_nfts_package` must be called before this function.
676/// Executes a transaction to create an NFT and returns the sender address, the
677/// object id of the NFT, and the digest of the transaction.
678pub async fn create_nft(
679    context: &WalletContext,
680    package_id: ObjectID,
681) -> (IotaAddress, ObjectID, TransactionDigest) {
682    let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
683    let rgp = context.get_reference_gas_price().await.unwrap();
684
685    let txn = context.sign_transaction(
686        &TestTransactionBuilder::new(sender, gas_object, rgp)
687            .call_nft_create(package_id)
688            .build(),
689    );
690    let resp = context.execute_transaction_must_succeed(txn).await;
691
692    let object_id = resp
693        .effects
694        .as_ref()
695        .unwrap()
696        .created()
697        .first()
698        .unwrap()
699        .reference
700        .object_id;
701
702    (sender, object_id, resp.digest)
703}
704
705/// Executes a transaction to delete the given NFT.
706pub async fn delete_nft(
707    context: &WalletContext,
708    sender: IotaAddress,
709    package_id: ObjectID,
710    nft_to_delete: ObjectRef,
711) -> IotaTransactionBlockResponse {
712    let gas = context
713        .get_one_gas_object_owned_by_address(sender)
714        .await
715        .unwrap()
716        .unwrap_or_else(|| panic!("Expect {sender} to have at least one gas object"));
717    let rgp = context.get_reference_gas_price().await.unwrap();
718    let txn = context.sign_transaction(
719        &TestTransactionBuilder::new(sender, gas, rgp)
720            .call_nft_delete(package_id, nft_to_delete)
721            .build(),
722    );
723    context.execute_transaction_must_succeed(txn).await
724}