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    /// Call a move function from a published package.
387    pub async fn move_call(
388        &self,
389        signer: IotaAddress,
390        package_object_id: ObjectID,
391        module: &str,
392        function: &str,
393        type_args: Vec<IotaTypeTag>,
394        call_args: Vec<IotaJsonValue>,
395        gas: impl Into<Option<ObjectID>>,
396        gas_budget: u64,
397        gas_price: impl Into<Option<u64>>,
398    ) -> anyhow::Result<TransactionData> {
399        let gas_price = gas_price.into();
400
401        let mut builder = ProgrammableTransactionBuilder::new();
402        self.single_move_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        let input_objects = pt
413            .input_objects()?
414            .iter()
415            .flat_map(|obj| match obj {
416                InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
417                _ => None,
418            })
419            .collect();
420        let gas_price = if let Some(gas_price) = gas_price {
421            gas_price
422        } else {
423            self.0.get_reference_gas_price().await?
424        };
425        let gas = self
426            .select_gas(signer, gas, gas_budget, input_objects, gas_price)
427            .await?;
428
429        Ok(TransactionData::new(
430            TransactionKind::programmable(pt),
431            signer,
432            gas,
433            gas_budget,
434            gas_price,
435        ))
436    }
437
438    /// Add a single move call to the provided
439    /// [`ProgrammableTransactionBuilder`].
440    pub async fn single_move_call(
441        &self,
442        builder: &mut ProgrammableTransactionBuilder,
443        package: ObjectID,
444        module: &str,
445        function: &str,
446        type_args: Vec<IotaTypeTag>,
447        call_args: Vec<IotaJsonValue>,
448    ) -> anyhow::Result<()> {
449        let module = Identifier::from_str(module)?;
450        let function = Identifier::from_str(function)?;
451
452        let type_args = type_args
453            .into_iter()
454            .map(|ty| ty.try_into())
455            .collect::<Result<Vec<_>, _>>()?;
456
457        let call_args = self
458            .resolve_and_checks_json_args(
459                builder, package, &module, &function, &type_args, call_args,
460            )
461            .await?;
462
463        builder.command(Command::move_call(
464            package, module, function, type_args, call_args,
465        ));
466        Ok(())
467    }
468
469    /// Adds a single move call to the provided
470    /// [`ProgrammableTransactionBuilder`].
471    ///
472    /// Accepting [`PtbInput`] so one can also provide results from previous
473    /// move calls.
474    pub async fn single_move_call_with_ptb_inputs(
475        &self,
476        builder: &mut ProgrammableTransactionBuilder,
477        package: ObjectID,
478        module: &str,
479        function: &str,
480        type_args: Vec<IotaTypeTag>,
481        call_args: Vec<PtbInput>,
482    ) -> anyhow::Result<()> {
483        let module = Identifier::from_str(module)?;
484        let function = Identifier::from_str(function)?;
485
486        let type_args = type_args
487            .into_iter()
488            .map(|ty| ty.try_into())
489            .collect::<Result<Vec<_>, _>>()?;
490
491        let call_args = self
492            .resolve_and_check_call_args(
493                builder, package, &module, &function, &type_args, call_args,
494            )
495            .await?;
496
497        builder.command(Command::move_call(
498            package, module, function, type_args, call_args,
499        ));
500        Ok(())
501    }
502
503    /// Construct a transaction kind for the SplitCoin transaction type
504    /// It expects that only one of the two: split_amounts or split_count is
505    /// provided If both are provided, it will use split_amounts.
506    pub async fn split_coin_tx_kind(
507        &self,
508        coin_object_id: ObjectID,
509        split_amounts: impl Into<Option<Vec<u64>>>,
510        split_count: impl Into<Option<u64>>,
511    ) -> Result<TransactionKind, anyhow::Error> {
512        let split_amounts = split_amounts.into();
513        let split_count = split_count.into();
514
515        if split_amounts.is_none() && split_count.is_none() {
516            bail!(
517                "Either split_amounts or split_count must be provided for split_coin transaction."
518            );
519        }
520        let coin = self
521            .0
522            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
523            .await?
524            .into_object()?;
525        let coin_object_ref = coin.object_ref();
526        let coin: Object = coin.try_into()?;
527        let type_args = vec![coin.get_move_template_type()?];
528        let package = IOTA_FRAMEWORK_PACKAGE_ID;
529        let module = coin::PAY_MODULE_NAME.to_owned();
530
531        let (arguments, function) = if let Some(split_amounts) = split_amounts {
532            (
533                vec![
534                    CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
535                    CallArg::Pure(bcs::to_bytes(&split_amounts)?),
536                ],
537                coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
538            )
539        } else {
540            (
541                vec![
542                    CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
543                    CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
544                ],
545                coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
546            )
547        };
548        let mut builder = ProgrammableTransactionBuilder::new();
549        builder.move_call(package, module, function, type_args, arguments)?;
550        let pt = builder.finish();
551        let tx_kind = TransactionKind::programmable(pt);
552        Ok(tx_kind)
553    }
554
555    // TODO: consolidate this with Pay transactions
556    pub async fn split_coin(
557        &self,
558        signer: IotaAddress,
559        coin_object_id: ObjectID,
560        split_amounts: Vec<u64>,
561        gas: impl Into<Option<ObjectID>>,
562        gas_budget: u64,
563    ) -> anyhow::Result<TransactionData> {
564        let coin = self
565            .0
566            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
567            .await?
568            .into_object()?;
569        let coin_object_ref = coin.object_ref();
570        let coin: Object = coin.try_into()?;
571        let type_args = vec![coin.get_move_template_type()?];
572        let gas_price = self.0.get_reference_gas_price().await?;
573        let gas = self
574            .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
575            .await?;
576
577        TransactionData::new_move_call(
578            signer,
579            IOTA_FRAMEWORK_PACKAGE_ID,
580            coin::PAY_MODULE_NAME.to_owned(),
581            coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
582            type_args,
583            gas,
584            vec![
585                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
586                CallArg::Pure(bcs::to_bytes(&split_amounts)?),
587            ],
588            gas_budget,
589            gas_price,
590        )
591    }
592
593    // TODO: consolidate this with Pay transactions
594    pub async fn split_coin_equal(
595        &self,
596        signer: IotaAddress,
597        coin_object_id: ObjectID,
598        split_count: u64,
599        gas: impl Into<Option<ObjectID>>,
600        gas_budget: u64,
601    ) -> anyhow::Result<TransactionData> {
602        let coin = self
603            .0
604            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
605            .await?
606            .into_object()?;
607        let coin_object_ref = coin.object_ref();
608        let coin: Object = coin.try_into()?;
609        let type_args = vec![coin.get_move_template_type()?];
610        let gas_price = self.0.get_reference_gas_price().await?;
611        let gas = self
612            .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
613            .await?;
614
615        TransactionData::new_move_call(
616            signer,
617            IOTA_FRAMEWORK_PACKAGE_ID,
618            coin::PAY_MODULE_NAME.to_owned(),
619            coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
620            type_args,
621            gas,
622            vec![
623                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
624                CallArg::Pure(bcs::to_bytes(&split_count)?),
625            ],
626            gas_budget,
627            gas_price,
628        )
629    }
630
631    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains
632    /// [`Command::MergeCoins`] with the provided coins.
633    pub async fn merge_coins_tx_kind(
634        &self,
635        primary_coin: ObjectID,
636        coin_to_merge: ObjectID,
637    ) -> Result<TransactionKind, anyhow::Error> {
638        let coin = self
639            .0
640            .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
641            .await?
642            .into_object()?;
643        let primary_coin_ref = coin.object_ref();
644        let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
645        let coin: Object = coin.try_into()?;
646        let type_arguments = vec![coin.get_move_template_type()?];
647        let package = IOTA_FRAMEWORK_PACKAGE_ID;
648        let module = coin::COIN_MODULE_NAME.to_owned();
649        let function = coin::COIN_JOIN_FUNC_NAME.to_owned();
650        let arguments = vec![
651            CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
652            CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
653        ];
654        let pt = {
655            let mut builder = ProgrammableTransactionBuilder::new();
656            builder.move_call(package, module, function, type_arguments, arguments)?;
657            builder.finish()
658        };
659        let tx_kind = TransactionKind::programmable(pt);
660        Ok(tx_kind)
661    }
662
663    // TODO: consolidate this with Pay transactions
664    pub async fn merge_coins(
665        &self,
666        signer: IotaAddress,
667        primary_coin: ObjectID,
668        coin_to_merge: ObjectID,
669        gas: impl Into<Option<ObjectID>>,
670        gas_budget: u64,
671    ) -> anyhow::Result<TransactionData> {
672        let coin = self
673            .0
674            .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
675            .await?
676            .into_object()?;
677        let primary_coin_ref = coin.object_ref();
678        let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
679        let coin: Object = coin.try_into()?;
680        let type_args = vec![coin.get_move_template_type()?];
681        let gas_price = self.0.get_reference_gas_price().await?;
682        let gas = self
683            .select_gas(
684                signer,
685                gas,
686                gas_budget,
687                vec![primary_coin, coin_to_merge],
688                gas_price,
689            )
690            .await?;
691
692        TransactionData::new_move_call(
693            signer,
694            IOTA_FRAMEWORK_PACKAGE_ID,
695            coin::COIN_MODULE_NAME.to_owned(),
696            coin::COIN_JOIN_FUNC_NAME.to_owned(),
697            type_args,
698            gas,
699            vec![
700                CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
701                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
702            ],
703            gas_budget,
704            gas_price,
705        )
706    }
707
708    /// Create an unsigned batched transaction, useful for the JSON RPC.
709    pub async fn batch_transaction(
710        &self,
711        signer: IotaAddress,
712        single_transaction_params: Vec<RPCTransactionRequestParams>,
713        gas: impl Into<Option<ObjectID>>,
714        gas_budget: u64,
715    ) -> anyhow::Result<TransactionData> {
716        fp_ensure!(
717            !single_transaction_params.is_empty(),
718            UserInputError::InvalidBatchTransaction {
719                error: "Batch Transaction cannot be empty".to_owned(),
720            }
721            .into()
722        );
723        let mut builder = ProgrammableTransactionBuilder::new();
724        for param in single_transaction_params {
725            match param {
726                RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
727                    self.single_transfer_object(&mut builder, param.object_id, param.recipient)
728                        .await?
729                }
730                RPCTransactionRequestParams::MoveCallRequestParams(param) => {
731                    self.single_move_call_with_ptb_inputs(
732                        &mut builder,
733                        param.package_object_id,
734                        &param.module,
735                        &param.function,
736                        param.type_arguments,
737                        param.arguments,
738                    )
739                    .await?
740                }
741            };
742        }
743        let pt = builder.finish();
744        let all_inputs = pt.input_objects()?;
745        let inputs = all_inputs
746            .iter()
747            .flat_map(|obj| match obj {
748                InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
749                _ => None,
750            })
751            .collect();
752        let gas_price = self.0.get_reference_gas_price().await?;
753        let gas = self
754            .select_gas(signer, gas, gas_budget, inputs, gas_price)
755            .await?;
756
757        Ok(TransactionData::new(
758            TransactionKind::programmable(pt),
759            signer,
760            gas,
761            gas_budget,
762            gas_price,
763        ))
764    }
765}