sui_transaction_builder/
builder.rs

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