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