Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 73 additions & 15 deletions sui-move-call/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,55 @@ pub enum CallSpecError {
/// The provided function string is not a valid Move identifier.
#[error("invalid Move identifier for function: `{0}`")]
Function(String),
/// A call argument could not be encoded or converted.
#[error(transparent)]
Arg(#[from] CallArgError),
}

/// Move call target without encoded arguments.
///
/// This is the lower-level boundary for PTB composition: generated code owns package, module,
/// function, and type arguments, while `sui-move-ptb` can wire arbitrary PTB `Argument`s including
/// previous command results.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CallTarget {
/// Package ID that contains the Move module.
pub package: Address,
/// Move module identifier.
pub module: Identifier,
/// Move function identifier.
pub function: Identifier,
/// Type arguments for the call (Move `TypeTag`s).
pub type_arguments: Vec<TypeTag>,
}

impl CallTarget {
/// Create an empty call target for a `(package, module, function)` triple.
pub fn new(
package: Address,
module: impl AsRef<str>,
function: impl AsRef<str>,
) -> Result<Self, CallSpecError> {
let module_str = module.as_ref();
let function_str = function.as_ref();

let module = Identifier::from_str(module_str)
.map_err(|_| CallSpecError::Module(module_str.to_string()))?;
let function = Identifier::from_str(function_str)
.map_err(|_| CallSpecError::Function(function_str.to_string()))?;

Ok(Self {
package,
module,
function,
type_arguments: Vec::new(),
})
}

/// Append a type argument derived from a `MoveType`.
pub fn push_type_arg<T: MoveType>(&mut self) {
self.type_arguments.push(T::type_tag_static());
}
}

/// A description of a Move function call.
Expand Down Expand Up @@ -419,21 +468,30 @@ impl CallSpec {
module: impl AsRef<str>,
function: impl AsRef<str>,
) -> Result<Self, CallSpecError> {
let module_str = module.as_ref();
let function_str = function.as_ref();

let module = Identifier::from_str(module_str)
.map_err(|_| CallSpecError::Module(module_str.to_string()))?;
let function = Identifier::from_str(function_str)
.map_err(|_| CallSpecError::Function(function_str.to_string()))?;
Ok(Self::from_target(CallTarget::new(
package, module, function,
)?))
}

Ok(Self {
package,
module,
function,
type_arguments: Vec::new(),
/// Create an empty call spec from a generated call target.
pub fn from_target(target: CallTarget) -> Self {
Self {
package: target.package,
module: target.module,
function: target.function,
type_arguments: target.type_arguments,
arguments: Vec::new(),
})
}
}

/// Return this call spec's target without its encoded arguments.
pub fn target(&self) -> CallTarget {
CallTarget {
package: self.package,
module: self.module.clone(),
function: self.function.clone(),
type_arguments: self.type_arguments.clone(),
}
}

/// Append a type argument derived from a `MoveType`.
Expand Down Expand Up @@ -471,8 +529,8 @@ impl CallSpec {
/// Convenience re-exports for downstream code.
pub mod prelude {
pub use crate::{
CallArg, CallArgError, CallSpec, CallSpecError, MoveObject, ObjectArg, ReceivingMoveObject,
SharedMoveObject, ToCallArg, ToCallArgMut,
CallArg, CallArgError, CallSpec, CallSpecError, CallTarget, MoveObject, ObjectArg,
ReceivingMoveObject, SharedMoveObject, ToCallArg, ToCallArgMut,
};
pub use sui_move::prelude::*;
pub use sui_sdk_types::Mutability;
Expand Down
41 changes: 32 additions & 9 deletions sui-move-codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,48 @@ The pipeline is intentionally split in two:
(`NormalizedPackage`).
2. **Render (offline)**: render Rust source from that IR.

Because the IR is JSON-friendly, you can commit it and re-render deterministically in CI without
Because the IR is JSON-serializable, you can commit it and re-render deterministically in CI without
needing network access.

## What gets generated

Given a `NormalizedPackage` (either fetched from gRPC or loaded from JSON), this crate can render:

- A `pub const PACKAGE: Address` (the on-chain package id)
- `CALL_PACKAGE` / `TYPE_PACKAGE` constants for generated calls and type identity
- `call_package()` / `type_package()` / `with_packages(...)` helpers for scoped package-id overrides
- One Rust module per Move module (or a flat layout via `RenderOptions::flatten`)
- Move datatypes as Rust types (structs use `#[sui_move::move_struct]` via `sui-move`’s `derive`
feature)
- Move functions as Rust functions that return `sui_move_call::CallSpec`
- Move functions as generated `*_target` functions
- Optional typed Rust functions that return `sui_move_call::CallSpec`
- (optional) A `TxExt` trait implemented for `sui_move_runtime::Tx` (enable with
`RenderOptions::emit_tx_ext`)

Those generated call builders are designed to be used directly in higher layers:
- `sui-move-ptb` can consume `CallSpec` to build a `ProgrammableTransaction`
- `sui-move-runtime` can consume `CallSpec` via its tx builder (or `sui_move_runtime::tx!`)

Use `with_packages` when generated calls target one deployment package while type tags retain the
package that defines the Move types:

```rust
# use sui_sdk_types::Address;
# mod bindings {
# pub fn with_packages<R>(
# _call_package: sui_sdk_types::Address,
# _type_package: sui_sdk_types::Address,
# f: impl FnOnce() -> R,
# ) -> R { f() }
# pub mod m {
# pub fn f(_arg0: u64) {}
# }
# }
# let localnet_package = Address::ZERO;
bindings::with_packages(localnet_package, localnet_package, || {
bindings::m::f(10);
});
```

## Example: render from an in-memory IR

```rust
Expand Down Expand Up @@ -101,7 +124,7 @@ let pkg = NormalizedPackage {
};

let code = render_package(&pkg, &RenderOptions::default());
assert!(code.contains("pub const PACKAGE"));
assert!(code.contains("pub const CALL_PACKAGE"));
assert!(code.contains("pub struct S"));
assert!(code.contains("pub fn f"));
```
Expand Down Expand Up @@ -185,7 +208,7 @@ let pkg = NormalizedPackage {
};

let code = render_package(&pkg, &RenderOptions::default());
let start = code.find("pub fn mutate").unwrap();
let start = code.find("pub fn mutate(").unwrap();
let sig_end = start + code[start..].find('{').unwrap();
let sig = &code[start..sig_end];

Expand Down Expand Up @@ -312,14 +335,14 @@ let pkg = NormalizedPackage {
};

let code = render_package(&pkg, &RenderOptions::default());
let start = code.find("pub fn id").unwrap();
let start = code.find("pub fn id<T0>(").unwrap();
let sig_end = start + code[start..].find('{').unwrap();
let sig = &code[start..sig_end];

assert!(sig.contains("pub fn id<T0>(arg0: Vec<T0>)"));
assert!(sig.contains("where"));
assert!(sig.contains("T0: sm::MoveType + sm::HasStore"));
assert!(code.contains("spec.push_type_arg::<T0>();"));
assert!(code.contains("target.push_type_arg::<T0>();"));
```

## Example: fetch over gRPC
Expand All @@ -334,14 +357,14 @@ let package_id: Address = "0x2".parse()?;

let pkg = fetch_package(&mut client, package_id).await?;
let json = pkg.to_json_string()?;
println!("{json}");
println!("{} bytes", json.len());
# Ok(())
# }
```

## Recommended workflow

To keep builds deterministic, fetch metadata once and commit it (JSON), then render from JSON:
To keep builds deterministic, fetch metadata once and commit it as JSON, then render from JSON:

1. Fetch and save `NormalizedPackage` JSON (out-of-band; not in `build.rs`)
2. Render Rust bindings from that JSON during builds or as a pre-generation step
Expand Down
Loading
Loading