Skip to content
Merged
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
bcs = "0.1.6"
ethnum = "1.5.3"
sui-sdk-types = { version = "0.3.1", features = ["serde"] }
sui-rpc = { version = "0.3.1" }
sui-crypto = { version = "0.3.0" }
Expand Down
27 changes: 27 additions & 0 deletions sui-move-codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,33 @@ The pipeline is intentionally split in two:
Because the IR is JSON-serializable, you can commit it and re-render deterministically in CI without
needing network access.

### Optional source parameter names

Sui package metadata contains function parameter types but not their source names. Network fetched
IR therefore uses deterministic names such as `arg0` and `arg1` by default.

Callers that own matching Move source can overlay parameter names before saving or rendering the
IR:

```rust,no_run
use sui_move_codegen::{apply_function_parameter_names_from_sources, fetch_package, GrpcClient};
use sui_sdk_types::Address;

# async fn demo() -> Result<(), Box<dyn std::error::Error>> {
let mut client = GrpcClient::new(GrpcClient::MAINNET_FULLNODE)?;
let package_id: Address = "0x2".parse()?;
let mut package = fetch_package(&mut client, package_id).await?;

apply_function_parameter_names_from_sources(&mut package, "path/to/package/sources")?;
# Ok(())
# }
```

The overlay changes names only. Package identity, function signatures, types, and abilities remain
network derived. The source is trusted metadata and is not verified against published bytecode.
Source and network parameter counts must match. Rust rendering preserves source spelling in Move
documentation and converts reserved Rust identifiers safely in generated APIs.

## What gets generated

Given a `NormalizedPackage` (either fetched from gRPC or loaded from JSON), this crate can render:
Expand Down
8 changes: 6 additions & 2 deletions sui-move-codegen/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,14 @@ impl TypeRef {
}
}

/// A function parameter (name is synthesized; Sui metadata does not carry parameter names).
/// A function parameter.
///
/// Sui package metadata does not contain source parameter names, so [`crate::fetch_package`]
/// assigns `argN`. Callers that own matching Move source can apply names with
/// [`crate::apply_function_parameter_names_from_sources`] before rendering.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionParam {
/// Parameter name (`arg0`, `arg1`, ...).
/// Parameter name, or `argN` when source names have not been applied.
pub name: String,
/// Parameter type.
pub ty: TypeRef,
Expand Down
10 changes: 4 additions & 6 deletions sui-move-codegen/src/render/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,14 @@ fn render_params_and_pushes(

let mut params = Vec::new();
let mut pushes = Vec::new();
let mut arg_idx: usize = 0;
let mut parameter_idents = idents::ParameterIdents::default();

for p in &f.parameters {
for (index, p) in f.parameters.iter().enumerate() {
if is_tx_context(&p.ty) {
continue;
}

let arg_ident = format_ident!("arg{arg_idx}");
arg_idx += 1;
let arg_ident = parameter_idents.next(&p.name, index);
let (ref_mutable, inner) = split_ref(&p.ty);

let is_object = is_object_type(inner, f, pkg, opts);
Expand Down Expand Up @@ -486,8 +485,7 @@ fn move_signature_string(module: &NormalizedModule, f: &Function) -> String {
let params = f
.parameters
.iter()
.enumerate()
.map(|(idx, p)| format!("arg{idx}: {}", move_type_string(&p.ty)))
.map(|p| format!("{}: {}", p.name, move_type_string(&p.ty)))
.collect::<Vec<_>>()
.join(", ");

Expand Down
27 changes: 27 additions & 0 deletions sui-move-codegen/src/render/idents.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
//! Identifier helpers for rendering Rust code.

use std::collections::BTreeSet;

use syn::Ident;

#[derive(Default)]
pub(crate) struct ParameterIdents {
used: BTreeSet<String>,
}

impl ParameterIdents {
pub(crate) fn next(&mut self, name: &str, index: usize) -> Ident {
let mut candidate = match name {
"_" => format!("arg{index}"),
"crate" | "self" | "Self" | "super" => format!("{name}_"),
_ => name.to_owned(),
};

if self.used.contains(&candidate) {
candidate = format!("arg{index}");
while self.used.contains(&candidate) {
candidate.push('_');
}
}

self.used.insert(candidate.clone());
ident(&candidate)
}
}

/// Turn a Move identifier into a valid Rust identifier.
///
/// Move identifiers are already close to Rust identifiers; this mainly exists to:
Expand Down
81 changes: 81 additions & 0 deletions sui-move-codegen/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,35 @@ mod tests {
}
}

fn named_parameter_pkg(names: &[&str]) -> NormalizedPackage {
NormalizedPackage {
storage_id: "0x1".into(),
original_id: None,
version: 0,
modules: BTreeMap::from([(
"m".into(),
NormalizedModule {
name: "m".into(),
datatypes: vec![],
functions: vec![Function {
name: "named".into(),
visibility: Visibility::Public,
is_entry: true,
type_parameters: vec![],
parameters: names
.iter()
.map(|name| FunctionParam {
name: (*name).into(),
ty: TypeRef::U64,
})
.collect(),
return_types: vec![],
}],
},
)]),
}
}

fn option_pkg() -> NormalizedPackage {
NormalizedPackage {
storage_id: "0x1".into(),
Expand Down Expand Up @@ -734,6 +763,42 @@ mod tests {
assert!(!code.contains("expect(\"encode arg\")"));
}

#[test]
fn generated_calls_keep_network_parameter_names_by_default() {
let code = render_package(&demo_pkg(), &RenderOptions::default());

assert!(code.contains("arg0: &mut impl sm_call::ObjectArg<Obj>"));
assert!(code.contains("spec.push_arg_mut(arg0)?;"));
}

#[test]
fn generated_calls_use_move_parameter_names() {
let package = named_parameter_pkg(&["amount", "type", "self", "_"]);
let code = render_package(&package, &RenderOptions::default());

assert!(code.contains("pub fn named("));
assert!(code.contains("amount: u64,"));
assert!(code.contains("r#type: u64,"));
assert!(code.contains("self_: u64,"));
assert!(code.contains("arg3: u64,"));
assert!(code.contains("spec.push_arg(&amount)?;"));
assert!(code.contains("spec.push_arg(&r#type)?;"));
assert!(code.contains("spec.push_arg(&self_)?;"));
assert!(code.contains("spec.push_arg(&arg3)?;"));
assert!(code.contains("named(amount: u64, type: u64, self: u64, _: u64)"));
}

#[test]
fn generated_parameter_names_remain_unique_after_rust_conversion() {
let package = named_parameter_pkg(&["self", "self_", "arg1"]);
let code = render_package(&package, &RenderOptions::default());

assert!(code.contains("pub fn named("));
assert!(code.contains("spec.push_arg(&self_)?;"));
assert!(code.contains("spec.push_arg(&arg1)?;"));
assert!(code.contains("spec.push_arg(&arg2)?;"));
}

#[test]
fn generated_calls_can_emit_targets_without_call_specs() {
let opts = RenderOptions {
Expand Down Expand Up @@ -953,6 +1018,22 @@ mod tests {
assert!(code.contains("self.call(spec)"));
}

#[test]
fn rendered_tx_ext_uses_move_parameter_names() {
let package = named_parameter_pkg(&["amount", "type", "self"]);
let opts = RenderOptions {
emit_tx_ext: true,
..RenderOptions::default()
};
let code = render_package(&package, &opts);

assert!(code.contains("fn m__named("));
assert!(code.contains("amount: u64,"));
assert!(code.contains("r#type: u64,"));
assert!(code.contains("self_: u64,"));
assert!(code.contains("let spec = m::named("));
}

#[test]
fn split_output_includes_tx_ext_in_mod_rs() {
let opts = RenderOptions {
Expand Down
10 changes: 4 additions & 6 deletions sui-move-codegen/src/render/tx_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,15 @@ fn render_params_and_args(
let mut params = Vec::new();
let mut args = Vec::new();
let mut skipped_tx_context = false;
let mut arg_idx: usize = 0;
let mut parameter_idents = idents::ParameterIdents::default();

for p in &f.parameters {
for (index, p) in f.parameters.iter().enumerate() {
if is_tx_context(&p.ty) {
skipped_tx_context = true;
continue;
}

let arg_ident = format_ident!("arg{arg_idx}");
arg_idx += 1;
let arg_ident = parameter_idents.next(&p.name, index);
let (ref_mutable, inner) = split_ref(&p.ty);

let is_object = is_object_type(inner, f, pkg, opts);
Expand Down Expand Up @@ -266,8 +265,7 @@ fn move_signature_string(module: &NormalizedModule, f: &Function) -> String {
let params = f
.parameters
.iter()
.enumerate()
.map(|(idx, p)| format!("arg{idx}: {}", move_type_string(&p.ty)))
.map(|p| format!("{}: {}", p.name, move_type_string(&p.ty)))
.collect::<Vec<_>>()
.join(", ");

Expand Down
4 changes: 4 additions & 0 deletions sui-move-codegen/src/source_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pub enum SourceNameError {
///
/// `source_dir` is the package `sources/` directory. Missing directories are treated as empty so
/// callers can invoke this uniformly for packages that may not have local sources.
///
/// This changes names only and does not verify source against published bytecode. Package identity,
/// function signatures, and types remain unchanged. Names are applied through
/// [`NormalizedPackage::apply_function_parameter_names`], which rejects parameter count mismatches.
pub fn apply_function_parameter_names_from_sources(
package: &mut NormalizedPackage,
source_dir: impl AsRef<Path>,
Expand Down
13 changes: 9 additions & 4 deletions sui-move-ptb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,17 +278,22 @@ impl PtbBuilder {
}
}

/// Add a coin withdrawn from the transaction sender's SIP-58 address balance.
/// Withdraw funds from the transaction sender's SIP-58 address balance and redeem them as a
/// `Coin<T>`.
pub fn funds_withdrawal_coin(
&mut self,
coin_type: TypeTag,
amount: u64,
) -> Result<Argument, BuildError> {
self.input(CallArg::FundsWithdrawal(FundsWithdrawal::new(
let withdrawal = self.input(CallArg::FundsWithdrawal(FundsWithdrawal::new(
amount,
coin_type,
coin_type.clone(),
WithdrawFrom::Sender,
)))
)))?;
let mut target = CallTarget::new(Address::from_static("0x2"), "coin", "redeem_funds")?;
target.type_arguments.push(coin_type);

self.call_target(target, vec![withdrawal])
}

/// Add the Sui clock object as an immutable shared PTB input.
Expand Down
16 changes: 13 additions & 3 deletions sui-move-ptb/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,23 @@ fn builds_raw_bcs_inputs_funds_withdrawals_and_typed_vectors() {
let vector = tx.move_vector::<u64>(vec![amount]).unwrap();

assert_eq!(raw, Argument::Input(0));
assert_eq!(coin, Argument::Input(2));
assert_eq!(vector, Argument::Result(0));
assert_eq!(coin, Argument::Result(0));
assert_eq!(vector, Argument::Result(1));

let pt = tx.finish();
assert!(matches!(pt.inputs[0], CallArg::Pure(_)));
assert!(matches!(pt.inputs[2], CallArg::FundsWithdrawal(_)));
assert!(matches!(pt.commands[0], Command::MakeMoveVector(_)));
match &pt.commands[0] {
Command::MoveCall(call) => {
assert_eq!(call.package, Address::from_static("0x2"));
assert_eq!(call.module.as_str(), "coin");
assert_eq!(call.function.as_str(), "redeem_funds");
assert_eq!(call.type_arguments, vec![TypeTag::U64]);
assert_eq!(call.arguments, vec![Argument::Input(2)]);
}
command => panic!("expected redeem_funds call, got {command:?}"),
}
assert!(matches!(pt.commands[1], Command::MakeMoveVector(_)));
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions sui-move/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ homepage.workspace = true
[dependencies]
serde = { workspace = true, features = ["derive"] }
bcs = { workspace = true }
ethnum = { workspace = true }
sui-sdk-types = { workspace = true }
thiserror = { workspace = true }
sui-move-derive = { path = "../sui-move-derive", optional = true }
Expand Down
7 changes: 5 additions & 2 deletions sui-move/src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@ mod tests {

#[test]
fn u256_has_move_type_tag_and_fixed_width_bcs() {
let value = crate::U256::from_le_bytes([7u8; 32]);
let integer = ethnum::U256::from_le_bytes([7u8; 32]);
let value = crate::U256::from(integer);

assert_eq!(crate::U256::type_tag_static(), sui_sdk_types::TypeTag::U256);
assert_eq!(bcs::to_bytes(&value).unwrap().len(), 32);
assert_eq!(bcs::to_bytes(&value).unwrap(), vec![7u8; 32]);
assert_eq!(bcs::from_bytes::<crate::U256>(&[7u8; 32]).unwrap(), value);
assert_eq!(ethnum::U256::from(value), integer);
}
}
Loading
Loading