1use std::path::PathBuf;
6
7use iota_genesis_builder::validator_info::GenesisValidatorMetadata;
8use iota_move_build::{BuildConfig, CompiledPackage};
9use iota_sdk::{
10 rpc_types::{
11 IotaObjectDataOptions, IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponse,
12 get_new_package_obj_from_response,
13 },
14 wallet_context::WalletContext,
15};
16use iota_sdk_crypto::Signer;
17use iota_sdk_transaction_builder::PTBArgumentList;
18use iota_sdk_types::{
19 Address, Identifier, Input, ObjectId, ObjectReference, Owner, ProgrammableTransaction,
20 SharedObjectReference, StructTag, Transaction, TransactionDigest, TransactionKind, TypeTag,
21 UserSignature, Version,
22 crypto::{BitmapUnit, MultisigAggregatedSignature, MultisigCommittee, SimpleSignature},
23};
24use iota_types::{
25 crypto::AccountPrivateKey,
26 transaction::{
27 CallArg, DEFAULT_VALIDATOR_GAS_PRICE, TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE,
28 TEST_ONLY_GAS_UNIT_FOR_TRANSFER, TransactionAPI, TransactionEnvelope,
29 },
30 utils::to_sender_signed_transaction,
31};
32use rand::RngExt;
33
34pub struct TestTransactionBuilder {
35 test_data: TestTransactionData,
36 sender: Address,
37 gas_object: ObjectReference,
38 gas_price: u64,
39 gas_budget: Option<u64>,
40 nonce: Option<u64>,
41}
42
43impl TestTransactionBuilder {
44 pub fn new(sender: Address, gas_object: ObjectReference, gas_price: u64) -> Self {
45 Self {
46 test_data: TestTransactionData::Empty,
47 sender,
48 gas_object,
49 gas_price,
50 gas_budget: None,
51 nonce: None,
52 }
53 }
54
55 pub fn ensure_unique(mut self) -> Self {
62 self.nonce = Some(rand::rng().random());
63 self
64 }
65
66 pub fn sender(&self) -> Address {
67 self.sender
68 }
69
70 pub fn gas_object(&self) -> ObjectReference {
71 self.gas_object
72 }
73
74 pub fn move_call(
76 mut self,
77 package_id: ObjectId,
78 module: &str,
79 function: &str,
80 args: Vec<CallArg>,
81 ) -> Self {
82 assert!(matches!(self.test_data, TestTransactionData::Empty));
83 self.test_data = TestTransactionData::Move(MoveData {
84 package_id,
85 module: Identifier::new(module).unwrap(),
86 function: Identifier::new(function).unwrap(),
87 args,
88 type_args: vec![],
89 });
90 self
91 }
92
93 pub fn with_type_args(mut self, type_args: Vec<TypeTag>) -> Self {
94 if let TestTransactionData::Move(data) = &mut self.test_data {
95 assert!(data.type_args.is_empty());
96 data.type_args = type_args;
97 } else {
98 panic!("Cannot set type args for non-move call");
99 }
100 self
101 }
102
103 pub fn with_gas_budget(mut self, gas_budget: u64) -> Self {
104 self.gas_budget = Some(gas_budget);
105 self
106 }
107
108 pub fn call_counter_create(self, package_id: ObjectId) -> Self {
109 self.move_call(package_id, "counter", "create", vec![])
110 }
111
112 pub fn call_counter_increment(
113 self,
114 package_id: ObjectId,
115 counter_id: ObjectId,
116 counter_initial_shared_version: Version,
117 ) -> Self {
118 self.move_call(
119 package_id,
120 "counter",
121 "increment",
122 vec![CallArg::Shared(SharedObjectReference::new(
123 counter_id,
124 counter_initial_shared_version,
125 true,
126 ))],
127 )
128 }
129
130 pub fn call_counter_read(
131 self,
132 package_id: ObjectId,
133 counter_id: ObjectId,
134 counter_initial_shared_version: Version,
135 ) -> Self {
136 self.move_call(
137 package_id,
138 "counter",
139 "value",
140 vec![CallArg::Shared(SharedObjectReference::new(
141 counter_id,
142 counter_initial_shared_version,
143 false,
144 ))],
145 )
146 }
147
148 pub fn call_counter_delete(
149 self,
150 package_id: ObjectId,
151 counter_id: ObjectId,
152 counter_initial_shared_version: Version,
153 ) -> Self {
154 self.move_call(
155 package_id,
156 "counter",
157 "delete",
158 vec![CallArg::Shared(SharedObjectReference::new(
159 counter_id,
160 counter_initial_shared_version,
161 true,
162 ))],
163 )
164 }
165
166 pub fn call_nft_create(self, package_id: ObjectId) -> Self {
167 self.move_call(
168 package_id,
169 "testnet_nft",
170 "mint_to_sender",
171 vec![
172 CallArg::pure(&"example_nft_name"),
173 CallArg::pure(&"example_nft_description"),
174 CallArg::pure(&"https://iota.org/_nuxt/img/iota-logo.8d3c44e.svg"),
175 ],
176 )
177 }
178
179 pub fn call_nft_delete(self, package_id: ObjectId, nft_to_delete: ObjectReference) -> Self {
180 self.move_call(
181 package_id,
182 "testnet_nft",
183 "burn",
184 vec![CallArg::ImmutableOrOwned(nft_to_delete)],
185 )
186 }
187
188 pub fn call_staking(self, stake_coin: ObjectReference, validator: Address) -> Self {
189 self.move_call(
190 ObjectId::SYSTEM,
191 Identifier::IOTA_SYSTEM_MODULE.as_str(),
192 "request_add_stake",
193 vec![
194 CallArg::IOTA_SYSTEM_MUTABLE,
195 CallArg::ImmutableOrOwned(stake_coin),
196 CallArg::pure(&validator),
197 ],
198 )
199 }
200
201 pub fn call_unstaking(self, staked_iota: ObjectReference) -> Self {
202 self.move_call(
203 ObjectId::SYSTEM,
204 Identifier::IOTA_SYSTEM_MODULE.as_str(),
205 "request_withdraw_stake",
206 vec![
207 CallArg::IOTA_SYSTEM_MUTABLE,
208 CallArg::ImmutableOrOwned(staked_iota),
209 ],
210 )
211 }
212
213 pub fn call_emit_random(
214 self,
215 package_id: ObjectId,
216 randomness_initial_shared_version: Version,
217 ) -> Self {
218 self.move_call(
219 package_id,
220 "random",
221 "new",
222 vec![CallArg::Shared(SharedObjectReference::new(
223 ObjectId::RANDOMNESS_STATE,
224 randomness_initial_shared_version,
225 false,
226 ))],
227 )
228 }
229
230 pub fn call_request_add_validator(self) -> Self {
231 self.move_call(
232 ObjectId::SYSTEM,
233 Identifier::IOTA_SYSTEM_MODULE.as_str(),
234 "request_add_validator",
235 vec![CallArg::IOTA_SYSTEM_MUTABLE],
236 )
237 }
238
239 pub fn call_request_add_validator_candidate(
240 self,
241 validator: &GenesisValidatorMetadata,
242 ) -> Self {
243 self.move_call(
244 ObjectId::SYSTEM,
245 Identifier::IOTA_SYSTEM_MODULE.as_str(),
246 "request_add_validator_candidate",
247 vec![
248 CallArg::IOTA_SYSTEM_MUTABLE,
249 CallArg::pure(&validator.authority_public_key),
250 CallArg::pure(&validator.network_public_key),
251 CallArg::pure(&validator.protocol_public_key),
252 CallArg::pure(&validator.proof_of_possession),
253 CallArg::pure(&validator.name),
254 CallArg::pure(&validator.description),
255 CallArg::pure(&validator.image_url),
256 CallArg::pure(&validator.project_url),
257 CallArg::pure(&validator.network_address),
258 CallArg::pure(&validator.p2p_address),
259 CallArg::pure(&validator.primary_address),
260 CallArg::pure(&DEFAULT_VALIDATOR_GAS_PRICE), CallArg::pure(&0u64), ],
263 )
264 }
265
266 pub fn call_request_remove_validator(self) -> Self {
267 self.move_call(
268 ObjectId::SYSTEM,
269 Identifier::IOTA_SYSTEM_MODULE.as_str(),
270 "request_remove_validator",
271 vec![CallArg::IOTA_SYSTEM_MUTABLE],
272 )
273 }
274
275 pub fn transfer(mut self, object: ObjectReference, recipient: Address) -> Self {
276 self.test_data = TestTransactionData::Transfer(TransferData { object, recipient });
277 self
278 }
279
280 pub fn transfer_iota(mut self, amount: Option<u64>, recipient: Address) -> Self {
281 self.test_data = TestTransactionData::TransferIota(TransferIotaData { amount, recipient });
282 self
283 }
284
285 pub fn split_coin(mut self, coin: ObjectReference, amounts: Vec<u64>) -> Self {
286 self.test_data = TestTransactionData::SplitCoin(SplitCoinData { coin, amounts });
287 self
288 }
289
290 pub fn publish(mut self, path: PathBuf) -> Self {
291 assert!(matches!(self.test_data, TestTransactionData::Empty));
292 self.test_data = TestTransactionData::Publish(PublishData::Source(path, false));
293 self
294 }
295
296 pub fn publish_with_deps(mut self, path: PathBuf) -> Self {
297 assert!(matches!(self.test_data, TestTransactionData::Empty));
298 self.test_data = TestTransactionData::Publish(PublishData::Source(path, true));
299 self
300 }
301
302 pub fn publish_with_data(mut self, data: PublishData) -> Self {
303 assert!(matches!(self.test_data, TestTransactionData::Empty));
304 self.test_data = TestTransactionData::Publish(data);
305 self
306 }
307
308 pub fn publish_examples(self, subpath: &'static str) -> Self {
309 let path = if let Ok(p) = std::env::var("MOVE_EXAMPLES_DIR") {
310 let mut path = PathBuf::from(p);
311 path.extend([subpath]);
312 path
313 } else {
314 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
315 path.extend(["..", "..", "examples", "move", subpath]);
316 path
317 };
318 self.publish(path)
319 }
320
321 pub fn programmable(mut self, programmable: ProgrammableTransaction) -> Self {
322 self.test_data = TestTransactionData::Programmable(programmable);
323 self
324 }
325
326 pub fn build(self) -> Transaction {
327 let nonce = self.nonce;
328 let mut data = self.build_inner();
329 if let Some(nonce) = nonce {
330 if let TransactionKind::Programmable(pt) = data.kind_mut() {
334 pt.inputs.push(Input::Pure(nonce.to_le_bytes().to_vec()));
335 }
336 }
337 data
338 }
339
340 fn build_inner(self) -> Transaction {
341 match self.test_data {
342 TestTransactionData::Move(data) => Transaction::new_move_call(
343 self.sender,
344 data.package_id,
345 data.module,
346 data.function,
347 data.type_args,
348 self.gas_object,
349 data.args,
350 self.gas_budget
351 .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE),
352 self.gas_price,
353 )
354 .unwrap(),
355 TestTransactionData::Transfer(data) => Transaction::new_transfer(
356 data.recipient,
357 data.object,
358 self.sender,
359 self.gas_object,
360 self.gas_budget
361 .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER),
362 self.gas_price,
363 ),
364 TestTransactionData::TransferIota(data) => Transaction::new_transfer_iota(
365 data.recipient,
366 self.sender,
367 data.amount,
368 self.gas_object,
369 self.gas_budget
370 .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER),
371 self.gas_price,
372 ),
373 TestTransactionData::SplitCoin(data) => Transaction::new_split_coin(
374 self.sender,
375 data.coin,
376 data.amounts,
377 self.gas_object,
378 self.gas_budget
379 .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER),
380 self.gas_price,
381 ),
382 TestTransactionData::Publish(data) => {
383 let (all_module_bytes, dependencies) = match data {
384 PublishData::Source(path, with_unpublished_deps) => {
385 let compiled_package = BuildConfig::new_for_testing().build(&path).unwrap();
386 let all_module_bytes =
387 compiled_package.get_package_bytes(with_unpublished_deps);
388 let dependencies = compiled_package.get_dependency_storage_package_ids();
389 (all_module_bytes, dependencies)
390 }
391 PublishData::ModuleBytes(bytecode) => (bytecode, vec![]),
392 PublishData::CompiledPackage(compiled_package) => {
393 let all_module_bytes = compiled_package.get_package_bytes(false);
394 let dependencies = compiled_package.get_dependency_storage_package_ids();
395 (all_module_bytes, dependencies)
396 }
397 };
398
399 Transaction::new_module(
400 self.sender,
401 self.gas_object,
402 all_module_bytes,
403 dependencies,
404 self.gas_budget.unwrap_or(
405 self.gas_price * TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE,
406 ),
407 self.gas_price,
408 )
409 }
410 TestTransactionData::Programmable(pt) => Transaction::new_programmable(
411 self.sender,
412 vec![self.gas_object],
413 pt,
414 self.gas_budget
415 .unwrap_or(self.gas_price * TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE),
416 self.gas_price,
417 ),
418 TestTransactionData::Empty => {
419 panic!("Cannot build empty transaction");
420 }
421 }
422 }
423
424 pub fn build_and_sign(self, signer: &impl Signer<SimpleSignature>) -> TransactionEnvelope {
425 TransactionEnvelope::from_data_and_signer(self.build(), vec![signer])
426 }
427
428 pub fn build_and_sign_multisig(
429 self,
430 multisig_pk: MultisigCommittee,
431 signers: &[&dyn Signer<SimpleSignature>],
432 bitmap: BitmapUnit,
433 ) -> TransactionEnvelope {
434 let data = self.build();
435 let digest = data.signing_digest();
436 let signatures = signers.iter().map(|s| s.sign(&digest).into()).collect();
437 let multisig = UserSignature::Multisig(MultisigAggregatedSignature::new_unchecked(
438 signatures,
439 bitmap,
440 multisig_pk,
441 ));
442
443 TransactionEnvelope::from_user_sig_data(data, vec![multisig])
444 }
445}
446
447#[expect(clippy::large_enum_variant)]
448enum TestTransactionData {
449 Move(MoveData),
450 Transfer(TransferData),
451 TransferIota(TransferIotaData),
452 SplitCoin(SplitCoinData),
453 Publish(PublishData),
454 Programmable(ProgrammableTransaction),
455 Empty,
456}
457
458struct MoveData {
459 package_id: ObjectId,
460 module: Identifier,
461 function: Identifier,
462 args: Vec<CallArg>,
463 type_args: Vec<TypeTag>,
464}
465
466#[expect(clippy::large_enum_variant)]
467pub enum PublishData {
468 Source(PathBuf, bool),
472 ModuleBytes(Vec<Vec<u8>>),
473 CompiledPackage(CompiledPackage),
474}
475
476struct TransferData {
477 object: ObjectReference,
478 recipient: Address,
479}
480
481struct TransferIotaData {
482 amount: Option<u64>,
483 recipient: Address,
484}
485
486struct SplitCoinData {
487 coin: ObjectReference,
488 amounts: Vec<u64>,
489}
490
491pub async fn batch_make_transfer_transactions(
502 context: &WalletContext,
503 max_txn_num: usize,
504) -> Vec<TransactionEnvelope> {
505 let recipient = Address::random();
506 let result = context.get_all_accounts_and_gas_objects().await;
507 let accounts_and_objs = result.unwrap();
508 let mut res = Vec::with_capacity(max_txn_num);
509
510 let gas_price = context.get_reference_gas_price().await.unwrap();
511 for (address, objs) in accounts_and_objs {
512 for obj in objs {
513 if res.len() >= max_txn_num {
514 return res;
515 }
516 let tx = Transaction::new_transfer_iota(
517 recipient,
518 address,
519 Some(2),
520 obj,
521 gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER,
522 gas_price,
523 );
524 let tx = context.sign_transaction(&tx);
525 res.push(tx);
526 }
527 }
528 res
529}
530
531pub async fn make_transfer_iota_transaction(
532 context: &WalletContext,
533 recipient: Option<Address>,
534 amount: Option<u64>,
535) -> TransactionEnvelope {
536 let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
537 let gas_price = context.get_reference_gas_price().await.unwrap();
538 context.sign_transaction(
539 &TestTransactionBuilder::new(sender, gas_object, gas_price)
540 .transfer_iota(amount, recipient.unwrap_or(sender))
541 .build(),
542 )
543}
544
545pub async fn make_staking_transaction(
546 context: &WalletContext,
547 validator_address: Address,
548) -> TransactionEnvelope {
549 let accounts_and_objs = context.get_all_accounts_and_gas_objects().await.unwrap();
550 let sender = accounts_and_objs[0].0;
551 let gas_object = accounts_and_objs[0].1[0];
552 let stake_object = accounts_and_objs[0].1[1];
553 let gas_price = context.get_reference_gas_price().await.unwrap();
554 context.sign_transaction(
555 &TestTransactionBuilder::new(sender, gas_object, gas_price)
556 .call_staking(stake_object, validator_address)
557 .build(),
558 )
559}
560
561pub async fn make_publish_transaction(
562 context: &WalletContext,
563 path: PathBuf,
564) -> TransactionEnvelope {
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 context.sign_transaction(
568 &TestTransactionBuilder::new(sender, gas_object, gas_price)
569 .publish(path)
570 .build(),
571 )
572}
573
574pub async fn make_publish_transaction_with_deps(
575 context: &WalletContext,
576 path: PathBuf,
577) -> TransactionEnvelope {
578 let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
579 let gas_price = context.get_reference_gas_price().await.unwrap();
580 context.sign_transaction(
581 &TestTransactionBuilder::new(sender, gas_object, gas_price)
582 .publish_with_deps(path)
583 .build(),
584 )
585}
586
587pub async fn publish_package(context: &WalletContext, path: PathBuf) -> ObjectReference {
588 let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
589 let gas_price = context.get_reference_gas_price().await.unwrap();
590 let txn = context.sign_transaction(
591 &TestTransactionBuilder::new(sender, gas_object, gas_price)
592 .publish(path)
593 .build(),
594 );
595 let resp = context.execute_transaction_must_succeed(txn).await;
596 get_new_package_obj_from_response(&resp).unwrap()
597}
598
599pub async fn publish_basics_package(context: &WalletContext) -> ObjectReference {
602 let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
603 let gas_price = context.get_reference_gas_price().await.unwrap();
604 let txn = context.sign_transaction(
605 &TestTransactionBuilder::new(sender, gas_object, gas_price)
606 .publish_examples("basics")
607 .build(),
608 );
609 let resp = context.execute_transaction_must_succeed(txn).await;
610 get_new_package_obj_from_response(&resp).unwrap()
611}
612
613pub async fn publish_basics_package_and_make_counter(
616 context: &WalletContext,
617) -> (ObjectReference, ObjectReference) {
618 let package_ref = publish_basics_package(context).await;
619 let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
620 let gas_price = context.get_reference_gas_price().await.unwrap();
621 let counter_creation_txn = context.sign_transaction(
622 &TestTransactionBuilder::new(sender, gas_object, gas_price)
623 .call_counter_create(package_ref.object_id)
624 .build(),
625 );
626 let resp = context
627 .execute_transaction_must_succeed(counter_creation_txn)
628 .await;
629 let counter_ref = resp
630 .effects
631 .unwrap()
632 .created()
633 .iter()
634 .find(|obj_ref| matches!(obj_ref.owner, Owner::Shared(_)))
635 .unwrap()
636 .reference;
637 (package_ref, counter_ref)
638}
639
640pub async fn increment_counter(
643 context: &WalletContext,
644 sender: Address,
645 gas_object_id: Option<ObjectId>,
646 package_id: ObjectId,
647 counter_id: ObjectId,
648 initial_shared_version: Version,
649) -> IotaTransactionBlockResponse {
650 let gas_object = if let Some(gas_object_id) = gas_object_id {
651 context.get_object_ref(gas_object_id).await.unwrap()
652 } else {
653 context
654 .get_one_gas_object_owned_by_address(sender)
655 .await
656 .unwrap()
657 .unwrap()
658 };
659 let rgp = context.get_reference_gas_price().await.unwrap();
660 let txn = context.sign_transaction(
661 &TestTransactionBuilder::new(sender, gas_object, rgp)
662 .call_counter_increment(package_id, counter_id, initial_shared_version)
663 .build(),
664 );
665 context.execute_transaction_must_succeed(txn).await
666}
667
668pub async fn emit_new_random_u128(
671 context: &WalletContext,
672 package_id: ObjectId,
673) -> IotaTransactionBlockResponse {
674 let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
675 let rgp = context.get_reference_gas_price().await.unwrap();
676 let random_call_arg = randomness_state_call_arg(context).await;
677
678 let txn = context.sign_transaction(
679 &TestTransactionBuilder::new(sender, gas_object, rgp)
680 .move_call(package_id, "random", "new", vec![random_call_arg])
681 .build(),
682 );
683 context.execute_transaction_must_succeed(txn).await
684}
685
686pub async fn randomness_state_call_arg(context: &WalletContext) -> CallArg {
688 let client = context.get_client().await.unwrap();
689 let random_obj = client
690 .read_api()
691 .get_object_with_options(
692 ObjectId::RANDOMNESS_STATE,
693 IotaObjectDataOptions::new().with_owner(),
694 )
695 .await
696 .unwrap()
697 .into_object()
698 .unwrap();
699 let random_obj_owner = random_obj
700 .owner
701 .expect("Expect Randomness object to have an owner");
702
703 let Owner::Shared(initial_shared_version) = random_obj_owner else {
704 panic!("Expect Randomness to be shared object")
705 };
706 CallArg::Shared(SharedObjectReference::new(
707 ObjectId::RANDOMNESS_STATE,
708 initial_shared_version,
709 false,
710 ))
711}
712
713pub async fn publish_example_package(
716 context: &WalletContext,
717 example_subpath: &'static str,
718 sender_key: &AccountPrivateKey,
719 sender: Address,
720 gas: ObjectReference,
721) -> (ObjectId, TransactionDigest) {
722 let gas_price = context.get_reference_gas_price().await.unwrap();
723 let tx = to_sender_signed_transaction(
724 TestTransactionBuilder::new(sender, gas, gas_price)
725 .publish_examples(example_subpath)
726 .build(),
727 sender_key,
728 );
729
730 let resp = context.execute_transaction_must_succeed(tx).await;
731 let package_id = get_new_package_obj_from_response(&resp).unwrap().object_id;
732 (package_id, resp.digest)
733}
734
735pub async fn publish_nfts_package(
738 context: &WalletContext,
739) -> (ObjectId, ObjectId, TransactionDigest) {
740 let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
741 let gas_id = gas_object.object_id;
742 let gas_price = context.get_reference_gas_price().await.unwrap();
743 let txn = context.sign_transaction(
744 &TestTransactionBuilder::new(sender, gas_object, gas_price)
745 .publish_examples("nft")
746 .build(),
747 );
748 let resp = context.execute_transaction_must_succeed(txn).await;
749 let package_id = get_new_package_obj_from_response(&resp).unwrap().object_id;
750 (package_id, gas_id, resp.digest)
751}
752
753pub async fn publish_simple_warrior_package(
756 context: &WalletContext,
757 sender_key: &AccountPrivateKey,
758 sender: Address,
759 gas: ObjectReference,
760) -> (ObjectId, TransactionDigest) {
761 publish_example_package(context, "simple_warrior", sender_key, sender, gas).await
762}
763
764pub async fn create_nft(
768 context: &WalletContext,
769 package_id: ObjectId,
770) -> (Address, ObjectId, TransactionDigest) {
771 let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap();
772 let rgp = context.get_reference_gas_price().await.unwrap();
773
774 let txn = context.sign_transaction(
775 &TestTransactionBuilder::new(sender, gas_object, rgp)
776 .call_nft_create(package_id)
777 .build(),
778 );
779 let resp = context.execute_transaction_must_succeed(txn).await;
780
781 let object_id = resp
782 .effects
783 .as_ref()
784 .unwrap()
785 .created()
786 .first()
787 .unwrap()
788 .reference
789 .object_id;
790
791 (sender, object_id, resp.digest)
792}
793
794pub async fn delete_nft(
796 context: &WalletContext,
797 sender: Address,
798 package_id: ObjectId,
799 nft_to_delete: ObjectReference,
800) -> IotaTransactionBlockResponse {
801 let gas = context
802 .get_one_gas_object_owned_by_address(sender)
803 .await
804 .unwrap()
805 .unwrap_or_else(|| panic!("Expect {sender} to have at least one gas object"));
806 let rgp = context.get_reference_gas_price().await.unwrap();
807 let txn = context.sign_transaction(
808 &TestTransactionBuilder::new(sender, gas, rgp)
809 .call_nft_delete(package_id, nft_to_delete)
810 .build(),
811 );
812 context.execute_transaction_must_succeed(txn).await
813}
814
815pub async fn select_gas_coin(
822 grpc_client: &iota_grpc_client::GrpcClient,
823 sender: Address,
824) -> ObjectId {
825 let gas_coin = grpc_client
826 .owned_objects(sender)
827 .object_type(StructTag::new_gas_coin())
828 .page_size(1)
829 .collect(Some(1))
830 .await
831 .expect("failed to fetch gas coin")
832 .into_inner()
833 .into_iter()
834 .next()
835 .expect("sender has no gas coin");
836 *gas_coin
837 .object_reference()
838 .expect("gas coin missing object reference")
839 .object_id()
840}
841
842pub async fn move_call_tx<A: PTBArgumentList>(
850 grpc_client: &iota_grpc_client::GrpcClient,
851 sender: Address,
852 package_id: ObjectId,
853 module: &str,
854 function: &str,
855 args: A,
856 gas_budget: u64,
857) -> Transaction {
858 let mut builder = grpc_client.transaction_builder(sender);
859
860 builder
861 .move_call(package_id, module, function)
862 .arguments(args);
863
864 builder.gas(vec![select_gas_coin(grpc_client, sender).await]);
865 builder.gas_budget(gas_budget);
866
867 builder
868 .finish()
869 .await
870 .expect("failed to construct move call transaction")
871}
872
873pub async fn split_coin_equal_tx(
879 grpc_client: &iota_grpc_client::GrpcClient,
880 sender: Address,
881 coin_to_split: ObjectId,
882 num_coins: u64,
883 gas_coin: Option<ObjectId>,
884 gas_budget: u64,
885) -> Transaction {
886 let coin_object = grpc_client
887 .objects([coin_to_split])
888 .await
889 .expect("failed to fetch coin")
890 .into_inner()
891 .into_iter()
892 .next()
893 .expect("no result for the requested coin")
894 .expect("coin not found")
895 .object()
896 .expect("invalid coin object");
897 let coin_balance = iota_sdk_types::Coin::try_from_object(&coin_object)
898 .expect("object is not a coin")
899 .balance();
900
901 let amount_per_split = coin_balance / num_coins;
904 let split_amounts: Vec<u64> = vec![amount_per_split; (num_coins - 1) as usize];
905
906 let mut builder = grpc_client.transaction_builder(sender);
907
908 let new_coin = builder.split_coins(coin_to_split, split_amounts).result();
912 builder.transfer_objects(sender, [new_coin]);
913
914 if let Some(gas) = gas_coin {
915 builder.gas([gas]);
916 }
917 builder.gas_budget(gas_budget);
918
919 builder
920 .finish()
921 .await
922 .expect("failed to construct split coin transaction")
923}
924
925#[cfg(test)]
926mod tests {
927 use iota_types::base_types::{dbg_addr, random_object_ref};
928
929 use super::*;
930
931 #[test]
932 fn ensure_unique_changes_digest() {
933 let sender = dbg_addr(1);
934 let recipient = dbg_addr(2);
935 let gas = random_object_ref();
936 let build =
937 || TestTransactionBuilder::new(sender, gas, 1000).transfer_iota(Some(1), recipient);
938
939 assert_eq!(build().build().digest(), build().build().digest());
941
942 let base = build().build();
943 let unique = build().ensure_unique().build();
944
945 assert_ne!(base.digest(), unique.digest());
948 match (base.kind(), unique.kind()) {
949 (TransactionKind::Programmable(base), TransactionKind::Programmable(unique)) => {
950 assert_eq!(unique.inputs.len(), base.inputs.len() + 1);
951 assert!(matches!(unique.inputs.last(), Some(Input::Pure(_))));
952 }
953 _ => panic!("expected programmable transactions"),
954 }
955
956 assert_ne!(
958 build().ensure_unique().build().digest(),
959 build().ensure_unique().build().digest()
960 );
961 }
962}