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 transactions. Use `resolve` to finalize the transaction data.
13#[derive(Default)]
14pub struct TransactionBuilder {
15    /// The gas objects that will be used to pay for the transaction. The most common way is to
16    /// use [`unresolved::Input::owned`] function to create a gas object and use the [`add_gas`]
17    /// method to set the gas objects.
18    pub(crate) gas: Vec<ObjectInput>,
19    /// The gas budget for the transaction.
20    gas_budget: Option<u64>,
21    /// The gas price for the transaction.
22    gas_price: Option<u64>,
23    /// The sender of the transaction.
24    sender: Option<Address>,
25    /// The sponsor of the transaction. If None, the sender is also the sponsor.
26    sponsor: Option<Address>,
27    /// The expiration of the transaction. The default value of this type is no expiration.
28    expiration: Option<TransactionExpiration>,
29
30    // Resolvers
31    #[cfg(feature = "intents")]
32    pub(crate) resolvers: BTreeMap<std::any::TypeId, Box<dyn crate::intent::IntentResolver>>,
33
34    pub(crate) arguments: BTreeMap<usize, ResolvedArgument>,
35    inputs: HashMap<InputArgKind, (usize, InputArg)>,
36    pub(crate) commands: BTreeMap<usize, Command>,
37    pub(crate) intents: BTreeMap<usize, Box<dyn std::any::Any + Send + Sync>>,
38}
39
40#[derive(Clone, Copy, Debug)]
41pub(crate) enum ResolvedArgument {
42    Unresolved,
43    #[allow(unused)]
44    ReplaceWith(Argument),
45    Resolved(sui_sdk_types::Argument),
46}
47
48#[derive(Debug, PartialEq, Eq, Hash)]
49pub(crate) enum InputArgKind {
50    Gas,
51    ObjectInput(Address),
52    PureInput(Vec<u8>),
53    UniquePureInput(usize),
54}
55
56pub(crate) enum InputArg {
57    Gas,
58    Pure(Vec<u8>),
59    Object(ObjectInput),
60}
61
62impl TransactionBuilder {
63    /// Create a new transaction builder and initialize its elements to default.
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    // Transaction Inputs
69
70    pub fn gas(&mut self) -> Argument {
71        if let Some((index, arg)) = self.inputs.get(&InputArgKind::Gas) {
72            assert!(matches!(arg, InputArg::Gas));
73            Argument::new(*index)
74        } else {
75            let id = self.arguments.len();
76            self.arguments.insert(id, ResolvedArgument::Unresolved);
77            self.inputs.insert(InputArgKind::Gas, (id, InputArg::Gas));
78            Argument::new(id)
79        }
80    }
81
82    pub fn pure_bytes(&mut self, bytes: Vec<u8>) -> Argument {
83        match self.inputs.entry(InputArgKind::PureInput(bytes.clone())) {
84            std::collections::hash_map::Entry::Occupied(o) => {
85                assert!(matches!(o.get().1, InputArg::Pure(_)));
86                Argument::new(o.get().0)
87            }
88            std::collections::hash_map::Entry::Vacant(v) => {
89                let id = self.arguments.len();
90                self.arguments.insert(id, ResolvedArgument::Unresolved);
91                v.insert((id, InputArg::Pure(bytes)));
92                Argument::new(id)
93            }
94        }
95    }
96
97    pub fn pure<T: serde::Serialize>(&mut self, value: &T) -> Argument {
98        let bytes = bcs::to_bytes(value).expect("bcs serialization failed");
99        self.pure_bytes(bytes)
100    }
101
102    pub fn pure_bytes_unique(&mut self, bytes: Vec<u8>) -> Argument {
103        let id = self.arguments.len();
104        self.arguments.insert(id, ResolvedArgument::Unresolved);
105        self.inputs.insert(
106            InputArgKind::UniquePureInput(id),
107            (id, InputArg::Pure(bytes)),
108        );
109        Argument::new(id)
110    }
111
112    pub fn pure_unique<T: serde::Serialize>(&mut self, value: &T) -> Argument {
113        let bytes = bcs::to_bytes(value).expect("bcs serialization failed");
114        self.pure_bytes_unique(bytes)
115    }
116
117    pub fn object(&mut self, object: ObjectInput) -> Argument {
118        match self
119            .inputs
120            .entry(InputArgKind::ObjectInput(object.object_id))
121        {
122            std::collections::hash_map::Entry::Occupied(mut o) => {
123                let id = o.get().0;
124                let InputArg::Object(object2) = &mut o.get_mut().1 else {
125                    panic!("BUG: invariant violation");
126                };
127
128                assert_eq!(
129                    object.object_id, object2.object_id,
130                    "BUG: invariant violation"
131                );
132
133                match (object.mutable, object2.mutable) {
134                    (Some(_), None) => object2.mutable = object.mutable,
135                    (Some(true), Some(false)) => object2.mutable = Some(true),
136                    _ => {}
137                }
138
139                if let (Some(kind), None) = (object.kind, object2.kind) {
140                    object2.kind = Some(kind);
141                }
142
143                if let (Some(version), None) = (object.version, object2.version) {
144                    object2.version = Some(version);
145                }
146
147                if let (Some(digest), None) = (object.digest, object2.digest) {
148                    object2.digest = Some(digest);
149                }
150
151                Argument::new(id)
152            }
153            std::collections::hash_map::Entry::Vacant(v) => {
154                let id = self.arguments.len();
155                self.arguments.insert(id, ResolvedArgument::Unresolved);
156                v.insert((id, InputArg::Object(object)));
157                Argument::new(id)
158            }
159        }
160    }
161
162    // Metadata
163
164    /// Add one or more gas objects to use to pay for the transaction.
165    pub fn add_gas_objects<O, I>(&mut self, gas: I)
166    where
167        O: Into<ObjectInput>,
168        I: IntoIterator<Item = O>,
169    {
170        self.gas.extend(gas.into_iter().map(|x| x.into()));
171    }
172
173    /// Set the gas budget for the transaction.
174    pub fn set_gas_budget(&mut self, budget: u64) {
175        self.gas_budget = Some(budget);
176    }
177
178    /// Set the gas price for the transaction.
179    pub fn set_gas_price(&mut self, price: u64) {
180        self.gas_price = Some(price);
181    }
182
183    /// Set the sender of the transaction.
184    pub fn set_sender(&mut self, sender: Address) {
185        self.sender = Some(sender);
186    }
187
188    /// Set the sponsor of the transaction.
189    pub fn set_sponsor(&mut self, sponsor: Address) {
190        self.sponsor = Some(sponsor);
191    }
192
193    /// Set the expiration of the transaction.
194    pub fn set_expiration(&mut self, expiration: TransactionExpiration) {
195        self.expiration = Some(expiration);
196    }
197
198    // Commands
199
200    fn command(&mut self, command: Command) -> Argument {
201        let id = self.arguments.len();
202        self.arguments.insert(id, ResolvedArgument::Unresolved);
203        self.commands.insert(id, command);
204        Argument::new(id)
205    }
206
207    /// Call a Move function with the given arguments.
208    ///
209    /// - `function` is a structured representation of a package::module::function argument,
210    ///   optionally with type arguments.
211    //
212    // The return value is a result argument that can be used in subsequent commands.
213    // If the move call returns multiple results, you can access them using the
214    // [`Argument::nested`] method.
215    pub fn move_call(&mut self, function: Function, arguments: Vec<Argument>) -> Argument {
216        let cmd = CommandKind::MoveCall(MoveCall {
217            package: function.package,
218            module: function.module,
219            function: function.function,
220            type_arguments: function.type_args,
221            arguments,
222        });
223        self.command(cmd.into())
224    }
225
226    /// Transfer a list of objects to the given address, without producing any result.
227    pub fn transfer_objects(&mut self, objects: Vec<Argument>, address: Argument) {
228        let cmd = CommandKind::TransferObjects(TransferObjects { objects, address });
229        self.command(cmd.into());
230    }
231
232    /// Split a coin by the provided amounts, returning multiple results (as many as there are
233    /// amounts). The returned vector of `Arguments` is guaranteed to be the same length as the
234    /// provided `amounts` vector.
235    pub fn split_coins(&mut self, coin: Argument, amounts: Vec<Argument>) -> Vec<Argument> {
236        let amounts_len = amounts.len();
237        let cmd = CommandKind::SplitCoins(SplitCoins { coin, amounts });
238        self.command(cmd.into()).to_nested(amounts_len)
239    }
240
241    /// Merge a list of coins into a single coin, without producing any result.
242    pub fn merge_coins(&mut self, coin: Argument, coins_to_merge: Vec<Argument>) {
243        let cmd = CommandKind::MergeCoins(MergeCoins {
244            coin,
245            coins_to_merge,
246        });
247        self.command(cmd.into());
248    }
249
250    /// Make a move vector from a list of elements. If the elements are not objects, or the vector
251    /// is empty, a type must be supplied.
252    /// It returns the Move vector as an argument, that can be used in subsequent commands.
253    pub fn make_move_vec(&mut self, type_: Option<TypeTag>, elements: Vec<Argument>) -> Argument {
254        let cmd = CommandKind::MakeMoveVector(MakeMoveVector { type_, elements });
255        self.command(cmd.into())
256    }
257
258    /// Publish a list of modules with the given dependencies. The result is the
259    /// `0x2::package::UpgradeCap` Move type. Note that the upgrade capability needs to be handled
260    /// after this call:
261    ///  - transfer it to the transaction sender or another address
262    ///  - burn it
263    ///  - wrap it for access control
264    ///  - discard the it to make a package immutable
265    ///
266    /// The arguments required for this command are:
267    ///  - `modules`: is the modules' bytecode to be published
268    ///  - `dependencies`: is the list of IDs of the transitive dependencies of the package
269    pub fn publish(&mut self, modules: Vec<Vec<u8>>, dependencies: Vec<Address>) -> Argument {
270        let cmd = CommandKind::Publish(Publish {
271            modules,
272            dependencies,
273        });
274        self.command(cmd.into())
275    }
276
277    /// Upgrade a Move package.
278    ///
279    ///  - `modules`: is the modules' bytecode for the modules to be published
280    ///  - `dependencies`: is the list of IDs of the transitive dependencies of the package to be
281    ///    upgraded
282    ///  - `package`: is the ID of the current package being upgraded
283    ///  - `ticket`: is the upgrade ticket
284    ///
285    ///  To get the ticket, you have to call the `0x2::package::authorize_upgrade` function,
286    ///  and pass the package ID, the upgrade policy, and package digest.
287    pub fn upgrade(
288        &mut self,
289        modules: Vec<Vec<u8>>,
290        dependencies: Vec<Address>,
291        package: Address,
292        ticket: Argument,
293    ) -> Argument {
294        let cmd = CommandKind::Upgrade(Upgrade {
295            modules,
296            dependencies,
297            package,
298            ticket,
299        });
300        self.command(cmd.into())
301    }
302
303    // Intents
304
305    // Register a transaction intent which may be resolved later to either an input or a sequence
306    // of commands.
307    #[cfg(feature = "intents")]
308    #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
309    #[allow(private_bounds)]
310    pub fn intent<I: crate::intent::Intent>(&mut self, intent: I) -> Argument {
311        intent.register(self)
312    }
313
314    // Building and resolving
315
316    /// Assuming everything is resolved, convert this transaction into the
317    /// resolved form. Returns a [`Transaction`] if successful, or an `Error` if not.
318    pub fn try_build(mut self) -> Result<Transaction, Error> {
319        let Some(sender) = self.sender else {
320            return Err(Error::MissingSender);
321        };
322        if self.gas.is_empty() {
323            return Err(Error::MissingGasObjects);
324        }
325        let Some(budget) = self.gas_budget else {
326            return Err(Error::MissingGasBudget);
327        };
328        let Some(price) = self.gas_price else {
329            return Err(Error::MissingGasPrice);
330        };
331
332        // Gas payment
333        let gas_payment = sui_sdk_types::GasPayment {
334            objects: self
335                .gas
336                .iter()
337                .map(ObjectInput::try_into_object_reference)
338                .collect::<Result<Vec<_>, _>>()?,
339            owner: self.sponsor.unwrap_or(sender),
340            price,
341            budget,
342        };
343
344        // Error out if there are any unresolved intents
345        if !self.intents.is_empty() {
346            return Err(Error::Input("unable to resolve intents offline".to_owned()));
347        }
348
349        //
350        // Inputs
351        //
352
353        let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
354        unresolved_inputs.sort_by_key(|(id, _input)| *id);
355
356        let mut resolved_inputs = Vec::new();
357        for (id, input) in unresolved_inputs {
358            let arg = match input {
359                InputArg::Gas => sui_sdk_types::Argument::Gas,
360                InputArg::Pure(value) => {
361                    resolved_inputs.push(sui_sdk_types::Input::Pure(value));
362                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
363                }
364                InputArg::Object(object_input) => {
365                    resolved_inputs.push(object_input.try_into_input()?);
366                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
367                }
368            };
369
370            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
371        }
372
373        //
374        // Commands
375        //
376
377        let mut resolved_commands = Vec::new();
378
379        for (id, command) in self.commands {
380            resolved_commands.push(
381                command
382                    .try_resolve(&self.arguments)
383                    .map_err(|e| e.unwrap_err())?,
384            );
385            let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
386
387            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
388        }
389
390        Ok(Transaction {
391            kind: sui_sdk_types::TransactionKind::ProgrammableTransaction(
392                sui_sdk_types::ProgrammableTransaction {
393                    inputs: resolved_inputs,
394                    commands: resolved_commands,
395                },
396            ),
397            sender,
398            gas_payment,
399            expiration: self.expiration.unwrap_or(TransactionExpiration::None),
400        })
401    }
402
403    #[cfg(feature = "intents")]
404    #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
405    pub async fn build(mut self, client: &mut sui_rpc::Client) -> Result<Transaction, Error> {
406        use sui_rpc::field::FieldMask;
407        use sui_rpc::field::FieldMaskUtil;
408        use sui_rpc::proto::sui::rpc::v2::Input;
409        use sui_rpc::proto::sui::rpc::v2::SimulateTransactionRequest;
410        use sui_rpc::proto::sui::rpc::v2::SimulateTransactionResponse;
411        use sui_rpc::proto::sui::rpc::v2::input::InputKind;
412
413        let Some(sender) = self.sender else {
414            return Err(Error::MissingSender);
415        };
416
417        let mut request = SimulateTransactionRequest::default()
418            .with_read_mask(FieldMask::from_paths([
419                SimulateTransactionResponse::path_builder()
420                    .transaction()
421                    .transaction()
422                    .finish(),
423                SimulateTransactionResponse::path_builder()
424                    .transaction()
425                    .effects()
426                    .finish(),
427            ]))
428            .with_do_gas_selection(true);
429        request.transaction_mut().set_sender(sender);
430
431        //
432        // Intents
433        //
434
435        // For now we'll be dumb and just run through the registered resolvers one by one and if we
436        // still have intents left we'll bail
437
438        let resolvers = std::mem::take(&mut self.resolvers);
439        for resolver in resolvers.values() {
440            resolver
441                .resolve(&mut self, client)
442                .await
443                .map_err(|e| Error::Input(e.to_string()))?;
444        }
445        // Error out if there are any remaining unresolved intents
446        if !self.intents.is_empty() {
447            return Err(Error::Input("unable to resolve all intents".to_owned()));
448        }
449
450        //
451        // Inputs
452        //
453
454        let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
455        unresolved_inputs.sort_by_key(|(id, _input)| *id);
456
457        let mut resolved_inputs = Vec::new();
458        for (id, input) in unresolved_inputs {
459            let arg = match input {
460                InputArg::Gas => sui_sdk_types::Argument::Gas,
461                InputArg::Pure(value) => {
462                    resolved_inputs
463                        .push(Input::default().with_kind(InputKind::Pure).with_pure(value));
464                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
465                }
466                InputArg::Object(object_input) => {
467                    resolved_inputs.push(object_input.to_input_proto());
468                    sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
469                }
470            };
471
472            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
473        }
474
475        //
476        // Commands
477        //
478
479        let mut resolved_commands = Vec::new();
480
481        let mut stack = Vec::new();
482        let mut to_resolve = self.commands.pop_first();
483        while let Some((id, command)) = to_resolve.take() {
484            let resolved = match command.try_resolve(&self.arguments) {
485                Ok(resolved) => resolved,
486                Err(Ok(next)) => {
487                    // Push the current command on the stack
488                    stack.push((id, command));
489                    // set the next one to be processed
490                    to_resolve = Some(
491                        self.commands
492                            .remove_entry(&next)
493                            .expect("command must be there if it wasn't resolved yet"),
494                    );
495                    continue;
496                }
497                Err(Err(e)) => return Err(e),
498            };
499
500            resolved_commands.push(resolved);
501            let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
502            *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
503
504            // Pick the next command to resolve, either walked back down the stack or getting the
505            // next in order
506            if let Some(from_stack) = stack.pop() {
507                to_resolve = Some(from_stack);
508            } else {
509                to_resolve = self.commands.pop_first();
510            }
511        }
512
513        let t = request.transaction_mut();
514        t.kind_mut()
515            .programmable_transaction_mut()
516            .set_inputs(resolved_inputs);
517        t.kind_mut()
518            .programmable_transaction_mut()
519            .set_commands(resolved_commands.into_iter().map(Into::into).collect());
520
521        // Gas payment
522        {
523            let payment = request.transaction_mut().gas_payment_mut();
524            payment.set_owner(self.sponsor.unwrap_or(sender));
525
526            if let Some(budget) = self.gas_budget {
527                payment.set_budget(budget);
528            }
529            if let Some(price) = self.gas_price {
530                payment.set_price(price);
531            };
532            payment.set_objects(
533                self.gas
534                    .iter()
535                    .map(ObjectInput::try_into_object_reference_proto)
536                    .collect::<Result<_, _>>()?,
537            );
538        }
539
540        let response = client
541            .execution_client()
542            .simulate_transaction(request)
543            .await
544            .map_err(|e| Error::Input(format!("error simulating transaction: {e}")))?;
545
546        if !response
547            .get_ref()
548            .transaction()
549            .effects()
550            .status()
551            .success()
552        {
553            return Err(Error::Input(format!(
554                "txn failed to execute: {}",
555                response
556                    .get_ref()
557                    .transaction()
558                    .effects()
559                    .status()
560                    .error()
561                    .description()
562            )));
563        }
564
565        response
566            .get_ref()
567            .transaction()
568            .transaction()
569            .bcs()
570            .deserialize()
571            .map_err(|e| Error::Input(e.to_string()))
572    }
573
574    #[cfg(feature = "intents")]
575    pub(crate) fn register_resolver<R: crate::intent::IntentResolver>(&mut self, resolver: R) {
576        self.resolvers
577            .insert(resolver.type_id(), Box::new(resolver));
578    }
579
580    #[cfg(feature = "intents")]
581    pub(crate) fn unresolved<T: std::any::Any + Send + Sync>(&mut self, unresolved: T) -> Argument {
582        let id = self.arguments.len();
583        self.arguments.insert(id, ResolvedArgument::Unresolved);
584        self.intents.insert(id, Box::new(unresolved));
585        Argument::new(id)
586    }
587
588    #[cfg(feature = "intents")]
589    pub(crate) fn sender(&self) -> Option<Address> {
590        self.sender
591    }
592}
593
594#[derive(Clone, Copy, Debug)]
595pub struct Argument {
596    id: usize,
597    sub_index: Option<usize>,
598}
599
600impl Argument {
601    pub(crate) fn new(id: usize) -> Self {
602        Self {
603            id,
604            sub_index: None,
605        }
606    }
607
608    pub fn to_nested(self, count: usize) -> Vec<Self> {
609        (0..count)
610            .map(|sub_index| Argument {
611                sub_index: Some(sub_index),
612                ..self
613            })
614            .collect()
615    }
616
617    fn try_resolve(
618        self,
619        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
620    ) -> Result<sui_sdk_types::Argument, Result<usize, Error>> {
621        let mut sub_index = self.sub_index;
622        let arg = {
623            let mut visited = BTreeSet::new();
624            let mut next_id = self.id;
625
626            loop {
627                if visited.contains(&next_id) {
628                    panic!("BUG: cyclic dependency");
629                }
630                visited.insert(next_id);
631
632                match resolved_arguments.get(&next_id).unwrap() {
633                    ResolvedArgument::Unresolved => return Err(Ok(next_id)),
634                    ResolvedArgument::ReplaceWith(argument) => {
635                        next_id = argument.id;
636                        sub_index = argument.sub_index;
637                    }
638                    ResolvedArgument::Resolved(argument) => break argument,
639                }
640            }
641        };
642
643        if let Some(sub_index) = sub_index {
644            if let Some(arg) = arg.nested(sub_index as u16) {
645                return Ok(arg);
646            } else {
647                return Err(Err(Error::Input(
648                    "unable to create nested argument".to_owned(),
649                )));
650            }
651        }
652
653        Ok(*arg)
654    }
655
656    fn try_resolve_many(
657        arguments: &[Self],
658        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
659    ) -> Result<Vec<sui_sdk_types::Argument>, Result<usize, Error>> {
660        arguments
661            .iter()
662            .map(|a| a.try_resolve(resolved_arguments))
663            .collect::<Result<_, _>>()
664    }
665}
666
667pub(crate) struct Command {
668    kind: CommandKind,
669    // A way to encode dependencies between commands when there aren't dependencies via explicit
670    // input/outputs
671    pub(crate) dependencies: Vec<Argument>,
672}
673
674impl From<CommandKind> for Command {
675    fn from(value: CommandKind) -> Self {
676        Self {
677            kind: value,
678            dependencies: Vec::new(),
679        }
680    }
681}
682
683pub(crate) enum CommandKind {
684    /// A call to either an entry or a public Move function
685    MoveCall(MoveCall),
686
687    /// `(Vec<forall T:key+store. T>, address)`
688    /// It sends n-objects to the specified address. These objects must have store
689    /// (public transfer) and either the previous owner must be an address or the object must
690    /// be newly created.
691    TransferObjects(TransferObjects),
692
693    /// `(&mut Coin<T>, Vec<u64>)` -> `Vec<Coin<T>>`
694    /// It splits off some amounts into a new coins with those amounts
695    SplitCoins(SplitCoins),
696
697    /// `(&mut Coin<T>, Vec<Coin<T>>)`
698    /// It merges n-coins into the first coin
699    MergeCoins(MergeCoins),
700
701    /// Publishes a Move package. It takes the package bytes and a list of the package's transitive
702    /// dependencies to link against on-chain.
703    Publish(Publish),
704
705    /// `forall T: Vec<T> -> vector<T>`
706    /// Given n-values of the same type, it constructs a vector. For non objects or an empty vector,
707    /// the type tag must be specified.
708    MakeMoveVector(MakeMoveVector),
709
710    /// Upgrades a Move package
711    /// Takes (in order):
712    /// 1. A vector of serialized modules for the package.
713    /// 2. A vector of object ids for the transitive dependencies of the new package.
714    /// 3. The object ID of the package being upgraded.
715    /// 4. An argument holding the `UpgradeTicket` that must have been produced from an earlier command in the same
716    ///    programmable transaction.
717    Upgrade(Upgrade),
718}
719
720impl Command {
721    fn try_resolve(
722        &self,
723        resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
724    ) -> Result<sui_sdk_types::Command, Result<usize, Error>> {
725        use sui_sdk_types::Command as C;
726
727        // try to resolve all dependencies first
728        Argument::try_resolve_many(&self.dependencies, resolved_arguments)?;
729
730        let cmd = match &self.kind {
731            CommandKind::MoveCall(MoveCall {
732                package,
733                module,
734                function,
735                type_arguments,
736                arguments,
737            }) => C::MoveCall(sui_sdk_types::MoveCall {
738                package: *package,
739                module: module.to_owned(),
740                function: function.to_owned(),
741                type_arguments: type_arguments.to_owned(),
742                arguments: Argument::try_resolve_many(arguments, resolved_arguments)?,
743            }),
744
745            CommandKind::TransferObjects(TransferObjects { objects, address }) => {
746                C::TransferObjects(sui_sdk_types::TransferObjects {
747                    objects: Argument::try_resolve_many(objects, resolved_arguments)?,
748                    address: address.try_resolve(resolved_arguments)?,
749                })
750            }
751
752            CommandKind::SplitCoins(SplitCoins { coin, amounts }) => {
753                C::SplitCoins(sui_sdk_types::SplitCoins {
754                    coin: coin.try_resolve(resolved_arguments)?,
755                    amounts: Argument::try_resolve_many(amounts, resolved_arguments)?,
756                })
757            }
758
759            CommandKind::MergeCoins(MergeCoins {
760                coin,
761                coins_to_merge,
762            }) => C::MergeCoins(sui_sdk_types::MergeCoins {
763                coin: coin.try_resolve(resolved_arguments)?,
764                coins_to_merge: Argument::try_resolve_many(coins_to_merge, resolved_arguments)?,
765            }),
766
767            CommandKind::Publish(Publish {
768                modules,
769                dependencies,
770            }) => C::Publish(sui_sdk_types::Publish {
771                modules: modules.to_owned(),
772                dependencies: dependencies.to_owned(),
773            }),
774
775            CommandKind::MakeMoveVector(MakeMoveVector { type_, elements }) => {
776                C::MakeMoveVector(sui_sdk_types::MakeMoveVector {
777                    type_: type_.to_owned(),
778                    elements: Argument::try_resolve_many(elements, resolved_arguments)?,
779                })
780            }
781
782            CommandKind::Upgrade(Upgrade {
783                modules,
784                dependencies,
785                package,
786                ticket,
787            }) => C::Upgrade(sui_sdk_types::Upgrade {
788                modules: modules.to_owned(),
789                dependencies: dependencies.to_owned(),
790                package: *package,
791                ticket: ticket.try_resolve(resolved_arguments)?,
792            }),
793        };
794        Ok(cmd)
795    }
796}
797
798pub(crate) struct TransferObjects {
799    /// Set of objects to transfer
800    pub objects: Vec<Argument>,
801
802    /// The address to transfer ownership to
803    pub address: Argument,
804}
805
806pub(crate) struct SplitCoins {
807    /// The coin to split
808    pub coin: Argument,
809
810    /// The amounts to split off
811    pub amounts: Vec<Argument>,
812}
813
814pub(crate) struct MergeCoins {
815    /// Coin to merge coins into
816    pub coin: Argument,
817
818    /// Set of coins to merge into `coin`
819    ///
820    /// All listed coins must be of the same type and be the same type as `coin`
821    pub coins_to_merge: Vec<Argument>,
822}
823
824pub(crate) struct Publish {
825    /// The serialized move modules
826    pub modules: Vec<Vec<u8>>,
827
828    /// Set of packages that the to-be published package depends on
829    pub dependencies: Vec<Address>,
830}
831
832pub(crate) struct MakeMoveVector {
833    /// Type of the individual elements
834    ///
835    /// This is required to be set when the type can't be inferred, for example when the set of
836    /// provided arguments are all pure input values.
837    pub type_: Option<TypeTag>,
838
839    /// The set individual elements to build the vector with
840    pub elements: Vec<Argument>,
841}
842
843pub(crate) struct Upgrade {
844    /// The serialized move modules
845    pub modules: Vec<Vec<u8>>,
846
847    /// Set of packages that the to-be published package depends on
848    pub dependencies: Vec<Address>,
849
850    /// Package id of the package to upgrade
851    pub package: Address,
852
853    /// Ticket authorizing the upgrade
854    pub ticket: Argument,
855}
856
857pub(crate) struct MoveCall {
858    /// The package containing the module and function.
859    pub package: Address,
860
861    /// The specific module in the package containing the function.
862    pub module: Identifier,
863
864    /// The function to be called.
865    pub function: Identifier,
866
867    /// The type arguments to the function.
868    pub type_arguments: Vec<TypeTag>,
869
870    /// The arguments to the function.
871    pub arguments: Vec<Argument>,
872    // Return value count??
873}
874
875pub struct ObjectInput {
876    object_id: Address,
877    kind: Option<ObjectKind>,
878    version: Option<u64>,
879    digest: Option<Digest>,
880    mutable: Option<bool>,
881}
882
883#[derive(Clone, Copy)]
884enum ObjectKind {
885    Shared,
886    Receiving,
887    ImmutableOrOwned,
888}
889
890impl ObjectInput {
891    pub fn new(object_id: Address) -> Self {
892        Self {
893            kind: None,
894            object_id,
895            version: None,
896            digest: None,
897            mutable: None,
898        }
899    }
900
901    /// Return an owned kind of object with all required fields.
902    pub fn owned(object_id: Address, version: u64, digest: Digest) -> Self {
903        Self {
904            kind: Some(ObjectKind::ImmutableOrOwned),
905            object_id,
906            version: Some(version),
907            digest: Some(digest),
908            mutable: None,
909        }
910    }
911
912    /// Return an immutable kind of object with all required fields.
913    pub fn immutable(object_id: Address, version: u64, digest: Digest) -> Self {
914        Self {
915            kind: Some(ObjectKind::ImmutableOrOwned),
916            object_id,
917            version: Some(version),
918            digest: Some(digest),
919            mutable: None,
920        }
921    }
922
923    /// Return a receiving kind of object with all required fields.
924    pub fn receiving(object_id: Address, version: u64, digest: Digest) -> Self {
925        Self {
926            kind: Some(ObjectKind::Receiving),
927            object_id,
928            version: Some(version),
929            digest: Some(digest),
930            mutable: None,
931        }
932    }
933
934    /// Return a shared object.
935    /// - `mutable` controls whether a command can accept the object by value or mutable reference.
936    /// - `version` is the first version the object was shared at.
937    pub fn shared(object_id: Address, version: u64, mutable: bool) -> Self {
938        Self {
939            kind: Some(ObjectKind::Shared),
940            object_id,
941            version: Some(version),
942            mutable: Some(mutable),
943            digest: None,
944        }
945    }
946
947    /// Set the object kind to immutable.
948    pub fn as_immutable(self) -> Self {
949        Self {
950            kind: Some(ObjectKind::ImmutableOrOwned),
951            ..self
952        }
953    }
954
955    /// Set the object kind to owned.
956    pub fn as_owned(self) -> Self {
957        Self {
958            kind: Some(ObjectKind::ImmutableOrOwned),
959            ..self
960        }
961    }
962
963    /// Set the object kind to receiving.
964    pub fn as_receiving(self) -> Self {
965        Self {
966            kind: Some(ObjectKind::Receiving),
967            ..self
968        }
969    }
970
971    /// Set the object kind to shared.
972    pub fn as_shared(self) -> Self {
973        Self {
974            kind: Some(ObjectKind::Shared),
975            ..self
976        }
977    }
978
979    /// Set the specified version.
980    pub fn with_version(self, version: u64) -> Self {
981        Self {
982            version: Some(version),
983            ..self
984        }
985    }
986
987    /// Set the specified digest.
988    pub fn with_digest(self, digest: Digest) -> Self {
989        Self {
990            digest: Some(digest),
991            ..self
992        }
993    }
994
995    pub fn with_mutable(self, mutable: bool) -> Self {
996        Self {
997            mutable: Some(mutable),
998            ..self
999        }
1000    }
1001}
1002
1003impl From<&sui_sdk_types::Object> for ObjectInput {
1004    fn from(object: &sui_sdk_types::Object) -> Self {
1005        let input = Self::new(object.object_id())
1006            .with_version(object.version())
1007            .with_digest(object.digest());
1008
1009        match object.owner() {
1010            sui_sdk_types::Owner::Address(_) => input.as_owned(),
1011            sui_sdk_types::Owner::Object(_) => input,
1012            sui_sdk_types::Owner::Shared(version) => input.with_version(*version).as_shared(),
1013            sui_sdk_types::Owner::Immutable => input.as_immutable(),
1014            sui_sdk_types::Owner::ConsensusAddress { start_version, .. } => {
1015                input.with_version(*start_version).as_shared()
1016            }
1017            _ => input,
1018        }
1019    }
1020}
1021
1022// impl TryFrom<&sui_rpc::proto::sui::rpc::v2::Object> for ObjectInput {
1023//     type Error = sui_rpc::proto::TryFromProtoError;
1024
1025//     fn try_from(object: &sui_rpc::proto::sui::rpc::v2::Object) -> Result<Self, Self::Error> {
1026//         todo!()
1027//     }
1028// }
1029
1030// private conversions
1031impl ObjectInput {
1032    fn try_into_object_reference(&self) -> Result<sui_sdk_types::ObjectReference, Error> {
1033        if matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None)
1034            && let Some(version) = self.version
1035            && let Some(digest) = self.digest
1036        {
1037            Ok(sui_sdk_types::ObjectReference::new(
1038                self.object_id,
1039                version,
1040                digest,
1041            ))
1042        } else {
1043            Err(Error::WrongGasObject)
1044        }
1045    }
1046
1047    fn try_into_input(&self) -> Result<sui_sdk_types::Input, Error> {
1048        let input = match self {
1049            // ImmutableOrOwned
1050            Self {
1051                object_id,
1052                kind: Some(ObjectKind::ImmutableOrOwned),
1053                version: Some(version),
1054                digest: Some(digest),
1055                ..
1056            }
1057            | Self {
1058                object_id,
1059                kind: None,
1060                version: Some(version),
1061                digest: Some(digest),
1062                mutable: None,
1063            } => sui_sdk_types::Input::ImmutableOrOwned(sui_sdk_types::ObjectReference::new(
1064                *object_id, *version, *digest,
1065            )),
1066
1067            // Receiving
1068            Self {
1069                object_id,
1070                kind: Some(ObjectKind::Receiving),
1071                version: Some(version),
1072                digest: Some(digest),
1073                ..
1074            } => sui_sdk_types::Input::Receiving(sui_sdk_types::ObjectReference::new(
1075                *object_id, *version, *digest,
1076            )),
1077
1078            // Shared
1079            Self {
1080                object_id,
1081                kind: Some(ObjectKind::Shared),
1082                version: Some(version),
1083                mutable: Some(mutable),
1084                ..
1085            }
1086            | Self {
1087                object_id,
1088                kind: None,
1089                version: Some(version),
1090                digest: None,
1091                mutable: Some(mutable),
1092            } => sui_sdk_types::Input::Shared(sui_sdk_types::SharedInput::new(
1093                *object_id, *version, *mutable,
1094            )),
1095
1096            _ => {
1097                return Err(Error::Input(format!(
1098                    "Input object {} is incomplete",
1099                    self.object_id
1100                )));
1101            }
1102        };
1103        Ok(input)
1104    }
1105
1106    #[cfg(feature = "intents")]
1107    fn to_input_proto(&self) -> sui_rpc::proto::sui::rpc::v2::Input {
1108        use sui_rpc::proto::sui::rpc::v2::input::InputKind;
1109
1110        let mut input =
1111            sui_rpc::proto::sui::rpc::v2::Input::default().with_object_id(self.object_id);
1112        match self.kind {
1113            Some(ObjectKind::Shared) => input.set_kind(InputKind::Shared),
1114            Some(ObjectKind::Receiving) => input.set_kind(InputKind::Receiving),
1115            Some(ObjectKind::ImmutableOrOwned) => input.set_kind(InputKind::ImmutableOrOwned),
1116            None => {}
1117        }
1118
1119        if let Some(version) = self.version {
1120            input.set_version(version);
1121        }
1122
1123        if let Some(digest) = self.digest {
1124            input.set_digest(digest);
1125        }
1126
1127        if let Some(mutable) = self.mutable {
1128            input.set_mutable(mutable);
1129        }
1130
1131        input
1132    }
1133
1134    #[cfg(feature = "intents")]
1135    fn try_into_object_reference_proto(
1136        &self,
1137    ) -> Result<sui_rpc::proto::sui::rpc::v2::ObjectReference, Error> {
1138        if !matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None) {
1139            return Err(Error::WrongGasObject);
1140        }
1141
1142        let mut input =
1143            sui_rpc::proto::sui::rpc::v2::ObjectReference::default().with_object_id(self.object_id);
1144        if let Some(version) = self.version {
1145            input.set_version(version);
1146        }
1147        if let Some(digest) = self.digest {
1148            input.set_digest(digest);
1149        }
1150        Ok(input)
1151    }
1152
1153    #[cfg(feature = "intents")]
1154    pub(crate) fn try_from_object_proto(
1155        object: &sui_rpc::proto::sui::rpc::v2::Object,
1156    ) -> Result<Self, Error> {
1157        use sui_rpc::proto::sui::rpc::v2::owner::OwnerKind;
1158
1159        let input = Self::new(
1160            object
1161                .object_id()
1162                .parse()
1163                .map_err(|_e| Error::MissingObjectId)?,
1164        );
1165
1166        Ok(match object.owner().kind() {
1167            OwnerKind::Address | OwnerKind::Immutable => {
1168                input.as_owned().with_version(object.version()).with_digest(
1169                    object
1170                        .digest()
1171                        .parse()
1172                        .map_err(|_| Error::Input("can't parse digest".to_owned()))?,
1173                )
1174            }
1175            OwnerKind::Object => return Err(Error::Input("invalid object type".to_owned())),
1176            OwnerKind::Shared | OwnerKind::ConsensusAddress => input
1177                .as_shared()
1178                .with_version(object.owner().version())
1179                .with_mutable(true),
1180            OwnerKind::Unknown | _ => input,
1181        })
1182    }
1183}
1184
1185/// A separate type to support denoting a function by a more structured representation.
1186pub struct Function {
1187    /// The package that contains the module with the function.
1188    package: Address,
1189    /// The module that contains the function.
1190    module: Identifier,
1191    /// The function name.
1192    function: Identifier,
1193    /// The type arguments for the function.
1194    type_args: Vec<TypeTag>,
1195}
1196
1197impl Function {
1198    /// Constructor for the function type.
1199    pub fn new(package: Address, module: Identifier, function: Identifier) -> Self {
1200        Self {
1201            package,
1202            module,
1203            function,
1204            type_args: Vec::new(),
1205        }
1206    }
1207
1208    pub fn with_type_args(self, type_args: Vec<TypeTag>) -> Self {
1209        Self { type_args, ..self }
1210    }
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215    use super::*;
1216
1217    #[test]
1218    fn simple_try_build() {
1219        let mut tx = TransactionBuilder::new();
1220        let _coin = tx.object(ObjectInput::owned(
1221            Address::from_static(
1222                "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1223            ),
1224            2,
1225            Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1226        ));
1227        let _gas = tx.gas();
1228
1229        let _recipient = tx.pure(&Address::from_static("0xabc"));
1230
1231        assert!(tx.try_build().is_err());
1232
1233        let mut tx = TransactionBuilder::new();
1234        let coin = tx.object(ObjectInput::owned(
1235            Address::from_static(
1236                "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1237            ),
1238            2,
1239            Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1240        ));
1241        let gas = tx.gas();
1242
1243        let recipient = tx.pure(&Address::from_static("0xabc"));
1244        tx.transfer_objects(vec![coin, gas], recipient);
1245        tx.set_gas_budget(500000000);
1246        tx.set_gas_price(1000);
1247        tx.add_gas_objects([ObjectInput::owned(
1248            Address::from_static(
1249                "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1250            ),
1251            2,
1252            Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1253        )]);
1254        tx.set_sender(Address::from_static(
1255            "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1256        ));
1257
1258        assert!(tx.try_build().is_ok());
1259    }
1260
1261    #[test]
1262    fn test_split_transfer() {
1263        let mut tx = TransactionBuilder::new();
1264
1265        // transfer 1 SUI from Gas coin
1266        let amount = tx.pure(&1_000_000_000u64);
1267        let gas = tx.gas();
1268        let result = tx.split_coins(gas, vec![amount; 5]);
1269        let recipient = tx.pure(&Address::from_static("0xabc"));
1270        tx.transfer_objects(result, recipient);
1271
1272        tx.set_gas_budget(500000000);
1273        tx.set_gas_price(1000);
1274        tx.add_gas_objects([ObjectInput::owned(
1275            Address::from_static(
1276                "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1277            ),
1278            2,
1279            Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1280        )]);
1281        tx.set_sender(Address::from_static(
1282            "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1283        ));
1284
1285        assert!(tx.try_build().is_ok());
1286    }
1287
1288    #[test]
1289    fn test_deterministic_building() {
1290        let build_tx = || {
1291            let mut tx = TransactionBuilder::new();
1292            let coin = tx.object(ObjectInput::owned(
1293                Address::from_static(
1294                    "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1295                ),
1296                2,
1297                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1298            ));
1299            let _ = tx.object(ObjectInput::owned(
1300                Address::from_static("0x12345"),
1301                2,
1302                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1303            ));
1304            let _ = tx.object(ObjectInput::owned(
1305                Address::from_static("0x12345"),
1306                2,
1307                Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1308            ));
1309            let gas = tx.gas();
1310            let _ = tx.pure(&Address::from_static("0xabc"));
1311            let _ = tx.pure(&Address::from_static("0xabc"));
1312            let _ = tx.pure(&Address::from_static("0xabc"));
1313            let _ = tx.pure(&Address::from_static("0xdef"));
1314            let _ = tx.pure(&1u64);
1315            let _ = tx.pure(&1u64);
1316            let _ = tx.pure(&1u64);
1317            let _ = tx.pure(&Some(2u8));
1318            let _ = tx.pure_unique(&Address::from_static("0xabc"));
1319            let _ = tx.pure_unique(&Address::from_static("0xabc"));
1320            let _ = tx.pure_unique(&1u64);
1321
1322            let recipient = tx.pure(&Address::from_static("0x123"));
1323            tx.transfer_objects(vec![coin, gas], recipient);
1324            tx.set_gas_budget(500000000);
1325            tx.set_gas_price(1000);
1326            tx.add_gas_objects([ObjectInput::owned(
1327                Address::from_static(
1328                    "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1329                ),
1330                2,
1331                Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1332            )]);
1333            tx.set_sender(Address::from_static(
1334                "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1335            ));
1336
1337            tx.try_build().unwrap()
1338        };
1339
1340        let digest = build_tx().digest();
1341
1342        assert!(
1343            (0..100)
1344                .map(|_| build_tx())
1345                .map(|tx| tx.digest())
1346                .all(|d| d == digest)
1347        )
1348    }
1349}