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