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    /// [`CoinWithBalance`](crate::intent::CoinWithBalance) for an example of an Intent.
591    ///
592    /// ```
593    /// use sui_transaction_builder::TransactionBuilder;
594    /// use sui_transaction_builder::intent::CoinWithBalance;
595    ///
596    /// let mut tx = TransactionBuilder::new();
597    /// let coin = tx.intent(CoinWithBalance::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    // Building and resolving
608
609    /// Build the transaction offline.
610    ///
611    /// All metadata (sender, gas budget, gas price, gas objects) and any object inputs must be
612    /// fully specified before calling this method. Returns an [`Error`](crate::Error) if any
613    /// required fields are missing or if unresolved intents remain.
614    ///
615    /// ```
616    /// use sui_sdk_types::Address;
617    /// use sui_sdk_types::Digest;
618    /// use sui_transaction_builder::ObjectInput;
619    /// use sui_transaction_builder::TransactionBuilder;
620    ///
621    /// let mut tx = TransactionBuilder::new();
622    ///
623    /// let gas = tx.gas();
624    /// let amount = tx.pure(&1_000_000_000u64);
625    /// let coins = tx.split_coins(gas, vec![amount]);
626    /// let recipient = tx.pure(&Address::ZERO);
627    /// tx.transfer_objects(coins, recipient);
628    ///
629    /// tx.set_sender(Address::ZERO);
630    /// tx.set_gas_budget(500_000_000);
631    /// tx.set_gas_price(1000);
632    /// tx.add_gas_objects([ObjectInput::owned(Address::ZERO, 1, Digest::ZERO)]);
633    ///
634    /// let transaction = tx.try_build().unwrap();
635    /// ```
636    pub fn try_build(mut self) -> Result<Transaction, Error> {
637        let Some(sender) = self.sender else {
638            return Err(Error::MissingSender);
639        };
640        if self.gas.is_empty() {
641            return Err(Error::MissingGasObjects);
642        }
643        let Some(budget) = self.gas_budget else {
644            return Err(Error::MissingGasBudget);
645        };
646        let Some(price) = self.gas_price else {
647            return Err(Error::MissingGasPrice);
648        };
649
650        // Gas payment
651        let gas_payment = sui_sdk_types::GasPayment {
652            objects: self
653                .gas
654                .iter()
655                .map(ObjectInput::try_into_object_reference)
656                .collect::<Result<Vec<_>, _>>()?,
657            owner: self.sponsor.unwrap_or(sender),
658            price,
659            budget,
660        };
661
662        // Error out if there are any unresolved intents
663        if !self.intents.is_empty() {
664            return Err(Error::Input("unable to resolve intents offline".to_owned()));
665        }
666
667        //
668        // Inputs
669        //
670
671        let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
672        unresolved_inputs.sort_by_key(|(id, _input)| *id);
673
674        let mut resolved_inputs = Vec::new();
675        for (id, input) in unresolved_inputs {
676            let arg = match input {
677                InputArg::Gas => sui_sdk_types::Argument::Gas,
678                InputArg::Pure(value) => {
679                    resolved_inputs.push(sui_sdk_types::Input::Pure(value));
680                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
681                }
682                InputArg::Object(object_input) => {
683                    resolved_inputs.push(object_input.try_into_input()?);
684                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
685                }
686                InputArg::FundsWithdrawal(funds_withdrawal) => {
687                    resolved_inputs.push(sui_sdk_types::Input::FundsWithdrawal(funds_withdrawal));
688                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
689                }
690            };
691
692            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
693        }
694
695        //
696        // Commands
697        //
698
699        let mut resolved_commands = Vec::new();
700
701        for (id, command) in self.commands {
702            resolved_commands.push(
703                command
704                    .try_resolve(&self.arguments)
705                    .map_err(|e| e.unwrap_err())?,
706            );
707            let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
708
709            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
710        }
711
712        Ok(Transaction {
713            kind: sui_sdk_types::TransactionKind::ProgrammableTransaction(
714                sui_sdk_types::ProgrammableTransaction {
715                    inputs: resolved_inputs,
716                    commands: resolved_commands,
717                },
718            ),
719            sender,
720            gas_payment,
721            expiration: self.expiration.unwrap_or(TransactionExpiration::None),
722        })
723    }
724
725    /// Build the transaction by resolving intents and gas via an RPC client.
726    ///
727    /// This method resolves any registered intents (e.g.,
728    /// [`CoinWithBalance`](crate::intent::CoinWithBalance)), performs gas selection if needed,
729    /// and simulates the transaction before returning the finalized
730    /// [`Transaction`].
731    ///
732    /// # Errors
733    ///
734    /// Returns an [`Error`](crate::Error) if the sender is not set, intent resolution fails,
735    /// or the simulated execution fails.
736    #[cfg(feature = "intents")]
737    #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
738    pub async fn build(mut self, client: &mut sui_rpc::Client) -> Result<Transaction, Error> {
739        use sui_rpc::field::FieldMask;
740        use sui_rpc::field::FieldMaskUtil;
741        use sui_rpc::proto::sui::rpc::v2::Input;
742        use sui_rpc::proto::sui::rpc::v2::SimulateTransactionRequest;
743        use sui_rpc::proto::sui::rpc::v2::SimulateTransactionResponse;
744        use sui_rpc::proto::sui::rpc::v2::input::InputKind;
745
746        let Some(sender) = self.sender else {
747            return Err(Error::MissingSender);
748        };
749
750        let mut request = SimulateTransactionRequest::default()
751            .with_read_mask(FieldMask::from_paths([
752                SimulateTransactionResponse::path_builder()
753                    .transaction()
754                    .transaction()
755                    .finish(),
756                SimulateTransactionResponse::path_builder()
757                    .transaction()
758                    .effects()
759                    .finish(),
760            ]))
761            .with_do_gas_selection(true);
762        request.transaction_mut().set_sender(sender);
763
764        //
765        // Intents
766        //
767
768        // For now we'll be dumb and just run through the registered resolvers one by one and if we
769        // still have intents left we'll bail
770
771        let resolvers = std::mem::take(&mut self.resolvers);
772        for resolver in resolvers.values() {
773            resolver
774                .resolve(&mut self, client)
775                .await
776                .map_err(|e| Error::Input(e.to_string()))?;
777        }
778        // Error out if there are any remaining unresolved intents
779        if !self.intents.is_empty() {
780            return Err(Error::Input("unable to resolve all intents".to_owned()));
781        }
782
783        //
784        // Inputs
785        //
786
787        let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
788        unresolved_inputs.sort_by_key(|(id, _input)| *id);
789
790        let mut resolved_inputs = Vec::new();
791        for (id, input) in unresolved_inputs {
792            let arg = match input {
793                InputArg::Gas => sui_sdk_types::Argument::Gas,
794                InputArg::Pure(value) => {
795                    resolved_inputs
796                        .push(Input::default().with_kind(InputKind::Pure).with_pure(value));
797                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
798                }
799                InputArg::Object(object_input) => {
800                    resolved_inputs.push(object_input.to_input_proto());
801                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
802                }
803                InputArg::FundsWithdrawal(funds_withdrawal) => {
804                    resolved_inputs
805                        .push(sui_sdk_types::Input::FundsWithdrawal(funds_withdrawal).into());
806                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
807                }
808            };
809
810            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
811        }
812
813        //
814        // Commands
815        //
816
817        let mut resolved_commands = Vec::new();
818
819        let mut stack = Vec::new();
820        let mut to_resolve = self.commands.pop_first();
821        while let Some((id, command)) = to_resolve.take() {
822            let resolved = match command.try_resolve(&self.arguments) {
823                Ok(resolved) => resolved,
824                Err(Ok(next)) => {
825                    // Push the current command on the stack
826                    stack.push((id, command));
827                    // set the next one to be processed
828                    to_resolve = Some(
829                        self.commands
830                            .remove_entry(&next)
831                            .expect("command must be there if it wasn't resolved yet"),
832                    );
833                    continue;
834                }
835                Err(Err(e)) => return Err(e),
836            };
837
838            resolved_commands.push(resolved);
839            let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
840            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
841
842            // Pick the next command to resolve, either walked back down the stack or getting the
843            // next in order
844            if let Some(from_stack) = stack.pop() {
845                to_resolve = Some(from_stack);
846            } else {
847                to_resolve = self.commands.pop_first();
848            }
849        }
850
851        let t = request.transaction_mut();
852        t.kind_mut()
853            .programmable_transaction_mut()
854            .set_inputs(resolved_inputs);
855        t.kind_mut()
856            .programmable_transaction_mut()
857            .set_commands(resolved_commands.into_iter().map(Into::into).collect());
858
859        // Gas payment
860        {
861            let payment = request.transaction_mut().gas_payment_mut();
862            payment.set_owner(self.sponsor.unwrap_or(sender));
863
864            if let Some(budget) = self.gas_budget {
865                payment.set_budget(budget);
866            }
867            if let Some(price) = self.gas_price {
868                payment.set_price(price);
869            };
870            payment.set_objects(
871                self.gas
872                    .iter()
873                    .map(ObjectInput::try_into_object_reference_proto)
874                    .collect::<Result<_, _>>()?,
875            );
876        }
877
878        let response = client
879            .execution_client()
880            .simulate_transaction(request)
881            .await
882            .map_err(|e| Error::Input(format!("error simulating transaction: {e}")))?;
883
884        if !response
885            .get_ref()
886            .transaction()
887            .effects()
888            .status()
889            .success()
890        {
891            let error = response
892                .get_ref()
893                .transaction()
894                .effects()
895                .status()
896                .error()
897                .clone();
898            return Err(Error::SimulationFailure(Box::new(
899                crate::error::SimulationFailure::new(error),
900            )));
901        }
902
903        response
904            .get_ref()
905            .transaction()
906            .transaction()
907            .bcs()
908            .deserialize()
909            .map_err(|e| Error::Input(e.to_string()))
910    }
911
912    #[cfg(feature = "intents")]
913    pub(crate) fn register_resolver<R: crate::intent::IntentResolver>(&mut self, resolver: R) {
914        self.resolvers
915            .insert(resolver.type_id(), Box::new(resolver));
916    }
917
918    #[cfg(feature = "intents")]
919    pub(crate) fn unresolved<T: std::any::Any + Send + Sync>(&mut self, unresolved: T) -> Argument {
920        let id = self.arguments.len();
921        self.arguments.insert(id, ResolvedArgument::Unresolved);
922        self.intents.insert(id, Box::new(unresolved));
923        Argument::new(id)
924    }
925
926    #[cfg(feature = "intents")]
927    pub(crate) fn sender(&self) -> Option<Address> {
928        self.sender
929    }
930
931    /// Collect the object IDs of all objects already used in the builder (gas objects + inputs).
932    #[cfg(feature = "intents")]
933    pub(crate) fn used_object_ids(&self) -> std::collections::HashSet<Address> {
934        let gas_ids = self.gas.iter().map(|o| o.object_id());
935        let input_ids = self.inputs.values().filter_map(|(_, input)| match input {
936            InputArg::Object(o) => Some(o.object_id()),
937            _ => None,
938        });
939        gas_ids.chain(input_ids).collect()
940    }
941}
942
943/// A opaque handle to a transaction input or command result.
944///
945/// Arguments are produced by builder methods like [`TransactionBuilder::pure`],
946/// [`TransactionBuilder::object`], and [`TransactionBuilder::move_call`], and consumed by
947/// command methods like [`TransactionBuilder::transfer_objects`].
948///
949/// For commands that return multiple values (e.g., [`TransactionBuilder::split_coins`]),
950/// use [`to_nested`](Self::to_nested) to access individual results.
951#[derive(Clone, Copy, Debug)]
952pub struct Argument {
953    id: usize,
954    sub_index: Option<usize>,
955}
956
957impl Argument {
958    pub(crate) fn new(id: usize) -> Self {
959        Self {
960            id,
961            sub_index: None,
962        }
963    }
964
965    /// Split this argument into `count` nested result arguments.
966    ///
967    /// This is used when a command (like a Move call) returns multiple values. Each element
968    /// in the returned vector refers to the corresponding result index.
969    ///
970    /// [`TransactionBuilder::split_coins`] calls this automatically, but you can use it
971    /// directly for Move calls that return multiple values:
972    ///
973    /// ```
974    /// use sui_sdk_types::Address;
975    /// use sui_sdk_types::Identifier;
976    /// use sui_transaction_builder::Function;
977    /// use sui_transaction_builder::TransactionBuilder;
978    ///
979    /// let mut tx = TransactionBuilder::new();
980    /// let result = tx.move_call(
981    ///     Function::new(
982    ///         Address::TWO,
983    ///         Identifier::from_static("my_module"),
984    ///         Identifier::from_static("multi_return"),
985    ///     ),
986    ///     vec![],
987    /// );
988    /// let nested = result.to_nested(3);
989    /// assert_eq!(nested.len(), 3);
990    /// ```
991    pub fn to_nested(self, count: usize) -> Vec<Self> {
992        (0..count)
993            .map(|sub_index| Argument {
994                sub_index: Some(sub_index),
995                ..self
996            })
997            .collect()
998    }
999
1000    fn try_resolve(
1001        self,
1002        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1003    ) -> Result<sui_sdk_types::Argument, Result<usize, Error>> {
1004        let mut sub_index = self.sub_index;
1005        let arg = {
1006            let mut visited = BTreeSet::new();
1007            let mut next_id = self.id;
1008
1009            loop {
1010                if visited.contains(&next_id) {
1011                    panic!("BUG: cyclic dependency");
1012                }
1013                visited.insert(next_id);
1014
1015                match resolved_arguments.get(&next_id).unwrap() {
1016                    ResolvedArgument::Unresolved => return Err(Ok(next_id)),
1017                    ResolvedArgument::ReplaceWith(argument) => {
1018                        next_id = argument.id;
1019                        sub_index = argument.sub_index;
1020                    }
1021                    ResolvedArgument::Resolved(argument) => break argument,
1022                }
1023            }
1024        };
1025
1026        if let Some(sub_index) = sub_index {
1027            if let Some(arg) = arg.nested(sub_index as u16) {
1028                return Ok(arg);
1029            } else {
1030                return Err(Err(Error::Input(
1031                    "unable to create nested argument".to_owned(),
1032                )));
1033            }
1034        }
1035
1036        Ok(*arg)
1037    }
1038
1039    fn try_resolve_many(
1040        arguments: &[Self],
1041        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1042    ) -> Result<Vec<sui_sdk_types::Argument>, Result<usize, Error>> {
1043        arguments
1044            .iter()
1045            .map(|a| a.try_resolve(resolved_arguments))
1046            .collect::<Result<_, _>>()
1047    }
1048}
1049
1050pub(crate) struct Command {
1051    kind: CommandKind,
1052    // A way to encode dependencies between commands when there aren't dependencies via explicit
1053    // input/outputs
1054    pub(crate) dependencies: Vec<Argument>,
1055}
1056
1057impl From<CommandKind> for Command {
1058    fn from(value: CommandKind) -> Self {
1059        Self {
1060            kind: value,
1061            dependencies: Vec::new(),
1062        }
1063    }
1064}
1065
1066pub(crate) enum CommandKind {
1067    /// A call to either an entry or a public Move function
1068    MoveCall(MoveCall),
1069
1070    /// `(Vec<forall T:key+store. T>, address)`
1071    /// It sends n-objects to the specified address. These objects must have store
1072    /// (public transfer) and either the previous owner must be an address or the object must
1073    /// be newly created.
1074    TransferObjects(TransferObjects),
1075
1076    /// `(&mut Coin<T>, Vec<u64>)` -> `Vec<Coin<T>>`
1077    /// It splits off some amounts into a new coins with those amounts
1078    SplitCoins(SplitCoins),
1079
1080    /// `(&mut Coin<T>, Vec<Coin<T>>)`
1081    /// It merges n-coins into the first coin
1082    MergeCoins(MergeCoins),
1083
1084    /// Publishes a Move package. It takes the package bytes and a list of the package's transitive
1085    /// dependencies to link against on-chain.
1086    Publish(Publish),
1087
1088    /// `forall T: Vec<T> -> vector<T>`
1089    /// Given n-values of the same type, it constructs a vector. For non objects or an empty vector,
1090    /// the type tag must be specified.
1091    MakeMoveVector(MakeMoveVector),
1092
1093    /// Upgrades a Move package
1094    /// Takes (in order):
1095    /// 1. A vector of serialized modules for the package.
1096    /// 2. A vector of object ids for the transitive dependencies of the new package.
1097    /// 3. The object ID of the package being upgraded.
1098    /// 4. An argument holding the `UpgradeTicket` that must have been produced from an earlier command in the same
1099    ///    programmable transaction.
1100    Upgrade(Upgrade),
1101}
1102
1103impl Command {
1104    fn try_resolve(
1105        &self,
1106        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1107    ) -> Result<sui_sdk_types::Command, Result<usize, Error>> {
1108        use sui_sdk_types::Command as C;
1109
1110        // try to resolve all dependencies first
1111        Argument::try_resolve_many(&self.dependencies, resolved_arguments)?;
1112
1113        let cmd = match &self.kind {
1114            CommandKind::MoveCall(MoveCall {
1115                package,
1116                module,
1117                function,
1118                type_arguments,
1119                arguments,
1120            }) => C::MoveCall(sui_sdk_types::MoveCall {
1121                package: *package,
1122                module: module.to_owned(),
1123                function: function.to_owned(),
1124                type_arguments: type_arguments.to_owned(),
1125                arguments: Argument::try_resolve_many(arguments, resolved_arguments)?,
1126            }),
1127
1128            CommandKind::TransferObjects(TransferObjects { objects, address }) => {
1129                C::TransferObjects(sui_sdk_types::TransferObjects {
1130                    objects: Argument::try_resolve_many(objects, resolved_arguments)?,
1131                    address: address.try_resolve(resolved_arguments)?,
1132                })
1133            }
1134
1135            CommandKind::SplitCoins(SplitCoins { coin, amounts }) => {
1136                C::SplitCoins(sui_sdk_types::SplitCoins {
1137                    coin: coin.try_resolve(resolved_arguments)?,
1138                    amounts: Argument::try_resolve_many(amounts, resolved_arguments)?,
1139                })
1140            }
1141
1142            CommandKind::MergeCoins(MergeCoins {
1143                coin,
1144                coins_to_merge,
1145            }) => C::MergeCoins(sui_sdk_types::MergeCoins {
1146                coin: coin.try_resolve(resolved_arguments)?,
1147                coins_to_merge: Argument::try_resolve_many(coins_to_merge, resolved_arguments)?,
1148            }),
1149
1150            CommandKind::Publish(Publish {
1151                modules,
1152                dependencies,
1153            }) => C::Publish(sui_sdk_types::Publish {
1154                modules: modules.to_owned(),
1155                dependencies: dependencies.to_owned(),
1156            }),
1157
1158            CommandKind::MakeMoveVector(MakeMoveVector { type_, elements }) => {
1159                C::MakeMoveVector(sui_sdk_types::MakeMoveVector {
1160                    type_: type_.to_owned(),
1161                    elements: Argument::try_resolve_many(elements, resolved_arguments)?,
1162                })
1163            }
1164
1165            CommandKind::Upgrade(Upgrade {
1166                modules,
1167                dependencies,
1168                package,
1169                ticket,
1170            }) => C::Upgrade(sui_sdk_types::Upgrade {
1171                modules: modules.to_owned(),
1172                dependencies: dependencies.to_owned(),
1173                package: *package,
1174                ticket: ticket.try_resolve(resolved_arguments)?,
1175            }),
1176        };
1177        Ok(cmd)
1178    }
1179}
1180
1181pub(crate) struct TransferObjects {
1182    /// Set of objects to transfer
1183    pub objects: Vec<Argument>,
1184
1185    /// The address to transfer ownership to
1186    pub address: Argument,
1187}
1188
1189pub(crate) struct SplitCoins {
1190    /// The coin to split
1191    pub coin: Argument,
1192
1193    /// The amounts to split off
1194    pub amounts: Vec<Argument>,
1195}
1196
1197pub(crate) struct MergeCoins {
1198    /// Coin to merge coins into
1199    pub coin: Argument,
1200
1201    /// Set of coins to merge into `coin`
1202    ///
1203    /// All listed coins must be of the same type and be the same type as `coin`
1204    pub coins_to_merge: Vec<Argument>,
1205}
1206
1207pub(crate) struct Publish {
1208    /// The serialized move modules
1209    pub modules: Vec<Vec<u8>>,
1210
1211    /// Set of packages that the to-be published package depends on
1212    pub dependencies: Vec<Address>,
1213}
1214
1215pub(crate) struct MakeMoveVector {
1216    /// Type of the individual elements
1217    ///
1218    /// This is required to be set when the type can't be inferred, for example when the set of
1219    /// provided arguments are all pure input values.
1220    pub type_: Option<TypeTag>,
1221
1222    /// The set individual elements to build the vector with
1223    pub elements: Vec<Argument>,
1224}
1225
1226pub(crate) struct Upgrade {
1227    /// The serialized move modules
1228    pub modules: Vec<Vec<u8>>,
1229
1230    /// Set of packages that the to-be published package depends on
1231    pub dependencies: Vec<Address>,
1232
1233    /// Package id of the package to upgrade
1234    pub package: Address,
1235
1236    /// Ticket authorizing the upgrade
1237    pub ticket: Argument,
1238}
1239
1240pub(crate) struct MoveCall {
1241    /// The package containing the module and function.
1242    pub package: Address,
1243
1244    /// The specific module in the package containing the function.
1245    pub module: Identifier,
1246
1247    /// The function to be called.
1248    pub function: Identifier,
1249
1250    /// The type arguments to the function.
1251    pub type_arguments: Vec<TypeTag>,
1252
1253    /// The arguments to the function.
1254    pub arguments: Vec<Argument>,
1255    // Return value count??
1256}
1257
1258/// Description of an on-chain object to use as a transaction input.
1259///
1260/// Use one of the constructors ([`new`](Self::new), [`owned`](Self::owned),
1261/// [`shared`](Self::shared), [`immutable`](Self::immutable), [`receiving`](Self::receiving))
1262/// and then optionally refine with builder methods like [`with_version`](Self::with_version),
1263/// [`with_digest`](Self::with_digest), and [`with_mutable`](Self::with_mutable).
1264///
1265/// ```
1266/// use sui_sdk_types::Address;
1267/// use sui_sdk_types::Digest;
1268/// use sui_transaction_builder::ObjectInput;
1269///
1270/// // Fully-specified owned object
1271/// let obj = ObjectInput::owned(Address::ZERO, 1, Digest::ZERO);
1272///
1273/// // Minimal object — additional fields can be filled in by the builder
1274/// let obj = ObjectInput::new(Address::ZERO);
1275///
1276/// // Shared object
1277/// let obj = ObjectInput::shared(Address::ZERO, 1, true);
1278/// ```
1279#[derive(Clone)]
1280pub struct ObjectInput {
1281    object_id: Address,
1282    kind: Option<ObjectKind>,
1283    version: Option<u64>,
1284    digest: Option<Digest>,
1285    mutable: Option<bool>,
1286}
1287
1288#[derive(Clone, Copy)]
1289enum ObjectKind {
1290    Shared,
1291    Receiving,
1292    ImmutableOrOwned,
1293}
1294
1295impl ObjectInput {
1296    /// Create a minimal object input with only an object ID.
1297    ///
1298    /// Additional metadata (kind, version, digest, mutability) can be later resolved when a
1299    /// transaction is built.
1300    pub fn new(object_id: Address) -> Self {
1301        Self {
1302            kind: None,
1303            object_id,
1304            version: None,
1305            digest: None,
1306            mutable: None,
1307        }
1308    }
1309
1310    /// Return an owned kind of object with all required fields.
1311    pub fn owned(object_id: Address, version: u64, digest: Digest) -> Self {
1312        Self {
1313            kind: Some(ObjectKind::ImmutableOrOwned),
1314            object_id,
1315            version: Some(version),
1316            digest: Some(digest),
1317            mutable: None,
1318        }
1319    }
1320
1321    /// Return an immutable kind of object with all required fields.
1322    pub fn immutable(object_id: Address, version: u64, digest: Digest) -> Self {
1323        Self {
1324            kind: Some(ObjectKind::ImmutableOrOwned),
1325            object_id,
1326            version: Some(version),
1327            digest: Some(digest),
1328            mutable: None,
1329        }
1330    }
1331
1332    /// Return a receiving kind of object with all required fields.
1333    pub fn receiving(object_id: Address, version: u64, digest: Digest) -> Self {
1334        Self {
1335            kind: Some(ObjectKind::Receiving),
1336            object_id,
1337            version: Some(version),
1338            digest: Some(digest),
1339            mutable: None,
1340        }
1341    }
1342
1343    /// Return a shared object.
1344    /// - `mutable` controls whether a command can accept the object by value or mutable reference.
1345    /// - `version` is the first version the object was shared at.
1346    pub fn shared(object_id: Address, version: u64, mutable: bool) -> Self {
1347        Self {
1348            kind: Some(ObjectKind::Shared),
1349            object_id,
1350            version: Some(version),
1351            mutable: Some(mutable),
1352            digest: None,
1353        }
1354    }
1355
1356    /// Set the object kind to immutable.
1357    pub fn as_immutable(self) -> Self {
1358        Self {
1359            kind: Some(ObjectKind::ImmutableOrOwned),
1360            ..self
1361        }
1362    }
1363
1364    /// Set the object kind to owned.
1365    pub fn as_owned(self) -> Self {
1366        Self {
1367            kind: Some(ObjectKind::ImmutableOrOwned),
1368            ..self
1369        }
1370    }
1371
1372    /// Set the object kind to receiving.
1373    pub fn as_receiving(self) -> Self {
1374        Self {
1375            kind: Some(ObjectKind::Receiving),
1376            ..self
1377        }
1378    }
1379
1380    /// Set the object kind to shared.
1381    pub fn as_shared(self) -> Self {
1382        Self {
1383            kind: Some(ObjectKind::Shared),
1384            ..self
1385        }
1386    }
1387
1388    /// Set the specified version.
1389    pub fn with_version(self, version: u64) -> Self {
1390        Self {
1391            version: Some(version),
1392            ..self
1393        }
1394    }
1395
1396    /// Set the specified digest.
1397    pub fn with_digest(self, digest: Digest) -> Self {
1398        Self {
1399            digest: Some(digest),
1400            ..self
1401        }
1402    }
1403
1404    /// Set whether this object is accessed mutably.
1405    ///
1406    /// This is primarily relevant for shared objects to indicate whether the command will
1407    /// take the object by value or mutable reference.
1408    pub fn with_mutable(self, mutable: bool) -> Self {
1409        Self {
1410            mutable: Some(mutable),
1411            ..self
1412        }
1413    }
1414
1415    #[cfg(feature = "intents")]
1416    pub(crate) fn object_id(&self) -> Address {
1417        self.object_id
1418    }
1419}
1420
1421impl From<&sui_sdk_types::Object> for ObjectInput {
1422    fn from(object: &sui_sdk_types::Object) -> Self {
1423        let input = Self::new(object.object_id())
1424            .with_version(object.version())
1425            .with_digest(object.digest());
1426
1427        match object.owner() {
1428            sui_sdk_types::Owner::Address(_) => input.as_owned(),
1429            sui_sdk_types::Owner::Object(_) => input,
1430            sui_sdk_types::Owner::Shared(version) => input.with_version(*version).as_shared(),
1431            sui_sdk_types::Owner::Immutable => input.as_immutable(),
1432            sui_sdk_types::Owner::ConsensusAddress { start_version, .. } => {
1433                input.with_version(*start_version).as_shared()
1434            }
1435            _ => input,
1436        }
1437    }
1438}
1439
1440// private conversions
1441impl ObjectInput {
1442    fn try_into_object_reference(&self) -> Result<sui_sdk_types::ObjectReference, Error> {
1443        if matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None)
1444            && let Some(version) = self.version
1445            && let Some(digest) = self.digest
1446        {
1447            Ok(sui_sdk_types::ObjectReference::new(
1448                self.object_id,
1449                version,
1450                digest,
1451            ))
1452        } else {
1453            Err(Error::WrongGasObject)
1454        }
1455    }
1456
1457    fn try_into_input(&self) -> Result<sui_sdk_types::Input, Error> {
1458        let input = match self {
1459            // ImmutableOrOwned
1460            Self {
1461                object_id,
1462                kind: Some(ObjectKind::ImmutableOrOwned),
1463                version: Some(version),
1464                digest: Some(digest),
1465                ..
1466            }
1467            | Self {
1468                object_id,
1469                kind: None,
1470                version: Some(version),
1471                digest: Some(digest),
1472                mutable: None,
1473            } => sui_sdk_types::Input::ImmutableOrOwned(sui_sdk_types::ObjectReference::new(
1474                *object_id, *version, *digest,
1475            )),
1476
1477            // Receiving
1478            Self {
1479                object_id,
1480                kind: Some(ObjectKind::Receiving),
1481                version: Some(version),
1482                digest: Some(digest),
1483                ..
1484            } => sui_sdk_types::Input::Receiving(sui_sdk_types::ObjectReference::new(
1485                *object_id, *version, *digest,
1486            )),
1487
1488            // Shared
1489            Self {
1490                object_id,
1491                kind: Some(ObjectKind::Shared),
1492                version: Some(version),
1493                mutable: Some(mutable),
1494                ..
1495            }
1496            | Self {
1497                object_id,
1498                kind: None,
1499                version: Some(version),
1500                digest: None,
1501                mutable: Some(mutable),
1502            } => sui_sdk_types::Input::Shared(sui_sdk_types::SharedInput::new(
1503                *object_id, *version, *mutable,
1504            )),
1505
1506            _ => {
1507                return Err(Error::Input(format!(
1508                    "Input object {} is incomplete",
1509                    self.object_id
1510                )));
1511            }
1512        };
1513        Ok(input)
1514    }
1515
1516    #[cfg(feature = "intents")]
1517    fn to_input_proto(&self) -> sui_rpc::proto::sui::rpc::v2::Input {
1518        use sui_rpc::proto::sui::rpc::v2::input::InputKind;
1519
1520        let mut input =
1521            sui_rpc::proto::sui::rpc::v2::Input::default().with_object_id(self.object_id);
1522        match &self.kind {
1523            Some(ObjectKind::Shared) => input.set_kind(InputKind::Shared),
1524            Some(ObjectKind::Receiving) => input.set_kind(InputKind::Receiving),
1525            Some(ObjectKind::ImmutableOrOwned) => input.set_kind(InputKind::ImmutableOrOwned),
1526            None => {}
1527        }
1528
1529        if let Some(version) = self.version {
1530            input.set_version(version);
1531        }
1532
1533        if let Some(digest) = self.digest {
1534            input.set_digest(digest);
1535        }
1536
1537        if let Some(mutable) = self.mutable {
1538            input.set_mutable(mutable);
1539        }
1540
1541        input
1542    }
1543
1544    #[cfg(feature = "intents")]
1545    fn try_into_object_reference_proto(
1546        &self,
1547    ) -> Result<sui_rpc::proto::sui::rpc::v2::ObjectReference, Error> {
1548        if !matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None) {
1549            return Err(Error::WrongGasObject);
1550        }
1551
1552        let mut input =
1553            sui_rpc::proto::sui::rpc::v2::ObjectReference::default().with_object_id(self.object_id);
1554        if let Some(version) = self.version {
1555            input.set_version(version);
1556        }
1557        if let Some(digest) = self.digest {
1558            input.set_digest(digest);
1559        }
1560        Ok(input)
1561    }
1562
1563    #[cfg(feature = "intents")]
1564    pub(crate) fn try_from_object_proto(
1565        object: &sui_rpc::proto::sui::rpc::v2::Object,
1566    ) -> Result<Self, Error> {
1567        use sui_rpc::proto::sui::rpc::v2::owner::OwnerKind;
1568
1569        let input = Self::new(
1570            object
1571                .object_id()
1572                .parse()
1573                .map_err(|_e| Error::MissingObjectId)?,
1574        );
1575
1576        Ok(match object.owner().kind() {
1577            OwnerKind::Address | OwnerKind::Immutable => {
1578                input.as_owned().with_version(object.version()).with_digest(
1579                    object
1580                        .digest()
1581                        .parse()
1582                        .map_err(|_| Error::Input("can't parse digest".to_owned()))?,
1583                )
1584            }
1585            OwnerKind::Object => return Err(Error::Input("invalid object type".to_owned())),
1586            OwnerKind::Shared | OwnerKind::ConsensusAddress => input
1587                .as_shared()
1588                .with_version(object.owner().version())
1589                .with_mutable(true),
1590            OwnerKind::Unknown | _ => input,
1591        })
1592    }
1593}
1594
1595/// A structured representation of a Move function (`package::module::function`), optionally
1596/// with type arguments.
1597///
1598/// Use [`Function::new`] to create a function reference, and
1599/// [`Function::with_type_args`] to add generic type parameters.
1600///
1601/// ```
1602/// use sui_sdk_types::Address;
1603/// use sui_sdk_types::Identifier;
1604/// use sui_transaction_builder::Function;
1605///
1606/// let f = Function::new(
1607///     Address::TWO,
1608///     Identifier::from_static("coin"),
1609///     Identifier::from_static("zero"),
1610/// )
1611/// .with_type_args(vec!["0x2::sui::SUI".parse().unwrap()]);
1612/// ```
1613pub struct Function {
1614    /// The package that contains the module with the function.
1615    package: Address,
1616    /// The module that contains the function.
1617    module: Identifier,
1618    /// The function name.
1619    function: Identifier,
1620    /// The type arguments for the function.
1621    type_args: Vec<TypeTag>,
1622}
1623
1624impl Function {
1625    /// Create a new function reference.
1626    ///
1627    /// ```
1628    /// use sui_sdk_types::Address;
1629    /// use sui_sdk_types::Identifier;
1630    /// use sui_transaction_builder::Function;
1631    ///
1632    /// let f = Function::new(
1633    ///     Address::TWO,
1634    ///     Identifier::from_static("coin"),
1635    ///     Identifier::from_static("zero"),
1636    /// );
1637    /// ```
1638    pub fn new(package: Address, module: Identifier, function: Identifier) -> Self {
1639        Self {
1640            package,
1641            module,
1642            function,
1643            type_args: Vec::new(),
1644        }
1645    }
1646
1647    /// Set the type arguments for the function call.
1648    ///
1649    /// ```
1650    /// use sui_sdk_types::Address;
1651    /// use sui_sdk_types::Identifier;
1652    /// use sui_transaction_builder::Function;
1653    ///
1654    /// let f = Function::new(
1655    ///     Address::TWO,
1656    ///     Identifier::from_static("coin"),
1657    ///     Identifier::from_static("zero"),
1658    /// )
1659    /// .with_type_args(vec!["0x2::sui::SUI".parse().unwrap()]);
1660    /// ```
1661    pub fn with_type_args(self, type_args: Vec<TypeTag>) -> Self {
1662        Self { type_args, ..self }
1663    }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668    use super::*;
1669
1670    #[test]
1671    fn simple_try_build() {
1672        let mut tx = TransactionBuilder::new();
1673        let _coin = tx.object(ObjectInput::owned(
1674            Address::from_static(
1675                "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1676            ),
1677            2,
1678            Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1679        ));
1680        let _gas = tx.gas();
1681
1682        let _recipient = tx.pure(&Address::from_static("0xabc"));
1683
1684        assert!(tx.try_build().is_err());
1685
1686        let mut tx = TransactionBuilder::new();
1687        let coin = tx.object(ObjectInput::owned(
1688            Address::from_static(
1689                "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1690            ),
1691            2,
1692            Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1693        ));
1694        let gas = tx.gas();
1695
1696        let recipient = tx.pure(&Address::from_static("0xabc"));
1697        tx.transfer_objects(vec![coin, gas], recipient);
1698        tx.set_gas_budget(500000000);
1699        tx.set_gas_price(1000);
1700        tx.add_gas_objects([ObjectInput::owned(
1701            Address::from_static(
1702                "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1703            ),
1704            2,
1705            Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1706        )]);
1707        tx.set_sender(Address::from_static(
1708            "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1709        ));
1710
1711        assert!(tx.try_build().is_ok());
1712    }
1713
1714    #[test]
1715    fn test_split_transfer() {
1716        let mut tx = TransactionBuilder::new();
1717
1718        // transfer 1 SUI from Gas coin
1719        let amount = tx.pure(&1_000_000_000u64);
1720        let gas = tx.gas();
1721        let result = tx.split_coins(gas, vec![amount; 5]);
1722        let recipient = tx.pure(&Address::from_static("0xabc"));
1723        tx.transfer_objects(result, recipient);
1724
1725        tx.set_gas_budget(500000000);
1726        tx.set_gas_price(1000);
1727        tx.add_gas_objects([ObjectInput::owned(
1728            Address::from_static(
1729                "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1730            ),
1731            2,
1732            Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1733        )]);
1734        tx.set_sender(Address::from_static(
1735            "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1736        ));
1737
1738        assert!(tx.try_build().is_ok());
1739    }
1740
1741    #[test]
1742    fn test_deterministic_building() {
1743        let build_tx = || {
1744            let mut tx = TransactionBuilder::new();
1745            let coin = tx.object(ObjectInput::owned(
1746                Address::from_static(
1747                    "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1748                ),
1749                2,
1750                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1751            ));
1752            let _ = tx.object(ObjectInput::owned(
1753                Address::from_static("0x12345"),
1754                2,
1755                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1756            ));
1757            let _ = tx.object(ObjectInput::owned(
1758                Address::from_static("0x12345"),
1759                2,
1760                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1761            ));
1762            let gas = tx.gas();
1763            let _ = tx.pure(&Address::from_static("0xabc"));
1764            let _ = tx.pure(&Address::from_static("0xabc"));
1765            let _ = tx.pure(&Address::from_static("0xabc"));
1766            let _ = tx.pure(&Address::from_static("0xdef"));
1767            let _ = tx.pure(&1u64);
1768            let _ = tx.pure(&1u64);
1769            let _ = tx.pure(&1u64);
1770            let _ = tx.pure(&Some(2u8));
1771            let _ = tx.pure_unique(&Address::from_static("0xabc"));
1772            let _ = tx.pure_unique(&Address::from_static("0xabc"));
1773            let _ = tx.pure_unique(&1u64);
1774
1775            let recipient = tx.pure(&Address::from_static("0x123"));
1776            tx.transfer_objects(vec![coin, gas], recipient);
1777            tx.set_gas_budget(500000000);
1778            tx.set_gas_price(1000);
1779            tx.add_gas_objects([ObjectInput::owned(
1780                Address::from_static(
1781                    "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1782                ),
1783                2,
1784                Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1785            )]);
1786            tx.set_sender(Address::from_static(
1787                "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1788            ));
1789
1790            tx.try_build().unwrap()
1791        };
1792
1793        let digest = build_tx().digest();
1794
1795        assert!(
1796            (0..100)
1797                .map(|_| build_tx())
1798                .map(|tx| tx.digest())
1799                .all(|d| d == digest)
1800        )
1801    }
1802}