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_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 single_move_call_with_ptb_inputs(
475 &self,
476 builder: &mut ProgrammableTransactionBuilder,
477 package: ObjectID,
478 module: &str,
479 function: &str,
480 type_args: Vec<IotaTypeTag>,
481 call_args: Vec<PtbInput>,
482 ) -> anyhow::Result<()> {
483 let module = Identifier::from_str(module)?;
484 let function = Identifier::from_str(function)?;
485
486 let type_args = type_args
487 .into_iter()
488 .map(|ty| ty.try_into())
489 .collect::<Result<Vec<_>, _>>()?;
490
491 let call_args = self
492 .resolve_and_check_call_args(
493 builder, package, &module, &function, &type_args, call_args,
494 )
495 .await?;
496
497 builder.command(Command::move_call(
498 package, module, function, type_args, call_args,
499 ));
500 Ok(())
501 }
502
503 pub async fn split_coin_tx_kind(
507 &self,
508 coin_object_id: ObjectID,
509 split_amounts: impl Into<Option<Vec<u64>>>,
510 split_count: impl Into<Option<u64>>,
511 ) -> Result<TransactionKind, anyhow::Error> {
512 let split_amounts = split_amounts.into();
513 let split_count = split_count.into();
514
515 if split_amounts.is_none() && split_count.is_none() {
516 bail!(
517 "Either split_amounts or split_count must be provided for split_coin transaction."
518 );
519 }
520 let coin = self
521 .0
522 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
523 .await?
524 .into_object()?;
525 let coin_object_ref = coin.object_ref();
526 let coin: Object = coin.try_into()?;
527 let type_args = vec![coin.get_move_template_type()?];
528 let package = IOTA_FRAMEWORK_PACKAGE_ID;
529 let module = coin::PAY_MODULE_NAME.to_owned();
530
531 let (arguments, function) = if let Some(split_amounts) = split_amounts {
532 (
533 vec![
534 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
535 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
536 ],
537 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
538 )
539 } else {
540 (
541 vec![
542 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
543 CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
544 ],
545 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
546 )
547 };
548 let mut builder = ProgrammableTransactionBuilder::new();
549 builder.move_call(package, module, function, type_args, arguments)?;
550 let pt = builder.finish();
551 let tx_kind = TransactionKind::programmable(pt);
552 Ok(tx_kind)
553 }
554
555 pub async fn split_coin(
557 &self,
558 signer: IotaAddress,
559 coin_object_id: ObjectID,
560 split_amounts: Vec<u64>,
561 gas: impl Into<Option<ObjectID>>,
562 gas_budget: u64,
563 ) -> anyhow::Result<TransactionData> {
564 let coin = self
565 .0
566 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
567 .await?
568 .into_object()?;
569 let coin_object_ref = coin.object_ref();
570 let coin: Object = coin.try_into()?;
571 let type_args = vec![coin.get_move_template_type()?];
572 let gas_price = self.0.get_reference_gas_price().await?;
573 let gas = self
574 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
575 .await?;
576
577 TransactionData::new_move_call(
578 signer,
579 IOTA_FRAMEWORK_PACKAGE_ID,
580 coin::PAY_MODULE_NAME.to_owned(),
581 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
582 type_args,
583 gas,
584 vec![
585 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
586 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
587 ],
588 gas_budget,
589 gas_price,
590 )
591 }
592
593 pub async fn split_coin_equal(
595 &self,
596 signer: IotaAddress,
597 coin_object_id: ObjectID,
598 split_count: u64,
599 gas: impl Into<Option<ObjectID>>,
600 gas_budget: u64,
601 ) -> anyhow::Result<TransactionData> {
602 let coin = self
603 .0
604 .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
605 .await?
606 .into_object()?;
607 let coin_object_ref = coin.object_ref();
608 let coin: Object = coin.try_into()?;
609 let type_args = vec![coin.get_move_template_type()?];
610 let gas_price = self.0.get_reference_gas_price().await?;
611 let gas = self
612 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
613 .await?;
614
615 TransactionData::new_move_call(
616 signer,
617 IOTA_FRAMEWORK_PACKAGE_ID,
618 coin::PAY_MODULE_NAME.to_owned(),
619 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
620 type_args,
621 gas,
622 vec![
623 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
624 CallArg::Pure(bcs::to_bytes(&split_count)?),
625 ],
626 gas_budget,
627 gas_price,
628 )
629 }
630
631 pub async fn merge_coins_tx_kind(
634 &self,
635 primary_coin: ObjectID,
636 coin_to_merge: ObjectID,
637 ) -> Result<TransactionKind, anyhow::Error> {
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_arguments = vec![coin.get_move_template_type()?];
647 let package = IOTA_FRAMEWORK_PACKAGE_ID;
648 let module = coin::COIN_MODULE_NAME.to_owned();
649 let function = coin::COIN_JOIN_FUNC_NAME.to_owned();
650 let arguments = vec![
651 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
652 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
653 ];
654 let pt = {
655 let mut builder = ProgrammableTransactionBuilder::new();
656 builder.move_call(package, module, function, type_arguments, arguments)?;
657 builder.finish()
658 };
659 let tx_kind = TransactionKind::programmable(pt);
660 Ok(tx_kind)
661 }
662
663 pub async fn merge_coins(
665 &self,
666 signer: IotaAddress,
667 primary_coin: ObjectID,
668 coin_to_merge: ObjectID,
669 gas: impl Into<Option<ObjectID>>,
670 gas_budget: u64,
671 ) -> anyhow::Result<TransactionData> {
672 let coin = self
673 .0
674 .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
675 .await?
676 .into_object()?;
677 let primary_coin_ref = coin.object_ref();
678 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
679 let coin: Object = coin.try_into()?;
680 let type_args = vec![coin.get_move_template_type()?];
681 let gas_price = self.0.get_reference_gas_price().await?;
682 let gas = self
683 .select_gas(
684 signer,
685 gas,
686 gas_budget,
687 vec![primary_coin, coin_to_merge],
688 gas_price,
689 )
690 .await?;
691
692 TransactionData::new_move_call(
693 signer,
694 IOTA_FRAMEWORK_PACKAGE_ID,
695 coin::COIN_MODULE_NAME.to_owned(),
696 coin::COIN_JOIN_FUNC_NAME.to_owned(),
697 type_args,
698 gas,
699 vec![
700 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
701 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
702 ],
703 gas_budget,
704 gas_price,
705 )
706 }
707
708 pub async fn batch_transaction(
710 &self,
711 signer: IotaAddress,
712 single_transaction_params: Vec<RPCTransactionRequestParams>,
713 gas: impl Into<Option<ObjectID>>,
714 gas_budget: u64,
715 ) -> anyhow::Result<TransactionData> {
716 fp_ensure!(
717 !single_transaction_params.is_empty(),
718 UserInputError::InvalidBatchTransaction {
719 error: "Batch Transaction cannot be empty".to_owned(),
720 }
721 .into()
722 );
723 let mut builder = ProgrammableTransactionBuilder::new();
724 for param in single_transaction_params {
725 match param {
726 RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
727 self.single_transfer_object(&mut builder, param.object_id, param.recipient)
728 .await?
729 }
730 RPCTransactionRequestParams::MoveCallRequestParams(param) => {
731 self.single_move_call_with_ptb_inputs(
732 &mut builder,
733 param.package_object_id,
734 ¶m.module,
735 ¶m.function,
736 param.type_arguments,
737 param.arguments,
738 )
739 .await?
740 }
741 };
742 }
743 let pt = builder.finish();
744 let all_inputs = pt.input_objects()?;
745 let inputs = all_inputs
746 .iter()
747 .flat_map(|obj| match obj {
748 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
749 _ => None,
750 })
751 .collect();
752 let gas_price = self.0.get_reference_gas_price().await?;
753 let gas = self
754 .select_gas(signer, gas, gas_budget, inputs, gas_price)
755 .await?;
756
757 Ok(TransactionData::new(
758 TransactionKind::programmable(pt),
759 signer,
760 gas,
761 gas_budget,
762 gas_price,
763 ))
764 }
765}