sui_transaction_builder/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    /// Get the object references for a list of object IDs
200    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    /// Construct a transaction kind for the PaySui transaction type
210    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        // [0] is safe because input_coins is non-empty and coins are of same length as input_coins.
237        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        // [0] is safe because input_coins is non-empty and coins are of same length as input_coins.
271        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                        // Is mutable if passed by mutable reference or by value
457                        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                                /* is_mutable_ref */ 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                // If the capability is owned by an object, then the module defining the owning
556                // object gets to decide how the upgrade capability should be used.
557                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    /// Construct a transaction kind for the SplitCoin transaction type
620    /// It expects that only one of the two: split_amounts or split_count is provided
621    /// If both are provided, it will use split_amounts.
622    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    // TODO: consolidate this with Pay transactions
664    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    // TODO: consolidate this with Pay transactions
697    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    // TODO: consolidate this with Pay transactions
755    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                        &param.module,
820                        &param.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    // TODO: we should add retrial to reduce the transaction building error rate
948    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}