Skip to main content

talus_sui_move_call/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4use std::marker::PhantomData;
5use std::str::FromStr;
6
7use sui_sdk_types::{Address, Identifier, Mutability, ObjectReference, SharedInput, TypeTag};
8use talus_sui_move::{HasKey, MoveStruct, MoveType};
9
10/// Canonical Sui transaction input kind.
11///
12/// This type is an alias for [`sui_sdk_types::Input`]. `talus-sui-move-call` uses it directly so
13/// this crate can represent every Sui transaction input kind without duplicating Sui's wire types.
14///
15/// In this crate, call arguments are typically produced via [`ToCallArg`].
16pub use sui_sdk_types::Input as CallArg;
17
18/// Typed object handle for Move structs that have the `key` ability.
19///
20/// This is a small wrapper around [`ObjectReference`] that carries the Rust type `T`.
21///
22/// `MoveObject<T>` is intentionally small: it does not fetch object contents, and it does not
23/// model shared or receiving objects. Higher layers can build on top.
24///
25/// Semantically, this corresponds to Sui's [`CallArg::ImmutableOrOwned`] input kind: use
26/// [`SharedMoveObject`] for shared objects and [`ReceivingMoveObject`] for receiving objects.
27///
28/// # Example
29/// ```
30/// use std::str::FromStr;
31/// use talus_sui_move_call::MoveObject;
32/// use sui_sdk_types::{Address, Digest, ObjectReference};
33///
34/// # #[talus_sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")]
35/// # struct ID { bytes: Address }
36/// #
37/// # #[talus_sui_move::move_struct(address = "0x2", module = "object", abilities = "store")]
38/// # struct UID { id: ID }
39/// #
40/// #[talus_sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")]
41/// struct Demo {
42///     id: UID,
43/// }
44///
45/// let id = Address::from_str("0x1").unwrap();
46/// let obj_ref = ObjectReference::new(id, 1, Digest::default());
47/// let _obj = MoveObject::<Demo>::new(obj_ref);
48/// ```
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct MoveObject<T> {
51    reference: ObjectReference,
52    phantom: PhantomData<T>,
53}
54
55impl<T: MoveStruct + HasKey> MoveObject<T> {
56    /// Create a typed handle from an object reference.
57    pub fn new(reference: ObjectReference) -> Self {
58        Self {
59            reference,
60            phantom: PhantomData,
61        }
62    }
63
64    /// Borrow the underlying object reference.
65    pub fn reference(&self) -> &ObjectReference {
66        &self.reference
67    }
68
69    /// Consume this handle and return the underlying object reference.
70    pub fn into_reference(self) -> ObjectReference {
71        self.reference
72    }
73
74    /// Replace the stored reference (useful after transaction effects).
75    pub fn update_reference(&mut self, reference: ObjectReference) {
76        self.reference = reference;
77    }
78}
79
80/// Typed handle for a shared object argument.
81///
82/// Shared objects are passed by `(object_id, initial_shared_version, mutability)` rather than an
83/// [`ObjectReference`]. This wrapper models that shape while carrying the expected Move type `T`.
84///
85/// # Example
86/// ```
87/// use std::str::FromStr;
88/// use talus_sui_move_call::SharedMoveObject;
89/// use sui_sdk_types::Address;
90///
91/// # #[talus_sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")]
92/// # struct ID { bytes: Address }
93/// #
94/// # #[talus_sui_move::move_struct(address = "0x2", module = "object", abilities = "store")]
95/// # struct UID { id: ID }
96/// #
97/// #[talus_sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")]
98/// struct SharedThing {
99///     id: UID,
100/// }
101///
102/// let object_id = Address::from_str("0x1").unwrap();
103/// let initial_shared_version = 1u64;
104/// let _shared = SharedMoveObject::<SharedThing>::mutable(object_id, initial_shared_version);
105/// ```
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct SharedMoveObject<T> {
108    input: SharedInput,
109    phantom: PhantomData<T>,
110}
111
112impl<T: MoveStruct + HasKey> SharedMoveObject<T> {
113    /// Create an immutable shared object handle.
114    pub fn immutable(object_id: Address, initial_shared_version: u64) -> Self {
115        Self::new(object_id, initial_shared_version, Mutability::Immutable)
116    }
117
118    /// Create a mutable shared object handle.
119    pub fn mutable(object_id: Address, initial_shared_version: u64) -> Self {
120        Self::new(object_id, initial_shared_version, Mutability::Mutable)
121    }
122
123    /// Create a shared object handle with `NonExclusiveWrite` mutability.
124    pub fn non_exclusive_write(object_id: Address, initial_shared_version: u64) -> Self {
125        Self::new(
126            object_id,
127            initial_shared_version,
128            Mutability::NonExclusiveWrite,
129        )
130    }
131
132    /// Create a shared object handle with an explicit mutability mode.
133    pub fn new(object_id: Address, initial_shared_version: u64, mutability: Mutability) -> Self {
134        Self {
135            input: SharedInput::new(object_id, initial_shared_version, mutability),
136            phantom: PhantomData,
137        }
138    }
139
140    /// Borrow the underlying shared input description.
141    pub fn shared_input(&self) -> &SharedInput {
142        &self.input
143    }
144
145    /// Shared object ID.
146    pub fn object_id(&self) -> Address {
147        self.input.object_id()
148    }
149
150    /// Initial shared version of the object.
151    pub fn initial_shared_version(&self) -> u64 {
152        self.input.version()
153    }
154
155    /// Requested mutability mode for this shared object argument.
156    pub fn mutability(&self) -> Mutability {
157        self.input.mutability()
158    }
159
160    /// Convenience: whether the requested mutability is writable.
161    pub fn is_mutable(&self) -> bool {
162        self.mutability().is_mutable()
163    }
164}
165
166/// Typed handle for a "receiving" object argument.
167///
168/// This corresponds to Sui's `Input::Receiving(ObjectReference)`.
169///
170/// Receiving is a **transaction input mode**, not an owner kind recorded on chain. It is used for
171/// the Move framework concept `sui::transfer::Receiving<T>`: a temporary “receiving ticket” that
172/// can be consumed by `sui::transfer::receive`/`public_receive` during one transaction.
173///
174/// This wrapper does not validate whether the referenced object can be received in the current
175/// transaction; invalid uses are rejected by Sui.
176///
177/// # Example
178/// ```
179/// use std::str::FromStr;
180/// use talus_sui_move_call::ReceivingMoveObject;
181/// use sui_sdk_types::{Address, Digest, ObjectReference};
182///
183/// # #[talus_sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")]
184/// # struct ID { bytes: Address }
185/// #
186/// # #[talus_sui_move::move_struct(address = "0x2", module = "object", abilities = "store")]
187/// # struct UID { id: ID }
188/// #
189/// #[talus_sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")]
190/// struct ReceivingThing {
191///     id: UID,
192/// }
193///
194/// let id = Address::from_str("0x1").unwrap();
195/// let obj_ref = ObjectReference::new(id, 1, Digest::default());
196/// let _recv = ReceivingMoveObject::<ReceivingThing>::new(obj_ref);
197/// ```
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct ReceivingMoveObject<T> {
200    reference: ObjectReference,
201    phantom: PhantomData<T>,
202}
203
204impl<T: MoveStruct + HasKey> ReceivingMoveObject<T> {
205    /// Create a typed receiving handle from an object reference.
206    pub fn new(reference: ObjectReference) -> Self {
207        Self {
208            reference,
209            phantom: PhantomData,
210        }
211    }
212
213    /// Borrow the underlying object reference.
214    pub fn reference(&self) -> &ObjectReference {
215        &self.reference
216    }
217
218    /// Consume this handle and return the underlying object reference.
219    pub fn into_reference(self) -> ObjectReference {
220        self.reference
221    }
222
223    /// Replace the stored reference.
224    pub fn update_reference(&mut self, reference: ObjectReference) {
225        self.reference = reference;
226    }
227}
228
229/// Errors that can occur when converting values into `CallArg`.
230#[derive(thiserror::Error, Debug)]
231pub enum CallArgError {
232    /// BCS encoding failed.
233    #[error(transparent)]
234    Bcs(#[from] bcs::Error),
235
236    /// The referenced object is tombstoned (e.g. deleted or wrapped).
237    ///
238    /// Higher layers may track object liveness across commits and refuse to use stale handles as
239    /// inputs.
240    #[error("object {object_id} is tombstoned ({reason})")]
241    Tombstoned {
242        /// Object id that was attempted to be used as an argument.
243        object_id: Address,
244        /// Explanation suitable for display, such as deleted, wrapped, or nonexistent.
245        reason: &'static str,
246    },
247
248    /// The referenced object kind does not match what the conversion requires.
249    ///
250    /// This is primarily used by higher layers that classify ownership from chain state and choose
251    /// the correct Sui input mode (owned/immutable vs shared).
252    #[error("object {object_id} has kind {actual}, expected {expected}")]
253    ObjectKind {
254        /// Object id that was attempted to be used as an argument.
255        object_id: Address,
256        /// Expected kind label (input mode / ownership kind).
257        expected: &'static str,
258        /// Actual kind label (input mode / ownership kind).
259        actual: &'static str,
260    },
261
262    /// A mutable shared input was required, but the provided shared handle is immutable.
263    #[error("shared object {object_id} has mutability {actual:?}, expected writable")]
264    SharedMutability {
265        /// Shared object id.
266        object_id: Address,
267        /// Actual mutability of the provided shared handle.
268        actual: Mutability,
269    },
270}
271
272/// Convert a value into a `CallArg`.
273///
274/// This is used to build `CallSpec` values ergonomically while keeping ownership simple: the
275/// conversion only needs an `&self`.
276///
277/// `ToCallArg` is intentionally generic:
278/// - for `T: MoveType`, it returns a `CallArg::Pure` by encoding the value with BCS
279/// - for object handle types, it returns the appropriate object input
280///
281/// Higher layers may implement `ToCallArg` for handles owned by the runtime. Those implementations
282/// can fail even without BCS encoding errors, for example if the handle is stale or its current
283/// owner kind makes it invalid for the requested input mode.
284///
285/// # Example
286/// ```
287/// use talus_sui_move_call::{CallArg, ToCallArg};
288///
289/// let arg = 7u64.to_call_arg().unwrap();
290/// let CallArg::Pure(bytes) = arg else {
291///     panic!("expected pure arg");
292/// };
293/// assert_eq!(bcs::from_bytes::<u64>(&bytes).unwrap(), 7);
294/// ```
295pub trait ToCallArg {
296    /// Convert this value into a `CallArg`.
297    fn to_call_arg(&self) -> Result<CallArg, CallArgError>;
298}
299
300/// Convert a value into a `CallArg`, forcing a "mutable object input" when applicable.
301///
302/// This is primarily used by code generation and higher layers that mirror Move `&mut` parameters:
303/// shared objects need their transaction input marked mutable, while owned objects use the same
304/// `ObjectReference` shape regardless.
305pub trait ToCallArgMut: ToCallArg {
306    /// Convert this value into a `CallArg` suitable for a Move `&mut` parameter.
307    fn to_call_arg_mutable(&self) -> Result<CallArg, CallArgError>;
308}
309
310impl<T: MoveType> ToCallArg for T {
311    fn to_call_arg(&self) -> Result<CallArg, CallArgError> {
312        Ok(CallArg::Pure(self.to_bcs()?))
313    }
314}
315
316impl<T: MoveStruct + HasKey> ToCallArg for MoveObject<T> {
317    fn to_call_arg(&self) -> Result<CallArg, CallArgError> {
318        Ok(CallArg::ImmutableOrOwned(self.reference().clone()))
319    }
320}
321
322impl<T: MoveStruct + HasKey> ToCallArg for SharedMoveObject<T> {
323    fn to_call_arg(&self) -> Result<CallArg, CallArgError> {
324        Ok(CallArg::Shared(self.input.clone()))
325    }
326}
327
328impl<T: MoveStruct + HasKey> ToCallArg for ReceivingMoveObject<T> {
329    fn to_call_arg(&self) -> Result<CallArg, CallArgError> {
330        Ok(CallArg::Receiving(self.reference().clone()))
331    }
332}
333
334impl<T: MoveStruct + HasKey> ToCallArgMut for MoveObject<T> {
335    fn to_call_arg_mutable(&self) -> Result<CallArg, CallArgError> {
336        self.to_call_arg()
337    }
338}
339
340impl<T: MoveStruct + HasKey> ToCallArgMut for SharedMoveObject<T> {
341    fn to_call_arg_mutable(&self) -> Result<CallArg, CallArgError> {
342        let actual = self.mutability();
343        if !actual.is_mutable() {
344            return Err(CallArgError::SharedMutability {
345                object_id: self.object_id(),
346                actual,
347            });
348        }
349        Ok(CallArg::Shared(self.input.clone()))
350    }
351}
352
353impl<T: MoveStruct + HasKey> ToCallArgMut for ReceivingMoveObject<T> {
354    fn to_call_arg_mutable(&self) -> Result<CallArg, CallArgError> {
355        self.to_call_arg()
356    }
357}
358
359/// Typed object arguments.
360///
361/// Generated interface code uses this trait to accept both owned/immutable and shared object
362/// handles while still carrying the expected Move type `T`.
363pub trait ObjectArg<T: MoveStruct + HasKey>: ToCallArgMut {}
364
365impl<T: MoveStruct + HasKey> ObjectArg<T> for MoveObject<T> {}
366impl<T: MoveStruct + HasKey> ObjectArg<T> for SharedMoveObject<T> {}
367
368/// Errors that can occur when constructing a `CallSpec`.
369#[derive(thiserror::Error, Debug)]
370pub enum CallSpecError {
371    /// The provided module string is not a valid Move identifier.
372    #[error("invalid Move identifier for module: `{0}`")]
373    Module(String),
374    /// The provided function string is not a valid Move identifier.
375    #[error("invalid Move identifier for function: `{0}`")]
376    Function(String),
377    /// A call argument could not be encoded or converted.
378    #[error(transparent)]
379    Arg(#[from] CallArgError),
380}
381
382/// Move call target without encoded arguments.
383///
384/// This is the lower level boundary for PTB composition: generated code owns package, module,
385/// function, and type arguments, while `talus-sui-move-ptb` can wire arbitrary PTB `Argument`s,
386/// including previous command results.
387#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
388pub struct CallTarget {
389    /// Package ID that contains the Move module.
390    pub package: Address,
391    /// Move module identifier.
392    pub module: Identifier,
393    /// Move function identifier.
394    pub function: Identifier,
395    /// Type arguments for the call (Move `TypeTag`s).
396    pub type_arguments: Vec<TypeTag>,
397}
398
399impl CallTarget {
400    /// Create an empty call target for a `(package, module, function)` triple.
401    pub fn new(
402        package: Address,
403        module: impl AsRef<str>,
404        function: impl AsRef<str>,
405    ) -> Result<Self, CallSpecError> {
406        let module_str = module.as_ref();
407        let function_str = function.as_ref();
408
409        let module = Identifier::from_str(module_str)
410            .map_err(|_| CallSpecError::Module(module_str.to_string()))?;
411        let function = Identifier::from_str(function_str)
412            .map_err(|_| CallSpecError::Function(function_str.to_string()))?;
413
414        Ok(Self {
415            package,
416            module,
417            function,
418            type_arguments: Vec::new(),
419        })
420    }
421
422    /// Append a type argument derived from a `MoveType`.
423    pub fn push_type_arg<T: MoveType>(&mut self) {
424        self.type_arguments.push(T::type_tag_static());
425    }
426}
427
428/// A description of a Move function call.
429///
430/// A `CallSpec` carries the call target plus already encoded arguments. It is designed to be
431/// consumed by a transaction building layer.
432///
433/// # Example
434/// ```
435/// use std::str::FromStr;
436/// use talus_sui_move_call::{CallSpec, MoveObject};
437/// use sui_sdk_types::{Address, Digest, ObjectReference, TypeTag};
438///
439/// # #[talus_sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")]
440/// # struct ID { bytes: Address }
441/// #
442/// # #[talus_sui_move::move_struct(address = "0x2", module = "object", abilities = "store")]
443/// # struct UID { id: ID }
444/// #
445/// #[talus_sui_move::move_struct(address = "0x1", module = "vault", abilities = "key")]
446/// struct Vault {
447///     id: UID,
448/// }
449///
450/// let package = Address::from_str("0x1").unwrap();
451/// let obj_ref = ObjectReference::new(package, 1, Digest::default());
452/// let vault = MoveObject::<Vault>::new(obj_ref);
453///
454/// let mut spec = CallSpec::new(package, "vault", "withdraw").unwrap();
455/// spec.push_type_arg::<u64>();
456/// spec.push_arg(&vault).unwrap();
457/// spec.push_arg(&1u64).unwrap();
458///
459/// assert_eq!(spec.type_arguments, vec![TypeTag::U64]);
460/// assert_eq!(spec.arguments.len(), 2);
461/// ```
462#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
463pub struct CallSpec {
464    /// Package ID that contains the Move module.
465    pub package: Address,
466    /// Move module identifier.
467    pub module: Identifier,
468    /// Move function identifier.
469    pub function: Identifier,
470    /// Type arguments for the call (Move `TypeTag`s).
471    pub type_arguments: Vec<TypeTag>,
472    /// Call arguments (Sui `Input`s).
473    pub arguments: Vec<CallArg>,
474}
475
476impl CallSpec {
477    /// Create an empty call spec for a `(package, module, function)` triple.
478    pub fn new(
479        package: Address,
480        module: impl AsRef<str>,
481        function: impl AsRef<str>,
482    ) -> Result<Self, CallSpecError> {
483        Ok(Self::from_target(CallTarget::new(
484            package, module, function,
485        )?))
486    }
487
488    /// Create an empty call spec from a generated call target.
489    pub fn from_target(target: CallTarget) -> Self {
490        Self {
491            package: target.package,
492            module: target.module,
493            function: target.function,
494            type_arguments: target.type_arguments,
495            arguments: Vec::new(),
496        }
497    }
498
499    /// Return this call spec's target without its encoded arguments.
500    pub fn target(&self) -> CallTarget {
501        CallTarget {
502            package: self.package,
503            module: self.module.clone(),
504            function: self.function.clone(),
505            type_arguments: self.type_arguments.clone(),
506        }
507    }
508
509    /// Append a type argument derived from a `MoveType`.
510    pub fn push_type_arg<T: MoveType>(&mut self) {
511        self.type_arguments.push(T::type_tag_static());
512    }
513
514    /// Append an argument by converting it into a `CallArg`.
515    ///
516    /// This can fail because a pure value cannot be encoded with BCS or because a runtime object
517    /// handle is stale, tombstoned, or incompatible with the requested input kind.
518    pub fn push_arg<A: ToCallArg>(&mut self, arg: &A) -> Result<(), CallArgError> {
519        self.arguments.push(arg.to_call_arg()?);
520        Ok(())
521    }
522
523    /// Append an argument as if it were used for a Move `&mut` parameter.
524    ///
525    /// This differs from [`CallSpec::push_arg`] only for shared object handles: shared inputs must
526    /// be marked mutable in the transaction input.
527    pub fn push_arg_mut<A: ToCallArgMut>(&mut self, arg: &A) -> Result<(), CallArgError> {
528        self.arguments.push(arg.to_call_arg_mutable()?);
529        Ok(())
530    }
531
532    /// Append a raw Sui transaction input.
533    ///
534    /// This is an escape hatch for `sui_sdk_types::Input` variants that don't have typed wrappers
535    /// in this crate.
536    pub fn push_input(&mut self, input: CallArg) {
537        self.arguments.push(input);
538    }
539}
540
541/// Common exports for downstream code.
542pub mod prelude {
543    pub use crate::{
544        CallArg, CallArgError, CallSpec, CallSpecError, CallTarget, MoveObject, ObjectArg,
545        ReceivingMoveObject, SharedMoveObject, ToCallArg, ToCallArgMut,
546    };
547    pub use sui_sdk_types::Mutability;
548    pub use talus_sui_move::prelude::*;
549}