sui_types/
programmable_transaction_builder.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Utility for generating programmable transactions, either by specifying a command or for
5//! migrating legacy transactions
6
7use anyhow::{Context, bail};
8use indexmap::IndexMap;
9use move_core_types::{ident_str, identifier::Identifier, language_storage::TypeTag};
10use serde::Serialize;
11
12use crate::{
13    SUI_FRAMEWORK_PACKAGE_ID,
14    balance::{
15        BALANCE_MODULE_NAME, BALANCE_REDEEM_FUNDS_FUNCTION_NAME, BALANCE_SEND_FUNDS_FUNCTION_NAME,
16    },
17    base_types::{FullObjectID, FullObjectRef, ObjectID, ObjectRef, SuiAddress},
18    move_package::PACKAGE_MODULE_NAME,
19    transaction::{
20        Argument, CallArg, Command, FundsWithdrawalArg, ObjectArg, ProgrammableTransaction,
21        SharedObjectMutability,
22    },
23    type_input::TypeInput,
24};
25
26#[cfg(test)]
27#[path = "unit_tests/programmable_transaction_builder_tests.rs"]
28mod programmable_transaction_builder_tests;
29
30#[derive(PartialEq, Eq, Hash)]
31enum BuilderArg {
32    Object(ObjectID),
33    Pure(Vec<u8>),
34    ForcedNonUniquePure(usize),
35    FundsWithdraw(usize),
36}
37
38#[derive(Default)]
39pub struct ProgrammableTransactionBuilder {
40    inputs: IndexMap<BuilderArg, CallArg>,
41    commands: Vec<Command>,
42}
43
44impl ProgrammableTransactionBuilder {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    pub fn finish(self) -> ProgrammableTransaction {
50        let Self { inputs, commands } = self;
51        let inputs = inputs.into_values().collect();
52        ProgrammableTransaction { inputs, commands }
53    }
54
55    pub fn pure_bytes(&mut self, bytes: Vec<u8>, force_separate: bool) -> Argument {
56        let arg = if force_separate {
57            BuilderArg::ForcedNonUniquePure(self.inputs.len())
58        } else {
59            BuilderArg::Pure(bytes.clone())
60        };
61        let (i, _) = self.inputs.insert_full(arg, CallArg::Pure(bytes));
62        Argument::Input(i as u16)
63    }
64
65    pub fn pure<T: Serialize>(&mut self, value: T) -> anyhow::Result<Argument> {
66        Ok(self.pure_bytes(
67            bcs::to_bytes(&value).context("Serializing pure argument.")?,
68            /* force separate */ false,
69        ))
70    }
71
72    /// Like pure but forces a separate input entry
73    pub fn force_separate_pure<T: Serialize>(&mut self, value: T) -> anyhow::Result<Argument> {
74        Ok(self.pure_bytes(
75            bcs::to_bytes(&value).context("Serializing pure argument.")?,
76            /* force separate */ true,
77        ))
78    }
79
80    pub fn obj(&mut self, obj_arg: ObjectArg) -> anyhow::Result<Argument> {
81        let id = obj_arg.id();
82        let obj_arg = if let Some(old_value) = self.inputs.get(&BuilderArg::Object(id)) {
83            let old_obj_arg = match old_value {
84                CallArg::Pure(_) => anyhow::bail!("invariant violation! object has pure argument"),
85                CallArg::Object(arg) => arg,
86                CallArg::FundsWithdrawal(_) => {
87                    anyhow::bail!("invariant violation! object has balance withdraw argument")
88                }
89            };
90            match (old_obj_arg, obj_arg) {
91                (
92                    ObjectArg::SharedObject {
93                        id: id1,
94                        initial_shared_version: v1,
95                        mutability: mut1,
96                    },
97                    ObjectArg::SharedObject {
98                        id: id2,
99                        initial_shared_version: v2,
100                        mutability: mut2,
101                    },
102                ) if v1 == &v2 => {
103                    anyhow::ensure!(
104                        id1 == &id2 && id == id2,
105                        "invariant violation! object has id does not match call arg"
106                    );
107                    ObjectArg::SharedObject {
108                        id,
109                        initial_shared_version: v2,
110                        mutability: if mut1 == &SharedObjectMutability::Mutable
111                            || mut2 == SharedObjectMutability::Mutable
112                        {
113                            SharedObjectMutability::Mutable
114                        } else {
115                            mut2
116                        },
117                    }
118                }
119                (old_obj_arg, obj_arg) => {
120                    anyhow::ensure!(
121                        old_obj_arg == &obj_arg,
122                        "Mismatched Object argument kind for object {id}. \
123                        {old_value:?} is not compatible with {obj_arg:?}"
124                    );
125                    obj_arg
126                }
127            }
128        } else {
129            obj_arg
130        };
131        let (i, _) = self
132            .inputs
133            .insert_full(BuilderArg::Object(id), CallArg::Object(obj_arg));
134        Ok(Argument::Input(i as u16))
135    }
136
137    pub fn funds_withdrawal(&mut self, arg: FundsWithdrawalArg) -> anyhow::Result<Argument> {
138        let (i, _) = self.inputs.insert_full(
139            BuilderArg::FundsWithdraw(self.inputs.len()),
140            CallArg::FundsWithdrawal(arg),
141        );
142        Ok(Argument::Input(i as u16))
143    }
144
145    pub fn input(&mut self, call_arg: CallArg) -> anyhow::Result<Argument> {
146        match call_arg {
147            CallArg::Pure(bytes) => Ok(self.pure_bytes(bytes, /* force separate */ false)),
148            CallArg::Object(obj) => self.obj(obj),
149            CallArg::FundsWithdrawal(arg) => self.funds_withdrawal(arg),
150        }
151    }
152
153    pub fn make_obj_vec(
154        &mut self,
155        objs: impl IntoIterator<Item = ObjectArg>,
156    ) -> anyhow::Result<Argument> {
157        let make_vec_args = objs
158            .into_iter()
159            .map(|obj| self.obj(obj))
160            .collect::<Result<_, _>>()?;
161        Ok(self.command(Command::MakeMoveVec(None, make_vec_args)))
162    }
163
164    pub fn command(&mut self, command: Command) -> Argument {
165        let i = self.commands.len();
166        self.commands.push(command);
167        Argument::Result(i as u16)
168    }
169
170    /// Will fail to generate if given an empty ObjVec
171    pub fn move_call(
172        &mut self,
173        package: ObjectID,
174        module: Identifier,
175        function: Identifier,
176        type_arguments: Vec<TypeTag>,
177        call_args: Vec<CallArg>,
178    ) -> anyhow::Result<()> {
179        let arguments = call_args
180            .into_iter()
181            .map(|a| self.input(a))
182            .collect::<Result<_, _>>()?;
183        self.command(Command::move_call(
184            package,
185            module,
186            function,
187            type_arguments,
188            arguments,
189        ));
190        Ok(())
191    }
192
193    pub fn programmable_move_call(
194        &mut self,
195        package: ObjectID,
196        module: Identifier,
197        function: Identifier,
198        type_arguments: Vec<TypeTag>,
199        arguments: Vec<Argument>,
200    ) -> Argument {
201        self.command(Command::move_call(
202            package,
203            module,
204            function,
205            type_arguments,
206            arguments,
207        ))
208    }
209
210    pub fn publish_upgradeable(
211        &mut self,
212        modules: Vec<Vec<u8>>,
213        dep_ids: Vec<ObjectID>,
214    ) -> Argument {
215        self.command(Command::Publish(modules, dep_ids))
216    }
217
218    pub fn publish_immutable(&mut self, modules: Vec<Vec<u8>>, dep_ids: Vec<ObjectID>) {
219        let cap = self.publish_upgradeable(modules, dep_ids);
220        self.commands.push(Command::move_call(
221            SUI_FRAMEWORK_PACKAGE_ID,
222            PACKAGE_MODULE_NAME.to_owned(),
223            ident_str!("make_immutable").to_owned(),
224            vec![],
225            vec![cap],
226        ));
227    }
228
229    pub fn upgrade(
230        &mut self,
231        current_package_object_id: ObjectID,
232        upgrade_ticket: Argument,
233        transitive_deps: Vec<ObjectID>,
234        modules: Vec<Vec<u8>>,
235    ) -> Argument {
236        self.command(Command::Upgrade(
237            modules,
238            transitive_deps,
239            current_package_object_id,
240            upgrade_ticket,
241        ))
242    }
243
244    pub fn transfer_arg(&mut self, recipient: SuiAddress, arg: Argument) {
245        self.transfer_args(recipient, vec![arg])
246    }
247
248    pub fn transfer_args(&mut self, recipient: SuiAddress, args: Vec<Argument>) {
249        let rec_arg = self.pure(recipient).unwrap();
250        self.commands.push(Command::TransferObjects(args, rec_arg));
251    }
252
253    pub fn transfer_object(
254        &mut self,
255        recipient: SuiAddress,
256        full_object_ref: FullObjectRef,
257    ) -> anyhow::Result<()> {
258        let rec_arg = self.pure(recipient).unwrap();
259        let obj_arg = self.obj(match full_object_ref.0 {
260            FullObjectID::Fastpath(_) => {
261                ObjectArg::ImmOrOwnedObject(full_object_ref.as_object_ref())
262            }
263            FullObjectID::Consensus((id, initial_shared_version)) => ObjectArg::SharedObject {
264                id,
265                initial_shared_version,
266                mutability: SharedObjectMutability::Mutable,
267            },
268        });
269        self.commands
270            .push(Command::TransferObjects(vec![obj_arg?], rec_arg));
271        Ok(())
272    }
273
274    pub fn transfer_sui(&mut self, recipient: SuiAddress, amount: Option<u64>) {
275        let rec_arg = self.pure(recipient).unwrap();
276        let coin_arg = if let Some(amount) = amount {
277            let amt_arg = self.pure(amount).unwrap();
278            self.command(Command::SplitCoins(Argument::GasCoin, vec![amt_arg]))
279        } else {
280            Argument::GasCoin
281        };
282        self.command(Command::TransferObjects(vec![coin_arg], rec_arg));
283    }
284
285    pub fn redeem_funds(&mut self, amount: u64, type_arg: TypeTag) -> anyhow::Result<Argument> {
286        let withdrawal_arg =
287            FundsWithdrawalArg::balance_from_sender(amount, TypeInput::from(type_arg.clone()));
288        let withdrawal_arg = self.funds_withdrawal(withdrawal_arg)?;
289        Ok(self.programmable_move_call(
290            SUI_FRAMEWORK_PACKAGE_ID,
291            BALANCE_MODULE_NAME.to_owned(),
292            BALANCE_REDEEM_FUNDS_FUNCTION_NAME.to_owned(),
293            vec![type_arg],
294            vec![withdrawal_arg],
295        ))
296    }
297
298    pub fn transfer_balance(
299        &mut self,
300        recipient: SuiAddress,
301        amount: u64,
302        type_arg: TypeTag,
303    ) -> anyhow::Result<()> {
304        let rec_arg = self.pure(recipient).unwrap();
305        let balance = self.redeem_funds(amount, type_arg.clone())?;
306
307        self.programmable_move_call(
308            SUI_FRAMEWORK_PACKAGE_ID,
309            BALANCE_MODULE_NAME.to_owned(),
310            BALANCE_SEND_FUNDS_FUNCTION_NAME.to_owned(),
311            vec![type_arg],
312            vec![balance, rec_arg],
313        );
314        Ok(())
315    }
316
317    pub fn pay_all_sui(&mut self, recipient: SuiAddress) {
318        let rec_arg = self.pure(recipient).unwrap();
319        self.command(Command::TransferObjects(vec![Argument::GasCoin], rec_arg));
320    }
321
322    /// Will fail to generate if recipients and amounts do not have the same lengths
323    pub fn pay_sui(
324        &mut self,
325        recipients: Vec<SuiAddress>,
326        amounts: Vec<u64>,
327    ) -> anyhow::Result<()> {
328        self.pay_impl(recipients, amounts, Argument::GasCoin)
329    }
330
331    pub fn split_coin(&mut self, recipient: SuiAddress, coin: ObjectRef, amounts: Vec<u64>) {
332        let coin_arg = self.obj(ObjectArg::ImmOrOwnedObject(coin)).unwrap();
333        let amounts_len = amounts.len();
334        let amt_args = amounts.into_iter().map(|a| self.pure(a).unwrap()).collect();
335        let result = self.command(Command::SplitCoins(coin_arg, amt_args));
336        let Argument::Result(result) = result else {
337            panic!("self.command should always give a Argument::Result");
338        };
339
340        let recipient = self.pure(recipient).unwrap();
341        self.command(Command::TransferObjects(
342            (0..amounts_len)
343                .map(|i| Argument::NestedResult(result, i as u16))
344                .collect(),
345            recipient,
346        ));
347    }
348
349    /// Merge `coins` into the `target` coin.
350    pub fn merge_coins(&mut self, target: ObjectRef, coins: Vec<ObjectRef>) -> anyhow::Result<()> {
351        let target_arg = self.obj(ObjectArg::ImmOrOwnedObject(target))?;
352        let coin_args = coins
353            .into_iter()
354            .map(|coin| self.obj(ObjectArg::ImmOrOwnedObject(coin)).unwrap())
355            .collect::<Vec<_>>();
356        self.command(Command::MergeCoins(target_arg, coin_args));
357        Ok(())
358    }
359
360    /// Merge all `coins` into the first coin in the vector.
361    /// Returns an `Argument` for the first coin.
362    pub fn smash_coins(&mut self, coins: Vec<ObjectRef>) -> anyhow::Result<Argument> {
363        let mut coins = coins.into_iter();
364        let Some(target) = coins.next() else {
365            bail!("coins vector is empty");
366        };
367        let target_arg = self.obj(ObjectArg::ImmOrOwnedObject(target))?;
368        let coin_args = coins
369            .map(|coin| self.obj(ObjectArg::ImmOrOwnedObject(coin)).unwrap())
370            .collect::<Vec<_>>();
371        self.command(Command::MergeCoins(target_arg, coin_args));
372        Ok(target_arg)
373    }
374
375    /// Will fail to generate if recipients and amounts do not have the same lengths.
376    /// Or if coins is empty
377    pub fn pay(
378        &mut self,
379        coins: Vec<ObjectRef>,
380        recipients: Vec<SuiAddress>,
381        amounts: Vec<u64>,
382    ) -> anyhow::Result<()> {
383        let mut coins = coins.into_iter();
384        let Some(coin) = coins.next() else {
385            anyhow::bail!("coins vector is empty");
386        };
387        let coin_arg = self.obj(ObjectArg::ImmOrOwnedObject(coin))?;
388        let merge_args: Vec<_> = coins
389            .map(|c| self.obj(ObjectArg::ImmOrOwnedObject(c)))
390            .collect::<Result<_, _>>()?;
391        if !merge_args.is_empty() {
392            self.command(Command::MergeCoins(coin_arg, merge_args));
393        }
394        self.pay_impl(recipients, amounts, coin_arg)
395    }
396
397    fn pay_impl(
398        &mut self,
399        recipients: Vec<SuiAddress>,
400        amounts: Vec<u64>,
401        coin: Argument,
402    ) -> anyhow::Result<()> {
403        if recipients.len() != amounts.len() {
404            anyhow::bail!(
405                "Recipients and amounts mismatch. Got {} recipients but {} amounts",
406                recipients.len(),
407                amounts.len()
408            )
409        }
410        if amounts.is_empty() {
411            return Ok(());
412        }
413
414        // collect recipients in the case where they are non-unique in order
415        // to minimize the number of transfers that must be performed
416        let mut recipient_map: IndexMap<SuiAddress, Vec<usize>> = IndexMap::new();
417        let mut amt_args = Vec::with_capacity(recipients.len());
418        for (i, (recipient, amount)) in recipients.into_iter().zip(amounts).enumerate() {
419            recipient_map.entry(recipient).or_default().push(i);
420            amt_args.push(self.pure(amount)?);
421        }
422        let Argument::Result(split_primary) = self.command(Command::SplitCoins(coin, amt_args))
423        else {
424            panic!("self.command should always give a Argument::Result")
425        };
426        for (recipient, split_secondaries) in recipient_map {
427            let rec_arg = self.pure(recipient).unwrap();
428            let coins = split_secondaries
429                .into_iter()
430                .map(|j| Argument::NestedResult(split_primary, j as u16))
431                .collect();
432            self.command(Command::TransferObjects(coins, rec_arg));
433        }
434        Ok(())
435    }
436}