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 } => ObjectArg::SharedObject {
406 id,
407 initial_shared_version,
408 mutability: if is_mutable_ref {
409 SharedObjectMutability::Mutable
410 } else {
411 SharedObjectMutability::Immutable
412 },
413 },
414 Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => {
415 ObjectArg::ImmOrOwnedObject(obj_ref)
416 }
417 })
418 }
419
420 pub async fn resolve_and_checks_json_args(
421 &self,
422 builder: &mut ProgrammableTransactionBuilder,
423 package_id: ObjectID,
424 module: &Identifier,
425 function: &Identifier,
426 type_args: &[TypeTag],
427 json_args: Vec<SuiJsonValue>,
428 ) -> Result<Vec<Argument>, anyhow::Error> {
429 let object = self.0.get_object(package_id).await?;
430 let sui_types::object::Data::Package(package) = &object.data else {
431 bail!(
432 "Bcs field in object [{}] is missing or not a package.",
433 package_id
434 );
435 };
436
437 let json_args_and_tokens = resolve_move_function_args(
438 package,
439 module.clone(),
440 function.clone(),
441 type_args,
442 json_args,
443 )?;
444
445 let mut args = Vec::new();
446 let mut objects = BTreeMap::new();
447 let module = package.deserialize_module(module, &BinaryConfig::standard())?;
448 for (arg, expected_type) in json_args_and_tokens {
449 args.push(match arg {
450 ResolvedCallArg::Pure(p) => builder.input(CallArg::Pure(p)),
451
452 ResolvedCallArg::Object(id) => builder.input(CallArg::Object(
453 self.get_object_arg(
454 id,
455 &mut objects,
456 matches!(expected_type, SignatureToken::MutableReference(_))
458 || !expected_type.is_reference(),
459 &module,
460 &expected_type,
461 )
462 .await?,
463 )),
464
465 ResolvedCallArg::ObjVec(v) => {
466 let mut object_ids = vec![];
467 for id in v {
468 object_ids.push(
469 self.get_object_arg(
470 id,
471 &mut objects,
472 false,
473 &module,
474 &expected_type,
475 )
476 .await?,
477 )
478 }
479 builder.make_obj_vec(object_ids)
480 }
481 }?);
482 }
483
484 Ok(args)
485 }
486
487 pub async fn publish_tx_kind(
488 &self,
489 sender: SuiAddress,
490 modules: Vec<Vec<u8>>,
491 dep_ids: Vec<ObjectID>,
492 ) -> Result<TransactionKind, anyhow::Error> {
493 let pt = {
494 let mut builder = ProgrammableTransactionBuilder::new();
495 let upgrade_cap = builder.publish_upgradeable(modules, dep_ids);
496 builder.transfer_arg(sender, upgrade_cap);
497 builder.finish()
498 };
499 Ok(TransactionKind::programmable(pt))
500 }
501
502 pub async fn publish(
503 &self,
504 sender: SuiAddress,
505 compiled_modules: Vec<Vec<u8>>,
506 dep_ids: Vec<ObjectID>,
507 gas: Option<ObjectID>,
508 gas_budget: u64,
509 ) -> anyhow::Result<TransactionData> {
510 let gas_price = self.0.get_reference_gas_price().await?;
511 let gas = self
512 .select_gas(sender, gas, gas_budget, vec![], gas_price)
513 .await?;
514 Ok(TransactionData::new_module(
515 sender,
516 gas,
517 compiled_modules,
518 dep_ids,
519 gas_budget,
520 gas_price,
521 ))
522 }
523
524 pub async fn upgrade_tx_kind(
525 &self,
526 package_id: ObjectID,
527 modules: Vec<Vec<u8>>,
528 dep_ids: Vec<ObjectID>,
529 upgrade_capability: ObjectID,
530 upgrade_policy: u8,
531 digest: Vec<u8>,
532 ) -> Result<TransactionKind, anyhow::Error> {
533 let upgrade_capability = self.0.get_object(upgrade_capability).await?;
534 let capability_owner = upgrade_capability.owner().clone();
535 let pt = {
536 let mut builder = ProgrammableTransactionBuilder::new();
537 let capability_arg = match capability_owner {
538 Owner::AddressOwner(_) => {
539 ObjectArg::ImmOrOwnedObject(upgrade_capability.compute_object_reference())
540 }
541 Owner::Shared {
542 initial_shared_version,
543 }
544 | Owner::ConsensusAddressOwner {
545 start_version: initial_shared_version,
546 ..
547 } => ObjectArg::SharedObject {
548 id: upgrade_capability.compute_object_reference().0,
549 initial_shared_version,
550 mutability: SharedObjectMutability::Mutable,
551 },
552 Owner::Immutable => {
553 bail!("Upgrade capability is stored immutably and cannot be used for upgrades")
554 }
555 Owner::ObjectOwner(_) => {
558 return Err(anyhow::anyhow!("Upgrade capability controlled by object"));
559 }
560 };
561 builder.obj(capability_arg).unwrap();
562 let upgrade_arg = builder.pure(upgrade_policy).unwrap();
563 let digest_arg = builder.pure(digest).unwrap();
564 let upgrade_ticket = builder.programmable_move_call(
565 SUI_FRAMEWORK_PACKAGE_ID,
566 ident_str!("package").to_owned(),
567 ident_str!("authorize_upgrade").to_owned(),
568 vec![],
569 vec![Argument::Input(0), upgrade_arg, digest_arg],
570 );
571 let upgrade_receipt = builder.upgrade(package_id, upgrade_ticket, dep_ids, modules);
572
573 builder.programmable_move_call(
574 SUI_FRAMEWORK_PACKAGE_ID,
575 ident_str!("package").to_owned(),
576 ident_str!("commit_upgrade").to_owned(),
577 vec![],
578 vec![Argument::Input(0), upgrade_receipt],
579 );
580
581 builder.finish()
582 };
583
584 Ok(TransactionKind::programmable(pt))
585 }
586
587 pub async fn upgrade(
588 &self,
589 sender: SuiAddress,
590 package_id: ObjectID,
591 compiled_modules: Vec<Vec<u8>>,
592 dep_ids: Vec<ObjectID>,
593 upgrade_capability: ObjectID,
594 upgrade_policy: u8,
595 digest: Vec<u8>,
596 gas: Option<ObjectID>,
597 gas_budget: u64,
598 ) -> anyhow::Result<TransactionData> {
599 let gas_price = self.0.get_reference_gas_price().await?;
600 let gas = self
601 .select_gas(sender, gas, gas_budget, vec![], gas_price)
602 .await?;
603 let upgrade_cap = self.0.get_object(upgrade_capability).await?;
604 let cap_owner = upgrade_cap.owner().clone();
605 TransactionData::new_upgrade(
606 sender,
607 gas,
608 package_id,
609 compiled_modules,
610 dep_ids,
611 (upgrade_cap.compute_object_reference(), cap_owner),
612 upgrade_policy,
613 digest,
614 gas_budget,
615 gas_price,
616 )
617 }
618
619 pub async fn split_coin_tx_kind(
623 &self,
624 coin_object_id: ObjectID,
625 split_amounts: Option<Vec<u64>>,
626 split_count: Option<u64>,
627 ) -> Result<TransactionKind, anyhow::Error> {
628 if split_amounts.is_none() && split_count.is_none() {
629 bail!(
630 "Either split_amounts or split_count must be provided for split_coin transaction."
631 );
632 }
633 let coin = self.0.get_object(coin_object_id).await?;
634 let coin_object_ref = coin.compute_object_reference();
635 let type_args = vec![coin.get_move_template_type()?];
636 let package = SUI_FRAMEWORK_PACKAGE_ID;
637 let module = coin::PAY_MODULE_NAME.to_owned();
638
639 let (arguments, function) = if let Some(split_amounts) = split_amounts {
640 (
641 vec![
642 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
643 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
644 ],
645 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
646 )
647 } else {
648 (
649 vec![
650 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
651 CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
652 ],
653 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
654 )
655 };
656 let mut builder = ProgrammableTransactionBuilder::new();
657 builder.move_call(package, module, function, type_args, arguments)?;
658 let pt = builder.finish();
659 let tx_kind = TransactionKind::programmable(pt);
660 Ok(tx_kind)
661 }
662
663 pub async fn split_coin(
665 &self,
666 signer: SuiAddress,
667 coin_object_id: ObjectID,
668 split_amounts: Vec<u64>,
669 gas: Option<ObjectID>,
670 gas_budget: u64,
671 ) -> anyhow::Result<TransactionData> {
672 let coin = self.0.get_object(coin_object_id).await?;
673 let coin_object_ref = coin.compute_object_reference();
674 let type_args = vec![coin.get_move_template_type()?];
675 let gas_price = self.0.get_reference_gas_price().await?;
676 let gas = self
677 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
678 .await?;
679
680 TransactionData::new_move_call(
681 signer,
682 SUI_FRAMEWORK_PACKAGE_ID,
683 coin::PAY_MODULE_NAME.to_owned(),
684 coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
685 type_args,
686 gas,
687 vec![
688 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
689 CallArg::Pure(bcs::to_bytes(&split_amounts)?),
690 ],
691 gas_budget,
692 gas_price,
693 )
694 }
695
696 pub async fn split_coin_equal(
698 &self,
699 signer: SuiAddress,
700 coin_object_id: ObjectID,
701 split_count: u64,
702 gas: Option<ObjectID>,
703 gas_budget: u64,
704 ) -> anyhow::Result<TransactionData> {
705 let coin = self.0.get_object(coin_object_id).await?;
706 let coin_object_ref = coin.compute_object_reference();
707 let type_args = vec![coin.get_move_template_type()?];
708 let gas_price = self.0.get_reference_gas_price().await?;
709 let gas = self
710 .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
711 .await?;
712
713 TransactionData::new_move_call(
714 signer,
715 SUI_FRAMEWORK_PACKAGE_ID,
716 coin::PAY_MODULE_NAME.to_owned(),
717 coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
718 type_args,
719 gas,
720 vec![
721 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
722 CallArg::Pure(bcs::to_bytes(&split_count)?),
723 ],
724 gas_budget,
725 gas_price,
726 )
727 }
728
729 pub async fn merge_coins_tx_kind(
730 &self,
731 primary_coin: ObjectID,
732 coin_to_merge: ObjectID,
733 ) -> Result<TransactionKind, anyhow::Error> {
734 let coin = self.0.get_object(primary_coin).await?;
735 let primary_coin_ref = coin.compute_object_reference();
736 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
737 let type_arguments = vec![coin.get_move_template_type()?];
738 let package = SUI_FRAMEWORK_PACKAGE_ID;
739 let module = coin::PAY_MODULE_NAME.to_owned();
740 let function = coin::PAY_JOIN_FUNC_NAME.to_owned();
741 let arguments = vec![
742 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
743 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
744 ];
745 let pt = {
746 let mut builder = ProgrammableTransactionBuilder::new();
747 builder.move_call(package, module, function, type_arguments, arguments)?;
748 builder.finish()
749 };
750 let tx_kind = TransactionKind::programmable(pt);
751 Ok(tx_kind)
752 }
753
754 pub async fn merge_coins(
756 &self,
757 signer: SuiAddress,
758 primary_coin: ObjectID,
759 coin_to_merge: ObjectID,
760 gas: Option<ObjectID>,
761 gas_budget: u64,
762 ) -> anyhow::Result<TransactionData> {
763 let coin = self.0.get_object(primary_coin).await?;
764 let primary_coin_ref = coin.compute_object_reference();
765 let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
766 let type_args = vec![coin.get_move_template_type()?];
767 let gas_price = self.0.get_reference_gas_price().await?;
768 let gas = self
769 .select_gas(
770 signer,
771 gas,
772 gas_budget,
773 vec![primary_coin, coin_to_merge],
774 gas_price,
775 )
776 .await?;
777
778 TransactionData::new_move_call(
779 signer,
780 SUI_FRAMEWORK_PACKAGE_ID,
781 coin::PAY_MODULE_NAME.to_owned(),
782 coin::PAY_JOIN_FUNC_NAME.to_owned(),
783 type_args,
784 gas,
785 vec![
786 CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
787 CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
788 ],
789 gas_budget,
790 gas_price,
791 )
792 }
793
794 pub async fn batch_transaction(
795 &self,
796 signer: SuiAddress,
797 single_transaction_params: Vec<RPCTransactionRequestParams>,
798 gas: Option<ObjectID>,
799 gas_budget: u64,
800 ) -> anyhow::Result<TransactionData> {
801 fp_ensure!(
802 !single_transaction_params.is_empty(),
803 UserInputError::InvalidBatchTransaction {
804 error: "Batch Transaction cannot be empty".to_owned(),
805 }
806 .into()
807 );
808 let mut builder = ProgrammableTransactionBuilder::new();
809 for param in single_transaction_params {
810 match param {
811 RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
812 self.single_transfer_object(&mut builder, param.object_id, param.recipient)
813 .await?
814 }
815 RPCTransactionRequestParams::MoveCallRequestParams(param) => {
816 self.single_move_call(
817 &mut builder,
818 param.package_object_id,
819 ¶m.module,
820 ¶m.function,
821 param.type_arguments,
822 param.arguments,
823 )
824 .await?
825 }
826 };
827 }
828 let pt = builder.finish();
829 let all_inputs = pt.input_objects()?;
830 let inputs = all_inputs
831 .iter()
832 .flat_map(|obj| match obj {
833 InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
834 _ => None,
835 })
836 .collect();
837 let gas_price = self.0.get_reference_gas_price().await?;
838 let gas = self
839 .select_gas(signer, gas, gas_budget, inputs, gas_price)
840 .await?;
841
842 Ok(TransactionData::new(
843 TransactionKind::programmable(pt),
844 signer,
845 gas,
846 gas_budget,
847 gas_price,
848 ))
849 }
850
851 pub async fn request_add_stake(
852 &self,
853 signer: SuiAddress,
854 mut coins: Vec<ObjectID>,
855 amount: Option<u64>,
856 validator: SuiAddress,
857 gas: Option<ObjectID>,
858 gas_budget: u64,
859 ) -> anyhow::Result<TransactionData> {
860 let gas_price = self.0.get_reference_gas_price().await?;
861 let gas = self
862 .select_gas(signer, gas, gas_budget, coins.clone(), gas_price)
863 .await?;
864
865 let mut obj_vec = vec![];
866 let coin = coins
867 .pop()
868 .ok_or_else(|| anyhow!("Coins input should contain at lease one coin object."))?;
869 let (oref, coin_type) = self.get_object_ref_and_type(coin).await?;
870
871 let ObjectType::Struct(type_) = &coin_type else {
872 return Err(anyhow!("Provided object [{coin}] is not a move object."));
873 };
874 ensure!(
875 type_.is_coin(),
876 "Expecting either Coin<T> input coin objects. Received [{type_}]"
877 );
878
879 for coin in coins {
880 let (oref, type_) = self.get_object_ref_and_type(coin).await?;
881 ensure!(
882 type_ == coin_type,
883 "All coins should be the same type, expecting {coin_type}, got {type_}."
884 );
885 obj_vec.push(ObjectArg::ImmOrOwnedObject(oref))
886 }
887 obj_vec.push(ObjectArg::ImmOrOwnedObject(oref));
888
889 let pt = {
890 let mut builder = ProgrammableTransactionBuilder::new();
891 let arguments = vec![
892 builder.input(CallArg::SUI_SYSTEM_MUT).unwrap(),
893 builder.make_obj_vec(obj_vec)?,
894 builder
895 .input(CallArg::Pure(bcs::to_bytes(&amount)?))
896 .unwrap(),
897 builder
898 .input(CallArg::Pure(bcs::to_bytes(&validator)?))
899 .unwrap(),
900 ];
901 builder.command(Command::move_call(
902 SUI_SYSTEM_PACKAGE_ID,
903 SUI_SYSTEM_MODULE_NAME.to_owned(),
904 ADD_STAKE_MUL_COIN_FUN_NAME.to_owned(),
905 vec![],
906 arguments,
907 ));
908 builder.finish()
909 };
910 Ok(TransactionData::new_programmable(
911 signer,
912 vec![gas],
913 pt,
914 gas_budget,
915 gas_price,
916 ))
917 }
918
919 pub async fn request_withdraw_stake(
920 &self,
921 signer: SuiAddress,
922 staked_sui: ObjectID,
923 gas: Option<ObjectID>,
924 gas_budget: u64,
925 ) -> anyhow::Result<TransactionData> {
926 let staked_sui = self.get_object_ref(staked_sui).await?;
927 let gas_price = self.0.get_reference_gas_price().await?;
928 let gas = self
929 .select_gas(signer, gas, gas_budget, vec![], gas_price)
930 .await?;
931 TransactionData::new_move_call(
932 signer,
933 SUI_SYSTEM_PACKAGE_ID,
934 SUI_SYSTEM_MODULE_NAME.to_owned(),
935 WITHDRAW_STAKE_FUN_NAME.to_owned(),
936 vec![],
937 gas,
938 vec![
939 CallArg::SUI_SYSTEM_MUT,
940 CallArg::Object(ObjectArg::ImmOrOwnedObject(staked_sui)),
941 ],
942 gas_budget,
943 gas_price,
944 )
945 }
946
947 pub async fn get_object_ref(&self, object_id: ObjectID) -> anyhow::Result<ObjectRef> {
949 self.get_object_ref_and_type(object_id)
950 .await
951 .map(|(oref, _)| oref)
952 }
953
954 pub async fn get_full_object_ref(&self, object_id: ObjectID) -> anyhow::Result<FullObjectRef> {
955 let object_data = self.0.get_object(object_id).await?;
956
957 Ok(object_data.compute_full_object_reference())
958 }
959
960 async fn get_object_ref_and_type(
961 &self,
962 object_id: ObjectID,
963 ) -> anyhow::Result<(ObjectRef, ObjectType)> {
964 let object = self.0.get_object(object_id).await?;
965
966 Ok((object.compute_object_reference(), ObjectType::from(&object)))
967 }
968
969 pub async fn get_full_object_ref_and_type(
970 &self,
971 object_id: ObjectID,
972 ) -> anyhow::Result<(FullObjectRef, ObjectType)> {
973 let object = self.0.get_object(object_id).await?;
974
975 let object_type = ObjectType::from(&object);
976
977 let full_object_ref = object.compute_full_object_reference();
978 Ok((full_object_ref, object_type))
979 }
980}