sui_transaction_builder/
builder.rs

1use crate::error::Error;
2use std::collections::BTreeMap;
3use std::collections::BTreeSet;
4use std::collections::HashMap;
5use sui_sdk_types::Address;
6use sui_sdk_types::Digest;
7use sui_sdk_types::Identifier;
8use sui_sdk_types::Transaction;
9use sui_sdk_types::TransactionExpiration;
10use sui_sdk_types::TypeTag;
11
12/// A builder for creating [programmable transaction blocks][ptb].
13///
14/// Inputs and commands are added incrementally through methods like [`pure`](Self::pure),
15/// [`object`](Self::object), [`move_call`](Self::move_call), and
16/// [`transfer_objects`](Self::transfer_objects). Once all commands and metadata have been set,
17/// call [`try_build`](Self::try_build) for offline building, or [`build`](Self::build) (with
18/// the `intents` feature) to resolve intents and gas via an RPC client.
19///
20/// [ptb]: https://docs.sui.io/concepts/transactions/prog-txn-blocks
21///
22/// # Example
23///
24/// ```
25/// use sui_sdk_types::Address;
26/// use sui_sdk_types::Digest;
27/// use sui_transaction_builder::ObjectInput;
28/// use sui_transaction_builder::TransactionBuilder;
29///
30/// let mut tx = TransactionBuilder::new();
31///
32/// let amount = tx.pure(&1_000_000_000u64);
33/// let gas = tx.gas();
34/// let coins = tx.split_coins(gas, vec![amount]);
35///
36/// let recipient = tx.pure(&Address::ZERO);
37/// tx.transfer_objects(coins, recipient);
38///
39/// tx.set_sender(Address::ZERO);
40/// tx.set_gas_budget(500_000_000);
41/// tx.set_gas_price(1000);
42/// tx.add_gas_objects([ObjectInput::owned(Address::ZERO, 1, Digest::ZERO)]);
43///
44/// let transaction = tx.try_build().expect("build should succeed");
45/// ```
46#[derive(Default)]
47pub struct TransactionBuilder {
48    /// The gas objects that will be used to pay for the transaction. The most common way is to
49    /// use [`unresolved::Input::owned`] function to create a gas object and use the [`add_gas`]
50    /// method to set the gas objects.
51    pub(crate) gas: Vec<ObjectInput>,
52    /// The gas budget for the transaction.
53    gas_budget: Option<u64>,
54    /// The gas price for the transaction.
55    gas_price: Option<u64>,
56    /// The sender of the transaction.
57    sender: Option<Address>,
58    /// The sponsor of the transaction. If None, the sender is also the sponsor.
59    sponsor: Option<Address>,
60    /// The expiration of the transaction. The default value of this type is no expiration.
61    expiration: Option<TransactionExpiration>,
62
63    // Resolvers
64    #[cfg(feature = "intents")]
65    pub(crate) resolvers: BTreeMap<std::any::TypeId, Box<dyn crate::intent::IntentResolver>>,
66
67    pub(crate) arguments: BTreeMap<usize, ResolvedArgument>,
68    inputs: HashMap<InputArgKind, (usize, InputArg)>,
69    pub(crate) commands: BTreeMap<usize, Command>,
70    pub(crate) intents: BTreeMap<usize, Box<dyn std::any::Any + Send + Sync>>,
71}
72
73#[derive(Clone, Copy, Debug)]
74pub(crate) enum ResolvedArgument {
75    Unresolved,
76    #[allow(unused)]
77    ReplaceWith(Argument),
78    Resolved(sui_sdk_types::Argument),
79}
80
81#[derive(Debug, PartialEq, Eq, Hash)]
82pub(crate) enum InputArgKind {
83    Gas,
84    ObjectInput(Address),
85    PureInput(Vec<u8>),
86    UniquePureInput(usize),
87    // All funds withdrawals are unique
88    FundsWithdrawal(usize),
89}
90
91pub(crate) enum InputArg {
92    Gas,
93    Pure(Vec<u8>),
94    Object(ObjectInput),
95    FundsWithdrawal(sui_sdk_types::FundsWithdrawal),
96}
97
98impl TransactionBuilder {
99    /// Create a new, empty transaction builder.
100    ///
101    /// ```
102    /// use sui_transaction_builder::TransactionBuilder;
103    ///
104    /// let tx = TransactionBuilder::new();
105    /// ```
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    // Transaction Inputs
111
112    /// Return an [`Argument`] referring to the gas coin.
113    ///
114    /// The gas coin can be used as an input to commands such as
115    /// [`split_coins`](Self::split_coins).
116    ///
117    /// Note: The gas coin cannot be used when using an account's Address Balance to pay for gas fees.
118    ///
119    /// ```
120    /// use sui_transaction_builder::TransactionBuilder;
121    ///
122    /// let mut tx = TransactionBuilder::new();
123    /// let gas = tx.gas();
124    /// ```
125    pub fn gas(&mut self) -> Argument {
126        if let Some((index, arg)) = self.inputs.get(&InputArgKind::Gas) {
127            assert!(matches!(arg, InputArg::Gas));
128            Argument::new(*index)
129        } else {
130            let id = self.arguments.len();
131            self.arguments.insert(id, ResolvedArgument::Unresolved);
132            self.inputs.insert(InputArgKind::Gas, (id, InputArg::Gas));
133            Argument::new(id)
134        }
135    }
136
137    /// Add a pure input from raw BCS bytes.
138    ///
139    /// If the same bytes have already been added, the existing [`Argument`] is returned
140    /// (inputs are deduplicated). Use [`pure_bytes_unique`](Self::pure_bytes_unique) when
141    /// deduplication is not desired.
142    ///
143    /// ```
144    /// use sui_transaction_builder::TransactionBuilder;
145    ///
146    /// let mut tx = TransactionBuilder::new();
147    /// let a = tx.pure_bytes(vec![1, 0, 0, 0, 0, 0, 0, 0]);
148    /// let b = tx.pure_bytes(vec![1, 0, 0, 0, 0, 0, 0, 0]);
149    /// // `a` and `b` refer to the same input
150    /// ```
151    pub fn pure_bytes(&mut self, bytes: Vec<u8>) -> Argument {
152        match self.inputs.entry(InputArgKind::PureInput(bytes.clone())) {
153            std::collections::hash_map::Entry::Occupied(o) => {
154                assert!(matches!(o.get().1, InputArg::Pure(_)));
155                Argument::new(o.get().0)
156            }
157            std::collections::hash_map::Entry::Vacant(v) => {
158                let id = self.arguments.len();
159                self.arguments.insert(id, ResolvedArgument::Unresolved);
160                v.insert((id, InputArg::Pure(bytes)));
161                Argument::new(id)
162            }
163        }
164    }
165
166    /// Add a pure input by serializing `value` to BCS.
167    ///
168    /// Pure inputs are values like integers, addresses, and strings — anything that is not an
169    /// on-chain object. Identical values are deduplicated; use
170    /// [`pure_unique`](Self::pure_unique) if each call must produce a distinct input.
171    ///
172    /// ```
173    /// use sui_sdk_types::Address;
174    /// use sui_transaction_builder::TransactionBuilder;
175    ///
176    /// let mut tx = TransactionBuilder::new();
177    /// let amount = tx.pure(&1_000_000_000u64);
178    /// let recipient = tx.pure(&Address::ZERO);
179    /// ```
180    pub fn pure<T: serde::Serialize>(&mut self, value: &T) -> Argument {
181        let bytes = bcs::to_bytes(value).expect("bcs serialization failed");
182        self.pure_bytes(bytes)
183    }
184
185    /// Add a pure input from raw BCS bytes, always creating a new input.
186    ///
187    /// Unlike [`pure_bytes`](Self::pure_bytes), this method never deduplicates — each call
188    /// produces a distinct input even if the bytes are identical.
189    ///
190    /// ```
191    /// use sui_transaction_builder::TransactionBuilder;
192    ///
193    /// let mut tx = TransactionBuilder::new();
194    /// let a = tx.pure_bytes_unique(vec![42]);
195    /// let b = tx.pure_bytes_unique(vec![42]);
196    /// // `a` and `b` are distinct inputs despite identical bytes
197    /// ```
198    pub fn pure_bytes_unique(&mut self, bytes: Vec<u8>) -> Argument {
199        let id = self.arguments.len();
200        self.arguments.insert(id, ResolvedArgument::Unresolved);
201        self.inputs.insert(
202            InputArgKind::UniquePureInput(id),
203            (id, InputArg::Pure(bytes)),
204        );
205        Argument::new(id)
206    }
207
208    /// Add a pure input by serializing `value` to BCS, always creating a new input.
209    ///
210    /// This is the non-deduplicating variant of [`pure`](Self::pure).
211    ///
212    /// ```
213    /// use sui_transaction_builder::TransactionBuilder;
214    ///
215    /// let mut tx = TransactionBuilder::new();
216    /// let a = tx.pure_unique(&1u64);
217    /// let b = tx.pure_unique(&1u64);
218    /// // `a` and `b` are distinct inputs
219    /// ```
220    pub fn pure_unique<T: serde::Serialize>(&mut self, value: &T) -> Argument {
221        let bytes = bcs::to_bytes(value).expect("bcs serialization failed");
222        self.pure_bytes_unique(bytes)
223    }
224
225    /// Add an object input to the transaction.
226    ///
227    /// If an object with the same ID has already been added, the existing [`Argument`] is
228    /// returned and any additional metadata (version, digest, mutability) from the new
229    /// [`ObjectInput`] is merged in.
230    ///
231    /// ```
232    /// use sui_sdk_types::Address;
233    /// use sui_sdk_types::Digest;
234    /// use sui_transaction_builder::ObjectInput;
235    /// use sui_transaction_builder::TransactionBuilder;
236    ///
237    /// let mut tx = TransactionBuilder::new();
238    /// let obj = tx.object(ObjectInput::owned(Address::ZERO, 1, Digest::ZERO));
239    /// ```
240    pub fn object(&mut self, object: ObjectInput) -> Argument {
241        match self
242            .inputs
243            .entry(InputArgKind::ObjectInput(object.object_id))
244        {
245            std::collections::hash_map::Entry::Occupied(mut o) => {
246                let id = o.get().0;
247                let InputArg::Object(object2) = &mut o.get_mut().1 else {
248                    panic!("BUG: invariant violation");
249                };
250
251                assert_eq!(
252                    object.object_id, object2.object_id,
253                    "BUG: invariant violation"
254                );
255
256                match (object.mutable, object2.mutable) {
257                    (Some(_), None) => object2.mutable = object.mutable,
258                    (Some(true), Some(false)) => object2.mutable = Some(true),
259                    _ => {}
260                }
261
262                if let (Some(kind), None) = (object.kind, object2.kind) {
263                    object2.kind = Some(kind);
264                }
265
266                if let (Some(version), None) = (object.version, object2.version) {
267                    object2.version = Some(version);
268                }
269
270                if let (Some(digest), None) = (object.digest, object2.digest) {
271                    object2.digest = Some(digest);
272                }
273
274                Argument::new(id)
275            }
276            std::collections::hash_map::Entry::Vacant(v) => {
277                let id = self.arguments.len();
278                self.arguments.insert(id, ResolvedArgument::Unresolved);
279                v.insert((id, InputArg::Object(object)));
280                Argument::new(id)
281            }
282        }
283    }
284
285    /// Add a funds-withdrawal input that requests `amount` of `coin_type` from the sender's
286    /// Address Balance.
287    ///
288    /// The returned [`Argument`] represents the raw `FundsWithdrawal` input. In most cases
289    /// you'll want [`funds_withdrawal_coin`](Self::funds_withdrawal_coin) or
290    /// [`funds_withdrawal_balance`](Self::funds_withdrawal_balance) which additionally call the
291    /// appropriate `redeem_funds` function.
292    pub fn funds_withdrawal(&mut self, coin_type: TypeTag, amount: u64) -> Argument {
293        let funds_withdrawal = sui_sdk_types::FundsWithdrawal::new(
294            amount,
295            coin_type,
296            sui_sdk_types::WithdrawFrom::Sender,
297        );
298
299        let id = self.arguments.len();
300        self.arguments.insert(id, ResolvedArgument::Unresolved);
301        self.inputs.insert(
302            InputArgKind::FundsWithdrawal(id),
303            (id, InputArg::FundsWithdrawal(funds_withdrawal)),
304        );
305        Argument::new(id)
306    }
307
308    /// Withdraw funds from the sender's Address Balance and redeem them as a `Coin<T>`.
309    ///
310    /// This adds a [`FundsWithdrawal`](sui_sdk_types::FundsWithdrawal) input and calls
311    /// `0x2::coin::redeem_funds` to convert it into a `Coin<T>`.
312    pub fn funds_withdrawal_coin(&mut self, coin_type: TypeTag, amount: u64) -> Argument {
313        let withdrawal = self.funds_withdrawal(coin_type.clone(), amount);
314        self.move_call(
315            Function {
316                package: const { Address::from_static("0x2") },
317                module: const { Identifier::from_static("coin") },
318                function: const { Identifier::from_static("redeem_funds") },
319                type_args: vec![coin_type],
320            },
321            vec![withdrawal],
322        )
323    }
324
325    /// Withdraw funds from the sender's Address Balance and redeem them as a `Balance<T>`.
326    ///
327    /// This adds a [`FundsWithdrawal`](sui_sdk_types::FundsWithdrawal) input and calls
328    /// `0x2::balance::redeem_funds` to convert it into a `Balance<T>`.
329    pub fn funds_withdrawal_balance(&mut self, coin_type: TypeTag, amount: u64) -> Argument {
330        let withdrawal = self.funds_withdrawal(coin_type.clone(), amount);
331        self.move_call(
332            Function {
333                package: const { Address::from_static("0x2") },
334                module: const { Identifier::from_static("balance") },
335                function: const { Identifier::from_static("redeem_funds") },
336                type_args: vec![coin_type],
337            },
338            vec![withdrawal],
339        )
340    }
341
342    // Metadata
343
344    /// Add one or more gas objects to use to pay for the transaction.
345    ///
346    /// ```
347    /// use sui_sdk_types::Address;
348    /// use sui_sdk_types::Digest;
349    /// use sui_transaction_builder::ObjectInput;
350    /// use sui_transaction_builder::TransactionBuilder;
351    ///
352    /// let mut tx = TransactionBuilder::new();
353    /// tx.add_gas_objects([ObjectInput::owned(Address::ZERO, 1, Digest::ZERO)]);
354    /// ```
355    pub fn add_gas_objects<O, I>(&mut self, gas: I)
356    where
357        O: Into<ObjectInput>,
358        I: IntoIterator<Item = O>,
359    {
360        self.gas.extend(gas.into_iter().map(|x| x.into()));
361    }
362
363    /// Set the gas budget for the transaction.
364    ///
365    /// ```
366    /// use sui_transaction_builder::TransactionBuilder;
367    ///
368    /// let mut tx = TransactionBuilder::new();
369    /// tx.set_gas_budget(500_000_000);
370    /// ```
371    pub fn set_gas_budget(&mut self, budget: u64) {
372        self.gas_budget = Some(budget);
373    }
374
375    /// Set the gas price for the transaction.
376    ///
377    /// ```
378    /// use sui_transaction_builder::TransactionBuilder;
379    ///
380    /// let mut tx = TransactionBuilder::new();
381    /// tx.set_gas_price(1000);
382    /// ```
383    pub fn set_gas_price(&mut self, price: u64) {
384        self.gas_price = Some(price);
385    }
386
387    /// Set the sender of the transaction.
388    ///
389    /// ```
390    /// use sui_sdk_types::Address;
391    /// use sui_transaction_builder::TransactionBuilder;
392    ///
393    /// let mut tx = TransactionBuilder::new();
394    /// tx.set_sender(Address::ZERO);
395    /// ```
396    pub fn set_sender(&mut self, sender: Address) {
397        self.sender = Some(sender);
398    }
399
400    /// Set the sponsor of the transaction.
401    ///
402    /// If not set, the sender is used as the gas owner.
403    pub fn set_sponsor(&mut self, sponsor: Address) {
404        self.sponsor = Some(sponsor);
405    }
406
407    /// Set the expiration of the transaction.
408    pub fn set_expiration(&mut self, expiration: TransactionExpiration) {
409        self.expiration = Some(expiration);
410    }
411
412    // Commands
413
414    fn command(&mut self, command: Command) -> Argument {
415        let id = self.arguments.len();
416        self.arguments.insert(id, ResolvedArgument::Unresolved);
417        self.commands.insert(id, command);
418        Argument::new(id)
419    }
420
421    /// Call a Move function with the given arguments.
422    ///
423    /// `function` is a structured representation of a `package::module::function`, optionally
424    /// with type arguments (see [`Function`] and [`Function::with_type_args`]).
425    ///
426    /// The return value is a result argument that can be used in subsequent commands. If the
427    /// Move call returns multiple results, access them with [`Argument::to_nested`].
428    ///
429    /// ```
430    /// use sui_sdk_types::Address;
431    /// use sui_sdk_types::Identifier;
432    /// use sui_transaction_builder::Function;
433    /// use sui_transaction_builder::TransactionBuilder;
434    ///
435    /// let mut tx = TransactionBuilder::new();
436    /// let result = tx.move_call(
437    ///     Function::new(
438    ///         Address::TWO,
439    ///         Identifier::from_static("coin"),
440    ///         Identifier::from_static("zero"),
441    ///     )
442    ///     .with_type_args(vec!["0x2::sui::SUI".parse().unwrap()]),
443    ///     vec![],
444    /// );
445    /// ```
446    pub fn move_call(&mut self, function: Function, arguments: Vec<Argument>) -> Argument {
447        let cmd = CommandKind::MoveCall(MoveCall {
448            package: function.package,
449            module: function.module,
450            function: function.function,
451            type_arguments: function.type_args,
452            arguments,
453        });
454        self.command(cmd.into())
455    }
456
457    /// Transfer a list of objects to the given address.
458    ///
459    /// ```
460    /// use sui_sdk_types::Address;
461    /// use sui_transaction_builder::TransactionBuilder;
462    ///
463    /// let mut tx = TransactionBuilder::new();
464    /// let gas = tx.gas();
465    /// let amount = tx.pure(&1_000_000_000u64);
466    /// let coins = tx.split_coins(gas, vec![amount]);
467    /// let recipient = tx.pure(&Address::ZERO);
468    /// tx.transfer_objects(coins, recipient);
469    /// ```
470    pub fn transfer_objects(&mut self, objects: Vec<Argument>, address: Argument) {
471        let cmd = CommandKind::TransferObjects(TransferObjects { objects, address });
472        self.command(cmd.into());
473    }
474
475    /// Split a coin by the provided amounts, returning multiple results (as many as there are
476    /// amounts). The returned vector of [`Argument`]s is guaranteed to be the same length as the
477    /// provided `amounts` vector.
478    ///
479    /// ```
480    /// use sui_transaction_builder::TransactionBuilder;
481    ///
482    /// let mut tx = TransactionBuilder::new();
483    /// let gas = tx.gas();
484    /// let a = tx.pure(&1_000u64);
485    /// let b = tx.pure(&2_000u64);
486    /// let coins = tx.split_coins(gas, vec![a, b]);
487    /// assert_eq!(coins.len(), 2);
488    /// ```
489    pub fn split_coins(&mut self, coin: Argument, amounts: Vec<Argument>) -> Vec<Argument> {
490        let amounts_len = amounts.len();
491        let cmd = CommandKind::SplitCoins(SplitCoins { coin, amounts });
492        self.command(cmd.into()).to_nested(amounts_len)
493    }
494
495    /// Merge a list of coins into a single coin.
496    ///
497    /// ```
498    /// use sui_sdk_types::Address;
499    /// use sui_sdk_types::Digest;
500    /// use sui_transaction_builder::ObjectInput;
501    /// use sui_transaction_builder::TransactionBuilder;
502    ///
503    /// let mut tx = TransactionBuilder::new();
504    /// let coin_a = tx.object(ObjectInput::owned(Address::ZERO, 1, Digest::ZERO));
505    /// let coin_b = tx.object(ObjectInput::owned(
506    ///     Address::from_static("0x1"),
507    ///     1,
508    ///     Digest::ZERO,
509    /// ));
510    /// tx.merge_coins(coin_a, vec![coin_b]);
511    /// ```
512    pub fn merge_coins(&mut self, coin: Argument, coins_to_merge: Vec<Argument>) {
513        let cmd = CommandKind::MergeCoins(MergeCoins {
514            coin,
515            coins_to_merge,
516        });
517        self.command(cmd.into());
518    }
519
520    /// Make a Move vector from a list of elements.
521    ///
522    /// If the elements are not objects, or the vector is empty, a `type_` must be supplied.
523    /// Returns the Move vector as an argument that can be used in subsequent commands.
524    ///
525    /// ```
526    /// use sui_transaction_builder::TransactionBuilder;
527    ///
528    /// let mut tx = TransactionBuilder::new();
529    /// let a = tx.pure(&1u64);
530    /// let b = tx.pure(&2u64);
531    /// let vec = tx.make_move_vec(Some("u64".parse().unwrap()), vec![a, b]);
532    /// ```
533    pub fn make_move_vec(&mut self, type_: Option<TypeTag>, elements: Vec<Argument>) -> Argument {
534        let cmd = CommandKind::MakeMoveVector(MakeMoveVector { type_, elements });
535        self.command(cmd.into())
536    }
537
538    /// Publish a list of modules with the given dependencies. The result is the
539    /// `0x2::package::UpgradeCap` Move type. Note that the upgrade capability needs to be handled
540    /// after this call:
541    ///  - transfer it to the transaction sender or another address
542    ///  - burn it
543    ///  - wrap it for access control
544    ///  - discard the it to make a package immutable
545    ///
546    /// The arguments required for this command are:
547    ///  - `modules`: is the modules' bytecode to be published
548    ///  - `dependencies`: is the list of IDs of the transitive dependencies of the package
549    pub fn publish(&mut self, modules: Vec<Vec<u8>>, dependencies: Vec<Address>) -> Argument {
550        let cmd = CommandKind::Publish(Publish {
551            modules,
552            dependencies,
553        });
554        self.command(cmd.into())
555    }
556
557    /// Upgrade a Move package.
558    ///
559    ///  - `modules`: is the modules' bytecode for the modules to be published
560    ///  - `dependencies`: is the list of IDs of the transitive dependencies of the package to be
561    ///    upgraded
562    ///  - `package`: is the ID of the current package being upgraded
563    ///  - `ticket`: is the upgrade ticket
564    ///
565    ///  To get the ticket, you have to call the `0x2::package::authorize_upgrade` function,
566    ///  and pass the package ID, the upgrade policy, and package digest.
567    pub fn upgrade(
568        &mut self,
569        modules: Vec<Vec<u8>>,
570        dependencies: Vec<Address>,
571        package: Address,
572        ticket: Argument,
573    ) -> Argument {
574        let cmd = CommandKind::Upgrade(Upgrade {
575            modules,
576            dependencies,
577            package,
578            ticket,
579        });
580        self.command(cmd.into())
581    }
582
583    // Intents
584
585    /// Register a transaction intent which is resolved later to either an input or a sequence
586    /// of commands.
587    ///
588    /// Intents are high-level descriptions of *what* the transaction needs (e.g., a coin of a
589    /// certain value) that get resolved when [`build`](Self::build) is called. See
590    /// [`Coin`](crate::intent::Coin) for an example of an Intent.
591    ///
592    /// ```
593    /// use sui_transaction_builder::TransactionBuilder;
594    /// use sui_transaction_builder::intent::Coin;
595    ///
596    /// let mut tx = TransactionBuilder::new();
597    /// let coin = tx.intent(Coin::sui(1_000_000_000));
598    /// // `coin` can be passed to subsequent commands
599    /// ```
600    #[cfg(feature = "intents")]
601    #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
602    #[allow(private_bounds)]
603    pub fn intent<I: crate::intent::Intent>(&mut self, intent: I) -> Argument {
604        intent.register(self)
605    }
606
607    /// Shorthand for `self.intent(Coin::new(coin_type, amount))`.
608    ///
609    /// Returns an [`Argument`] representing a `Coin<T>` with the requested
610    /// balance, resolved during [`build`](Self::build).
611    ///
612    /// ```
613    /// use sui_sdk_types::StructTag;
614    /// use sui_transaction_builder::TransactionBuilder;
615    ///
616    /// let mut tx = TransactionBuilder::new();
617    /// let coin = tx.coin(StructTag::sui(), 1_000_000_000);
618    /// ```
619    #[cfg(feature = "intents")]
620    #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
621    pub fn coin(&mut self, coin_type: sui_sdk_types::StructTag, amount: u64) -> Argument {
622        self.intent(crate::intent::Coin::new(coin_type, amount))
623    }
624
625    /// Shorthand for `self.intent(Balance::new(coin_type, amount))`.
626    ///
627    /// Returns an [`Argument`] representing a `Balance<T>` with the
628    /// requested amount, resolved during [`build`](Self::build).
629    ///
630    /// ```
631    /// use sui_sdk_types::StructTag;
632    /// use sui_transaction_builder::TransactionBuilder;
633    ///
634    /// let mut tx = TransactionBuilder::new();
635    /// let bal = tx.balance(StructTag::sui(), 1_000_000_000);
636    /// ```
637    #[cfg(feature = "intents")]
638    #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
639    pub fn balance(&mut self, coin_type: sui_sdk_types::StructTag, amount: u64) -> Argument {
640        self.intent(crate::intent::Balance::new(coin_type, amount))
641    }
642
643    // Building and resolving
644
645    /// Build the transaction offline.
646    ///
647    /// All metadata (sender, gas budget, gas price, gas objects) and any object inputs must be
648    /// fully specified before calling this method. Returns an [`Error`](crate::Error) if any
649    /// required fields are missing or if unresolved intents remain.
650    ///
651    /// ```
652    /// use sui_sdk_types::Address;
653    /// use sui_sdk_types::Digest;
654    /// use sui_transaction_builder::ObjectInput;
655    /// use sui_transaction_builder::TransactionBuilder;
656    ///
657    /// let mut tx = TransactionBuilder::new();
658    ///
659    /// let gas = tx.gas();
660    /// let amount = tx.pure(&1_000_000_000u64);
661    /// let coins = tx.split_coins(gas, vec![amount]);
662    /// let recipient = tx.pure(&Address::ZERO);
663    /// tx.transfer_objects(coins, recipient);
664    ///
665    /// tx.set_sender(Address::ZERO);
666    /// tx.set_gas_budget(500_000_000);
667    /// tx.set_gas_price(1000);
668    /// tx.add_gas_objects([ObjectInput::owned(Address::ZERO, 1, Digest::ZERO)]);
669    ///
670    /// let transaction = tx.try_build().unwrap();
671    /// ```
672    pub fn try_build(mut self) -> Result<Transaction, Error> {
673        let Some(sender) = self.sender else {
674            return Err(Error::MissingSender);
675        };
676        if self.gas.is_empty() {
677            return Err(Error::MissingGasObjects);
678        }
679        let Some(budget) = self.gas_budget else {
680            return Err(Error::MissingGasBudget);
681        };
682        let Some(price) = self.gas_price else {
683            return Err(Error::MissingGasPrice);
684        };
685
686        // Gas payment
687        let gas_payment = sui_sdk_types::GasPayment {
688            objects: self
689                .gas
690                .iter()
691                .map(ObjectInput::try_into_object_reference)
692                .collect::<Result<Vec<_>, _>>()?,
693            owner: self.sponsor.unwrap_or(sender),
694            price,
695            budget,
696        };
697
698        // Error out if there are any unresolved intents
699        if !self.intents.is_empty() {
700            return Err(Error::Input("unable to resolve intents offline".to_owned()));
701        }
702
703        //
704        // Inputs
705        //
706
707        let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
708        unresolved_inputs.sort_by_key(|(id, _input)| *id);
709
710        let mut resolved_inputs = Vec::new();
711        for (id, input) in unresolved_inputs {
712            let arg = match input {
713                InputArg::Gas => sui_sdk_types::Argument::Gas,
714                InputArg::Pure(value) => {
715                    resolved_inputs.push(sui_sdk_types::Input::Pure(value));
716                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
717                }
718                InputArg::Object(object_input) => {
719                    resolved_inputs.push(object_input.try_into_input()?);
720                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
721                }
722                InputArg::FundsWithdrawal(funds_withdrawal) => {
723                    resolved_inputs.push(sui_sdk_types::Input::FundsWithdrawal(funds_withdrawal));
724                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
725                }
726            };
727
728            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
729        }
730
731        //
732        // Commands
733        //
734
735        let mut resolved_commands = Vec::new();
736
737        for (id, command) in self.commands {
738            resolved_commands.push(
739                command
740                    .try_resolve(&self.arguments)
741                    .map_err(|e| e.unwrap_err())?,
742            );
743            let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
744
745            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
746        }
747
748        Ok(Transaction {
749            kind: sui_sdk_types::TransactionKind::ProgrammableTransaction(
750                sui_sdk_types::ProgrammableTransaction {
751                    inputs: resolved_inputs,
752                    commands: resolved_commands,
753                },
754            ),
755            sender,
756            gas_payment,
757            expiration: self.expiration.unwrap_or(TransactionExpiration::None),
758        })
759    }
760
761    /// Build the transaction by resolving intents and gas via an RPC client.
762    ///
763    /// This method resolves any registered intents (e.g.,
764    /// [`Coin`](crate::intent::Coin)), performs gas selection if needed,
765    /// and simulates the transaction before returning the finalized
766    /// [`Transaction`].
767    ///
768    /// # Errors
769    ///
770    /// Returns an [`Error`](crate::Error) if the sender is not set, intent resolution fails,
771    /// or the simulated execution fails.
772    #[cfg(feature = "intents")]
773    #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
774    pub async fn build(mut self, client: &mut sui_rpc::Client) -> Result<Transaction, Error> {
775        use sui_rpc::field::FieldMask;
776        use sui_rpc::field::FieldMaskUtil;
777        use sui_rpc::proto::sui::rpc::v2::Input;
778        use sui_rpc::proto::sui::rpc::v2::SimulateTransactionRequest;
779        use sui_rpc::proto::sui::rpc::v2::SimulateTransactionResponse;
780        use sui_rpc::proto::sui::rpc::v2::input::InputKind;
781
782        let Some(sender) = self.sender else {
783            return Err(Error::MissingSender);
784        };
785
786        let mut request = SimulateTransactionRequest::default()
787            .with_read_mask(FieldMask::from_paths([
788                SimulateTransactionResponse::path_builder()
789                    .transaction()
790                    .transaction()
791                    .finish(),
792                SimulateTransactionResponse::path_builder()
793                    .transaction()
794                    .effects()
795                    .finish(),
796            ]))
797            .with_do_gas_selection(true);
798        request.transaction_mut().set_sender(sender);
799
800        //
801        // Intents
802        //
803
804        // For now we'll be dumb and just run through the registered resolvers one by one and if we
805        // still have intents left we'll bail
806
807        let resolvers = std::mem::take(&mut self.resolvers);
808        for resolver in resolvers.values() {
809            resolver
810                .resolve(&mut self, client)
811                .await
812                .map_err(|e| Error::Input(e.to_string()))?;
813        }
814        // Error out if there are any remaining unresolved intents
815        if !self.intents.is_empty() {
816            return Err(Error::Input("unable to resolve all intents".to_owned()));
817        }
818
819        //
820        // Inputs
821        //
822
823        let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
824        unresolved_inputs.sort_by_key(|(id, _input)| *id);
825
826        let mut resolved_inputs = Vec::new();
827        for (id, input) in unresolved_inputs {
828            let arg = match input {
829                InputArg::Gas => sui_sdk_types::Argument::Gas,
830                InputArg::Pure(value) => {
831                    resolved_inputs
832                        .push(Input::default().with_kind(InputKind::Pure).with_pure(value));
833                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
834                }
835                InputArg::Object(object_input) => {
836                    resolved_inputs.push(object_input.to_input_proto());
837                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
838                }
839                InputArg::FundsWithdrawal(funds_withdrawal) => {
840                    resolved_inputs
841                        .push(sui_sdk_types::Input::FundsWithdrawal(funds_withdrawal).into());
842                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
843                }
844            };
845
846            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
847        }
848
849        //
850        // Commands
851        //
852
853        let mut resolved_commands = Vec::new();
854
855        let mut stack = Vec::new();
856        let mut to_resolve = self.commands.pop_first();
857        while let Some((id, command)) = to_resolve.take() {
858            let resolved = match command.try_resolve(&self.arguments) {
859                Ok(resolved) => resolved,
860                Err(Ok(next)) => {
861                    // Push the current command on the stack
862                    stack.push((id, command));
863                    // set the next one to be processed
864                    to_resolve = Some(
865                        self.commands
866                            .remove_entry(&next)
867                            .expect("command must be there if it wasn't resolved yet"),
868                    );
869                    continue;
870                }
871                Err(Err(e)) => return Err(e),
872            };
873
874            resolved_commands.push(resolved);
875            let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
876            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
877
878            // Pick the next command to resolve, either walked back down the stack or getting the
879            // next in order
880            if let Some(from_stack) = stack.pop() {
881                to_resolve = Some(from_stack);
882            } else {
883                to_resolve = self.commands.pop_first();
884            }
885        }
886
887        let t = request.transaction_mut();
888        t.kind_mut()
889            .programmable_transaction_mut()
890            .set_inputs(resolved_inputs);
891        t.kind_mut()
892            .programmable_transaction_mut()
893            .set_commands(resolved_commands.into_iter().map(Into::into).collect());
894
895        // Gas payment
896        {
897            let payment = request.transaction_mut().gas_payment_mut();
898            payment.set_owner(self.sponsor.unwrap_or(sender));
899
900            if let Some(budget) = self.gas_budget {
901                payment.set_budget(budget);
902            }
903            if let Some(price) = self.gas_price {
904                payment.set_price(price);
905            };
906            payment.set_objects(
907                self.gas
908                    .iter()
909                    .map(ObjectInput::try_into_object_reference_proto)
910                    .collect::<Result<_, _>>()?,
911            );
912        }
913
914        let response = client
915            .execution_client()
916            .simulate_transaction(request)
917            .await
918            .map_err(|e| Error::Input(format!("error simulating transaction: {e}")))?;
919
920        if !response
921            .get_ref()
922            .transaction()
923            .effects()
924            .status()
925            .success()
926        {
927            let error = response
928                .get_ref()
929                .transaction()
930                .effects()
931                .status()
932                .error()
933                .clone();
934            return Err(Error::SimulationFailure(Box::new(
935                crate::error::SimulationFailure::new(error),
936            )));
937        }
938
939        response
940            .get_ref()
941            .transaction()
942            .transaction()
943            .bcs()
944            .deserialize()
945            .map_err(|e| Error::Input(e.to_string()))
946    }
947
948    #[cfg(feature = "intents")]
949    pub(crate) fn register_resolver<R: crate::intent::IntentResolver>(&mut self, resolver: R) {
950        self.resolvers
951            .insert(resolver.type_id(), Box::new(resolver));
952    }
953
954    #[cfg(feature = "intents")]
955    pub(crate) fn unresolved<T: std::any::Any + Send + Sync>(&mut self, unresolved: T) -> Argument {
956        let id = self.arguments.len();
957        self.arguments.insert(id, ResolvedArgument::Unresolved);
958        self.intents.insert(id, Box::new(unresolved));
959        Argument::new(id)
960    }
961
962    #[cfg(feature = "intents")]
963    pub(crate) fn sender(&self) -> Option<Address> {
964        self.sender
965    }
966
967    /// Collect the object IDs of all objects already used in the builder (gas objects + inputs).
968    #[cfg(feature = "intents")]
969    pub(crate) fn used_object_ids(&self) -> std::collections::HashSet<Address> {
970        let gas_ids = self.gas.iter().map(|o| o.object_id());
971        let input_ids = self.inputs.values().filter_map(|(_, input)| match input {
972            InputArg::Object(o) => Some(o.object_id()),
973            _ => None,
974        });
975        gas_ids.chain(input_ids).collect()
976    }
977}
978
979/// A opaque handle to a transaction input or command result.
980///
981/// Arguments are produced by builder methods like [`TransactionBuilder::pure`],
982/// [`TransactionBuilder::object`], and [`TransactionBuilder::move_call`], and consumed by
983/// command methods like [`TransactionBuilder::transfer_objects`].
984///
985/// For commands that return multiple values (e.g., [`TransactionBuilder::split_coins`]),
986/// use [`to_nested`](Self::to_nested) to access individual results.
987#[derive(Clone, Copy, Debug)]
988pub struct Argument {
989    id: usize,
990    sub_index: Option<usize>,
991}
992
993impl Argument {
994    pub(crate) fn new(id: usize) -> Self {
995        Self {
996            id,
997            sub_index: None,
998        }
999    }
1000
1001    /// Split this argument into `count` nested result arguments.
1002    ///
1003    /// This is used when a command (like a Move call) returns multiple values. Each element
1004    /// in the returned vector refers to the corresponding result index.
1005    ///
1006    /// [`TransactionBuilder::split_coins`] calls this automatically, but you can use it
1007    /// directly for Move calls that return multiple values:
1008    ///
1009    /// ```
1010    /// use sui_sdk_types::Address;
1011    /// use sui_sdk_types::Identifier;
1012    /// use sui_transaction_builder::Function;
1013    /// use sui_transaction_builder::TransactionBuilder;
1014    ///
1015    /// let mut tx = TransactionBuilder::new();
1016    /// let result = tx.move_call(
1017    ///     Function::new(
1018    ///         Address::TWO,
1019    ///         Identifier::from_static("my_module"),
1020    ///         Identifier::from_static("multi_return"),
1021    ///     ),
1022    ///     vec![],
1023    /// );
1024    /// let nested = result.to_nested(3);
1025    /// assert_eq!(nested.len(), 3);
1026    /// ```
1027    pub fn to_nested(self, count: usize) -> Vec<Self> {
1028        (0..count)
1029            .map(|sub_index| Argument {
1030                sub_index: Some(sub_index),
1031                ..self
1032            })
1033            .collect()
1034    }
1035
1036    fn try_resolve(
1037        self,
1038        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1039    ) -> Result<sui_sdk_types::Argument, Result<usize, Error>> {
1040        let mut sub_index = self.sub_index;
1041        let arg = {
1042            let mut visited = BTreeSet::new();
1043            let mut next_id = self.id;
1044
1045            loop {
1046                if visited.contains(&next_id) {
1047                    panic!("BUG: cyclic dependency");
1048                }
1049                visited.insert(next_id);
1050
1051                match resolved_arguments.get(&next_id).unwrap() {
1052                    ResolvedArgument::Unresolved => return Err(Ok(next_id)),
1053                    ResolvedArgument::ReplaceWith(argument) => {
1054                        next_id = argument.id;
1055                        sub_index = argument.sub_index;
1056                    }
1057                    ResolvedArgument::Resolved(argument) => break argument,
1058                }
1059            }
1060        };
1061
1062        if let Some(sub_index) = sub_index {
1063            if let Some(arg) = arg.nested(sub_index as u16) {
1064                return Ok(arg);
1065            } else {
1066                return Err(Err(Error::Input(
1067                    "unable to create nested argument".to_owned(),
1068                )));
1069            }
1070        }
1071
1072        Ok(*arg)
1073    }
1074
1075    fn try_resolve_many(
1076        arguments: &[Self],
1077        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1078    ) -> Result<Vec<sui_sdk_types::Argument>, Result<usize, Error>> {
1079        arguments
1080            .iter()
1081            .map(|a| a.try_resolve(resolved_arguments))
1082            .collect::<Result<_, _>>()
1083    }
1084}
1085
1086pub(crate) struct Command {
1087    kind: CommandKind,
1088    // A way to encode dependencies between commands when there aren't dependencies via explicit
1089    // input/outputs
1090    pub(crate) dependencies: Vec<Argument>,
1091}
1092
1093impl From<CommandKind> for Command {
1094    fn from(value: CommandKind) -> Self {
1095        Self {
1096            kind: value,
1097            dependencies: Vec::new(),
1098        }
1099    }
1100}
1101
1102pub(crate) enum CommandKind {
1103    /// A call to either an entry or a public Move function
1104    MoveCall(MoveCall),
1105
1106    /// `(Vec<forall T:key+store. T>, address)`
1107    /// It sends n-objects to the specified address. These objects must have store
1108    /// (public transfer) and either the previous owner must be an address or the object must
1109    /// be newly created.
1110    TransferObjects(TransferObjects),
1111
1112    /// `(&mut Coin<T>, Vec<u64>)` -> `Vec<Coin<T>>`
1113    /// It splits off some amounts into a new coins with those amounts
1114    SplitCoins(SplitCoins),
1115
1116    /// `(&mut Coin<T>, Vec<Coin<T>>)`
1117    /// It merges n-coins into the first coin
1118    MergeCoins(MergeCoins),
1119
1120    /// Publishes a Move package. It takes the package bytes and a list of the package's transitive
1121    /// dependencies to link against on-chain.
1122    Publish(Publish),
1123
1124    /// `forall T: Vec<T> -> vector<T>`
1125    /// Given n-values of the same type, it constructs a vector. For non objects or an empty vector,
1126    /// the type tag must be specified.
1127    MakeMoveVector(MakeMoveVector),
1128
1129    /// Upgrades a Move package
1130    /// Takes (in order):
1131    /// 1. A vector of serialized modules for the package.
1132    /// 2. A vector of object ids for the transitive dependencies of the new package.
1133    /// 3. The object ID of the package being upgraded.
1134    /// 4. An argument holding the `UpgradeTicket` that must have been produced from an earlier command in the same
1135    ///    programmable transaction.
1136    Upgrade(Upgrade),
1137}
1138
1139impl Command {
1140    fn try_resolve(
1141        &self,
1142        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1143    ) -> Result<sui_sdk_types::Command, Result<usize, Error>> {
1144        use sui_sdk_types::Command as C;
1145
1146        // try to resolve all dependencies first
1147        Argument::try_resolve_many(&self.dependencies, resolved_arguments)?;
1148
1149        let cmd = match &self.kind {
1150            CommandKind::MoveCall(MoveCall {
1151                package,
1152                module,
1153                function,
1154                type_arguments,
1155                arguments,
1156            }) => C::MoveCall(sui_sdk_types::MoveCall {
1157                package: *package,
1158                module: module.to_owned(),
1159                function: function.to_owned(),
1160                type_arguments: type_arguments.to_owned(),
1161                arguments: Argument::try_resolve_many(arguments, resolved_arguments)?,
1162            }),
1163
1164            CommandKind::TransferObjects(TransferObjects { objects, address }) => {
1165                C::TransferObjects(sui_sdk_types::TransferObjects {
1166                    objects: Argument::try_resolve_many(objects, resolved_arguments)?,
1167                    address: address.try_resolve(resolved_arguments)?,
1168                })
1169            }
1170
1171            CommandKind::SplitCoins(SplitCoins { coin, amounts }) => {
1172                C::SplitCoins(sui_sdk_types::SplitCoins {
1173                    coin: coin.try_resolve(resolved_arguments)?,
1174                    amounts: Argument::try_resolve_many(amounts, resolved_arguments)?,
1175                })
1176            }
1177
1178            CommandKind::MergeCoins(MergeCoins {
1179                coin,
1180                coins_to_merge,
1181            }) => C::MergeCoins(sui_sdk_types::MergeCoins {
1182                coin: coin.try_resolve(resolved_arguments)?,
1183                coins_to_merge: Argument::try_resolve_many(coins_to_merge, resolved_arguments)?,
1184            }),
1185
1186            CommandKind::Publish(Publish {
1187                modules,
1188                dependencies,
1189            }) => C::Publish(sui_sdk_types::Publish {
1190                modules: modules.to_owned(),
1191                dependencies: dependencies.to_owned(),
1192            }),
1193
1194            CommandKind::MakeMoveVector(MakeMoveVector { type_, elements }) => {
1195                C::MakeMoveVector(sui_sdk_types::MakeMoveVector {
1196                    type_: type_.to_owned(),
1197                    elements: Argument::try_resolve_many(elements, resolved_arguments)?,
1198                })
1199            }
1200
1201            CommandKind::Upgrade(Upgrade {
1202                modules,
1203                dependencies,
1204                package,
1205                ticket,
1206            }) => C::Upgrade(sui_sdk_types::Upgrade {
1207                modules: modules.to_owned(),
1208                dependencies: dependencies.to_owned(),
1209                package: *package,
1210                ticket: ticket.try_resolve(resolved_arguments)?,
1211            }),
1212        };
1213        Ok(cmd)
1214    }
1215}
1216
1217pub(crate) struct TransferObjects {
1218    /// Set of objects to transfer
1219    pub objects: Vec<Argument>,
1220
1221    /// The address to transfer ownership to
1222    pub address: Argument,
1223}
1224
1225pub(crate) struct SplitCoins {
1226    /// The coin to split
1227    pub coin: Argument,
1228
1229    /// The amounts to split off
1230    pub amounts: Vec<Argument>,
1231}
1232
1233pub(crate) struct MergeCoins {
1234    /// Coin to merge coins into
1235    pub coin: Argument,
1236
1237    /// Set of coins to merge into `coin`
1238    ///
1239    /// All listed coins must be of the same type and be the same type as `coin`
1240    pub coins_to_merge: Vec<Argument>,
1241}
1242
1243pub(crate) struct Publish {
1244    /// The serialized move modules
1245    pub modules: Vec<Vec<u8>>,
1246
1247    /// Set of packages that the to-be published package depends on
1248    pub dependencies: Vec<Address>,
1249}
1250
1251pub(crate) struct MakeMoveVector {
1252    /// Type of the individual elements
1253    ///
1254    /// This is required to be set when the type can't be inferred, for example when the set of
1255    /// provided arguments are all pure input values.
1256    pub type_: Option<TypeTag>,
1257
1258    /// The set individual elements to build the vector with
1259    pub elements: Vec<Argument>,
1260}
1261
1262pub(crate) struct Upgrade {
1263    /// The serialized move modules
1264    pub modules: Vec<Vec<u8>>,
1265
1266    /// Set of packages that the to-be published package depends on
1267    pub dependencies: Vec<Address>,
1268
1269    /// Package id of the package to upgrade
1270    pub package: Address,
1271
1272    /// Ticket authorizing the upgrade
1273    pub ticket: Argument,
1274}
1275
1276pub(crate) struct MoveCall {
1277    /// The package containing the module and function.
1278    pub package: Address,
1279
1280    /// The specific module in the package containing the function.
1281    pub module: Identifier,
1282
1283    /// The function to be called.
1284    pub function: Identifier,
1285
1286    /// The type arguments to the function.
1287    pub type_arguments: Vec<TypeTag>,
1288
1289    /// The arguments to the function.
1290    pub arguments: Vec<Argument>,
1291    // Return value count??
1292}
1293
1294/// Description of an on-chain object to use as a transaction input.
1295///
1296/// Use one of the constructors ([`new`](Self::new), [`owned`](Self::owned),
1297/// [`shared`](Self::shared), [`immutable`](Self::immutable), [`receiving`](Self::receiving))
1298/// and then optionally refine with builder methods like [`with_version`](Self::with_version),
1299/// [`with_digest`](Self::with_digest), and [`with_mutable`](Self::with_mutable).
1300///
1301/// ```
1302/// use sui_sdk_types::Address;
1303/// use sui_sdk_types::Digest;
1304/// use sui_transaction_builder::ObjectInput;
1305///
1306/// // Fully-specified owned object
1307/// let obj = ObjectInput::owned(Address::ZERO, 1, Digest::ZERO);
1308///
1309/// // Minimal object — additional fields can be filled in by the builder
1310/// let obj = ObjectInput::new(Address::ZERO);
1311///
1312/// // Shared object
1313/// let obj = ObjectInput::shared(Address::ZERO, 1, true);
1314/// ```
1315#[derive(Clone)]
1316pub struct ObjectInput {
1317    object_id: Address,
1318    kind: Option<ObjectKind>,
1319    version: Option<u64>,
1320    digest: Option<Digest>,
1321    mutable: Option<bool>,
1322}
1323
1324#[derive(Clone, Copy)]
1325enum ObjectKind {
1326    Shared,
1327    Receiving,
1328    ImmutableOrOwned,
1329}
1330
1331impl ObjectInput {
1332    /// Create a minimal object input with only an object ID.
1333    ///
1334    /// Additional metadata (kind, version, digest, mutability) can be later resolved when a
1335    /// transaction is built.
1336    pub fn new(object_id: Address) -> Self {
1337        Self {
1338            kind: None,
1339            object_id,
1340            version: None,
1341            digest: None,
1342            mutable: None,
1343        }
1344    }
1345
1346    /// Return an owned kind of object with all required fields.
1347    pub fn owned(object_id: Address, version: u64, digest: Digest) -> Self {
1348        Self {
1349            kind: Some(ObjectKind::ImmutableOrOwned),
1350            object_id,
1351            version: Some(version),
1352            digest: Some(digest),
1353            mutable: None,
1354        }
1355    }
1356
1357    /// Return an immutable kind of object with all required fields.
1358    pub fn immutable(object_id: Address, version: u64, digest: Digest) -> Self {
1359        Self {
1360            kind: Some(ObjectKind::ImmutableOrOwned),
1361            object_id,
1362            version: Some(version),
1363            digest: Some(digest),
1364            mutable: None,
1365        }
1366    }
1367
1368    /// Return a receiving kind of object with all required fields.
1369    pub fn receiving(object_id: Address, version: u64, digest: Digest) -> Self {
1370        Self {
1371            kind: Some(ObjectKind::Receiving),
1372            object_id,
1373            version: Some(version),
1374            digest: Some(digest),
1375            mutable: None,
1376        }
1377    }
1378
1379    /// Return a shared object.
1380    /// - `mutable` controls whether a command can accept the object by value or mutable reference.
1381    /// - `version` is the first version the object was shared at.
1382    pub fn shared(object_id: Address, version: u64, mutable: bool) -> Self {
1383        Self {
1384            kind: Some(ObjectKind::Shared),
1385            object_id,
1386            version: Some(version),
1387            mutable: Some(mutable),
1388            digest: None,
1389        }
1390    }
1391
1392    /// Set the object kind to immutable.
1393    pub fn as_immutable(self) -> Self {
1394        Self {
1395            kind: Some(ObjectKind::ImmutableOrOwned),
1396            ..self
1397        }
1398    }
1399
1400    /// Set the object kind to owned.
1401    pub fn as_owned(self) -> Self {
1402        Self {
1403            kind: Some(ObjectKind::ImmutableOrOwned),
1404            ..self
1405        }
1406    }
1407
1408    /// Set the object kind to receiving.
1409    pub fn as_receiving(self) -> Self {
1410        Self {
1411            kind: Some(ObjectKind::Receiving),
1412            ..self
1413        }
1414    }
1415
1416    /// Set the object kind to shared.
1417    pub fn as_shared(self) -> Self {
1418        Self {
1419            kind: Some(ObjectKind::Shared),
1420            ..self
1421        }
1422    }
1423
1424    /// Set the specified version.
1425    pub fn with_version(self, version: u64) -> Self {
1426        Self {
1427            version: Some(version),
1428            ..self
1429        }
1430    }
1431
1432    /// Set the specified digest.
1433    pub fn with_digest(self, digest: Digest) -> Self {
1434        Self {
1435            digest: Some(digest),
1436            ..self
1437        }
1438    }
1439
1440    /// Set whether this object is accessed mutably.
1441    ///
1442    /// This is primarily relevant for shared objects to indicate whether the command will
1443    /// take the object by value or mutable reference.
1444    pub fn with_mutable(self, mutable: bool) -> Self {
1445        Self {
1446            mutable: Some(mutable),
1447            ..self
1448        }
1449    }
1450
1451    #[cfg(feature = "intents")]
1452    pub(crate) fn object_id(&self) -> Address {
1453        self.object_id
1454    }
1455}
1456
1457impl From<&sui_sdk_types::Object> for ObjectInput {
1458    fn from(object: &sui_sdk_types::Object) -> Self {
1459        let input = Self::new(object.object_id())
1460            .with_version(object.version())
1461            .with_digest(object.digest());
1462
1463        match object.owner() {
1464            sui_sdk_types::Owner::Address(_) => input.as_owned(),
1465            sui_sdk_types::Owner::Object(_) => input,
1466            sui_sdk_types::Owner::Shared(version) => input.with_version(*version).as_shared(),
1467            sui_sdk_types::Owner::Immutable => input.as_immutable(),
1468            sui_sdk_types::Owner::ConsensusAddress { start_version, .. } => {
1469                input.with_version(*start_version).as_shared()
1470            }
1471            _ => input,
1472        }
1473    }
1474}
1475
1476// private conversions
1477impl ObjectInput {
1478    fn try_into_object_reference(&self) -> Result<sui_sdk_types::ObjectReference, Error> {
1479        if matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None)
1480            && let Some(version) = self.version
1481            && let Some(digest) = self.digest
1482        {
1483            Ok(sui_sdk_types::ObjectReference::new(
1484                self.object_id,
1485                version,
1486                digest,
1487            ))
1488        } else {
1489            Err(Error::WrongGasObject)
1490        }
1491    }
1492
1493    fn try_into_input(&self) -> Result<sui_sdk_types::Input, Error> {
1494        let input = match self {
1495            // ImmutableOrOwned
1496            Self {
1497                object_id,
1498                kind: Some(ObjectKind::ImmutableOrOwned),
1499                version: Some(version),
1500                digest: Some(digest),
1501                ..
1502            }
1503            | Self {
1504                object_id,
1505                kind: None,
1506                version: Some(version),
1507                digest: Some(digest),
1508                mutable: None,
1509            } => sui_sdk_types::Input::ImmutableOrOwned(sui_sdk_types::ObjectReference::new(
1510                *object_id, *version, *digest,
1511            )),
1512
1513            // Receiving
1514            Self {
1515                object_id,
1516                kind: Some(ObjectKind::Receiving),
1517                version: Some(version),
1518                digest: Some(digest),
1519                ..
1520            } => sui_sdk_types::Input::Receiving(sui_sdk_types::ObjectReference::new(
1521                *object_id, *version, *digest,
1522            )),
1523
1524            // Shared
1525            Self {
1526                object_id,
1527                kind: Some(ObjectKind::Shared),
1528                version: Some(version),
1529                mutable: Some(mutable),
1530                ..
1531            }
1532            | Self {
1533                object_id,
1534                kind: None,
1535                version: Some(version),
1536                digest: None,
1537                mutable: Some(mutable),
1538            } => sui_sdk_types::Input::Shared(sui_sdk_types::SharedInput::new(
1539                *object_id, *version, *mutable,
1540            )),
1541
1542            _ => {
1543                return Err(Error::Input(format!(
1544                    "Input object {} is incomplete",
1545                    self.object_id
1546                )));
1547            }
1548        };
1549        Ok(input)
1550    }
1551
1552    #[cfg(feature = "intents")]
1553    fn to_input_proto(&self) -> sui_rpc::proto::sui::rpc::v2::Input {
1554        use sui_rpc::proto::sui::rpc::v2::input::InputKind;
1555
1556        let mut input =
1557            sui_rpc::proto::sui::rpc::v2::Input::default().with_object_id(self.object_id);
1558        match &self.kind {
1559            Some(ObjectKind::Shared) => input.set_kind(InputKind::Shared),
1560            Some(ObjectKind::Receiving) => input.set_kind(InputKind::Receiving),
1561            Some(ObjectKind::ImmutableOrOwned) => input.set_kind(InputKind::ImmutableOrOwned),
1562            None => {}
1563        }
1564
1565        if let Some(version) = self.version {
1566            input.set_version(version);
1567        }
1568
1569        if let Some(digest) = self.digest {
1570            input.set_digest(digest);
1571        }
1572
1573        if let Some(mutable) = self.mutable {
1574            input.set_mutable(mutable);
1575        }
1576
1577        input
1578    }
1579
1580    #[cfg(feature = "intents")]
1581    fn try_into_object_reference_proto(
1582        &self,
1583    ) -> Result<sui_rpc::proto::sui::rpc::v2::ObjectReference, Error> {
1584        if !matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None) {
1585            return Err(Error::WrongGasObject);
1586        }
1587
1588        let mut input =
1589            sui_rpc::proto::sui::rpc::v2::ObjectReference::default().with_object_id(self.object_id);
1590        if let Some(version) = self.version {
1591            input.set_version(version);
1592        }
1593        if let Some(digest) = self.digest {
1594            input.set_digest(digest);
1595        }
1596        Ok(input)
1597    }
1598
1599    #[cfg(feature = "intents")]
1600    pub(crate) fn try_from_object_proto(
1601        object: &sui_rpc::proto::sui::rpc::v2::Object,
1602    ) -> Result<Self, Error> {
1603        use sui_rpc::proto::sui::rpc::v2::owner::OwnerKind;
1604
1605        let input = Self::new(
1606            object
1607                .object_id()
1608                .parse()
1609                .map_err(|_e| Error::MissingObjectId)?,
1610        );
1611
1612        Ok(match object.owner().kind() {
1613            OwnerKind::Address | OwnerKind::Immutable => {
1614                input.as_owned().with_version(object.version()).with_digest(
1615                    object
1616                        .digest()
1617                        .parse()
1618                        .map_err(|_| Error::Input("can't parse digest".to_owned()))?,
1619                )
1620            }
1621            OwnerKind::Object => return Err(Error::Input("invalid object type".to_owned())),
1622            OwnerKind::Shared | OwnerKind::ConsensusAddress => input
1623                .as_shared()
1624                .with_version(object.owner().version())
1625                .with_mutable(true),
1626            OwnerKind::Unknown | _ => input,
1627        })
1628    }
1629}
1630
1631/// A structured representation of a Move function (`package::module::function`), optionally
1632/// with type arguments.
1633///
1634/// Use [`Function::new`] to create a function reference, and
1635/// [`Function::with_type_args`] to add generic type parameters.
1636///
1637/// ```
1638/// use sui_sdk_types::Address;
1639/// use sui_sdk_types::Identifier;
1640/// use sui_transaction_builder::Function;
1641///
1642/// let f = Function::new(
1643///     Address::TWO,
1644///     Identifier::from_static("coin"),
1645///     Identifier::from_static("zero"),
1646/// )
1647/// .with_type_args(vec!["0x2::sui::SUI".parse().unwrap()]);
1648/// ```
1649pub struct Function {
1650    /// The package that contains the module with the function.
1651    package: Address,
1652    /// The module that contains the function.
1653    module: Identifier,
1654    /// The function name.
1655    function: Identifier,
1656    /// The type arguments for the function.
1657    type_args: Vec<TypeTag>,
1658}
1659
1660impl Function {
1661    /// Create a new function reference.
1662    ///
1663    /// ```
1664    /// use sui_sdk_types::Address;
1665    /// use sui_sdk_types::Identifier;
1666    /// use sui_transaction_builder::Function;
1667    ///
1668    /// let f = Function::new(
1669    ///     Address::TWO,
1670    ///     Identifier::from_static("coin"),
1671    ///     Identifier::from_static("zero"),
1672    /// );
1673    /// ```
1674    pub fn new(package: Address, module: Identifier, function: Identifier) -> Self {
1675        Self {
1676            package,
1677            module,
1678            function,
1679            type_args: Vec::new(),
1680        }
1681    }
1682
1683    /// Set the type arguments for the function call.
1684    ///
1685    /// ```
1686    /// use sui_sdk_types::Address;
1687    /// use sui_sdk_types::Identifier;
1688    /// use sui_transaction_builder::Function;
1689    ///
1690    /// let f = Function::new(
1691    ///     Address::TWO,
1692    ///     Identifier::from_static("coin"),
1693    ///     Identifier::from_static("zero"),
1694    /// )
1695    /// .with_type_args(vec!["0x2::sui::SUI".parse().unwrap()]);
1696    /// ```
1697    pub fn with_type_args(self, type_args: Vec<TypeTag>) -> Self {
1698        Self { type_args, ..self }
1699    }
1700}
1701
1702#[cfg(test)]
1703mod tests {
1704    use super::*;
1705
1706    #[test]
1707    fn simple_try_build() {
1708        let mut tx = TransactionBuilder::new();
1709        let _coin = tx.object(ObjectInput::owned(
1710            Address::from_static(
1711                "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1712            ),
1713            2,
1714            Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1715        ));
1716        let _gas = tx.gas();
1717
1718        let _recipient = tx.pure(&Address::from_static("0xabc"));
1719
1720        assert!(tx.try_build().is_err());
1721
1722        let mut tx = TransactionBuilder::new();
1723        let coin = tx.object(ObjectInput::owned(
1724            Address::from_static(
1725                "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1726            ),
1727            2,
1728            Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1729        ));
1730        let gas = tx.gas();
1731
1732        let recipient = tx.pure(&Address::from_static("0xabc"));
1733        tx.transfer_objects(vec![coin, gas], recipient);
1734        tx.set_gas_budget(500000000);
1735        tx.set_gas_price(1000);
1736        tx.add_gas_objects([ObjectInput::owned(
1737            Address::from_static(
1738                "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1739            ),
1740            2,
1741            Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1742        )]);
1743        tx.set_sender(Address::from_static(
1744            "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1745        ));
1746
1747        assert!(tx.try_build().is_ok());
1748    }
1749
1750    #[test]
1751    fn test_split_transfer() {
1752        let mut tx = TransactionBuilder::new();
1753
1754        // transfer 1 SUI from Gas coin
1755        let amount = tx.pure(&1_000_000_000u64);
1756        let gas = tx.gas();
1757        let result = tx.split_coins(gas, vec![amount; 5]);
1758        let recipient = tx.pure(&Address::from_static("0xabc"));
1759        tx.transfer_objects(result, recipient);
1760
1761        tx.set_gas_budget(500000000);
1762        tx.set_gas_price(1000);
1763        tx.add_gas_objects([ObjectInput::owned(
1764            Address::from_static(
1765                "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1766            ),
1767            2,
1768            Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1769        )]);
1770        tx.set_sender(Address::from_static(
1771            "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1772        ));
1773
1774        assert!(tx.try_build().is_ok());
1775    }
1776
1777    #[test]
1778    fn test_deterministic_building() {
1779        let build_tx = || {
1780            let mut tx = TransactionBuilder::new();
1781            let coin = tx.object(ObjectInput::owned(
1782                Address::from_static(
1783                    "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1784                ),
1785                2,
1786                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1787            ));
1788            let _ = tx.object(ObjectInput::owned(
1789                Address::from_static("0x12345"),
1790                2,
1791                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1792            ));
1793            let _ = tx.object(ObjectInput::owned(
1794                Address::from_static("0x12345"),
1795                2,
1796                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1797            ));
1798            let gas = tx.gas();
1799            let _ = tx.pure(&Address::from_static("0xabc"));
1800            let _ = tx.pure(&Address::from_static("0xabc"));
1801            let _ = tx.pure(&Address::from_static("0xabc"));
1802            let _ = tx.pure(&Address::from_static("0xdef"));
1803            let _ = tx.pure(&1u64);
1804            let _ = tx.pure(&1u64);
1805            let _ = tx.pure(&1u64);
1806            let _ = tx.pure(&Some(2u8));
1807            let _ = tx.pure_unique(&Address::from_static("0xabc"));
1808            let _ = tx.pure_unique(&Address::from_static("0xabc"));
1809            let _ = tx.pure_unique(&1u64);
1810
1811            let recipient = tx.pure(&Address::from_static("0x123"));
1812            tx.transfer_objects(vec![coin, gas], recipient);
1813            tx.set_gas_budget(500000000);
1814            tx.set_gas_price(1000);
1815            tx.add_gas_objects([ObjectInput::owned(
1816                Address::from_static(
1817                    "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1818                ),
1819                2,
1820                Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1821            )]);
1822            tx.set_sender(Address::from_static(
1823                "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1824            ));
1825
1826            tx.try_build().unwrap()
1827        };
1828
1829        let digest = build_tx().digest();
1830
1831        assert!(
1832            (0..100)
1833                .map(|_| build_tx())
1834                .map(|tx| tx.digest())
1835                .all(|d| d == digest)
1836        )
1837    }
1838}