1use std::{
6 fmt,
7 fs::{self, File},
8 path::PathBuf,
9 sync::Arc,
10 time::{Duration, Instant},
11};
12
13use anyhow::{Result, anyhow, bail};
14use async_trait::async_trait;
15use dashmap::{DashMap, DashSet};
16use futures::future::join_all;
17use iota_json_rpc_types::{
18 IotaExecutionStatus, IotaObjectDataOptions, IotaTransactionBlockDataAPI,
19 IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponse,
20 IotaTransactionBlockResponseOptions,
21};
22use iota_sdk::{IotaClient, IotaClientBuilder};
23use iota_sdk_crypto::{ToFromBech32, simple::SimpleKeypair};
24use iota_sdk_types::{Address, ObjectId, ObjectReference, Transaction, TransactionDigest};
25use iota_types::{
26 crypto::{AccountPrivateKey, get_key_pair},
27 quorum_driver_types::ExecuteTransactionRequestType,
28 transaction::TransactionEnvelope,
29};
30use serde::{Serialize, de::DeserializeOwned};
31use tokio::{sync::RwLock, time::sleep};
32use tracing::{debug, info};
33
34use super::MultiGetTransactionBlocks;
35use crate::{
36 load_test::LoadTestConfig,
37 payload::{
38 Command, CommandData, DryRun, GetAllBalances, GetCheckpoints, GetObject, MultiGetObjects,
39 Payload, ProcessPayload, Processor, QueryTransactionBlocks, SignerInfo,
40 checkpoint_utils::get_latest_checkpoint_stats, validation::chunk_entities,
41 },
42};
43
44pub(crate) const DEFAULT_GAS_BUDGET: u64 = 500_000_000;
45pub(crate) const DEFAULT_LARGE_GAS_BUDGET: u64 = 50_000_000_000;
46pub(crate) const MAX_NUM_NEW_OBJECTS_IN_SINGLE_TRANSACTION: usize = 120;
47
48#[derive(Clone)]
49pub struct RpcCommandProcessor {
50 clients: Arc<RwLock<Vec<IotaClient>>>,
51 object_ref_cache: Arc<DashMap<ObjectId, ObjectReference>>,
53 transaction_digests: Arc<DashSet<TransactionDigest>>,
54 addresses: Arc<DashSet<Address>>,
55 data_dir: String,
56}
57
58impl RpcCommandProcessor {
59 pub async fn new(urls: &[String], data_dir: String) -> Self {
60 let clients = join_all(urls.iter().map(|url| async {
61 IotaClientBuilder::default()
62 .max_concurrent_requests(usize::MAX)
63 .request_timeout(Duration::from_secs(60))
64 .build(url.clone())
65 .await
66 .unwrap()
67 }))
68 .await;
69
70 Self {
71 clients: Arc::new(RwLock::new(clients)),
72 object_ref_cache: Arc::new(DashMap::new()),
73 transaction_digests: Arc::new(DashSet::new()),
74 addresses: Arc::new(DashSet::new()),
75 data_dir,
76 }
77 }
78
79 async fn process_command_data(
80 &self,
81 command: &CommandData,
82 signer_info: &Option<SignerInfo>,
83 ) -> Result<()> {
84 match command {
85 CommandData::DryRun(ref v) => self.process(v, signer_info).await,
86 CommandData::GetCheckpoints(ref v) => self.process(v, signer_info).await,
87 CommandData::PayIota(ref v) => self.process(v, signer_info).await,
88 CommandData::QueryTransactionBlocks(ref v) => self.process(v, signer_info).await,
89 CommandData::MultiGetTransactionBlocks(ref v) => self.process(v, signer_info).await,
90 CommandData::MultiGetObjects(ref v) => self.process(v, signer_info).await,
91 CommandData::GetObject(ref v) => self.process(v, signer_info).await,
92 CommandData::GetAllBalances(ref v) => self.process(v, signer_info).await,
93 CommandData::GetReferenceGasPrice(ref v) => self.process(v, signer_info).await,
94 }
95 }
96
97 pub(crate) async fn get_clients(&self) -> Result<Vec<IotaClient>> {
98 let read = self.clients.read().await;
99 Ok(read.clone())
100 }
101
102 pub(crate) async fn sign_and_execute(
104 &self,
105 client: &IotaClient,
106 keypair: &SimpleKeypair,
107 tx: Transaction,
108 request_type: ExecuteTransactionRequestType,
109 ) -> IotaTransactionBlockResponse {
110 let resp = sign_and_execute(client, keypair, tx, request_type).await;
111 let effects = resp.effects.as_ref().unwrap();
112 let object_ref_cache = self.object_ref_cache.clone();
113 for (owned_object_ref, _) in effects.all_changed_objects() {
115 let id = owned_object_ref.object_id();
116 let current = object_ref_cache.get_mut(&id);
117 match current {
118 Some(mut c) => {
119 if c.version < owned_object_ref.version() {
120 *c = owned_object_ref.reference;
121 }
122 }
123 None => {
124 object_ref_cache.insert(id, owned_object_ref.reference);
125 }
126 };
127 }
128 resp
129 }
130
131 pub(crate) async fn get_object_ref(
134 &self,
135 client: &IotaClient,
136 object_id: &ObjectId,
137 ) -> ObjectReference {
138 let object_ref_cache = self.object_ref_cache.clone();
139 let current = object_ref_cache.get_mut(object_id);
140 match current {
141 Some(c) => *c,
142 None => {
143 let resp = client
144 .read_api()
145 .get_object_with_options(*object_id, IotaObjectDataOptions::new())
146 .await
147 .unwrap_or_else(|_| panic!("unable to fetch object reference {object_id}"));
148 let object_ref = resp.object_ref_if_exists().unwrap_or_else(|| {
149 panic!("unable to extract object reference {object_id} from response {resp:?}")
150 });
151 object_ref_cache.insert(*object_id, object_ref);
152 object_ref
153 }
154 }
155 }
156
157 pub(crate) fn add_transaction_digests(&self, digests: Vec<TransactionDigest>) {
158 for digest in digests {
161 self.transaction_digests.insert(digest);
162 }
163 }
164
165 pub(crate) fn add_addresses_from_response(&self, responses: &[IotaTransactionBlockResponse]) {
166 for response in responses {
167 let transaction = &response.transaction;
168 if let Some(transaction) = transaction {
169 let data = &transaction.data;
170 self.addresses.insert(*data.sender());
171 }
172 }
173 }
174
175 pub(crate) fn add_object_ids_from_response(&self, responses: &[IotaTransactionBlockResponse]) {
176 for response in responses {
177 let effects = &response.effects;
178 if let Some(effects) = effects {
179 let all_changed_objects = effects.all_changed_objects();
180 for (object_ref, _) in all_changed_objects {
181 self.object_ref_cache
182 .insert(object_ref.object_id(), object_ref.reference);
183 }
184 }
185 }
186 }
187
188 pub(crate) fn dump_cache_to_file(&self) {
189 let digests: Vec<TransactionDigest> = self.transaction_digests.iter().map(|x| *x).collect();
191 if !digests.is_empty() {
192 debug!("dumping transaction digests to file {:?}", digests.len());
193 write_data_to_file(
194 &digests,
195 &format!("{}/{}", self.data_dir, CacheType::TransactionDigest),
196 )
197 .unwrap();
198 }
199
200 let addresses: Vec<Address> = self.addresses.iter().map(|x| *x).collect();
201 if !addresses.is_empty() {
202 debug!("dumping addresses to file {:?}", addresses.len());
203 write_data_to_file(
204 &addresses,
205 &format!("{}/{}", self.data_dir, CacheType::Address),
206 )
207 .unwrap();
208 }
209
210 let mut object_ids: Vec<ObjectId> = Vec::new();
211 let cloned_object_cache = self.object_ref_cache.clone();
212
213 for item in cloned_object_cache.iter() {
214 let object_id = item.key();
215 object_ids.push(*object_id);
216 }
217
218 if !object_ids.is_empty() {
219 debug!("dumping object_ids to file {:?}", object_ids.len());
220 write_data_to_file(
221 &object_ids,
222 &format!("{}/{}", self.data_dir, CacheType::ObjectId),
223 )
224 .unwrap();
225 }
226 }
227}
228
229#[async_trait]
230impl Processor for RpcCommandProcessor {
231 async fn apply(&self, payload: &Payload) -> Result<()> {
232 let commands = &payload.commands;
233 for command in commands.iter() {
234 let repeat_interval = command.repeat_interval;
235 let repeat_n_times = command.repeat_n_times;
236 for i in 0..=repeat_n_times {
237 let start_time = Instant::now();
238
239 self.process_command_data(&command.data, &payload.signer_info)
240 .await?;
241
242 let elapsed_time = start_time.elapsed();
243 if elapsed_time < repeat_interval {
244 let sleep_duration = repeat_interval - elapsed_time;
245 sleep(sleep_duration).await;
246 }
247 let clients = self.get_clients().await?;
248 let checkpoint_stats = get_latest_checkpoint_stats(&clients, None).await;
249 info!(
250 "Repeat {i}: Checkpoint stats {checkpoint_stats}, elapse {:.4} since last repeat",
251 elapsed_time.as_secs_f64()
252 );
253 }
254 }
255 Ok(())
256 }
257
258 async fn prepare(&self, config: &LoadTestConfig) -> Result<Vec<Payload>> {
259 let clients = self.get_clients().await?;
260 let Command {
261 repeat_n_times,
262 repeat_interval,
263 ..
264 } = &config.command;
265 let command_payloads = match &config.command.data {
266 CommandData::GetCheckpoints(data) => {
267 if !config.divide_tasks {
268 vec![config.command.clone(); config.num_threads]
269 } else {
270 divide_checkpoint_tasks(&clients, data, config.num_threads).await
271 }
272 }
273 CommandData::QueryTransactionBlocks(data) => {
274 if !config.divide_tasks {
275 vec![config.command.clone(); config.num_threads]
276 } else {
277 divide_query_transaction_blocks_tasks(data, config.num_threads).await
278 }
279 }
280 CommandData::MultiGetTransactionBlocks(data) => {
281 if !config.divide_tasks {
282 vec![config.command.clone(); config.num_threads]
283 } else {
284 divide_multi_get_transaction_blocks_tasks(data, config.num_threads).await
285 }
286 }
287 CommandData::GetAllBalances(data) => {
288 if !config.divide_tasks {
289 vec![config.command.clone(); config.num_threads]
290 } else {
291 divide_get_all_balances_tasks(data, config.num_threads).await
292 }
293 }
294 CommandData::MultiGetObjects(data) => {
295 if !config.divide_tasks {
296 vec![config.command.clone(); config.num_threads]
297 } else {
298 divide_multi_get_objects_tasks(data, config.num_threads).await
299 }
300 }
301 CommandData::GetObject(data) => {
302 if !config.divide_tasks {
303 vec![config.command.clone(); config.num_threads]
304 } else {
305 divide_get_object_tasks(data, config.num_threads).await
306 }
307 }
308 _ => vec![config.command.clone(); config.num_threads],
309 };
310
311 let command_payloads = command_payloads.into_iter().map(|command| {
312 command
313 .with_repeat_interval(*repeat_interval)
314 .with_repeat_n_times(*repeat_n_times)
315 });
316
317 let coins_and_keys = if let Some(signer_info) = &config.signer_info {
318 Some(
319 prepare_new_signer_and_coins(
320 clients.first().unwrap(),
321 signer_info,
322 config.num_threads * config.num_chunks_per_thread,
323 config.max_repeat as u64 + 1,
324 )
325 .await,
326 )
327 } else {
328 None
329 };
330
331 let num_chunks = config.num_chunks_per_thread;
332 Ok(command_payloads
333 .into_iter()
334 .enumerate()
335 .map(|(i, command)| Payload {
336 commands: vec![command], signer_info: coins_and_keys
338 .as_ref()
339 .map(|(coins, encoded_keypair)| SignerInfo {
340 encoded_keypair: encoded_keypair.clone(),
341 gas_payment: Some(coins[num_chunks * i..(i + 1) * num_chunks].to_vec()),
342 gas_budget: None,
343 }),
344 })
345 .collect())
346 }
347
348 fn dump_cache_to_file(&self, config: &LoadTestConfig) {
349 if let CommandData::GetCheckpoints(data) = &config.command.data {
350 if data.record {
351 self.dump_cache_to_file();
352 }
353 }
354 }
355}
356
357#[async_trait]
358impl<'a> ProcessPayload<'a, &'a DryRun> for RpcCommandProcessor {
359 async fn process(&'a self, _op: &'a DryRun, _signer_info: &Option<SignerInfo>) -> Result<()> {
360 debug!("DryRun");
361 Ok(())
362 }
363}
364
365fn write_data_to_file<T: Serialize>(data: &T, file_path: &str) -> Result<(), anyhow::Error> {
366 let mut path_buf = PathBuf::from(&file_path);
367 path_buf.pop();
368 fs::create_dir_all(&path_buf).map_err(|e| anyhow!("error creating directory: {e}"))?;
369
370 let file_name = format!("{file_path}.json");
371 let file = File::create(file_name).map_err(|e| anyhow!("error creating file: {e}"))?;
372 serde_json::to_writer(file, data).map_err(|e| anyhow!("error writing to file: {e}"))?;
373
374 Ok(())
375}
376
377pub enum CacheType {
378 Address,
379 TransactionDigest,
380 ObjectId,
381}
382
383impl fmt::Display for CacheType {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 match self {
386 CacheType::Address => write!(f, "IotaAddress"),
389 CacheType::TransactionDigest => write!(f, "TransactionDigest"),
390 CacheType::ObjectId => write!(f, "ObjectID"),
392 }
393 }
394}
395
396pub fn load_addresses_from_file(filepath: String) -> Vec<Address> {
399 let path = format!("{}/{}", filepath, CacheType::Address);
400 let addresses: Vec<Address> = read_data_from_file(&path).expect("failed to read addresses");
401 addresses
402}
403
404pub fn load_objects_from_file(filepath: String) -> Vec<ObjectId> {
405 let path = format!("{}/{}", filepath, CacheType::ObjectId);
406 let objects: Vec<ObjectId> = read_data_from_file(&path).expect("failed to read objects");
407 objects
408}
409
410pub fn load_digests_from_file(filepath: String) -> Vec<TransactionDigest> {
411 let path = format!("{}/{}", filepath, CacheType::TransactionDigest);
412 let digests: Vec<TransactionDigest> =
413 read_data_from_file(&path).expect("failed to read transaction digests");
414 digests
415}
416
417fn read_data_from_file<T: DeserializeOwned>(file_path: &str) -> Result<T, anyhow::Error> {
418 let mut path_buf = PathBuf::from(file_path);
419
420 if path_buf.extension().is_none_or(|ext| ext != "json") {
422 path_buf.set_extension("json");
424 }
425
426 let path = path_buf.as_path();
427 if !path.exists() {
428 bail!("file not found: {file_path}");
429 }
430
431 let file = File::open(path).map_err(|e| anyhow!("error opening file: {e}"))?;
432 let deserialized_data: T =
433 serde_json::from_reader(file).map_err(|e| anyhow!("deserialization error: {e}"))?;
434
435 Ok(deserialized_data)
436}
437
438async fn divide_checkpoint_tasks(
439 clients: &[IotaClient],
440 data: &GetCheckpoints,
441 num_chunks: usize,
442) -> Vec<Command> {
443 let start = data.start;
444 let end = match data.end {
445 Some(end) => end,
446 None => {
447 let end_checkpoints = join_all(clients.iter().map(|client| async {
448 client
449 .read_api()
450 .get_latest_checkpoint_sequence_number()
451 .await
452 .expect("get_latest_checkpoint_sequence_number should not fail")
453 }))
454 .await;
455 *end_checkpoints
456 .iter()
457 .max()
458 .expect("get_latest_checkpoint_sequence_number should not return empty")
459 }
460 };
461
462 let chunk_size = (end - start) / num_chunks as u64;
463 (0..num_chunks)
464 .map(|i| {
465 let start_checkpoint = start + (i as u64) * chunk_size;
466 let end_checkpoint = end.min(start + ((i + 1) as u64) * chunk_size);
467 Command::new_get_checkpoints(
468 start_checkpoint,
469 Some(end_checkpoint),
470 data.verify_transactions,
471 data.verify_objects,
472 data.record,
473 )
474 })
475 .collect()
476}
477
478async fn divide_query_transaction_blocks_tasks(
479 data: &QueryTransactionBlocks,
480 num_chunks: usize,
481) -> Vec<Command> {
482 let chunk_size = if data.addresses.len() < num_chunks {
483 1
484 } else {
485 data.addresses.len() as u64 / num_chunks as u64
486 };
487 let chunked = chunk_entities(data.addresses.as_slice(), Some(chunk_size as usize));
488 chunked
489 .into_iter()
490 .map(|chunk| Command::new_query_transaction_blocks(data.address_type.clone(), chunk))
491 .collect()
492}
493
494async fn divide_multi_get_transaction_blocks_tasks(
495 data: &MultiGetTransactionBlocks,
496 num_chunks: usize,
497) -> Vec<Command> {
498 let chunk_size = if data.digests.len() < num_chunks {
499 1
500 } else {
501 data.digests.len() as u64 / num_chunks as u64
502 };
503 let chunked = chunk_entities(data.digests.as_slice(), Some(chunk_size as usize));
504 chunked
505 .into_iter()
506 .map(Command::new_multi_get_transaction_blocks)
507 .collect()
508}
509
510async fn divide_get_all_balances_tasks(data: &GetAllBalances, num_threads: usize) -> Vec<Command> {
511 let per_thread_size = if data.addresses.len() < num_threads {
512 1
513 } else {
514 data.addresses.len() / num_threads
515 };
516
517 let chunked = chunk_entities(data.addresses.as_slice(), Some(per_thread_size));
518 chunked
519 .into_iter()
520 .map(|chunk| Command::new_get_all_balances(chunk, data.chunk_size))
521 .collect()
522}
523
524async fn divide_multi_get_objects_tasks(data: &MultiGetObjects, num_chunks: usize) -> Vec<Command> {
526 let chunk_size = if data.object_ids.len() < num_chunks {
527 1
528 } else {
529 data.object_ids.len() as u64 / num_chunks as u64
530 };
531 let chunked = chunk_entities(data.object_ids.as_slice(), Some(chunk_size as usize));
532 chunked
533 .into_iter()
534 .map(Command::new_multi_get_objects)
535 .collect()
536}
537
538async fn divide_get_object_tasks(data: &GetObject, num_threads: usize) -> Vec<Command> {
539 let per_thread_size = if data.object_ids.len() < num_threads {
540 1
541 } else {
542 data.object_ids.len() / num_threads
543 };
544
545 let chunked = chunk_entities(data.object_ids.as_slice(), Some(per_thread_size));
546 chunked
547 .into_iter()
548 .map(|chunk| Command::new_get_object(chunk, data.chunk_size))
549 .collect()
550}
551
552async fn prepare_new_signer_and_coins(
553 client: &IotaClient,
554 signer_info: &SignerInfo,
555 num_coins: usize,
556 num_transactions_per_coin: u64,
557) -> (Vec<ObjectId>, String) {
558 let amount_per_coin = num_transactions_per_coin * DEFAULT_GAS_BUDGET;
560 let pay_amount = amount_per_coin * num_coins as u64;
561 let num_split_txns =
562 num_transactions_needed(num_coins, MAX_NUM_NEW_OBJECTS_IN_SINGLE_TRANSACTION);
563 let (gas_fee_for_split, gas_fee_for_pay_iota) = (
564 DEFAULT_LARGE_GAS_BUDGET * num_split_txns as u64,
565 DEFAULT_GAS_BUDGET,
566 );
567
568 let primary_keypair = SimpleKeypair::from_bech32(&signer_info.encoded_keypair)
569 .expect("decoding keypair should not fail");
570 let sender = primary_keypair.public_key().derive_address();
571 let (coin, balance) = get_coin_with_max_balance(client, sender).await;
572 let required_balance = pay_amount + gas_fee_for_split + gas_fee_for_pay_iota;
576 if required_balance > balance {
577 panic!(
578 "current balance {balance} is smaller than require amount of NANOS to fund the operation {required_balance}"
579 );
580 }
581
582 let split_amounts = calculate_split_amounts(
585 num_coins,
586 amount_per_coin,
587 MAX_NUM_NEW_OBJECTS_IN_SINGLE_TRANSACTION,
588 );
589
590 debug!("split_amounts {split_amounts:?}");
591
592 let (burner_address, burner_key): (_, AccountPrivateKey) = get_key_pair();
597 let burner_keypair = SimpleKeypair::from(burner_key);
598 let pay_amounts = split_amounts
599 .iter()
600 .map(|(amount, _)| *amount)
601 .chain(std::iter::once(gas_fee_for_split))
602 .collect::<Vec<_>>();
603
604 debug!("pay_amounts {pay_amounts:?}");
605
606 pay_iota(
607 client,
608 &primary_keypair,
609 vec![coin],
610 DEFAULT_GAS_BUDGET,
611 vec![burner_address; pay_amounts.len()],
612 pay_amounts,
613 )
614 .await;
615
616 let coins = get_iota_coin_ids(client, burner_address).await;
617 let gas_coin_id = get_coin_with_balance(&coins, gas_fee_for_split);
618 let primary_coin = get_coin_with_balance(&coins, split_amounts[0].0);
619 assert!(!coins.is_empty());
620 let mut results: Vec<ObjectId> = vec![];
621 assert!(!split_amounts.is_empty());
622 if split_amounts.len() == 1 && split_amounts[0].1 == 0 {
623 results.push(get_coin_with_balance(&coins, split_amounts[0].0));
624 } else if split_amounts.len() == 1 {
625 results.extend(
626 split_coins(
627 client,
628 &burner_keypair,
629 primary_coin,
630 gas_coin_id,
631 split_amounts[0].1 as u64,
632 )
633 .await,
634 );
635 } else {
636 let (max_amount, max_split) = &split_amounts[0];
637 let (remainder_amount, remainder_split) = split_amounts.last().unwrap();
638 let primary_coins = coins
639 .iter()
640 .filter(|(_, balance)| balance == max_amount)
641 .map(|(id, _)| (*id, *max_split as u64))
642 .chain(
643 coins
644 .iter()
645 .filter(|(_, balance)| balance == remainder_amount)
646 .map(|(id, _)| (*id, *remainder_split as u64)),
647 )
648 .collect::<Vec<_>>();
649
650 for (coin_id, splits) in primary_coins {
651 results
652 .extend(split_coins(client, &burner_keypair, coin_id, gas_coin_id, splits).await);
653 }
654 }
655 assert_eq!(results.len(), num_coins);
656 debug!("Split off {} coins for gas payment {results:?}", num_coins);
657 (
658 results,
659 burner_keypair
660 .to_bech32()
661 .expect("encoding keypair should not fail"),
662 )
663}
664
665fn num_transactions_needed(num_coins: usize, new_coins_per_txn: usize) -> usize {
668 assert!(new_coins_per_txn > 0);
669 if num_coins == 1 {
670 return 0;
671 }
672 num_coins.div_ceil(new_coins_per_txn)
673}
674
675fn calculate_split_amounts(
679 num_coins: usize,
680 amount_per_coin: u64,
681 max_coins_per_txn: usize,
682) -> Vec<(u64, usize)> {
683 let total_amount = amount_per_coin * num_coins as u64;
684 let num_transactions = num_transactions_needed(num_coins, max_coins_per_txn);
685
686 if num_transactions == 0 {
687 return vec![(total_amount, 0)];
688 }
689
690 let amount_per_transaction = max_coins_per_txn as u64 * amount_per_coin;
691 let remaining_amount = total_amount - amount_per_transaction * (num_transactions as u64 - 1);
692 let mut split_amounts: Vec<(u64, usize)> =
693 vec![(amount_per_transaction, max_coins_per_txn); num_transactions - 1];
694 split_amounts.push((
695 remaining_amount,
696 num_coins - max_coins_per_txn * (num_transactions - 1),
697 ));
698 split_amounts
699}
700
701async fn get_coin_with_max_balance(client: &IotaClient, address: Address) -> (ObjectId, u64) {
702 let coins = get_iota_coin_ids(client, address).await;
703 assert!(!coins.is_empty());
704 coins.into_iter().max_by(|a, b| a.1.cmp(&b.1)).unwrap()
705}
706
707fn get_coin_with_balance(coins: &[(ObjectId, u64)], target: u64) -> ObjectId {
708 coins.iter().find(|(_, b)| b == &target).unwrap().0
709}
710
711async fn get_iota_coin_ids(client: &IotaClient, address: Address) -> Vec<(ObjectId, u64)> {
713 match client
714 .coin_read_api()
715 .get_coins(address, None, None, None)
716 .await
717 {
718 Ok(page) => page
719 .data
720 .into_iter()
721 .map(|c| (c.coin_object_id, c.balance))
722 .collect::<Vec<_>>(),
723 Err(e) => {
724 panic!("get_iota_coin_ids error for address {address} {e}")
725 }
726 }
727 }
729
730async fn pay_iota(
731 client: &IotaClient,
732 keypair: &SimpleKeypair,
733 input_coins: Vec<ObjectId>,
734 gas_budget: u64,
735 recipients: Vec<Address>,
736 amounts: Vec<u64>,
737) -> IotaTransactionBlockResponse {
738 let sender = keypair.public_key().derive_address();
739 let tx = client
740 .transaction_builder()
741 .pay(sender, input_coins, recipients, amounts, None, gas_budget)
742 .await
743 .expect("failed to construct pay iota transaction");
744 sign_and_execute(
745 client,
746 keypair,
747 tx,
748 ExecuteTransactionRequestType::WaitForLocalExecution,
749 )
750 .await
751}
752
753async fn split_coins(
754 client: &IotaClient,
755 keypair: &SimpleKeypair,
756 coin_to_split: ObjectId,
757 gas_payment: ObjectId,
758 num_coins: u64,
759) -> Vec<ObjectId> {
760 let sender = keypair.public_key().derive_address();
761 let split_coin_tx = client
762 .transaction_builder()
763 .split_coin_equal(
764 sender,
765 coin_to_split,
766 num_coins,
767 Some(gas_payment),
768 DEFAULT_LARGE_GAS_BUDGET,
769 )
770 .await
771 .expect("failed to construct split coin transaction");
772 sign_and_execute(
773 client,
774 keypair,
775 split_coin_tx,
776 ExecuteTransactionRequestType::WaitForLocalExecution,
777 )
778 .await
779 .effects
780 .unwrap()
781 .created()
782 .iter()
783 .map(|owned_object_ref| owned_object_ref.reference.object_id)
784 .chain(std::iter::once(coin_to_split))
785 .collect::<Vec<_>>()
786}
787
788pub(crate) async fn sign_and_execute(
789 client: &IotaClient,
790 keypair: &SimpleKeypair,
791 tx: Transaction,
792 request_type: ExecuteTransactionRequestType,
793) -> IotaTransactionBlockResponse {
794 let transaction_response = match client
795 .quorum_driver_api()
796 .execute_transaction_block(
797 TransactionEnvelope::from_data_and_signer(tx, vec![keypair]),
798 IotaTransactionBlockResponseOptions::new().with_effects(),
799 Some(request_type),
800 )
801 .await
802 {
803 Ok(response) => response,
804 Err(e) => {
805 panic!("sign_and_execute error {e}")
806 }
807 };
808
809 match &transaction_response.effects {
810 Some(effects) => {
811 if let IotaExecutionStatus::Failure { error } = effects.status() {
812 panic!(
813 "transaction {} failed with error: {}. Transaction Response: {:?}",
814 transaction_response.digest, error, transaction_response
815 );
816 }
817 }
818 None => {
819 panic!(
820 "transaction {} has no effects. Response {:?}",
821 transaction_response.digest, transaction_response
822 );
823 }
824 };
825 transaction_response
826}
827
828#[cfg(test)]
829mod tests {
830 use std::{assert_eq, vec};
831
832 use super::*;
833
834 #[test]
835 fn test_calculate_split_amounts_no_split_needed() {
836 let num_coins = 10;
837 let amount_per_coin = 100;
838 let max_coins_per_txn = 20;
839 let expected = vec![(1000, 10)];
840 let result = calculate_split_amounts(num_coins, amount_per_coin, max_coins_per_txn);
841
842 assert_eq!(expected, result);
843 }
844
845 #[test]
846 fn test_calculate_split_amounts_exact_split() {
847 let num_coins = 10;
848 let amount_per_coin = 100;
849 let max_coins_per_txn = 5;
850 let expected = vec![(500, 5), (500, 5)];
851 let result = calculate_split_amounts(num_coins, amount_per_coin, max_coins_per_txn);
852
853 assert_eq!(expected, result);
854 }
855
856 #[test]
857 fn test_calculate_split_amounts_with_remainder() {
858 let num_coins = 12;
859 let amount_per_coin = 100;
860 let max_coins_per_txn = 5;
861 let expected = vec![(500, 5), (500, 5), (200, 2)];
862 let result = calculate_split_amounts(num_coins, amount_per_coin, max_coins_per_txn);
863
864 assert_eq!(expected, result);
865 }
866
867 #[test]
868 fn test_calculate_split_amounts_single_coin() {
869 let num_coins = 1;
870 let amount_per_coin = 100;
871 let max_coins_per_txn = 5;
872 let expected = vec![(100, 0)];
873 let result = calculate_split_amounts(num_coins, amount_per_coin, max_coins_per_txn);
874
875 assert_eq!(expected, result);
876 }
877
878 #[test]
879 fn test_calculate_split_amounts_max_coins_equals_num_coins() {
880 let num_coins = 5;
881 let amount_per_coin = 100;
882 let max_coins_per_txn = 5;
883 let expected = vec![(500, 5)];
884 let result = calculate_split_amounts(num_coins, amount_per_coin, max_coins_per_txn);
885
886 assert_eq!(expected, result);
887 }
888
889 #[test]
890 #[should_panic(expected = "assertion failed: new_coins_per_txn > 0")]
891 fn test_calculate_split_amounts_zero_max_coins() {
892 let num_coins = 5;
893 let amount_per_coin = 100;
894 let max_coins_per_txn = 0;
895
896 calculate_split_amounts(num_coins, amount_per_coin, max_coins_per_txn);
897 }
898}