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