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