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_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 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()), CallArg::Pure(bcs::to_bytes(&0u64).unwrap()), ],
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 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
438pub 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
543pub 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
557pub 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
585pub 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
613pub 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
657pub 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
675pub 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
705pub 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}