Skip to main content

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