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}