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