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::{Ok, anyhow, bail};
12use async_trait::async_trait;
13use iota_json::IotaJsonValue;
14use iota_json_rpc_types::{
15    IotaObjectDataOptions, IotaObjectResponse, IotaTypeTag, 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                return Err(anyhow!(
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    /// Construct a transaction kind for the SplitCoin transaction type
470    /// It expects that only one of the two: split_amounts or split_count is
471    /// provided If both are provided, it will use split_amounts.
472    pub async fn split_coin_tx_kind(
473        &self,
474        coin_object_id: ObjectID,
475        split_amounts: impl Into<Option<Vec<u64>>>,
476        split_count: impl Into<Option<u64>>,
477    ) -> Result<TransactionKind, anyhow::Error> {
478        let split_amounts = split_amounts.into();
479        let split_count = split_count.into();
480
481        if split_amounts.is_none() && split_count.is_none() {
482            bail!(
483                "Either split_amounts or split_count must be provided for split_coin transaction."
484            );
485        }
486        let coin = self
487            .0
488            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
489            .await?
490            .into_object()?;
491        let coin_object_ref = coin.object_ref();
492        let coin: Object = coin.try_into()?;
493        let type_args = vec![coin.get_move_template_type()?];
494        let package = IOTA_FRAMEWORK_PACKAGE_ID;
495        let module = coin::PAY_MODULE_NAME.to_owned();
496
497        let (arguments, function) = if let Some(split_amounts) = split_amounts {
498            (
499                vec![
500                    CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
501                    CallArg::Pure(bcs::to_bytes(&split_amounts)?),
502                ],
503                coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
504            )
505        } else {
506            (
507                vec![
508                    CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
509                    CallArg::Pure(bcs::to_bytes(&split_count.unwrap())?),
510                ],
511                coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
512            )
513        };
514        let mut builder = ProgrammableTransactionBuilder::new();
515        builder.move_call(package, module, function, type_args, arguments)?;
516        let pt = builder.finish();
517        let tx_kind = TransactionKind::programmable(pt);
518        Ok(tx_kind)
519    }
520
521    // TODO: consolidate this with Pay transactions
522    pub async fn split_coin(
523        &self,
524        signer: IotaAddress,
525        coin_object_id: ObjectID,
526        split_amounts: Vec<u64>,
527        gas: impl Into<Option<ObjectID>>,
528        gas_budget: u64,
529    ) -> anyhow::Result<TransactionData> {
530        let coin = self
531            .0
532            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
533            .await?
534            .into_object()?;
535        let coin_object_ref = coin.object_ref();
536        let coin: Object = coin.try_into()?;
537        let type_args = vec![coin.get_move_template_type()?];
538        let gas_price = self.0.get_reference_gas_price().await?;
539        let gas = self
540            .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
541            .await?;
542
543        TransactionData::new_move_call(
544            signer,
545            IOTA_FRAMEWORK_PACKAGE_ID,
546            coin::PAY_MODULE_NAME.to_owned(),
547            coin::PAY_SPLIT_VEC_FUNC_NAME.to_owned(),
548            type_args,
549            gas,
550            vec![
551                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
552                CallArg::Pure(bcs::to_bytes(&split_amounts)?),
553            ],
554            gas_budget,
555            gas_price,
556        )
557    }
558
559    // TODO: consolidate this with Pay transactions
560    pub async fn split_coin_equal(
561        &self,
562        signer: IotaAddress,
563        coin_object_id: ObjectID,
564        split_count: u64,
565        gas: impl Into<Option<ObjectID>>,
566        gas_budget: u64,
567    ) -> anyhow::Result<TransactionData> {
568        let coin = self
569            .0
570            .get_object_with_options(coin_object_id, IotaObjectDataOptions::bcs_lossless())
571            .await?
572            .into_object()?;
573        let coin_object_ref = coin.object_ref();
574        let coin: Object = coin.try_into()?;
575        let type_args = vec![coin.get_move_template_type()?];
576        let gas_price = self.0.get_reference_gas_price().await?;
577        let gas = self
578            .select_gas(signer, gas, gas_budget, vec![coin_object_id], gas_price)
579            .await?;
580
581        TransactionData::new_move_call(
582            signer,
583            IOTA_FRAMEWORK_PACKAGE_ID,
584            coin::PAY_MODULE_NAME.to_owned(),
585            coin::PAY_SPLIT_N_FUNC_NAME.to_owned(),
586            type_args,
587            gas,
588            vec![
589                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_object_ref)),
590                CallArg::Pure(bcs::to_bytes(&split_count)?),
591            ],
592            gas_budget,
593            gas_price,
594        )
595    }
596
597    /// Build a [`TransactionKind::ProgrammableTransaction`] that contains
598    /// [`Command::MergeCoins`] with the provided coins.
599    pub async fn merge_coins_tx_kind(
600        &self,
601        primary_coin: ObjectID,
602        coin_to_merge: ObjectID,
603    ) -> Result<TransactionKind, anyhow::Error> {
604        let coin = self
605            .0
606            .get_object_with_options(primary_coin, IotaObjectDataOptions::bcs_lossless())
607            .await?
608            .into_object()?;
609        let primary_coin_ref = coin.object_ref();
610        let coin_to_merge_ref = self.get_object_ref(coin_to_merge).await?;
611        let coin: Object = coin.try_into()?;
612        let type_arguments = vec![coin.get_move_template_type()?];
613        let package = IOTA_FRAMEWORK_PACKAGE_ID;
614        let module = coin::COIN_MODULE_NAME.to_owned();
615        let function = coin::COIN_JOIN_FUNC_NAME.to_owned();
616        let arguments = vec![
617            CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
618            CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
619        ];
620        let pt = {
621            let mut builder = ProgrammableTransactionBuilder::new();
622            builder.move_call(package, module, function, type_arguments, arguments)?;
623            builder.finish()
624        };
625        let tx_kind = TransactionKind::programmable(pt);
626        Ok(tx_kind)
627    }
628
629    // TODO: consolidate this with Pay transactions
630    pub async fn merge_coins(
631        &self,
632        signer: IotaAddress,
633        primary_coin: ObjectID,
634        coin_to_merge: ObjectID,
635        gas: impl Into<Option<ObjectID>>,
636        gas_budget: u64,
637    ) -> anyhow::Result<TransactionData> {
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_args = vec![coin.get_move_template_type()?];
647        let gas_price = self.0.get_reference_gas_price().await?;
648        let gas = self
649            .select_gas(
650                signer,
651                gas,
652                gas_budget,
653                vec![primary_coin, coin_to_merge],
654                gas_price,
655            )
656            .await?;
657
658        TransactionData::new_move_call(
659            signer,
660            IOTA_FRAMEWORK_PACKAGE_ID,
661            coin::COIN_MODULE_NAME.to_owned(),
662            coin::COIN_JOIN_FUNC_NAME.to_owned(),
663            type_args,
664            gas,
665            vec![
666                CallArg::Object(ObjectArg::ImmOrOwnedObject(primary_coin_ref)),
667                CallArg::Object(ObjectArg::ImmOrOwnedObject(coin_to_merge_ref)),
668            ],
669            gas_budget,
670            gas_price,
671        )
672    }
673
674    /// Create an unsigned batched transaction, useful for the JSON RPC.
675    pub async fn batch_transaction(
676        &self,
677        signer: IotaAddress,
678        single_transaction_params: Vec<RPCTransactionRequestParams>,
679        gas: impl Into<Option<ObjectID>>,
680        gas_budget: u64,
681    ) -> anyhow::Result<TransactionData> {
682        fp_ensure!(
683            !single_transaction_params.is_empty(),
684            UserInputError::InvalidBatchTransaction {
685                error: "Batch Transaction cannot be empty".to_owned(),
686            }
687            .into()
688        );
689        let mut builder = ProgrammableTransactionBuilder::new();
690        for param in single_transaction_params {
691            match param {
692                RPCTransactionRequestParams::TransferObjectRequestParams(param) => {
693                    self.single_transfer_object(&mut builder, param.object_id, param.recipient)
694                        .await?
695                }
696                RPCTransactionRequestParams::MoveCallRequestParams(param) => {
697                    self.single_move_call(
698                        &mut builder,
699                        param.package_object_id,
700                        &param.module,
701                        &param.function,
702                        param.type_arguments,
703                        param.arguments,
704                    )
705                    .await?
706                }
707            };
708        }
709        let pt = builder.finish();
710        let all_inputs = pt.input_objects()?;
711        let inputs = all_inputs
712            .iter()
713            .flat_map(|obj| match obj {
714                InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
715                _ => None,
716            })
717            .collect();
718        let gas_price = self.0.get_reference_gas_price().await?;
719        let gas = self
720            .select_gas(signer, gas, gas_budget, inputs, gas_price)
721            .await?;
722
723        Ok(TransactionData::new(
724            TransactionKind::programmable(pt),
725            signer,
726            gas,
727            gas_budget,
728            gas_price,
729        ))
730    }
731}