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, 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 bail!(
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_view_call_tx_kind(
391 &self,
392 package_object_id: ObjectID,
393 module: &str,
394 function: &str,
395 type_args: Vec<IotaTypeTag>,
396 call_args: Vec<IotaJsonValue>,
397 ) -> Result<TransactionKind, anyhow::Error> {
398 let mut builder = ProgrammableTransactionBuilder::new();
399 self.single_move_view_call(
400 &mut builder,
401 package_object_id,
402 module,
403 function,
404 type_args,
405 call_args,
406 )
407 .await?;
408 let pt = builder.finish();
409 Ok(TransactionKind::programmable(pt))
410 }
411
412 pub async fn move_call(
414 &self,
415 signer: IotaAddress,
416 package_object_id: ObjectID,
417 module: &str,
418 function: &str,
419 type_args: Vec<IotaTypeTag>,
420 call_args: Vec<IotaJsonValue>,
421 gas: impl Into<Option<ObjectID>>,
422 gas_budget: u64,
423 gas_price: impl Into<Option<u64>>,
424 ) -> anyhow::Result<TransactionData> {
425 let gas_price = gas_price.into();
426
427 let mut builder = ProgrammableTransactionBuilder::new();
428 self.single_move_call(
429 &mut builder,
430 package_object_id,
431 module,
432 function,
433 type_args,
434 call_args,
435 )
436 .await?;
437 let pt = builder.finish();
438 let input_objects = pt
439 .input_objects()?
440 .iter()
441 .flat_map(|obj| match obj {
442 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
443 _ => None,
444 })
445 .collect();
446 let gas_price = if let Some(gas_price) = gas_price {
447 gas_price
448 } else {
449 self.0.get_reference_gas_price().await?
450 };
451 let gas = self
452 .select_gas(signer, gas, gas_budget, input_objects, gas_price)
453 .await?;
454
455 Ok(TransactionData::new(
456 TransactionKind::programmable(pt),
457 signer,
458 gas,
459 gas_budget,
460 gas_price,
461 ))
462 }
463
464 pub async fn single_move_call(
467 &self,
468 builder: &mut ProgrammableTransactionBuilder,
469 package: ObjectID,
470 module: &str,
471 function: &str,
472 type_args: Vec<IotaTypeTag>,
473 call_args: Vec<IotaJsonValue>,
474 ) -> anyhow::Result<()> {
475 let module = Identifier::from_str(module)?;
476 let function = Identifier::from_str(function)?;
477
478 let type_args = type_args
479 .into_iter()
480 .map(|ty| ty.try_into())
481 .collect::<Result<Vec<_>, _>>()?;
482
483 let call_args = self
484 .resolve_and_checks_json_args(
485 builder, package, &module, &function, &type_args, call_args,
486 )
487 .await?;
488
489 builder.command(Command::move_call(
490 package, module, function, type_args, call_args,
491 ));
492 Ok(())
493 }
494
495 pub async fn single_move_call_with_ptb_inputs(
501 &self,
502 builder: &mut ProgrammableTransactionBuilder,
503 package: ObjectID,
504 module: &str,
505 function: &str,
506 type_args: Vec<IotaTypeTag>,
507 call_args: Vec<PtbInput>,
508 ) -> anyhow::Result<()> {
509 let module = Identifier::from_str(module)?;
510 let function = Identifier::from_str(function)?;
511
512 let type_args = type_args
513 .into_iter()
514 .map(|ty| ty.try_into())
515 .collect::<Result<Vec<_>, _>>()?;
516
517 let call_args = self
518 .resolve_and_check_call_args(
519 builder, package, &module, &function, &type_args, call_args,
520 )
521 .await?;
522
523 builder.command(Command::move_call(
524 package, module, function, type_args, call_args,
525 ));
526 Ok(())
527 }
528
529 pub async fn single_move_view_call(
533 &self,
534 builder: &mut ProgrammableTransactionBuilder,
535 package: ObjectID,
536 module: &str,
537 function: &str,
538 type_args: Vec<IotaTypeTag>,
539 call_args: Vec<IotaJsonValue>,
540 ) -> anyhow::Result<()> {
541 let module = Identifier::from_str(module)?;
542 let function = Identifier::from_str(function)?;
543
544 let type_args = type_args
545 .into_iter()
546 .map(|ty| ty.try_into())
547 .collect::<Result<Vec<_>, _>>()?;
548
549 let call_args = self
550 .resolve_and_checks_json_view_args(
551 builder, package, &module, &function, &type_args, call_args,
552 )
553 .await?;
554
555 builder.command(Command::move_call(
556 package, module, function, type_args, call_args,
557 ));
558 Ok(())
559 }
560
561 pub async fn split_coin_tx_kind(
565 &self,
566 coin_object_id: ObjectID,
567 split_amounts: impl Into<Option<Vec<u64>>>,
568 split_count: impl Into<Option<u64>>,
569 ) -> Result<TransactionKind, anyhow::Error> {
570 let split_amounts = split_amounts.into();
571 let split_count = split_count.into();
572
573 if split_amounts.is_none() && split_count.is_none() {
574 bail!(
575 "Either split_amounts or split_count must be provided for split_coin transaction."
576 );
577 }
578 let coin = self
579 .0
580 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
581 .await?
582 .into_object()?;
583 let coin_object_ref = coin.object_ref();
584 let coin: Object = coin.try_into()?;
585 let type_args = vec![coin.get_move_template_type()?];
586 let package = IOTA_FRAMEWORK_PACKAGE_ID;
587 let module = coin::PAY_MODULE_NAME.to_owned();
588
589 let (arguments, function) = if let Some(split_amounts) = split_amounts {
590 (
591 vec![
592 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
593 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
594 ],
595 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
596 )
597 } else {
598 (
599 vec![
600 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
601 CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
602 ],
603 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
604 )
605 };
606 let mut builder = ProgrammableTransactionBuilder::new();
607 builder.move_call(package, module, function, type_args, arguments)?;
608 let pt = builder.finish();
609 let tx_kind = TransactionKind::programmable(pt);
610 Ok(tx_kind)
611 }
612
613 pub async fn split_coin(
615 &self,
616 signer: IotaAddress,
617 coin_object_id: ObjectID,
618 split_amounts: Vec<u64>,
619 gas: impl Into<Option<ObjectID>>,
620 gas_budget: u64,
621 ) -> anyhow::Result<TransactionData> {
622 let coin = self
623 .0
624 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
625 .await?
626 .into_object()?;
627 let coin_object_ref = coin.object_ref();
628 let coin: Object = coin.try_into()?;
629 let type_args = vec![coin.get_move_template_type()?];
630 let gas_price = self.0.get_reference_gas_price().await?;
631 let gas = self
632 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
633 .await?;
634
635 TransactionData::new_move_call(
636 signer,
637 IOTA_FRAMEWORK_PACKAGE_ID,
638 coin::PAY_MODULE_NAME.to_owned(),
639 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
640 type_args,
641 gas,
642 vec![
643 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
644 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
645 ],
646 gas_budget,
647 gas_price,
648 )
649 }
650
651 pub async fn split_coin_equal(
653 &self,
654 signer: IotaAddress,
655 coin_object_id: ObjectID,
656 split_count: u64,
657 gas: impl Into<Option<ObjectID>>,
658 gas_budget: u64,
659 ) -> anyhow::Result<TransactionData> {
660 let coin = self
661 .0
662 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
663 .await?
664 .into_object()?;
665 let coin_object_ref = coin.object_ref();
666 let coin: Object = coin.try_into()?;
667 let type_args = vec![coin.get_move_template_type()?];
668 let gas_price = self.0.get_reference_gas_price().await?;
669 let gas = self
670 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
671 .await?;
672
673 TransactionData::new_move_call(
674 signer,
675 IOTA_FRAMEWORK_PACKAGE_ID,
676 coin::PAY_MODULE_NAME.to_owned(),
677 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
678 type_args,
679 gas,
680 vec![
681 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
682 CallArg::Pure(bcs::to_bytes(&split_count)?),
683 ],
684 gas_budget,
685 gas_price,
686 )
687 }
688
689 pub async fn merge_coins_tx_kind(
692 &self,
693 primary_coin: ObjectID,
694 coin_to_merge: ObjectID,
695 ) -> Result<TransactionKind, anyhow::Error> {
696 let coin = self
697 .0
698 .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
699 .await?
700 .into_object()?;
701 let primary_coin_ref = coin.object_ref();
702 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
703 let coin: Object = coin.try_into()?;
704 let type_arguments = vec![coin.get_move_template_type()?];
705 let package = IOTA_FRAMEWORK_PACKAGE_ID;
706 let module = coin::COIN_MODULE_NAME.to_owned();
707 let function = coin::COIN_JOIN_FUNC_NAME.to_owned();
708 let arguments = vec![
709 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
710 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
711 ];
712 let pt = {
713 let mut builder = ProgrammableTransactionBuilder::new();
714 builder.move_call(package, module, function, type_arguments, arguments)?;
715 builder.finish()
716 };
717 let tx_kind = TransactionKind::programmable(pt);
718 Ok(tx_kind)
719 }
720
721 pub async fn merge_coins(
723 &self,
724 signer: IotaAddress,
725 primary_coin: ObjectID,
726 coin_to_merge: ObjectID,
727 gas: impl Into<Option<ObjectID>>,
728 gas_budget: u64,
729 ) -> anyhow::Result<TransactionData> {
730 let coin = self
731 .0
732 .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
733 .await?
734 .into_object()?;
735 let primary_coin_ref = coin.object_ref();
736 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
737 let coin: Object = coin.try_into()?;
738 let type_args = vec![coin.get_move_template_type()?];
739 let gas_price = self.0.get_reference_gas_price().await?;
740 let gas = self
741 .select_gas(
742 signer,
743 gas,
744 gas_budget,
745 vec![primary_coin, coin_to_merge],
746 gas_price,
747 )
748 .await?;
749
750 TransactionData::new_move_call(
751 signer,
752 IOTA_FRAMEWORK_PACKAGE_ID,
753 coin::COIN_MODULE_NAME.to_owned(),
754 coin::COIN_JOIN_FUNC_NAME.to_owned(),
755 type_args,
756 gas,
757 vec![
758 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
759 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
760 ],
761 gas_budget,
762 gas_price,
763 )
764 }
765
766 pub async fn batch_transaction(
768 &self,
769 signer: IotaAddress,
770 single_transaction_params: Vec<RPCTransactionRequestParams>,
771 gas: impl Into<Option<ObjectID>>,
772 gas_budget: u64,
773 ) -> anyhow::Result<TransactionData> {
774 fp_ensure!(
775 !single_transaction_params.is_empty(),
776 UserInputError::InvalidBatchTransaction {
777 error: "Batch Transaction cannot be empty".to_owned(),
778 }
779 .into()
780 );
781 let mut builder = ProgrammableTransactionBuilder::new();
782 for param in single_transaction_params {
783 match param {
784 RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
785 self.single_transfer_object(&mut builder, param.object_id, param.recipient)
786 .await?
787 }
788 RPCTransactionRequestParams::MoveCallRequestParams(param) => {
789 self.single_move_call_with_ptb_inputs(
790 &mut builder,
791 param.package_object_id,
792 ¶m.module,
793 ¶m.function,
794 param.type_arguments,
795 param.arguments,
796 )
797 .await?
798 }
799 };
800 }
801 let pt = builder.finish();
802 let all_inputs = pt.input_objects()?;
803 let inputs = all_inputs
804 .iter()
805 .flat_map(|obj| match obj {
806 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
807 _ => None,
808 })
809 .collect();
810 let gas_price = self.0.get_reference_gas_price().await?;
811 let gas = self
812 .select_gas(signer, gas, gas_budget, inputs, gas_price)
813 .await?;
814
815 Ok(TransactionData::new(
816 TransactionKind::programmable(pt),
817 signer,
818 gas,
819 gas_budget,
820 gas_price,
821 ))
822 }
823}