iota_transaction_builder/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5pub mod package;
6pub mod stake;
7pub mod utils;
8
9use std::{result::Result, str::FromStr, sync::Arc};
10
11use anyhow::bail;
12use async_trait::async_trait;
13use iota_json::IotaJsonValue;
14use iota_json_rpc_types::{
15    IotaObjectDataOptions, IotaObjectResponse, IotaTypeTag, PtbInput, RPCTransactionRequestParams,
16};
17use iota_types::{
18    IOTA_FRAMEWORK_PACKAGE_ID,
19    base_types::{IotaAddress, ObjectID, ObjectInfo},
20    coin,
21    error::UserInputError,
22    fp_ensure,
23    object::Object,
24    programmable_transaction_builder::ProgrammableTransactionBuilder,
25    transaction::{CallArg, Command, InputObjectKind, ObjectArg, TransactionData, TransactionKind},
26};
27use move_core_types::{identifier::Identifier, language_storage::StructTag};
28
29#[async_trait]
30pub trait DataReader {
31    async fn get_owned_objects(
32        &self,
33        address: IotaAddress,
34        object_type: StructTag,
35    ) -> Result<Vec<ObjectInfo>, anyhow::Error>;
36
37    async fn get_object_with_options(
38        &self,
39        object_id: ObjectID,
40        options: IotaObjectDataOptions,
41    ) -> Result<IotaObjectResponse, anyhow::Error>;
42
43    async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error>;
44}
45
46#[derive(Clone)]
47pub struct TransactionBuilder(Arc<dyn DataReader + Sync + Send>);
48
49impl TransactionBuilder {
50    pub fn new(data_reader: Arc<dyn DataReader + Sync + Send>) -> Self {
51        Self(data_reader)
52    }
53
54    /// Construct the transaction data for a dry run
55    pub async fn tx_data_for_dry_run(
56        &self,
57        sender: IotaAddress,
58        kind: TransactionKind,
59        gas_budget: u64,
60        gas_price: u64,
61        gas_payment: impl Into<Option<Vec<ObjectID>>>,
62        gas_sponsor: impl Into<Option<IotaAddress>>,
63    ) -> TransactionData {
64        let gas_payment = self
65            .input_refs(gas_payment.into().unwrap_or_default().as_ref())
66            .await
67            .unwrap_or_default();
68        let gas_sponsor = gas_sponsor.into().unwrap_or(sender);
69        TransactionData::new_with_gas_coins_allow_sponsor(
70            kind,
71            sender,
72            gas_payment,
73            gas_budget,
74            gas_price,
75            gas_sponsor,
76        )
77    }
78
79    /// Construct the transaction data from a transaction kind, and other
80    /// parameters. If the gas_payment list is empty, it will pick the first
81    /// gas coin that has at least the required gas budget that is not in
82    /// the input coins.
83    pub async fn tx_data(
84        &self,
85        sender: IotaAddress,
86        kind: TransactionKind,
87        gas_budget: u64,
88        gas_price: u64,
89        gas_payment: Vec<ObjectID>,
90        gas_sponsor: impl Into<Option<IotaAddress>>,
91    ) -> Result<TransactionData, anyhow::Error> {
92        let gas_payment = if gas_payment.is_empty() {
93            let input_objs = kind
94                .input_objects()?
95                .iter()
96                .flat_map(|obj| match obj {
97                    InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
98                    _ => None,
99                })
100                .collect();
101            vec![
102                self.select_gas(sender, None, gas_budget, input_objs, gas_price)
103                    .await?,
104            ]
105        } else {
106            self.input_refs(&gas_payment).await?
107        };
108        Ok(TransactionData::new_with_gas_coins_allow_sponsor(
109            kind,
110            sender,
111            gas_payment,
112            gas_budget,
113            gas_price,
114            gas_sponsor.into().unwrap_or(sender),
115        ))
116    }
117
118    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains a
119    /// [`Command::TransferObjects`].
120    pub async fn transfer_object_tx_kind(
121        &self,
122        object_id: ObjectID,
123        recipient: IotaAddress,
124    ) -> Result<TransactionKind, anyhow::Error> {
125        let obj_ref = self.get_object_ref(object_id).await?;
126        let mut builder = ProgrammableTransactionBuilder::new();
127        builder.transfer_object(recipient, obj_ref)?;
128        Ok(TransactionKind::programmable(builder.finish()))
129    }
130
131    /// Transfer an object to the specified recipient address.
132    pub async fn transfer_object(
133        &self,
134        signer: IotaAddress,
135        object_id: ObjectID,
136        gas: impl Into<Option<ObjectID>>,
137        gas_budget: u64,
138        recipient: IotaAddress,
139    ) -> anyhow::Result<TransactionData> {
140        let mut builder = ProgrammableTransactionBuilder::new();
141        self.single_transfer_object(&mut builder, object_id, recipient)
142            .await?;
143        let gas_price = self.0.get_reference_gas_price().await?;
144        let gas = self
145            .select_gas(signer, gas, gas_budget, vec![object_id], gas_price)
146            .await?;
147
148        Ok(TransactionData::new(
149            TransactionKind::programmable(builder.finish()),
150            signer,
151            gas,
152            gas_budget,
153            gas_price,
154        ))
155    }
156
157    /// Add a [`Command::TransferObjects`] to the provided
158    /// [`ProgrammableTransactionBuilder`].
159    async fn single_transfer_object(
160        &self,
161        builder: &mut ProgrammableTransactionBuilder,
162        object_id: ObjectID,
163        recipient: IotaAddress,
164    ) -> anyhow::Result<()> {
165        builder.transfer_object(recipient, self.get_object_ref(object_id).await?)?;
166        Ok(())
167    }
168
169    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains a
170    /// [`Command::SplitCoins`] if some amount is provided and then transfers
171    /// the split amount or the whole gas object with
172    /// [`Command::TransferObjects`] to the recipient.
173    pub fn transfer_iota_tx_kind(
174        &self,
175        recipient: IotaAddress,
176        amount: impl Into<Option<u64>>,
177    ) -> TransactionKind {
178        let mut builder = ProgrammableTransactionBuilder::new();
179        builder.transfer_iota(recipient, amount.into());
180        let pt = builder.finish();
181        TransactionKind::programmable(pt)
182    }
183
184    /// Transfer IOTA from the provided coin object to the recipient address.
185    /// The provided coin object is also used for the gas payment.
186    pub async fn transfer_iota(
187        &self,
188        signer: IotaAddress,
189        iota_object_id: ObjectID,
190        gas_budget: u64,
191        recipient: IotaAddress,
192        amount: impl Into<Option<u64>>,
193    ) -> anyhow::Result<TransactionData> {
194        let object = self.get_object_ref(iota_object_id).await?;
195        let gas_price = self.0.get_reference_gas_price().await?;
196        Ok(TransactionData::new_transfer_iota(
197            recipient,
198            signer,
199            amount.into(),
200            object,
201            gas_budget,
202            gas_price,
203        ))
204    }
205
206    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains a
207    /// [`Command::MergeCoins`] if multiple inputs coins are provided and then a
208    /// [`Command::SplitCoins`] together with [`Command::TransferObjects`] for
209    /// each recipient + amount.
210    /// The length of the vectors for recipients and amounts must be the same.
211    pub async fn pay_tx_kind(
212        &self,
213        input_coins: Vec<ObjectID>,
214        recipients: Vec<IotaAddress>,
215        amounts: Vec<u64>,
216    ) -> Result<TransactionKind, anyhow::Error> {
217        let mut builder = ProgrammableTransactionBuilder::new();
218        let coins = self.input_refs(&input_coins).await?;
219        builder.pay(coins, recipients, amounts)?;
220        let pt = builder.finish();
221        Ok(TransactionKind::programmable(pt))
222    }
223
224    /// Take multiple coins and send to multiple addresses following the
225    /// specified amount list. The length of the vectors must be the same.
226    /// Take any type of coin, including IOTA.
227    /// A separate IOTA object will be used for gas payment.
228    ///
229    /// If the recipient and sender are the same, it's effectively a
230    /// generalized version of `split_coin` and `merge_coin`.
231    pub async fn pay(
232        &self,
233        signer: IotaAddress,
234        input_coins: Vec<ObjectID>,
235        recipients: Vec<IotaAddress>,
236        amounts: Vec<u64>,
237        gas: impl Into<Option<ObjectID>>,
238        gas_budget: u64,
239    ) -> anyhow::Result<TransactionData> {
240        let gas = gas.into();
241
242        if let Some(gas) = gas {
243            if input_coins.contains(&gas) {
244                bail!(
245                    "Gas coin is in input coins of Pay transaction, use PayIota transaction instead!"
246                );
247            }
248        }
249
250        let coin_refs = self.input_refs(&input_coins).await?;
251        let gas_price = self.0.get_reference_gas_price().await?;
252        let gas = self
253            .select_gas(signer, gas, gas_budget, input_coins, gas_price)
254            .await?;
255
256        TransactionData::new_pay(
257            signer, coin_refs, recipients, amounts, gas, gas_budget, gas_price,
258        )
259    }
260
261    /// Construct a transaction kind for the PayIota transaction type.
262    ///
263    /// Use this function together with tx_data_for_dry_run or tx_data
264    /// for maximum reusability.
265    /// The length of the vectors must be the same.
266    pub fn pay_iota_tx_kind(
267        &self,
268        recipients: Vec<IotaAddress>,
269        amounts: Vec<u64>,
270    ) -> Result<TransactionKind, anyhow::Error> {
271        let mut builder = ProgrammableTransactionBuilder::new();
272        builder.pay_iota(recipients.clone(), amounts.clone())?;
273        let pt = builder.finish();
274        let tx_kind = TransactionKind::programmable(pt);
275        Ok(tx_kind)
276    }
277
278    /// Take multiple IOTA coins and send to multiple addresses following the
279    /// specified amount list. The length of the vectors must be the same.
280    /// Only takes IOTA coins and does not require a gas coin object.
281    ///
282    /// The first IOTA coin object input will be used for gas payment, so the
283    /// balance of this IOTA coin has to be equal to or greater than the gas
284    /// budget.
285    /// The total IOTA coin balance input must be sufficient to cover both the
286    /// gas budget and the amounts to be transferred.
287    pub async fn pay_iota(
288        &self,
289        signer: IotaAddress,
290        input_coins: Vec<ObjectID>,
291        recipients: Vec<IotaAddress>,
292        amounts: Vec<u64>,
293        gas_budget: u64,
294    ) -> anyhow::Result<TransactionData> {
295        fp_ensure!(
296            !input_coins.is_empty(),
297            UserInputError::EmptyInputCoins.into()
298        );
299
300        let mut coin_refs = self.input_refs(&input_coins).await?;
301        // [0] is safe because input_coins is non-empty and coins are of same length as
302        // input_coins.
303        let gas_object_ref = coin_refs.remove(0);
304        let gas_price = self.0.get_reference_gas_price().await?;
305        TransactionData::new_pay_iota(
306            signer,
307            coin_refs,
308            recipients,
309            amounts,
310            gas_object_ref,
311            gas_budget,
312            gas_price,
313        )
314    }
315
316    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains a
317    /// [`Command::TransferObjects`] that sends the gas coin to the recipient.
318    pub fn pay_all_iota_tx_kind(&self, recipient: IotaAddress) -> TransactionKind {
319        let mut builder = ProgrammableTransactionBuilder::new();
320        builder.pay_all_iota(recipient);
321        let pt = builder.finish();
322        TransactionKind::programmable(pt)
323    }
324
325    /// Take multiple IOTA coins and send them to one recipient, after gas
326    /// payment deduction. After the transaction, strictly zero of the IOTA
327    /// coins input will be left under the sender’s address.
328    ///
329    /// The first IOTA coin object input will be used for gas payment, so the
330    /// balance of this IOTA coin has to be equal or greater than the gas
331    /// budget.
332    /// A sender can transfer all their IOTA coins to another
333    /// address with strictly zero IOTA left in one transaction via this
334    /// transaction type.
335    pub async fn pay_all_iota(
336        &self,
337        signer: IotaAddress,
338        input_coins: Vec<ObjectID>,
339        recipient: IotaAddress,
340        gas_budget: u64,
341    ) -> anyhow::Result<TransactionData> {
342        fp_ensure!(
343            !input_coins.is_empty(),
344            UserInputError::EmptyInputCoins.into()
345        );
346
347        let mut coin_refs = self.input_refs(&input_coins).await?;
348        // [0] is safe because input_coins is non-empty and coins are of same length as
349        // input_coins.
350        let gas_object_ref = coin_refs.remove(0);
351        let gas_price = self.0.get_reference_gas_price().await?;
352        Ok(TransactionData::new_pay_all_iota(
353            signer,
354            coin_refs,
355            recipient,
356            gas_object_ref,
357            gas_budget,
358            gas_price,
359        ))
360    }
361
362    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains a
363    /// [`Command::MoveCall`].
364    pub async fn move_call_tx_kind(
365        &self,
366        package_object_id: ObjectID,
367        module: &str,
368        function: &str,
369        type_args: Vec<IotaTypeTag>,
370        call_args: Vec<IotaJsonValue>,
371    ) -> Result<TransactionKind, anyhow::Error> {
372        let mut builder = ProgrammableTransactionBuilder::new();
373        self.single_move_call(
374            &mut builder,
375            package_object_id,
376            module,
377            function,
378            type_args,
379            call_args,
380        )
381        .await?;
382        let pt = builder.finish();
383        Ok(TransactionKind::programmable(pt))
384    }
385
386    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains a
387    /// [`Command::MoveCall`] to a Move View Function.
388    /// The method verifies that the signature of the function passed as input
389    /// complies with the Move View Function definition.
390    pub async fn move_view_call_tx_kind(
391        &self,
392        package_object_id: ObjectID,
393        module: &str,
394        function: &str,
395        type_args: Vec<IotaTypeTag>,
396        call_args: Vec<IotaJsonValue>,
397    ) -> Result<TransactionKind, anyhow::Error> {
398        let mut builder = ProgrammableTransactionBuilder::new();
399        self.single_move_view_call(
400            &mut builder,
401            package_object_id,
402            module,
403            function,
404            type_args,
405            call_args,
406        )
407        .await?;
408        let pt = builder.finish();
409        Ok(TransactionKind::programmable(pt))
410    }
411
412    /// Call a move function from a published package.
413    pub async fn move_call(
414        &self,
415        signer: IotaAddress,
416        package_object_id: ObjectID,
417        module: &str,
418        function: &str,
419        type_args: Vec<IotaTypeTag>,
420        call_args: Vec<IotaJsonValue>,
421        gas: impl Into<Option<ObjectID>>,
422        gas_budget: u64,
423        gas_price: impl Into<Option<u64>>,
424    ) -> anyhow::Result<TransactionData> {
425        let gas_price = gas_price.into();
426
427        let mut builder = ProgrammableTransactionBuilder::new();
428        self.single_move_call(
429            &mut builder,
430            package_object_id,
431            module,
432            function,
433            type_args,
434            call_args,
435        )
436        .await?;
437        let pt = builder.finish();
438        let input_objects = pt
439            .input_objects()?
440            .iter()
441            .flat_map(|obj| match obj {
442                InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
443                _ => None,
444            })
445            .collect();
446        let gas_price = if let Some(gas_price) = gas_price {
447            gas_price
448        } else {
449            self.0.get_reference_gas_price().await?
450        };
451        let gas = self
452            .select_gas(signer, gas, gas_budget, input_objects, gas_price)
453            .await?;
454
455        Ok(TransactionData::new(
456            TransactionKind::programmable(pt),
457            signer,
458            gas,
459            gas_budget,
460            gas_price,
461        ))
462    }
463
464    /// Add a single move call to the provided
465    /// [`ProgrammableTransactionBuilder`].
466    pub async fn single_move_call(
467        &self,
468        builder: &mut ProgrammableTransactionBuilder,
469        package: ObjectID,
470        module: &str,
471        function: &str,
472        type_args: Vec<IotaTypeTag>,
473        call_args: Vec<IotaJsonValue>,
474    ) -> anyhow::Result<()> {
475        let module = Identifier::from_str(module)?;
476        let function = Identifier::from_str(function)?;
477
478        let type_args = type_args
479            .into_iter()
480            .map(|ty| ty.try_into())
481            .collect::<Result<Vec<_>, _>>()?;
482
483        let call_args = self
484            .resolve_and_checks_json_args(
485                builder, package, &module, &function, &type_args, call_args,
486            )
487            .await?;
488
489        builder.command(Command::move_call(
490            package, module, function, type_args, call_args,
491        ));
492        Ok(())
493    }
494
495    /// Adds a single move call to the provided
496    /// [`ProgrammableTransactionBuilder`].
497    ///
498    /// Accepting [`PtbInput`] so one can also provide results from previous
499    /// move calls.
500    pub async fn single_move_call_with_ptb_inputs(
501        &self,
502        builder: &mut ProgrammableTransactionBuilder,
503        package: ObjectID,
504        module: &str,
505        function: &str,
506        type_args: Vec<IotaTypeTag>,
507        call_args: Vec<PtbInput>,
508    ) -> anyhow::Result<()> {
509        let module = Identifier::from_str(module)?;
510        let function = Identifier::from_str(function)?;
511
512        let type_args = type_args
513            .into_iter()
514            .map(|ty| ty.try_into())
515            .collect::<Result<Vec<_>, _>>()?;
516
517        let call_args = self
518            .resolve_and_check_call_args(
519                builder, package, &module, &function, &type_args, call_args,
520            )
521            .await?;
522
523        builder.command(Command::move_call(
524            package, module, function, type_args, call_args,
525        ));
526        Ok(())
527    }
528
529    /// Add a single move call to the provided
530    /// [`ProgrammableTransactionBuilder`]. Check that the passed function is
531    /// compliant to the Move View Function specification.
532    pub async fn single_move_view_call(
533        &self,
534        builder: &mut ProgrammableTransactionBuilder,
535        package: ObjectID,
536        module: &str,
537        function: &str,
538        type_args: Vec<IotaTypeTag>,
539        call_args: Vec<IotaJsonValue>,
540    ) -> anyhow::Result<()> {
541        let module = Identifier::from_str(module)?;
542        let function = Identifier::from_str(function)?;
543
544        let type_args = type_args
545            .into_iter()
546            .map(|ty| ty.try_into())
547            .collect::<Result<Vec<_>, _>>()?;
548
549        let call_args = self
550            .resolve_and_checks_json_view_args(
551                builder, package, &module, &function, &type_args, call_args,
552            )
553            .await?;
554
555        builder.command(Command::move_call(
556            package, module, function, type_args, call_args,
557        ));
558        Ok(())
559    }
560
561    /// Construct a transaction kind for the SplitCoin transaction type
562    /// It expects that only one of the two: split_amounts or split_count is
563    /// provided If both are provided, it will use split_amounts.
564    pub async fn split_coin_tx_kind(
565        &self,
566        coin_object_id: ObjectID,
567        split_amounts: impl Into<Option<Vec<u64>>>,
568        split_count: impl Into<Option<u64>>,
569    ) -> Result<TransactionKind, anyhow::Error> {
570        let split_amounts = split_amounts.into();
571        let split_count = split_count.into();
572
573        if split_amounts.is_none() && split_count.is_none() {
574            bail!(
575                "Either split_amounts or split_count must be provided for split_coin transaction."
576            );
577        }
578        let coin = self
579            .0
580            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
581            .await?
582            .into_object()?;
583        let coin_object_ref = coin.object_ref();
584        let coin: Object = coin.try_into()?;
585        let type_args = vec![coin.get_move_template_type()?];
586        let package = IOTA_FRAMEWORK_PACKAGE_ID;
587        let module = coin::PAY_MODULE_NAME.to_owned();
588
589        let (arguments, function) = if let Some(split_amounts) = split_amounts {
590            (
591                vec![
592                    CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
593                    CallArg::Pure(bcs::to_bytes(&split_amounts)?),
594                ],
595                coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
596            )
597        } else {
598            (
599                vec![
600                    CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
601                    CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
602                ],
603                coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
604            )
605        };
606        let mut builder = ProgrammableTransactionBuilder::new();
607        builder.move_call(package, module, function, type_args, arguments)?;
608        let pt = builder.finish();
609        let tx_kind = TransactionKind::programmable(pt);
610        Ok(tx_kind)
611    }
612
613    // TODO: consolidate this with Pay transactions
614    pub async fn split_coin(
615        &self,
616        signer: IotaAddress,
617        coin_object_id: ObjectID,
618        split_amounts: Vec<u64>,
619        gas: impl Into<Option<ObjectID>>,
620        gas_budget: u64,
621    ) -> anyhow::Result<TransactionData> {
622        let coin = self
623            .0
624            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
625            .await?
626            .into_object()?;
627        let coin_object_ref = coin.object_ref();
628        let coin: Object = coin.try_into()?;
629        let type_args = vec![coin.get_move_template_type()?];
630        let gas_price = self.0.get_reference_gas_price().await?;
631        let gas = self
632            .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
633            .await?;
634
635        TransactionData::new_move_call(
636            signer,
637            IOTA_FRAMEWORK_PACKAGE_ID,
638            coin::PAY_MODULE_NAME.to_owned(),
639            coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
640            type_args,
641            gas,
642            vec![
643                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
644                CallArg::Pure(bcs::to_bytes(&split_amounts)?),
645            ],
646            gas_budget,
647            gas_price,
648        )
649    }
650
651    // TODO: consolidate this with Pay transactions
652    pub async fn split_coin_equal(
653        &self,
654        signer: IotaAddress,
655        coin_object_id: ObjectID,
656        split_count: u64,
657        gas: impl Into<Option<ObjectID>>,
658        gas_budget: u64,
659    ) -> anyhow::Result<TransactionData> {
660        let coin = self
661            .0
662            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
663            .await?
664            .into_object()?;
665        let coin_object_ref = coin.object_ref();
666        let coin: Object = coin.try_into()?;
667        let type_args = vec![coin.get_move_template_type()?];
668        let gas_price = self.0.get_reference_gas_price().await?;
669        let gas = self
670            .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
671            .await?;
672
673        TransactionData::new_move_call(
674            signer,
675            IOTA_FRAMEWORK_PACKAGE_ID,
676            coin::PAY_MODULE_NAME.to_owned(),
677            coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
678            type_args,
679            gas,
680            vec![
681                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
682                CallArg::Pure(bcs::to_bytes(&split_count)?),
683            ],
684            gas_budget,
685            gas_price,
686        )
687    }
688
689    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains
690    /// [`Command::MergeCoins`] with the provided coins.
691    pub async fn merge_coins_tx_kind(
692        &self,
693        primary_coin: ObjectID,
694        coin_to_merge: ObjectID,
695    ) -> Result<TransactionKind, anyhow::Error> {
696        let coin = self
697            .0
698            .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
699            .await?
700            .into_object()?;
701        let primary_coin_ref = coin.object_ref();
702        let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
703        let coin: Object = coin.try_into()?;
704        let type_arguments = vec![coin.get_move_template_type()?];
705        let package = IOTA_FRAMEWORK_PACKAGE_ID;
706        let module = coin::COIN_MODULE_NAME.to_owned();
707        let function = coin::COIN_JOIN_FUNC_NAME.to_owned();
708        let arguments = vec![
709            CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
710            CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
711        ];
712        let pt = {
713            let mut builder = ProgrammableTransactionBuilder::new();
714            builder.move_call(package, module, function, type_arguments, arguments)?;
715            builder.finish()
716        };
717        let tx_kind = TransactionKind::programmable(pt);
718        Ok(tx_kind)
719    }
720
721    // TODO: consolidate this with Pay transactions
722    pub async fn merge_coins(
723        &self,
724        signer: IotaAddress,
725        primary_coin: ObjectID,
726        coin_to_merge: ObjectID,
727        gas: impl Into<Option<ObjectID>>,
728        gas_budget: u64,
729    ) -> anyhow::Result<TransactionData> {
730        let coin = self
731            .0
732            .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
733            .await?
734            .into_object()?;
735        let primary_coin_ref = coin.object_ref();
736        let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
737        let coin: Object = coin.try_into()?;
738        let type_args = vec![coin.get_move_template_type()?];
739        let gas_price = self.0.get_reference_gas_price().await?;
740        let gas = self
741            .select_gas(
742                signer,
743                gas,
744                gas_budget,
745                vec![primary_coin, coin_to_merge],
746                gas_price,
747            )
748            .await?;
749
750        TransactionData::new_move_call(
751            signer,
752            IOTA_FRAMEWORK_PACKAGE_ID,
753            coin::COIN_MODULE_NAME.to_owned(),
754            coin::COIN_JOIN_FUNC_NAME.to_owned(),
755            type_args,
756            gas,
757            vec![
758                CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
759                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
760            ],
761            gas_budget,
762            gas_price,
763        )
764    }
765
766    /// Create an unsigned batched transaction, useful for the JSON RPC.
767    pub async fn batch_transaction(
768        &self,
769        signer: IotaAddress,
770        single_transaction_params: Vec<RPCTransactionRequestParams>,
771        gas: impl Into<Option<ObjectID>>,
772        gas_budget: u64,
773    ) -> anyhow::Result<TransactionData> {
774        fp_ensure!(
775            !single_transaction_params.is_empty(),
776            UserInputError::InvalidBatchTransaction {
777                error: "Batch Transaction cannot be empty".to_owned(),
778            }
779            .into()
780        );
781        let mut builder = ProgrammableTransactionBuilder::new();
782        for param in single_transaction_params {
783            match param {
784                RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
785                    self.single_transfer_object(&mut builder, param.object_id, param.recipient)
786                        .await?
787                }
788                RPCTransactionRequestParams::MoveCallRequestParams(param) => {
789                    self.single_move_call_with_ptb_inputs(
790                        &mut builder,
791                        param.package_object_id,
792                        &param.module,
793                        &param.function,
794                        param.type_arguments,
795                        param.arguments,
796                    )
797                    .await?
798                }
799            };
800        }
801        let pt = builder.finish();
802        let all_inputs = pt.input_objects()?;
803        let inputs = all_inputs
804            .iter()
805            .flat_map(|obj| match obj {
806                InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
807                _ => None,
808            })
809            .collect();
810        let gas_price = self.0.get_reference_gas_price().await?;
811        let gas = self
812            .select_gas(signer, gas, gas_budget, inputs, gas_price)
813            .await?;
814
815        Ok(TransactionData::new(
816            TransactionKind::programmable(pt),
817            signer,
818            gas,
819            gas_budget,
820            gas_price,
821        ))
822    }
823}