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