diff --git a/Cargo.toml b/Cargo.toml index 4dfb470..cfd39d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/sui-move-codegen/README.md b/sui-move-codegen/README.md index 996c904..e1ab513 100644 --- a/sui-move-codegen/README.md +++ b/sui-move-codegen/README.md @@ -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> { +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: diff --git a/sui-move-codegen/src/ir.rs b/sui-move-codegen/src/ir.rs index e67c3be..1b19ec7 100644 --- a/sui-move-codegen/src/ir.rs +++ b/sui-move-codegen/src/ir.rs @@ -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, diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index 70953a0..c6cadf3 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -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); @@ -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::>() .join(", "); diff --git a/sui-move-codegen/src/render/idents.rs b/sui-move-codegen/src/render/idents.rs index 8c515c6..fff4649 100644 --- a/sui-move-codegen/src/render/idents.rs +++ b/sui-move-codegen/src/render/idents.rs @@ -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, +} + +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: diff --git a/sui-move-codegen/src/render/mod.rs b/sui-move-codegen/src/render/mod.rs index 8c2ae50..3e68f78 100644 --- a/sui-move-codegen/src/render/mod.rs +++ b/sui-move-codegen/src/render/mod.rs @@ -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(), @@ -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")); + 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 { @@ -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 { diff --git a/sui-move-codegen/src/render/tx_ext.rs b/sui-move-codegen/src/render/tx_ext.rs index 8b20aa7..49fbb2c 100644 --- a/sui-move-codegen/src/render/tx_ext.rs +++ b/sui-move-codegen/src/render/tx_ext.rs @@ -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); @@ -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::>() .join(", "); diff --git a/sui-move-codegen/src/source_names.rs b/sui-move-codegen/src/source_names.rs index cd0b39a..78696db 100644 --- a/sui-move-codegen/src/source_names.rs +++ b/sui-move-codegen/src/source_names.rs @@ -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, diff --git a/sui-move-ptb/src/lib.rs b/sui-move-ptb/src/lib.rs index 3f18631..5f2b690 100644 --- a/sui-move-ptb/src/lib.rs +++ b/sui-move-ptb/src/lib.rs @@ -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`. pub fn funds_withdrawal_coin( &mut self, coin_type: TypeTag, amount: u64, ) -> Result { - 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. diff --git a/sui-move-ptb/tests/basic.rs b/sui-move-ptb/tests/basic.rs index 073a1e4..246e0dc 100644 --- a/sui-move-ptb/tests/basic.rs +++ b/sui-move-ptb/tests/basic.rs @@ -121,13 +121,23 @@ fn builds_raw_bcs_inputs_funds_withdrawals_and_typed_vectors() { let vector = tx.move_vector::(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] diff --git a/sui-move/Cargo.toml b/sui-move/Cargo.toml index e0e4bc3..8cb578d 100644 --- a/sui-move/Cargo.toml +++ b/sui-move/Cargo.toml @@ -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 } diff --git a/sui-move/src/builtins.rs b/sui-move/src/builtins.rs index 9488902..d4b7bf9 100644 --- a/sui-move/src/builtins.rs +++ b/sui-move/src/builtins.rs @@ -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::(&[7u8; 32]).unwrap(), value); + assert_eq!(ethnum::U256::from(value), integer); } } diff --git a/sui-move/src/lib.rs b/sui-move/src/lib.rs index 51cc955..f6a7bb9 100644 --- a/sui-move/src/lib.rs +++ b/sui-move/src/lib.rs @@ -31,24 +31,53 @@ mod builtins; pub mod decode; pub use decode::{decode_copyable, decode_keyed, decode_storable}; -/// Move `u256` value represented as 32 little-endian bytes. +/// Move `u256` value backed by [`ethnum::U256`]. /// -/// This intentionally keeps arithmetic out of `sui-move`; the type exists so generated bindings -/// can carry, serialize, and type-tag Move `u256` values without inventing package-specific -/// representations. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(transparent)] -pub struct U256([u8; 32]); +/// Serialization uses the 32 byte little endian representation required by Move rather than the +/// hexadecimal string representation provided by `ethnum`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct U256(pub ethnum::U256); impl U256 { - /// Construct a `u256` from its little-endian byte representation. - pub const fn from_le_bytes(bytes: [u8; 32]) -> Self { - Self(bytes) + /// Construct a `u256` from its little endian byte representation. + pub fn from_le_bytes(bytes: [u8; 32]) -> Self { + Self(ethnum::U256::from_le_bytes(bytes)) } - /// Return the little-endian byte representation. - pub const fn to_le_bytes(self) -> [u8; 32] { - self.0 + /// Return the little endian byte representation. + pub fn to_le_bytes(self) -> [u8; 32] { + self.0.to_le_bytes() + } +} + +impl From for U256 { + fn from(value: ethnum::U256) -> Self { + Self(value) + } +} + +impl From for ethnum::U256 { + fn from(value: U256) -> Self { + value.0 + } +} + +impl Serialize for U256 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_le_bytes().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for U256 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + <[u8; 32]>::deserialize(deserializer).map(Self::from_le_bytes) } }