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