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