1pub mod package;
6pub mod stake;
7pub mod utils;
8
9use std::{result::Result, str::FromStr, sync::Arc};
10
11use anyhow::bail;
12use async_trait::async_trait;
13use iota_json::IotaJsonValue;
14use iota_json_rpc_types::{
15 IotaObjectDataOptions, IotaObjectResponse, IotaTypeTag, PtbInput, RPCTransactionRequestParams,
16};
17use iota_types::{
18 IOTA_FRAMEWORK_PACKAGE_ID,
19 base_types::{IotaAddress, ObjectID},
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 cursor: Option<ObjectID>,
36 limit: Option<usize>,
37 options: IotaObjectDataOptions,
38 ) -> Result<iota_json_rpc_types::ObjectsPage, anyhow::Error>;
39
40 async fn get_object_with_options(
41 &self,
42 object_id: ObjectID,
43 options: IotaObjectDataOptions,
44 ) -> Result<IotaObjectResponse, anyhow::Error>;
45
46 async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error>;
47}
48
49#[derive(Clone)]
50pub struct TransactionBuilder(Arc<dyn DataReader + Sync + Send>);
51
52impl TransactionBuilder {
53 pub fn new(data_reader: Arc<dyn DataReader + Sync + Send>) -> Self {
54 Self(data_reader)
55 }
56
57 pub async fn tx_data_for_dry_run(
59 &self,
60 sender: IotaAddress,
61 kind: TransactionKind,
62 gas_budget: u64,
63 gas_price: u64,
64 gas_payment: impl Into<Option<Vec<ObjectID>>>,
65 gas_sponsor: impl Into<Option<IotaAddress>>,
66 ) -> TransactionData {
67 let gas_payment = self
68 .input_refs(gas_payment.into().unwrap_or_default().as_ref())
69 .await
70 .unwrap_or_default();
71 let gas_sponsor = gas_sponsor.into().unwrap_or(sender);
72 TransactionData::new_with_gas_coins_allow_sponsor(
73 kind,
74 sender,
75 gas_payment,
76 gas_budget,
77 gas_price,
78 gas_sponsor,
79 )
80 }
81
82 pub async fn tx_data(
87 &self,
88 sender: IotaAddress,
89 kind: TransactionKind,
90 gas_budget: u64,
91 gas_price: u64,
92 gas_payment: Vec<ObjectID>,
93 gas_sponsor: impl Into<Option<IotaAddress>>,
94 ) -> Result<TransactionData, anyhow::Error> {
95 let gas_payment = if gas_payment.is_empty() {
96 let input_objs = kind
97 .input_objects()?
98 .iter()
99 .flat_map(|obj| match obj {
100 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
101 _ => None,
102 })
103 .collect();
104 vec![
105 self.select_gas(sender, None, gas_budget, input_objs, gas_price)
106 .await?,
107 ]
108 } else {
109 self.input_refs(&gas_payment).await?
110 };
111 Ok(TransactionData::new_with_gas_coins_allow_sponsor(
112 kind,
113 sender,
114 gas_payment,
115 gas_budget,
116 gas_price,
117 gas_sponsor.into().unwrap_or(sender),
118 ))
119 }
120
121 pub async fn transfer_object_tx_kind(
124 &self,
125 object_id: ObjectID,
126 recipient: IotaAddress,
127 ) -> Result<TransactionKind, anyhow::Error> {
128 let obj_ref = self.get_object_ref(object_id).await?;
129 let mut builder = ProgrammableTransactionBuilder::new();
130 builder.transfer_object(recipient, obj_ref)?;
131 Ok(TransactionKind::programmable(builder.finish()))
132 }
133
134 pub async fn transfer_object(
136 &self,
137 signer: IotaAddress,
138 object_id: ObjectID,
139 gas: impl Into<Option<ObjectID>>,
140 gas_budget: u64,
141 recipient: IotaAddress,
142 ) -> anyhow::Result<TransactionData> {
143 let mut builder = ProgrammableTransactionBuilder::new();
144 self.single_transfer_object(&mut builder, object_id, recipient)
145 .await?;
146 let gas_price = self.0.get_reference_gas_price().await?;
147 let gas = self
148 .select_gas(signer, gas, gas_budget, vec![object_id], gas_price)
149 .await?;
150
151 Ok(TransactionData::new(
152 TransactionKind::programmable(builder.finish()),
153 signer,
154 gas,
155 gas_budget,
156 gas_price,
157 ))
158 }
159
160 async fn single_transfer_object(
163 &self,
164 builder: &mut ProgrammableTransactionBuilder,
165 object_id: ObjectID,
166 recipient: IotaAddress,
167 ) -> anyhow::Result<()> {
168 builder.transfer_object(recipient, self.get_object_ref(object_id).await?)?;
169 Ok(())
170 }
171
172 pub fn transfer_iota_tx_kind(
177 &self,
178 recipient: IotaAddress,
179 amount: impl Into<Option<u64>>,
180 ) -> TransactionKind {
181 let mut builder = ProgrammableTransactionBuilder::new();
182 builder.transfer_iota(recipient, amount.into());
183 let pt = builder.finish();
184 TransactionKind::programmable(pt)
185 }
186
187 pub async fn transfer_iota(
190 &self,
191 signer: IotaAddress,
192 iota_object_id: ObjectID,
193 gas_budget: u64,
194 recipient: IotaAddress,
195 amount: impl Into<Option<u64>>,
196 ) -> anyhow::Result<TransactionData> {
197 let object = self.get_object_ref(iota_object_id).await?;
198 let gas_price = self.0.get_reference_gas_price().await?;
199 Ok(TransactionData::new_transfer_iota(
200 recipient,
201 signer,
202 amount.into(),
203 object,
204 gas_budget,
205 gas_price,
206 ))
207 }
208
209 pub async fn pay_tx_kind(
215 &self,
216 input_coins: Vec<ObjectID>,
217 recipients: Vec<IotaAddress>,
218 amounts: Vec<u64>,
219 ) -> Result<TransactionKind, anyhow::Error> {
220 let mut builder = ProgrammableTransactionBuilder::new();
221 let coins = self.input_refs(&input_coins).await?;
222 builder.pay(coins, recipients, amounts)?;
223 let pt = builder.finish();
224 Ok(TransactionKind::programmable(pt))
225 }
226
227 pub async fn pay(
235 &self,
236 signer: IotaAddress,
237 input_coins: Vec<ObjectID>,
238 recipients: Vec<IotaAddress>,
239 amounts: Vec<u64>,
240 gas: impl Into<Option<ObjectID>>,
241 gas_budget: u64,
242 ) -> anyhow::Result<TransactionData> {
243 let gas = gas.into();
244
245 if let Some(gas) = gas {
246 if input_coins.contains(&gas) {
247 bail!(
248 "Gas coin is in input coins of Pay transaction, use PayIota transaction instead!"
249 );
250 }
251 }
252
253 let coin_refs = self.input_refs(&input_coins).await?;
254 let gas_price = self.0.get_reference_gas_price().await?;
255 let gas = self
256 .select_gas(signer, gas, gas_budget, input_coins, gas_price)
257 .await?;
258
259 TransactionData::new_pay(
260 signer, coin_refs, recipients, amounts, gas, gas_budget, gas_price,
261 )
262 }
263
264 pub fn pay_iota_tx_kind(
270 &self,
271 recipients: Vec<IotaAddress>,
272 amounts: Vec<u64>,
273 ) -> Result<TransactionKind, anyhow::Error> {
274 let mut builder = ProgrammableTransactionBuilder::new();
275 builder.pay_iota(recipients.clone(), amounts.clone())?;
276 let pt = builder.finish();
277 let tx_kind = TransactionKind::programmable(pt);
278 Ok(tx_kind)
279 }
280
281 pub async fn pay_iota(
291 &self,
292 signer: IotaAddress,
293 input_coins: Vec<ObjectID>,
294 recipients: Vec<IotaAddress>,
295 amounts: Vec<u64>,
296 gas_budget: u64,
297 ) -> anyhow::Result<TransactionData> {
298 fp_ensure!(
299 !input_coins.is_empty(),
300 UserInputError::EmptyInputCoins.into()
301 );
302
303 let mut coin_refs = self.input_refs(&input_coins).await?;
304 let gas_object_ref = coin_refs.remove(0);
307 let gas_price = self.0.get_reference_gas_price().await?;
308 TransactionData::new_pay_iota(
309 signer,
310 coin_refs,
311 recipients,
312 amounts,
313 gas_object_ref,
314 gas_budget,
315 gas_price,
316 )
317 }
318
319 pub fn pay_all_iota_tx_kind(&self, recipient: IotaAddress) -> TransactionKind {
322 let mut builder = ProgrammableTransactionBuilder::new();
323 builder.pay_all_iota(recipient);
324 let pt = builder.finish();
325 TransactionKind::programmable(pt)
326 }
327
328 pub async fn pay_all_iota(
339 &self,
340 signer: IotaAddress,
341 input_coins: Vec<ObjectID>,
342 recipient: IotaAddress,
343 gas_budget: u64,
344 ) -> anyhow::Result<TransactionData> {
345 fp_ensure!(
346 !input_coins.is_empty(),
347 UserInputError::EmptyInputCoins.into()
348 );
349
350 let mut coin_refs = self.input_refs(&input_coins).await?;
351 let gas_object_ref = coin_refs.remove(0);
354 let gas_price = self.0.get_reference_gas_price().await?;
355 Ok(TransactionData::new_pay_all_iota(
356 signer,
357 coin_refs,
358 recipient,
359 gas_object_ref,
360 gas_budget,
361 gas_price,
362 ))
363 }
364
365 pub async fn move_call_tx_kind(
368 &self,
369 package_object_id: ObjectID,
370 module: &str,
371 function: &str,
372 type_args: Vec<IotaTypeTag>,
373 call_args: Vec<IotaJsonValue>,
374 ) -> Result<TransactionKind, anyhow::Error> {
375 let mut builder = ProgrammableTransactionBuilder::new();
376 self.single_move_call(
377 &mut builder,
378 package_object_id,
379 module,
380 function,
381 type_args,
382 call_args,
383 )
384 .await?;
385 let pt = builder.finish();
386 Ok(TransactionKind::programmable(pt))
387 }
388
389 pub async fn move_view_call_tx_kind(
394 &self,
395 package_object_id: ObjectID,
396 module: &str,
397 function: &str,
398 type_args: Vec<IotaTypeTag>,
399 call_args: Vec<IotaJsonValue>,
400 ) -> Result<TransactionKind, anyhow::Error> {
401 let mut builder = ProgrammableTransactionBuilder::new();
402 self.single_move_view_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 Ok(TransactionKind::programmable(pt))
413 }
414
415 pub async fn move_call(
417 &self,
418 signer: IotaAddress,
419 package_object_id: ObjectID,
420 module: &str,
421 function: &str,
422 type_args: Vec<IotaTypeTag>,
423 call_args: Vec<IotaJsonValue>,
424 gas: impl Into<Option<ObjectID>>,
425 gas_budget: u64,
426 gas_price: impl Into<Option<u64>>,
427 ) -> anyhow::Result<TransactionData> {
428 let gas_price = gas_price.into();
429
430 let mut builder = ProgrammableTransactionBuilder::new();
431 self.single_move_call(
432 &mut builder,
433 package_object_id,
434 module,
435 function,
436 type_args,
437 call_args,
438 )
439 .await?;
440 let pt = builder.finish();
441 let input_objects = pt
442 .input_objects()?
443 .iter()
444 .flat_map(|obj| match obj {
445 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
446 _ => None,
447 })
448 .collect();
449 let gas_price = if let Some(gas_price) = gas_price {
450 gas_price
451 } else {
452 self.0.get_reference_gas_price().await?
453 };
454 let gas = self
455 .select_gas(signer, gas, gas_budget, input_objects, gas_price)
456 .await?;
457
458 Ok(TransactionData::new(
459 TransactionKind::programmable(pt),
460 signer,
461 gas,
462 gas_budget,
463 gas_price,
464 ))
465 }
466
467 pub async fn single_move_call(
470 &self,
471 builder: &mut ProgrammableTransactionBuilder,
472 package: ObjectID,
473 module: &str,
474 function: &str,
475 type_args: Vec<IotaTypeTag>,
476 call_args: Vec<IotaJsonValue>,
477 ) -> anyhow::Result<()> {
478 let module = Identifier::from_str(module)?;
479 let function = Identifier::from_str(function)?;
480
481 let type_args = type_args
482 .into_iter()
483 .map(|ty| ty.try_into())
484 .collect::<Result<Vec<_>, _>>()?;
485
486 let call_args = self
487 .resolve_and_checks_json_args(
488 builder, package, &module, &function, &type_args, call_args,
489 )
490 .await?;
491
492 builder.command(Command::move_call(
493 package, module, function, type_args, call_args,
494 ));
495 Ok(())
496 }
497
498 pub async fn single_move_call_with_ptb_inputs(
504 &self,
505 builder: &mut ProgrammableTransactionBuilder,
506 package: ObjectID,
507 module: &str,
508 function: &str,
509 type_args: Vec<IotaTypeTag>,
510 call_args: Vec<PtbInput>,
511 ) -> anyhow::Result<()> {
512 let module = Identifier::from_str(module)?;
513 let function = Identifier::from_str(function)?;
514
515 let type_args = type_args
516 .into_iter()
517 .map(|ty| ty.try_into())
518 .collect::<Result<Vec<_>, _>>()?;
519
520 let call_args = self
521 .resolve_and_check_call_args(
522 builder, package, &module, &function, &type_args, call_args,
523 )
524 .await?;
525
526 builder.command(Command::move_call(
527 package, module, function, type_args, call_args,
528 ));
529 Ok(())
530 }
531
532 pub async fn single_move_view_call(
536 &self,
537 builder: &mut ProgrammableTransactionBuilder,
538 package: ObjectID,
539 module: &str,
540 function: &str,
541 type_args: Vec<IotaTypeTag>,
542 call_args: Vec<IotaJsonValue>,
543 ) -> anyhow::Result<()> {
544 let module = Identifier::from_str(module)?;
545 let function = Identifier::from_str(function)?;
546
547 let type_args = type_args
548 .into_iter()
549 .map(|ty| ty.try_into())
550 .collect::<Result<Vec<_>, _>>()?;
551
552 let call_args = self
553 .resolve_and_checks_json_view_args(
554 builder, package, &module, &function, &type_args, call_args,
555 )
556 .await?;
557
558 builder.command(Command::move_call(
559 package, module, function, type_args, call_args,
560 ));
561 Ok(())
562 }
563
564 pub async fn split_coin_tx_kind(
568 &self,
569 coin_object_id: ObjectID,
570 split_amounts: impl Into<Option<Vec<u64>>>,
571 split_count: impl Into<Option<u64>>,
572 ) -> Result<TransactionKind, anyhow::Error> {
573 let split_amounts = split_amounts.into();
574 let split_count = split_count.into();
575
576 if split_amounts.is_none() && split_count.is_none() {
577 bail!(
578 "Either split_amounts or split_count must be provided for split_coin transaction."
579 );
580 }
581 let coin = self
582 .0
583 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
584 .await?
585 .into_object()?;
586 let coin_object_ref = coin.object_ref();
587 let coin: Object = coin.try_into()?;
588 let type_args = vec![coin.get_move_template_type()?];
589 let package = IOTA_FRAMEWORK_PACKAGE_ID;
590 let module = coin::PAY_MODULE_NAME.to_owned();
591
592 let (arguments, function) = if let Some(split_amounts) = split_amounts {
593 (
594 vec![
595 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
596 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
597 ],
598 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
599 )
600 } else {
601 (
602 vec![
603 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
604 CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
605 ],
606 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
607 )
608 };
609 let mut builder = ProgrammableTransactionBuilder::new();
610 builder.move_call(package, module, function, type_args, arguments)?;
611 let pt = builder.finish();
612 let tx_kind = TransactionKind::programmable(pt);
613 Ok(tx_kind)
614 }
615
616 pub async fn split_coin(
618 &self,
619 signer: IotaAddress,
620 coin_object_id: ObjectID,
621 split_amounts: Vec<u64>,
622 gas: impl Into<Option<ObjectID>>,
623 gas_budget: u64,
624 ) -> anyhow::Result<TransactionData> {
625 let coin = self
626 .0
627 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
628 .await?
629 .into_object()?;
630 let coin_object_ref = coin.object_ref();
631 let coin: Object = coin.try_into()?;
632 let type_args = vec![coin.get_move_template_type()?];
633 let gas_price = self.0.get_reference_gas_price().await?;
634 let gas = self
635 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
636 .await?;
637
638 TransactionData::new_move_call(
639 signer,
640 IOTA_FRAMEWORK_PACKAGE_ID,
641 coin::PAY_MODULE_NAME.to_owned(),
642 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
643 type_args,
644 gas,
645 vec![
646 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
647 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
648 ],
649 gas_budget,
650 gas_price,
651 )
652 }
653
654 pub async fn split_coin_equal(
656 &self,
657 signer: IotaAddress,
658 coin_object_id: ObjectID,
659 split_count: u64,
660 gas: impl Into<Option<ObjectID>>,
661 gas_budget: u64,
662 ) -> anyhow::Result<TransactionData> {
663 let coin = self
664 .0
665 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
666 .await?
667 .into_object()?;
668 let coin_object_ref = coin.object_ref();
669 let coin: Object = coin.try_into()?;
670 let type_args = vec![coin.get_move_template_type()?];
671 let gas_price = self.0.get_reference_gas_price().await?;
672 let gas = self
673 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
674 .await?;
675
676 TransactionData::new_move_call(
677 signer,
678 IOTA_FRAMEWORK_PACKAGE_ID,
679 coin::PAY_MODULE_NAME.to_owned(),
680 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
681 type_args,
682 gas,
683 vec![
684 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
685 CallArg::Pure(bcs::to_bytes(&split_count)?),
686 ],
687 gas_budget,
688 gas_price,
689 )
690 }
691
692 pub async fn merge_coins_tx_kind(
695 &self,
696 primary_coin: ObjectID,
697 coin_to_merge: ObjectID,
698 ) -> Result<TransactionKind, anyhow::Error> {
699 let coin = self
700 .0
701 .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
702 .await?
703 .into_object()?;
704 let primary_coin_ref = coin.object_ref();
705 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
706 let coin: Object = coin.try_into()?;
707 let type_arguments = vec![coin.get_move_template_type()?];
708 let package = IOTA_FRAMEWORK_PACKAGE_ID;
709 let module = coin::COIN_MODULE_NAME.to_owned();
710 let function = coin::COIN_JOIN_FUNC_NAME.to_owned();
711 let arguments = vec![
712 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
713 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
714 ];
715 let pt = {
716 let mut builder = ProgrammableTransactionBuilder::new();
717 builder.move_call(package, module, function, type_arguments, arguments)?;
718 builder.finish()
719 };
720 let tx_kind = TransactionKind::programmable(pt);
721 Ok(tx_kind)
722 }
723
724 pub async fn merge_coins(
726 &self,
727 signer: IotaAddress,
728 primary_coin: ObjectID,
729 coin_to_merge: ObjectID,
730 gas: impl Into<Option<ObjectID>>,
731 gas_budget: u64,
732 ) -> anyhow::Result<TransactionData> {
733 let coin = self
734 .0
735 .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
736 .await?
737 .into_object()?;
738 let primary_coin_ref = coin.object_ref();
739 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
740 let coin: Object = coin.try_into()?;
741 let type_args = vec![coin.get_move_template_type()?];
742 let gas_price = self.0.get_reference_gas_price().await?;
743 let gas = self
744 .select_gas(
745 signer,
746 gas,
747 gas_budget,
748 vec![primary_coin, coin_to_merge],
749 gas_price,
750 )
751 .await?;
752
753 TransactionData::new_move_call(
754 signer,
755 IOTA_FRAMEWORK_PACKAGE_ID,
756 coin::COIN_MODULE_NAME.to_owned(),
757 coin::COIN_JOIN_FUNC_NAME.to_owned(),
758 type_args,
759 gas,
760 vec![
761 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
762 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
763 ],
764 gas_budget,
765 gas_price,
766 )
767 }
768
769 pub async fn batch_transaction(
771 &self,
772 signer: IotaAddress,
773 single_transaction_params: Vec<RPCTransactionRequestParams>,
774 gas: impl Into<Option<ObjectID>>,
775 gas_budget: u64,
776 ) -> anyhow::Result<TransactionData> {
777 fp_ensure!(
778 !single_transaction_params.is_empty(),
779 UserInputError::InvalidBatchTransaction {
780 error: "Batch Transaction cannot be empty".to_owned(),
781 }
782 .into()
783 );
784 let mut builder = ProgrammableTransactionBuilder::new();
785 for param in single_transaction_params {
786 match param {
787 RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
788 self.single_transfer_object(&mut builder, param.object_id, param.recipient)
789 .await?
790 }
791 RPCTransactionRequestParams::MoveCallRequestParams(param) => {
792 self.single_move_call_with_ptb_inputs(
793 &mut builder,
794 param.package_object_id,
795 ¶m.module,
796 ¶m.function,
797 param.type_arguments,
798 param.arguments,
799 )
800 .await?
801 }
802 };
803 }
804 let pt = builder.finish();
805 let all_inputs = pt.input_objects()?;
806 let inputs = all_inputs
807 .iter()
808 .flat_map(|obj| match obj {
809 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
810 _ => None,
811 })
812 .collect();
813 let gas_price = self.0.get_reference_gas_price().await?;
814 let gas = self
815 .select_gas(signer, gas, gas_budget, inputs, gas_price)
816 .await?;
817
818 Ok(TransactionData::new(
819 TransactionKind::programmable(pt),
820 signer,
821 gas,
822 gas_budget,
823 gas_price,
824 ))
825 }
826}