1pub mod package;
6pub mod stake;
7pub mod utils;
8
9use std::{result::Result, str::FromStr, sync::Arc};
10
11use anyhow::{Ok, anyhow, bail};
12use async_trait::async_trait;
13use iota_json::IotaJsonValue;
14use iota_json_rpc_types::{
15 IotaObjectDataOptions, IotaObjectResponse, IotaTypeTag, RPCTransactionRequestParams,
16};
17use iota_types::{
18 IOTA_FRAMEWORK_PACKAGE_ID,
19 base_types::{IotaAddress, ObjectID, ObjectInfo},
20 coin,
21 error::UserInputError,
22 fp_ensure,
23 object::Object,
24 programmable_transaction_builder::ProgrammableTransactionBuilder,
25 transaction::{CallArg, Command, InputObjectKind, ObjectArg, TransactionData, TransactionKind},
26};
27use move_core_types::{identifier::Identifier, language_storage::StructTag};
28
29#[async_trait]
30pub trait DataReader {
31 async fn get_owned_objects(
32 &self,
33 address: IotaAddress,
34 object_type: StructTag,
35 ) -> Result<Vec<ObjectInfo>, anyhow::Error>;
36
37 async fn get_object_with_options(
38 &self,
39 object_id: ObjectID,
40 options: IotaObjectDataOptions,
41 ) -> Result<IotaObjectResponse, anyhow::Error>;
42
43 async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error>;
44}
45
46#[derive(Clone)]
47pub struct TransactionBuilder(Arc<dyn DataReader + Sync + Send>);
48
49impl TransactionBuilder {
50 pub fn new(data_reader: Arc<dyn DataReader + Sync + Send>) -> Self {
51 Self(data_reader)
52 }
53
54 pub async fn tx_data_for_dry_run(
56 &self,
57 sender: IotaAddress,
58 kind: TransactionKind,
59 gas_budget: u64,
60 gas_price: u64,
61 gas_payment: impl Into<Option<Vec<ObjectID>>>,
62 gas_sponsor: impl Into<Option<IotaAddress>>,
63 ) -> TransactionData {
64 let gas_payment = self
65 .input_refs(gas_payment.into().unwrap_or_default().as_ref())
66 .await
67 .unwrap_or_default();
68 let gas_sponsor = gas_sponsor.into().unwrap_or(sender);
69 TransactionData::new_with_gas_coins_allow_sponsor(
70 kind,
71 sender,
72 gas_payment,
73 gas_budget,
74 gas_price,
75 gas_sponsor,
76 )
77 }
78
79 pub async fn tx_data(
84 &self,
85 sender: IotaAddress,
86 kind: TransactionKind,
87 gas_budget: u64,
88 gas_price: u64,
89 gas_payment: Vec<ObjectID>,
90 gas_sponsor: impl Into<Option<IotaAddress>>,
91 ) -> Result<TransactionData, anyhow::Error> {
92 let gas_payment = if gas_payment.is_empty() {
93 let input_objs = kind
94 .input_objects()?
95 .iter()
96 .flat_map(|obj| match obj {
97 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
98 _ => None,
99 })
100 .collect();
101 vec![
102 self.select_gas(sender, None, gas_budget, input_objs, gas_price)
103 .await?,
104 ]
105 } else {
106 self.input_refs(&gas_payment).await?
107 };
108 Ok(TransactionData::new_with_gas_coins_allow_sponsor(
109 kind,
110 sender,
111 gas_payment,
112 gas_budget,
113 gas_price,
114 gas_sponsor.into().unwrap_or(sender),
115 ))
116 }
117
118 pub async fn transfer_object_tx_kind(
121 &self,
122 object_id: ObjectID,
123 recipient: IotaAddress,
124 ) -> Result<TransactionKind, anyhow::Error> {
125 let obj_ref = self.get_object_ref(object_id).await?;
126 let mut builder = ProgrammableTransactionBuilder::new();
127 builder.transfer_object(recipient, obj_ref)?;
128 Ok(TransactionKind::programmable(builder.finish()))
129 }
130
131 pub async fn transfer_object(
133 &self,
134 signer: IotaAddress,
135 object_id: ObjectID,
136 gas: impl Into<Option<ObjectID>>,
137 gas_budget: u64,
138 recipient: IotaAddress,
139 ) -> anyhow::Result<TransactionData> {
140 let mut builder = ProgrammableTransactionBuilder::new();
141 self.single_transfer_object(&mut builder, object_id, recipient)
142 .await?;
143 let gas_price = self.0.get_reference_gas_price().await?;
144 let gas = self
145 .select_gas(signer, gas, gas_budget, vec![object_id], gas_price)
146 .await?;
147
148 Ok(TransactionData::new(
149 TransactionKind::programmable(builder.finish()),
150 signer,
151 gas,
152 gas_budget,
153 gas_price,
154 ))
155 }
156
157 async fn single_transfer_object(
160 &self,
161 builder: &mut ProgrammableTransactionBuilder,
162 object_id: ObjectID,
163 recipient: IotaAddress,
164 ) -> anyhow::Result<()> {
165 builder.transfer_object(recipient, self.get_object_ref(object_id).await?)?;
166 Ok(())
167 }
168
169 pub fn transfer_iota_tx_kind(
174 &self,
175 recipient: IotaAddress,
176 amount: impl Into<Option<u64>>,
177 ) -> TransactionKind {
178 let mut builder = ProgrammableTransactionBuilder::new();
179 builder.transfer_iota(recipient, amount.into());
180 let pt = builder.finish();
181 TransactionKind::programmable(pt)
182 }
183
184 pub async fn transfer_iota(
187 &self,
188 signer: IotaAddress,
189 iota_object_id: ObjectID,
190 gas_budget: u64,
191 recipient: IotaAddress,
192 amount: impl Into<Option<u64>>,
193 ) -> anyhow::Result<TransactionData> {
194 let object = self.get_object_ref(iota_object_id).await?;
195 let gas_price = self.0.get_reference_gas_price().await?;
196 Ok(TransactionData::new_transfer_iota(
197 recipient,
198 signer,
199 amount.into(),
200 object,
201 gas_budget,
202 gas_price,
203 ))
204 }
205
206 pub async fn pay_tx_kind(
212 &self,
213 input_coins: Vec<ObjectID>,
214 recipients: Vec<IotaAddress>,
215 amounts: Vec<u64>,
216 ) -> Result<TransactionKind, anyhow::Error> {
217 let mut builder = ProgrammableTransactionBuilder::new();
218 let coins = self.input_refs(&input_coins).await?;
219 builder.pay(coins, recipients, amounts)?;
220 let pt = builder.finish();
221 Ok(TransactionKind::programmable(pt))
222 }
223
224 pub async fn pay(
232 &self,
233 signer: IotaAddress,
234 input_coins: Vec<ObjectID>,
235 recipients: Vec<IotaAddress>,
236 amounts: Vec<u64>,
237 gas: impl Into<Option<ObjectID>>,
238 gas_budget: u64,
239 ) -> anyhow::Result<TransactionData> {
240 let gas = gas.into();
241
242 if let Some(gas) = gas {
243 if input_coins.contains(&gas) {
244 return Err(anyhow!(
245 "Gas coin is in input coins of Pay transaction, use PayIota transaction instead!"
246 ));
247 }
248 }
249
250 let coin_refs = self.input_refs(&input_coins).await?;
251 let gas_price = self.0.get_reference_gas_price().await?;
252 let gas = self
253 .select_gas(signer, gas, gas_budget, input_coins, gas_price)
254 .await?;
255
256 TransactionData::new_pay(
257 signer, coin_refs, recipients, amounts, gas, gas_budget, gas_price,
258 )
259 }
260
261 pub fn pay_iota_tx_kind(
267 &self,
268 recipients: Vec<IotaAddress>,
269 amounts: Vec<u64>,
270 ) -> Result<TransactionKind, anyhow::Error> {
271 let mut builder = ProgrammableTransactionBuilder::new();
272 builder.pay_iota(recipients.clone(), amounts.clone())?;
273 let pt = builder.finish();
274 let tx_kind = TransactionKind::programmable(pt);
275 Ok(tx_kind)
276 }
277
278 pub async fn pay_iota(
288 &self,
289 signer: IotaAddress,
290 input_coins: Vec<ObjectID>,
291 recipients: Vec<IotaAddress>,
292 amounts: Vec<u64>,
293 gas_budget: u64,
294 ) -> anyhow::Result<TransactionData> {
295 fp_ensure!(
296 !input_coins.is_empty(),
297 UserInputError::EmptyInputCoins.into()
298 );
299
300 let mut coin_refs = self.input_refs(&input_coins).await?;
301 let gas_object_ref = coin_refs.remove(0);
304 let gas_price = self.0.get_reference_gas_price().await?;
305 TransactionData::new_pay_iota(
306 signer,
307 coin_refs,
308 recipients,
309 amounts,
310 gas_object_ref,
311 gas_budget,
312 gas_price,
313 )
314 }
315
316 pub fn pay_all_iota_tx_kind(&self, recipient: IotaAddress) -> TransactionKind {
319 let mut builder = ProgrammableTransactionBuilder::new();
320 builder.pay_all_iota(recipient);
321 let pt = builder.finish();
322 TransactionKind::programmable(pt)
323 }
324
325 pub async fn pay_all_iota(
336 &self,
337 signer: IotaAddress,
338 input_coins: Vec<ObjectID>,
339 recipient: IotaAddress,
340 gas_budget: u64,
341 ) -> anyhow::Result<TransactionData> {
342 fp_ensure!(
343 !input_coins.is_empty(),
344 UserInputError::EmptyInputCoins.into()
345 );
346
347 let mut coin_refs = self.input_refs(&input_coins).await?;
348 let gas_object_ref = coin_refs.remove(0);
351 let gas_price = self.0.get_reference_gas_price().await?;
352 Ok(TransactionData::new_pay_all_iota(
353 signer,
354 coin_refs,
355 recipient,
356 gas_object_ref,
357 gas_budget,
358 gas_price,
359 ))
360 }
361
362 pub async fn move_call_tx_kind(
365 &self,
366 package_object_id: ObjectID,
367 module: &str,
368 function: &str,
369 type_args: Vec<IotaTypeTag>,
370 call_args: Vec<IotaJsonValue>,
371 ) -> Result<TransactionKind, anyhow::Error> {
372 let mut builder = ProgrammableTransactionBuilder::new();
373 self.single_move_call(
374 &mut builder,
375 package_object_id,
376 module,
377 function,
378 type_args,
379 call_args,
380 )
381 .await?;
382 let pt = builder.finish();
383 Ok(TransactionKind::programmable(pt))
384 }
385
386 pub async fn move_call(
388 &self,
389 signer: IotaAddress,
390 package_object_id: ObjectID,
391 module: &str,
392 function: &str,
393 type_args: Vec<IotaTypeTag>,
394 call_args: Vec<IotaJsonValue>,
395 gas: impl Into<Option<ObjectID>>,
396 gas_budget: u64,
397 gas_price: impl Into<Option<u64>>,
398 ) -> anyhow::Result<TransactionData> {
399 let gas_price = gas_price.into();
400
401 let mut builder = ProgrammableTransactionBuilder::new();
402 self.single_move_call(
403 &mut builder,
404 package_object_id,
405 module,
406 function,
407 type_args,
408 call_args,
409 )
410 .await?;
411 let pt = builder.finish();
412 let input_objects = pt
413 .input_objects()?
414 .iter()
415 .flat_map(|obj| match obj {
416 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
417 _ => None,
418 })
419 .collect();
420 let gas_price = if let Some(gas_price) = gas_price {
421 gas_price
422 } else {
423 self.0.get_reference_gas_price().await?
424 };
425 let gas = self
426 .select_gas(signer, gas, gas_budget, input_objects, gas_price)
427 .await?;
428
429 Ok(TransactionData::new(
430 TransactionKind::programmable(pt),
431 signer,
432 gas,
433 gas_budget,
434 gas_price,
435 ))
436 }
437
438 pub async fn single_move_call(
441 &self,
442 builder: &mut ProgrammableTransactionBuilder,
443 package: ObjectID,
444 module: &str,
445 function: &str,
446 type_args: Vec<IotaTypeTag>,
447 call_args: Vec<IotaJsonValue>,
448 ) -> anyhow::Result<()> {
449 let module = Identifier::from_str(module)?;
450 let function = Identifier::from_str(function)?;
451
452 let type_args = type_args
453 .into_iter()
454 .map(|ty| ty.try_into())
455 .collect::<Result<Vec<_>, _>>()?;
456
457 let call_args = self
458 .resolve_and_checks_json_args(
459 builder, package, &module, &function, &type_args, call_args,
460 )
461 .await?;
462
463 builder.command(Command::move_call(
464 package, module, function, type_args, call_args,
465 ));
466 Ok(())
467 }
468
469 pub async fn split_coin_tx_kind(
473 &self,
474 coin_object_id: ObjectID,
475 split_amounts: impl Into<Option<Vec<u64>>>,
476 split_count: impl Into<Option<u64>>,
477 ) -> Result<TransactionKind, anyhow::Error> {
478 let split_amounts = split_amounts.into();
479 let split_count = split_count.into();
480
481 if split_amounts.is_none() && split_count.is_none() {
482 bail!(
483 "Either split_amounts or split_count must be provided for split_coin transaction."
484 );
485 }
486 let coin = self
487 .0
488 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
489 .await?
490 .into_object()?;
491 let coin_object_ref = coin.object_ref();
492 let coin: Object = coin.try_into()?;
493 let type_args = vec![coin.get_move_template_type()?];
494 let package = IOTA_FRAMEWORK_PACKAGE_ID;
495 let module = coin::PAY_MODULE_NAME.to_owned();
496
497 let (arguments, function) = if let Some(split_amounts) = split_amounts {
498 (
499 vec![
500 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
501 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
502 ],
503 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
504 )
505 } else {
506 (
507 vec![
508 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
509 CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
510 ],
511 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
512 )
513 };
514 let mut builder = ProgrammableTransactionBuilder::new();
515 builder.move_call(package, module, function, type_args, arguments)?;
516 let pt = builder.finish();
517 let tx_kind = TransactionKind::programmable(pt);
518 Ok(tx_kind)
519 }
520
521 pub async fn split_coin(
523 &self,
524 signer: IotaAddress,
525 coin_object_id: ObjectID,
526 split_amounts: Vec<u64>,
527 gas: impl Into<Option<ObjectID>>,
528 gas_budget: u64,
529 ) -> anyhow::Result<TransactionData> {
530 let coin = self
531 .0
532 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
533 .await?
534 .into_object()?;
535 let coin_object_ref = coin.object_ref();
536 let coin: Object = coin.try_into()?;
537 let type_args = vec![coin.get_move_template_type()?];
538 let gas_price = self.0.get_reference_gas_price().await?;
539 let gas = self
540 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
541 .await?;
542
543 TransactionData::new_move_call(
544 signer,
545 IOTA_FRAMEWORK_PACKAGE_ID,
546 coin::PAY_MODULE_NAME.to_owned(),
547 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
548 type_args,
549 gas,
550 vec![
551 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
552 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
553 ],
554 gas_budget,
555 gas_price,
556 )
557 }
558
559 pub async fn split_coin_equal(
561 &self,
562 signer: IotaAddress,
563 coin_object_id: ObjectID,
564 split_count: u64,
565 gas: impl Into<Option<ObjectID>>,
566 gas_budget: u64,
567 ) -> anyhow::Result<TransactionData> {
568 let coin = self
569 .0
570 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
571 .await?
572 .into_object()?;
573 let coin_object_ref = coin.object_ref();
574 let coin: Object = coin.try_into()?;
575 let type_args = vec![coin.get_move_template_type()?];
576 let gas_price = self.0.get_reference_gas_price().await?;
577 let gas = self
578 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
579 .await?;
580
581 TransactionData::new_move_call(
582 signer,
583 IOTA_FRAMEWORK_PACKAGE_ID,
584 coin::PAY_MODULE_NAME.to_owned(),
585 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
586 type_args,
587 gas,
588 vec![
589 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
590 CallArg::Pure(bcs::to_bytes(&split_count)?),
591 ],
592 gas_budget,
593 gas_price,
594 )
595 }
596
597 pub async fn merge_coins_tx_kind(
600 &self,
601 primary_coin: ObjectID,
602 coin_to_merge: ObjectID,
603 ) -> Result<TransactionKind, anyhow::Error> {
604 let coin = self
605 .0
606 .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
607 .await?
608 .into_object()?;
609 let primary_coin_ref = coin.object_ref();
610 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
611 let coin: Object = coin.try_into()?;
612 let type_arguments = vec![coin.get_move_template_type()?];
613 let package = IOTA_FRAMEWORK_PACKAGE_ID;
614 let module = coin::COIN_MODULE_NAME.to_owned();
615 let function = coin::COIN_JOIN_FUNC_NAME.to_owned();
616 let arguments = vec![
617 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
618 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
619 ];
620 let pt = {
621 let mut builder = ProgrammableTransactionBuilder::new();
622 builder.move_call(package, module, function, type_arguments, arguments)?;
623 builder.finish()
624 };
625 let tx_kind = TransactionKind::programmable(pt);
626 Ok(tx_kind)
627 }
628
629 pub async fn merge_coins(
631 &self,
632 signer: IotaAddress,
633 primary_coin: ObjectID,
634 coin_to_merge: ObjectID,
635 gas: impl Into<Option<ObjectID>>,
636 gas_budget: u64,
637 ) -> anyhow::Result<TransactionData> {
638 let coin = self
639 .0
640 .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
641 .await?
642 .into_object()?;
643 let primary_coin_ref = coin.object_ref();
644 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
645 let coin: Object = coin.try_into()?;
646 let type_args = vec![coin.get_move_template_type()?];
647 let gas_price = self.0.get_reference_gas_price().await?;
648 let gas = self
649 .select_gas(
650 signer,
651 gas,
652 gas_budget,
653 vec![primary_coin, coin_to_merge],
654 gas_price,
655 )
656 .await?;
657
658 TransactionData::new_move_call(
659 signer,
660 IOTA_FRAMEWORK_PACKAGE_ID,
661 coin::COIN_MODULE_NAME.to_owned(),
662 coin::COIN_JOIN_FUNC_NAME.to_owned(),
663 type_args,
664 gas,
665 vec![
666 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
667 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
668 ],
669 gas_budget,
670 gas_price,
671 )
672 }
673
674 pub async fn batch_transaction(
676 &self,
677 signer: IotaAddress,
678 single_transaction_params: Vec<RPCTransactionRequestParams>,
679 gas: impl Into<Option<ObjectID>>,
680 gas_budget: u64,
681 ) -> anyhow::Result<TransactionData> {
682 fp_ensure!(
683 !single_transaction_params.is_empty(),
684 UserInputError::InvalidBatchTransaction {
685 error: "Batch Transaction cannot be empty".to_owned(),
686 }
687 .into()
688 );
689 let mut builder = ProgrammableTransactionBuilder::new();
690 for param in single_transaction_params {
691 match param {
692 RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
693 self.single_transfer_object(&mut builder, param.object_id, param.recipient)
694 .await?
695 }
696 RPCTransactionRequestParams::MoveCallRequestParams(param) => {
697 self.single_move_call(
698 &mut builder,
699 param.package_object_id,
700 ¶m.module,
701 ¶m.function,
702 param.type_arguments,
703 param.arguments,
704 )
705 .await?
706 }
707 };
708 }
709 let pt = builder.finish();
710 let all_inputs = pt.input_objects()?;
711 let inputs = all_inputs
712 .iter()
713 .flat_map(|obj| match obj {
714 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
715 _ => None,
716 })
717 .collect();
718 let gas_price = self.0.get_reference_gas_price().await?;
719 let gas = self
720 .select_gas(signer, gas, gas_budget, inputs, gas_price)
721 .await?;
722
723 Ok(TransactionData::new(
724 TransactionKind::programmable(pt),
725 signer,
726 gas,
727 gas_budget,
728 gas_price,
729 ))
730 }
731}