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}