1use std::collections::BTreeMap;
5use std::result::Result;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use anyhow::{Ok, anyhow, bail, ensure};
10use async_trait::async_trait;
11use futures::future::join_all;
12use move_binary_format::CompiledModule;
13use move_binary_format::binary_config::BinaryConfig;
14use move_binary_format::file_format::SignatureToken;
15use move_core_types::ident_str;
16use move_core_types::identifier::Identifier;
17use move_core_types::language_storage::{StructTag, TypeTag};
18use sui_json::{ResolvedCallArg, SuiJsonValue, is_receiving_argument, resolve_move_function_args};
19use sui_json_rpc_types::{
20 RPCTransactionRequestParams, SuiData, SuiObjectDataOptions, SuiObjectResponse, SuiRawData,
21 SuiTypeTag,
22};
23use sui_protocol_config::ProtocolConfig;
24use sui_types::base_types::{
25 FullObjectRef, ObjectID, ObjectInfo, ObjectRef, ObjectType, SuiAddress,
26};
27use sui_types::error::UserInputError;
28use sui_types::gas_coin::GasCoin;
29use sui_types::governance::{ADD_STAKE_MUL_COIN_FUN_NAME, WITHDRAW_STAKE_FUN_NAME};
30use sui_types::move_package::MovePackage;
31use sui_types::object::{Object, Owner};
32use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
33use sui_types::sui_system_state::SUI_SYSTEM_MODULE_NAME;
34use sui_types::transaction::{
35 Argument, CallArg, Command, InputObjectKind, ObjectArg, SharedObjectMutability,
36 TransactionData, TransactionKind,
37};
38use sui_types::{SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, coin, fp_ensure};
39
40#[async_trait]
41pub trait DataReader {
42 async fn get_owned_objects(
43 &self,
44 address: SuiAddress,
45 object_type: StructTag,
46 ) -> Result<Vec<ObjectInfo>, anyhow::Error>;
47
48 async fn get_object_with_options(
49 &self,
50 object_id: ObjectID,
51 options: SuiObjectDataOptions,
52 ) -> Result<SuiObjectResponse, anyhow::Error>;
53
54 async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error>;
55}
56
57#[derive(Clone)]
58pub struct TransactionBuilder(Arc<dyn DataReader + Sync + Send>);
59
60impl TransactionBuilder {
61 pub fn new(data_reader: Arc<dyn DataReader + Sync + Send>) -> Self {
62 Self(data_reader)
63 }
64
65 pub async fn select_gas(
66 &self,
67 signer: SuiAddress,
68 input_gas: Option<ObjectID>,
69 gas_budget: u64,
70 input_objects: Vec<ObjectID>,
71 gas_price: u64,
72 ) -> Result<ObjectRef, anyhow::Error> {
73 if gas_budget < gas_price {
74 bail!(
75 "Gas budget {gas_budget} is less than the reference gas price {gas_price}. The gas budget must be at least the current reference gas price of {gas_price}."
76 )
77 }
78 if let Some(gas) = input_gas {
79 self.get_object_ref(gas).await
80 } else {
81 let gas_objs = self.0.get_owned_objects(signer, GasCoin::type_()).await?;
82
83 for obj in gas_objs {
84 let response = self
85 .0
86 .get_object_with_options(obj.object_id, SuiObjectDataOptions::new().with_bcs())
87 .await?;
88 let obj = response.object()?;
89 let gas: GasCoin = bcs::from_bytes(
90 &obj.bcs
91 .as_ref()
92 .ok_or_else(|| anyhow!("bcs field is unexpectedly empty"))?
93 .try_as_move()
94 .ok_or_else(|| anyhow!("Cannot parse move object to gas object"))?
95 .bcs_bytes,
96 )?;
97 if !input_objects.contains(&obj.object_id) && gas.value() >= gas_budget {
98 return Ok(obj.object_ref());
99 }
100 }
101 Err(anyhow!(
102 "Cannot find gas coin for signer address {signer} with amount sufficient for the required gas budget {gas_budget}. If you are using the pay or transfer commands, you can use pay-sui or transfer-sui commands instead, which will use the only object as gas payment."
103 ))
104 }
105 }
106
107 pub async fn transfer_object_tx_kind(
108 &self,
109 object_id: ObjectID,
110 recipient: SuiAddress,
111 ) -> Result<TransactionKind, anyhow::Error> {
112 let full_obj_ref = self.get_full_object_ref(object_id).await?;
113 let mut builder = ProgrammableTransactionBuilder::new();
114 builder.transfer_object(recipient, full_obj_ref)?;
115 Ok(TransactionKind::programmable(builder.finish()))
116 }
117
118 pub async fn transfer_object(
119 &self,
120 signer: SuiAddress,
121 object_id: ObjectID,
122 gas: Option<ObjectID>,
123 gas_budget: u64,
124 recipient: SuiAddress,
125 ) -> anyhow::Result<TransactionData> {
126 let mut builder = ProgrammableTransactionBuilder::new();
127 self.single_transfer_object(&mut builder, object_id, recipient)
128 .await?;
129 let gas_price = self.0.get_reference_gas_price().await?;
130 let gas = self
131 .select_gas(signer, gas, gas_budget, vec![object_id], gas_price)
132 .await?;
133
134 Ok(TransactionData::new(
135 TransactionKind::programmable(builder.finish()),
136 signer,
137 gas,
138 gas_budget,
139 gas_price,
140 ))
141 }
142
143 async fn single_transfer_object(
144 &self,
145 builder: &mut ProgrammableTransactionBuilder,
146 object_id: ObjectID,
147 recipient: SuiAddress,
148 ) -> anyhow::Result<()> {
149 builder.transfer_object(recipient, self.get_full_object_ref(object_id).await?)?;
150 Ok(())
151 }
152
153 pub fn transfer_sui_tx_kind(
154 &self,
155 recipient: SuiAddress,
156 amount: Option<u64>,
157 ) -> TransactionKind {
158 let mut builder = ProgrammableTransactionBuilder::new();
159 builder.transfer_sui(recipient, amount);
160 let pt = builder.finish();
161 TransactionKind::programmable(pt)
162 }
163
164 pub async fn transfer_sui(
165 &self,
166 signer: SuiAddress,
167 sui_object_id: ObjectID,
168 gas_budget: u64,
169 recipient: SuiAddress,
170 amount: Option<u64>,
171 ) -> anyhow::Result<TransactionData> {
172 let object = self.get_object_ref(sui_object_id).await?;
173 let gas_price = self.0.get_reference_gas_price().await?;
174 Ok(TransactionData::new_transfer_sui(
175 recipient, signer, amount, object, gas_budget, gas_price,
176 ))
177 }
178
179 pub async fn pay_tx_kind(
180 &self,
181 input_coins: Vec<ObjectID>,
182 recipients: Vec<SuiAddress>,
183 amounts: Vec<u64>,
184 ) -> Result<TransactionKind, anyhow::Error> {
185 let mut builder = ProgrammableTransactionBuilder::new();
186 let coins = self.input_refs(&input_coins).await?;
187 builder.pay(coins, recipients, amounts)?;
188 let pt = builder.finish();
189 Ok(TransactionKind::programmable(pt))
190 }
191 pub async fn pay(
192 &self,
193 signer: SuiAddress,
194 input_coins: Vec<ObjectID>,
195 recipients: Vec<SuiAddress>,
196 amounts: Vec<u64>,
197 gas: Option<ObjectID>,
198 gas_budget: u64,
199 ) -> anyhow::Result<TransactionData> {
200 if let Some(gas) = gas
201 && input_coins.contains(&gas)
202 {
203 return Err(anyhow!(
204 "Gas coin is in input coins of Pay transaction, use PaySui transaction instead!"
205 ));
206 }
207
208 let coin_refs = self.input_refs(&input_coins).await?;
209 let gas_price = self.0.get_reference_gas_price().await?;
210 let gas = self
211 .select_gas(signer, gas, gas_budget, input_coins, gas_price)
212 .await?;
213
214 TransactionData::new_pay(
215 signer, coin_refs, recipients, amounts, gas, gas_budget, gas_price,
216 )
217 }
218
219 pub async fn input_refs(&self, obj_ids: &[ObjectID]) -> Result<Vec<ObjectRef>, anyhow::Error> {
221 let handles: Vec<_> = obj_ids.iter().map(|id| self.get_object_ref(*id)).collect();
222 let obj_refs = join_all(handles)
223 .await
224 .into_iter()
225 .collect::<anyhow::Result<Vec<ObjectRef>>>()?;
226 Ok(obj_refs)
227 }
228
229 pub fn pay_sui_tx_kind(
231 &self,
232 recipients: Vec<SuiAddress>,
233 amounts: Vec<u64>,
234 ) -> Result<TransactionKind, anyhow::Error> {
235 let mut builder = ProgrammableTransactionBuilder::new();
236 builder.pay_sui(recipients.clone(), amounts.clone())?;
237 let pt = builder.finish();
238 let tx_kind = TransactionKind::programmable(pt);
239 Ok(tx_kind)
240 }
241
242 pub async fn pay_sui(
243 &self,
244 signer: SuiAddress,
245 input_coins: Vec<ObjectID>,
246 recipients: Vec<SuiAddress>,
247 amounts: Vec<u64>,
248 gas_budget: u64,
249 ) -> anyhow::Result<TransactionData> {
250 fp_ensure!(
251 !input_coins.is_empty(),
252 UserInputError::EmptyInputCoins.into()
253 );
254
255 let mut coin_refs = self.input_refs(&input_coins).await?;
256 let gas_object_ref = coin_refs.remove(0);
258 let gas_price = self.0.get_reference_gas_price().await?;
259 TransactionData::new_pay_sui(
260 signer,
261 coin_refs,
262 recipients,
263 amounts,
264 gas_object_ref,
265 gas_budget,
266 gas_price,
267 )
268 }
269
270 pub fn pay_all_sui_tx_kind(&self, recipient: SuiAddress) -> TransactionKind {
271 let mut builder = ProgrammableTransactionBuilder::new();
272 builder.pay_all_sui(recipient);
273 let pt = builder.finish();
274 TransactionKind::programmable(pt)
275 }
276
277 pub async fn pay_all_sui(
278 &self,
279 signer: SuiAddress,
280 input_coins: Vec<ObjectID>,
281 recipient: SuiAddress,
282 gas_budget: u64,
283 ) -> anyhow::Result<TransactionData> {
284 fp_ensure!(
285 !input_coins.is_empty(),
286 UserInputError::EmptyInputCoins.into()
287 );
288
289 let mut coin_refs = self.input_refs(&input_coins).await?;
290 let gas_object_ref = coin_refs.remove(0);
292 let gas_price = self.0.get_reference_gas_price().await?;
293 Ok(TransactionData::new_pay_all_sui(
294 signer,
295 coin_refs,
296 recipient,
297 gas_object_ref,
298 gas_budget,
299 gas_price,
300 ))
301 }
302
303 pub async fn move_call_tx_kind(
304 &self,
305 package_object_id: ObjectID,
306 module: &str,
307 function: &str,
308 type_args: Vec<SuiTypeTag>,
309 call_args: Vec<SuiJsonValue>,
310 ) -> Result<TransactionKind, anyhow::Error> {
311 let mut builder = ProgrammableTransactionBuilder::new();
312 self.single_move_call(
313 &mut builder,
314 package_object_id,
315 module,
316 function,
317 type_args,
318 call_args,
319 )
320 .await?;
321 let pt = builder.finish();
322 Ok(TransactionKind::programmable(pt))
323 }
324
325 pub async fn move_call(
326 &self,
327 signer: SuiAddress,
328 package_object_id: ObjectID,
329 module: &str,
330 function: &str,
331 type_args: Vec<SuiTypeTag>,
332 call_args: Vec<SuiJsonValue>,
333 gas: Option<ObjectID>,
334 gas_budget: u64,
335 gas_price: Option<u64>,
336 ) -> anyhow::Result<TransactionData> {
337 let mut builder = ProgrammableTransactionBuilder::new();
338 self.single_move_call(
339 &mut builder,
340 package_object_id,
341 module,
342 function,
343 type_args,
344 call_args,
345 )
346 .await?;
347 let pt = builder.finish();
348 let input_objects = pt
349 .input_objects()?
350 .iter()
351 .flat_map(|obj| match obj {
352 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
353 _ => None,
354 })
355 .collect();
356 let gas_price = if let Some(gas_price) = gas_price {
357 gas_price
358 } else {
359 self.0.get_reference_gas_price().await?
360 };
361 let gas = self
362 .select_gas(signer, gas, gas_budget, input_objects, gas_price)
363 .await?;
364
365 Ok(TransactionData::new(
366 TransactionKind::programmable(pt),
367 signer,
368 gas,
369 gas_budget,
370 gas_price,
371 ))
372 }
373
374 pub async fn single_move_call(
375 &self,
376 builder: &mut ProgrammableTransactionBuilder,
377 package: ObjectID,
378 module: &str,
379 function: &str,
380 type_args: Vec<SuiTypeTag>,
381 call_args: Vec<SuiJsonValue>,
382 ) -> anyhow::Result<()> {
383 let module = Identifier::from_str(module)?;
384 let function = Identifier::from_str(function)?;
385
386 let type_args = type_args
387 .into_iter()
388 .map(|ty| ty.try_into())
389 .collect::<Result<Vec<_>, _>>()?;
390
391 let call_args = self
392 .resolve_and_checks_json_args(
393 builder, package, &module, &function, &type_args, call_args,
394 )
395 .await?;
396
397 builder.command(Command::move_call(
398 package, module, function, type_args, call_args,
399 ));
400 Ok(())
401 }
402
403 async fn get_object_arg(
404 &self,
405 id: ObjectID,
406 objects: &mut BTreeMap<ObjectID, Object>,
407 is_mutable_ref: bool,
408 view: &CompiledModule,
409 arg_type: &SignatureToken,
410 ) -> Result<ObjectArg, anyhow::Error> {
411 let response = self
412 .0
413 .get_object_with_options(id, SuiObjectDataOptions::bcs_lossless())
414 .await?;
415
416 let obj: Object = response.into_object()?.try_into()?;
417 let obj_ref = obj.compute_object_reference();
418 let owner = obj.owner.clone();
419 objects.insert(id, obj);
420 if is_receiving_argument(view, arg_type) {
421 return Ok(ObjectArg::Receiving(obj_ref));
422 }
423 Ok(match owner {
424 Owner::Shared {
425 initial_shared_version,
426 }
427 | Owner::ConsensusAddressOwner {
428 start_version: initial_shared_version,
429 ..
430 } => ObjectArg::SharedObject {
431 id,
432 initial_shared_version,
433 mutability: if is_mutable_ref {
434 SharedObjectMutability::Mutable
435 } else {
436 SharedObjectMutability::Immutable
437 },
438 },
439 Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => {
440 ObjectArg::ImmOrOwnedObject(obj_ref)
441 }
442 })
443 }
444
445 pub async fn resolve_and_checks_json_args(
446 &self,
447 builder: &mut ProgrammableTransactionBuilder,
448 package_id: ObjectID,
449 module: &Identifier,
450 function: &Identifier,
451 type_args: &[TypeTag],
452 json_args: Vec<SuiJsonValue>,
453 ) -> Result<Vec<Argument>, anyhow::Error> {
454 let object = self
455 .0
456 .get_object_with_options(package_id, SuiObjectDataOptions::bcs_lossless())
457 .await?
458 .into_object()?;
459 let Some(SuiRawData::Package(package)) = object.bcs else {
460 bail!(
461 "Bcs field in object [{}] is missing or not a package.",
462 package_id
463 );
464 };
465 let package: MovePackage = MovePackage::new(
466 package.id,
467 object.version,
468 package.module_map,
469 ProtocolConfig::get_for_min_version().max_move_package_size(),
470 package.type_origin_table,
471 package.linkage_table,
472 )?;
473
474 let json_args_and_tokens = resolve_move_function_args(
475 &package,
476 module.clone(),
477 function.clone(),
478 type_args,
479 json_args,
480 )?;
481
482 let mut args = Vec::new();
483 let mut objects = BTreeMap::new();
484 let module = package.deserialize_module(module, &BinaryConfig::standard())?;
485 for (arg, expected_type) in json_args_and_tokens {
486 args.push(match arg {
487 ResolvedCallArg::Pure(p) => builder.input(CallArg::Pure(p)),
488
489 ResolvedCallArg::Object(id) => builder.input(CallArg::Object(
490 self.get_object_arg(
491 id,
492 &mut objects,
493 matches!(expected_type, SignatureToken::MutableReference(_))
495 || !expected_type.is_reference(),
496 &module,
497 &expected_type,
498 )
499 .await?,
500 )),
501
502 ResolvedCallArg::ObjVec(v) => {
503 let mut object_ids = vec![];
504 for id in v {
505 object_ids.push(
506 self.get_object_arg(
507 id,
508 &mut objects,
509 false,
510 &module,
511 &expected_type,
512 )
513 .await?,
514 )
515 }
516 builder.make_obj_vec(object_ids)
517 }
518 }?);
519 }
520
521 Ok(args)
522 }
523
524 pub async fn publish_tx_kind(
525 &self,
526 sender: SuiAddress,
527 modules: Vec<Vec<u8>>,
528 dep_ids: Vec<ObjectID>,
529 ) -> Result<TransactionKind, anyhow::Error> {
530 let pt = {
531 let mut builder = ProgrammableTransactionBuilder::new();
532 let upgrade_cap = builder.publish_upgradeable(modules, dep_ids);
533 builder.transfer_arg(sender, upgrade_cap);
534 builder.finish()
535 };
536 Ok(TransactionKind::programmable(pt))
537 }
538
539 pub async fn publish(
540 &self,
541 sender: SuiAddress,
542 compiled_modules: Vec<Vec<u8>>,
543 dep_ids: Vec<ObjectID>,
544 gas: Option<ObjectID>,
545 gas_budget: u64,
546 ) -> anyhow::Result<TransactionData> {
547 let gas_price = self.0.get_reference_gas_price().await?;
548 let gas = self
549 .select_gas(sender, gas, gas_budget, vec![], gas_price)
550 .await?;
551 Ok(TransactionData::new_module(
552 sender,
553 gas,
554 compiled_modules,
555 dep_ids,
556 gas_budget,
557 gas_price,
558 ))
559 }
560
561 pub async fn upgrade_tx_kind(
562 &self,
563 package_id: ObjectID,
564 modules: Vec<Vec<u8>>,
565 dep_ids: Vec<ObjectID>,
566 upgrade_capability: ObjectID,
567 upgrade_policy: u8,
568 digest: Vec<u8>,
569 ) -> Result<TransactionKind, anyhow::Error> {
570 let upgrade_capability = self
571 .0
572 .get_object_with_options(upgrade_capability, SuiObjectDataOptions::new().with_owner())
573 .await?
574 .into_object()?;
575 let capability_owner = upgrade_capability
576 .owner
577 .clone()
578 .ok_or_else(|| anyhow!("Unable to determine ownership of upgrade capability"))?;
579 let pt = {
580 let mut builder = ProgrammableTransactionBuilder::new();
581 let capability_arg = match capability_owner {
582 Owner::AddressOwner(_) => {
583 ObjectArg::ImmOrOwnedObject(upgrade_capability.object_ref())
584 }
585 Owner::Shared {
586 initial_shared_version,
587 }
588 | Owner::ConsensusAddressOwner {
589 start_version: initial_shared_version,
590 ..
591 } => ObjectArg::SharedObject {
592 id: upgrade_capability.object_ref().0,
593 initial_shared_version,
594 mutability: SharedObjectMutability::Mutable,
595 },
596 Owner::Immutable => {
597 bail!("Upgrade capability is stored immutably and cannot be used for upgrades")
598 }
599 Owner::ObjectOwner(_) => {
602 return Err(anyhow::anyhow!("Upgrade capability controlled by object"));
603 }
604 };
605 builder.obj(capability_arg).unwrap();
606 let upgrade_arg = builder.pure(upgrade_policy).unwrap();
607 let digest_arg = builder.pure(digest).unwrap();
608 let upgrade_ticket = builder.programmable_move_call(
609 SUI_FRAMEWORK_PACKAGE_ID,
610 ident_str!("package").to_owned(),
611 ident_str!("authorize_upgrade").to_owned(),
612 vec![],
613 vec![Argument::Input(0), upgrade_arg, digest_arg],
614 );
615 let upgrade_receipt = builder.upgrade(package_id, upgrade_ticket, dep_ids, modules);
616
617 builder.programmable_move_call(
618 SUI_FRAMEWORK_PACKAGE_ID,
619 ident_str!("package").to_owned(),
620 ident_str!("commit_upgrade").to_owned(),
621 vec![],
622 vec![Argument::Input(0), upgrade_receipt],
623 );
624
625 builder.finish()
626 };
627
628 Ok(TransactionKind::programmable(pt))
629 }
630
631 pub async fn upgrade(
632 &self,
633 sender: SuiAddress,
634 package_id: ObjectID,
635 compiled_modules: Vec<Vec<u8>>,
636 dep_ids: Vec<ObjectID>,
637 upgrade_capability: ObjectID,
638 upgrade_policy: u8,
639 digest: Vec<u8>,
640 gas: Option<ObjectID>,
641 gas_budget: u64,
642 ) -> anyhow::Result<TransactionData> {
643 let gas_price = self.0.get_reference_gas_price().await?;
644 let gas = self
645 .select_gas(sender, gas, gas_budget, vec![], gas_price)
646 .await?;
647 let upgrade_cap = self
648 .0
649 .get_object_with_options(upgrade_capability, SuiObjectDataOptions::new().with_owner())
650 .await?
651 .into_object()?;
652 let cap_owner = upgrade_cap
653 .owner
654 .clone()
655 .ok_or_else(|| anyhow!("Unable to determine ownership of upgrade capability"))?;
656 TransactionData::new_upgrade(
657 sender,
658 gas,
659 package_id,
660 compiled_modules,
661 dep_ids,
662 (upgrade_cap.object_ref(), cap_owner),
663 upgrade_policy,
664 digest,
665 gas_budget,
666 gas_price,
667 )
668 }
669
670 pub async fn split_coin_tx_kind(
674 &self,
675 coin_object_id: ObjectID,
676 split_amounts: Option<Vec<u64>>,
677 split_count: Option<u64>,
678 ) -> Result<TransactionKind, anyhow::Error> {
679 if split_amounts.is_none() && split_count.is_none() {
680 bail!(
681 "Either split_amounts or split_count must be provided for split_coin transaction."
682 );
683 }
684 let coin = self
685 .0
686 .get_object_with_options(coin_object_id, SuiObjectDataOptions::bcs_lossless())
687 .await?
688 .into_object()?;
689 let coin_object_ref = coin.object_ref();
690 let coin: Object = coin.try_into()?;
691 let type_args = vec![coin.get_move_template_type()?];
692 let package = SUI_FRAMEWORK_PACKAGE_ID;
693 let module = coin::PAY_MODULE_NAME.to_owned();
694
695 let (arguments, function) = if let Some(split_amounts) = split_amounts {
696 (
697 vec![
698 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
699 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
700 ],
701 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
702 )
703 } else {
704 (
705 vec![
706 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
707 CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
708 ],
709 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
710 )
711 };
712 let mut builder = ProgrammableTransactionBuilder::new();
713 builder.move_call(package, module, function, type_args, arguments)?;
714 let pt = builder.finish();
715 let tx_kind = TransactionKind::programmable(pt);
716 Ok(tx_kind)
717 }
718
719 pub async fn split_coin(
721 &self,
722 signer: SuiAddress,
723 coin_object_id: ObjectID,
724 split_amounts: Vec<u64>,
725 gas: Option<ObjectID>,
726 gas_budget: u64,
727 ) -> anyhow::Result<TransactionData> {
728 let coin = self
729 .0
730 .get_object_with_options(coin_object_id, SuiObjectDataOptions::bcs_lossless())
731 .await?
732 .into_object()?;
733 let coin_object_ref = coin.object_ref();
734 let coin: Object = coin.try_into()?;
735 let type_args = vec![coin.get_move_template_type()?];
736 let gas_price = self.0.get_reference_gas_price().await?;
737 let gas = self
738 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
739 .await?;
740
741 TransactionData::new_move_call(
742 signer,
743 SUI_FRAMEWORK_PACKAGE_ID,
744 coin::PAY_MODULE_NAME.to_owned(),
745 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
746 type_args,
747 gas,
748 vec![
749 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
750 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
751 ],
752 gas_budget,
753 gas_price,
754 )
755 }
756
757 pub async fn split_coin_equal(
759 &self,
760 signer: SuiAddress,
761 coin_object_id: ObjectID,
762 split_count: u64,
763 gas: Option<ObjectID>,
764 gas_budget: u64,
765 ) -> anyhow::Result<TransactionData> {
766 let coin = self
767 .0
768 .get_object_with_options(coin_object_id, SuiObjectDataOptions::bcs_lossless())
769 .await?
770 .into_object()?;
771 let coin_object_ref = coin.object_ref();
772 let coin: Object = coin.try_into()?;
773 let type_args = vec![coin.get_move_template_type()?];
774 let gas_price = self.0.get_reference_gas_price().await?;
775 let gas = self
776 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
777 .await?;
778
779 TransactionData::new_move_call(
780 signer,
781 SUI_FRAMEWORK_PACKAGE_ID,
782 coin::PAY_MODULE_NAME.to_owned(),
783 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
784 type_args,
785 gas,
786 vec![
787 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
788 CallArg::Pure(bcs::to_bytes(&split_count)?),
789 ],
790 gas_budget,
791 gas_price,
792 )
793 }
794
795 pub async fn merge_coins_tx_kind(
796 &self,
797 primary_coin: ObjectID,
798 coin_to_merge: ObjectID,
799 ) -> Result<TransactionKind, anyhow::Error> {
800 let coin = self
801 .0
802 .get_object_with_options(primary_coin, SuiObjectDataOptions::bcs_lossless())
803 .await?
804 .into_object()?;
805 let primary_coin_ref = coin.object_ref();
806 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
807 let coin: Object = coin.try_into()?;
808 let type_arguments = vec![coin.get_move_template_type()?];
809 let package = SUI_FRAMEWORK_PACKAGE_ID;
810 let module = coin::PAY_MODULE_NAME.to_owned();
811 let function = coin::PAY_JOIN_FUNC_NAME.to_owned();
812 let arguments = vec![
813 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
814 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
815 ];
816 let pt = {
817 let mut builder = ProgrammableTransactionBuilder::new();
818 builder.move_call(package, module, function, type_arguments, arguments)?;
819 builder.finish()
820 };
821 let tx_kind = TransactionKind::programmable(pt);
822 Ok(tx_kind)
823 }
824
825 pub async fn merge_coins(
827 &self,
828 signer: SuiAddress,
829 primary_coin: ObjectID,
830 coin_to_merge: ObjectID,
831 gas: Option<ObjectID>,
832 gas_budget: u64,
833 ) -> anyhow::Result<TransactionData> {
834 let coin = self
835 .0
836 .get_object_with_options(primary_coin, SuiObjectDataOptions::bcs_lossless())
837 .await?
838 .into_object()?;
839 let primary_coin_ref = coin.object_ref();
840 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
841 let coin: Object = coin.try_into()?;
842 let type_args = vec![coin.get_move_template_type()?];
843 let gas_price = self.0.get_reference_gas_price().await?;
844 let gas = self
845 .select_gas(
846 signer,
847 gas,
848 gas_budget,
849 vec![primary_coin, coin_to_merge],
850 gas_price,
851 )
852 .await?;
853
854 TransactionData::new_move_call(
855 signer,
856 SUI_FRAMEWORK_PACKAGE_ID,
857 coin::PAY_MODULE_NAME.to_owned(),
858 coin::PAY_JOIN_FUNC_NAME.to_owned(),
859 type_args,
860 gas,
861 vec![
862 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
863 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
864 ],
865 gas_budget,
866 gas_price,
867 )
868 }
869
870 pub async fn batch_transaction(
871 &self,
872 signer: SuiAddress,
873 single_transaction_params: Vec<RPCTransactionRequestParams>,
874 gas: Option<ObjectID>,
875 gas_budget: u64,
876 ) -> anyhow::Result<TransactionData> {
877 fp_ensure!(
878 !single_transaction_params.is_empty(),
879 UserInputError::InvalidBatchTransaction {
880 error: "Batch Transaction cannot be empty".to_owned(),
881 }
882 .into()
883 );
884 let mut builder = ProgrammableTransactionBuilder::new();
885 for param in single_transaction_params {
886 match param {
887 RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
888 self.single_transfer_object(&mut builder, param.object_id, param.recipient)
889 .await?
890 }
891 RPCTransactionRequestParams::MoveCallRequestParams(param) => {
892 self.single_move_call(
893 &mut builder,
894 param.package_object_id,
895 ¶m.module,
896 ¶m.function,
897 param.type_arguments,
898 param.arguments,
899 )
900 .await?
901 }
902 };
903 }
904 let pt = builder.finish();
905 let all_inputs = pt.input_objects()?;
906 let inputs = all_inputs
907 .iter()
908 .flat_map(|obj| match obj {
909 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
910 _ => None,
911 })
912 .collect();
913 let gas_price = self.0.get_reference_gas_price().await?;
914 let gas = self
915 .select_gas(signer, gas, gas_budget, inputs, gas_price)
916 .await?;
917
918 Ok(TransactionData::new(
919 TransactionKind::programmable(pt),
920 signer,
921 gas,
922 gas_budget,
923 gas_price,
924 ))
925 }
926
927 pub async fn request_add_stake(
928 &self,
929 signer: SuiAddress,
930 mut coins: Vec<ObjectID>,
931 amount: Option<u64>,
932 validator: SuiAddress,
933 gas: Option<ObjectID>,
934 gas_budget: u64,
935 ) -> anyhow::Result<TransactionData> {
936 let gas_price = self.0.get_reference_gas_price().await?;
937 let gas = self
938 .select_gas(signer, gas, gas_budget, coins.clone(), gas_price)
939 .await?;
940
941 let mut obj_vec = vec![];
942 let coin = coins
943 .pop()
944 .ok_or_else(|| anyhow!("Coins input should contain at lease one coin object."))?;
945 let (oref, coin_type) = self.get_object_ref_and_type(coin).await?;
946
947 let ObjectType::Struct(type_) = &coin_type else {
948 return Err(anyhow!("Provided object [{coin}] is not a move object."));
949 };
950 ensure!(
951 type_.is_coin(),
952 "Expecting either Coin<T> input coin objects. Received [{type_}]"
953 );
954
955 for coin in coins {
956 let (oref, type_) = self.get_object_ref_and_type(coin).await?;
957 ensure!(
958 type_ == coin_type,
959 "All coins should be the same type, expecting {coin_type}, got {type_}."
960 );
961 obj_vec.push(ObjectArg::ImmOrOwnedObject(oref))
962 }
963 obj_vec.push(ObjectArg::ImmOrOwnedObject(oref));
964
965 let pt = {
966 let mut builder = ProgrammableTransactionBuilder::new();
967 let arguments = vec![
968 builder.input(CallArg::SUI_SYSTEM_MUT).unwrap(),
969 builder.make_obj_vec(obj_vec)?,
970 builder
971 .input(CallArg::Pure(bcs::to_bytes(&amount)?))
972 .unwrap(),
973 builder
974 .input(CallArg::Pure(bcs::to_bytes(&validator)?))
975 .unwrap(),
976 ];
977 builder.command(Command::move_call(
978 SUI_SYSTEM_PACKAGE_ID,
979 SUI_SYSTEM_MODULE_NAME.to_owned(),
980 ADD_STAKE_MUL_COIN_FUN_NAME.to_owned(),
981 vec![],
982 arguments,
983 ));
984 builder.finish()
985 };
986 Ok(TransactionData::new_programmable(
987 signer,
988 vec![gas],
989 pt,
990 gas_budget,
991 gas_price,
992 ))
993 }
994
995 pub async fn request_withdraw_stake(
996 &self,
997 signer: SuiAddress,
998 staked_sui: ObjectID,
999 gas: Option<ObjectID>,
1000 gas_budget: u64,
1001 ) -> anyhow::Result<TransactionData> {
1002 let staked_sui = self.get_object_ref(staked_sui).await?;
1003 let gas_price = self.0.get_reference_gas_price().await?;
1004 let gas = self
1005 .select_gas(signer, gas, gas_budget, vec![], gas_price)
1006 .await?;
1007 TransactionData::new_move_call(
1008 signer,
1009 SUI_SYSTEM_PACKAGE_ID,
1010 SUI_SYSTEM_MODULE_NAME.to_owned(),
1011 WITHDRAW_STAKE_FUN_NAME.to_owned(),
1012 vec![],
1013 gas,
1014 vec![
1015 CallArg::SUI_SYSTEM_MUT,
1016 CallArg::Object(ObjectArg::ImmOrOwnedObject(staked_sui)),
1017 ],
1018 gas_budget,
1019 gas_price,
1020 )
1021 }
1022
1023 pub async fn get_object_ref(&self, object_id: ObjectID) -> anyhow::Result<ObjectRef> {
1025 self.get_object_ref_and_type(object_id)
1026 .await
1027 .map(|(oref, _)| oref)
1028 }
1029
1030 pub async fn get_full_object_ref(&self, object_id: ObjectID) -> anyhow::Result<FullObjectRef> {
1031 let object_data = self
1032 .0
1033 .get_object_with_options(object_id, SuiObjectDataOptions::new().with_owner())
1034 .await?
1035 .into_object()?;
1036
1037 let object_ref = object_data.object_ref();
1038 let owner = object_data.owner.unwrap();
1039
1040 Ok(FullObjectRef::from_object_ref_and_owner(object_ref, &owner))
1041 }
1042
1043 async fn get_object_ref_and_type(
1044 &self,
1045 object_id: ObjectID,
1046 ) -> anyhow::Result<(ObjectRef, ObjectType)> {
1047 let object = self
1048 .0
1049 .get_object_with_options(object_id, SuiObjectDataOptions::new().with_type())
1050 .await?
1051 .into_object()?;
1052
1053 Ok((object.object_ref(), object.object_type()?))
1054 }
1055
1056 pub async fn get_full_object_ref_and_type(
1057 &self,
1058 object_id: ObjectID,
1059 ) -> anyhow::Result<(FullObjectRef, ObjectType)> {
1060 let object_data = self
1061 .0
1062 .get_object_with_options(
1063 object_id,
1064 SuiObjectDataOptions::new().with_owner().with_type(),
1065 )
1066 .await?
1067 .into_object()?;
1068
1069 let object_ref = object_data.object_ref();
1070 let object_type = object_data.object_type()?;
1071 let owner = object_data.owner.unwrap();
1072
1073 let full_object_ref = FullObjectRef::from_object_ref_and_owner(object_ref, &owner);
1074 Ok((full_object_ref, object_type))
1075 }
1076}