From 1b822ecd72318ad6a8ce7f30362967b74277c362 Mon Sep 17 00:00:00 2001 From: Pavel Koch Date: Wed, 4 Feb 2026 16:19:31 +0100 Subject: [PATCH 1/6] wip SDK integration --- Cargo.toml | 10 ++++++--- sui-move-call/Cargo.toml | 4 ++-- sui-move-codegen/Cargo.toml | 4 ++-- sui-move-codegen/src/lib.rs | 2 ++ sui-move-codegen/src/render/builtins.rs | 3 +++ sui-move-codegen/src/source.rs | 4 +++- sui-move-derive/Cargo.toml | 4 ++-- sui-move-ptb/Cargo.toml | 4 ++-- sui-move-runtime/Cargo.toml | 4 ++-- sui-move/Cargo.toml | 4 ++-- sui-move/src/containers.rs | 28 +++++++++++++++++++++++++ 11 files changed, 55 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9bf71f3..d40798b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -resolver = "2" +resolver = "3" members = [ "sui-move", "sui-move-derive", @@ -9,14 +9,18 @@ members = [ "sui-move-codegen", ] +[workspace.package] +edition = "2021" +version = "0.1.0" + [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" bcs = "0.1.6" -sui-sdk-types = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["serde"] } +sui-sdk-types = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["rand", "serde"] } sui-rpc = { git = "https://github.com/mystenlabs/sui-rust-sdk" } -sui-crypto = { git = "https://github.com/mystenlabs/sui-rust-sdk" } +sui-crypto = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["ed25519", "pem"] } proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full"] } diff --git a/sui-move-call/Cargo.toml b/sui-move-call/Cargo.toml index 7a526ca..d773bb1 100644 --- a/sui-move-call/Cargo.toml +++ b/sui-move-call/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sui-move-call" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true description = "Typed call arguments and call specifications for Move calls on Sui." readme = "README.md" diff --git a/sui-move-codegen/Cargo.toml b/sui-move-codegen/Cargo.toml index 39dd42e..22eccd4 100644 --- a/sui-move-codegen/Cargo.toml +++ b/sui-move-codegen/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sui-move-codegen" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true description = "Generate typed Rust bindings (types + CallSpec builders) from Sui Move package metadata." readme = "README.md" diff --git a/sui-move-codegen/src/lib.rs b/sui-move-codegen/src/lib.rs index 654c336..5666034 100644 --- a/sui-move-codegen/src/lib.rs +++ b/sui-move-codegen/src/lib.rs @@ -12,6 +12,8 @@ pub mod ir; pub mod render; pub use crate::source::fetch_package; +pub use crate::source::Address; +pub use crate::source::Client; /// Errors from sourcing or normalizing package metadata. #[derive(thiserror::Error, Debug)] diff --git a/sui-move-codegen/src/render/builtins.rs b/sui-move-codegen/src/render/builtins.rs index d9efe4f..8682cc7 100644 --- a/sui-move-codegen/src/render/builtins.rs +++ b/sui-move-codegen/src/render/builtins.rs @@ -47,6 +47,9 @@ pub(crate) fn map_builtin(type_name: &TypeName, use_aliases: bool) -> Option (sm(use_aliases, quote! { containers::MoveOption }), false), ("0x2", "table", "Table") => (sm(use_aliases, quote! { containers::Table }), false), + ("0x2", "table_vec", "TableVec") => { + (sm(use_aliases, quote! { containers::TableVec }), false) + } ("0x2", "dynamic_field", "Field") => { (sm(use_aliases, quote! { containers::DynamicField }), false) } diff --git a/sui-move-codegen/src/source.rs b/sui-move-codegen/src/source.rs index bf48a55..bdca648 100644 --- a/sui-move-codegen/src/source.rs +++ b/sui-move-codegen/src/source.rs @@ -3,7 +3,9 @@ use std::collections::BTreeMap; use sui_rpc::proto::sui::rpc::v2 as proto; -use sui_sdk_types::Address; + +pub use sui_rpc::Client; +pub use sui_sdk_types::Address; use crate::ir::{ Ability, Datatype, DatatypeKind, Field, Function, FunctionParam, NormalizedModule, diff --git a/sui-move-derive/Cargo.toml b/sui-move-derive/Cargo.toml index a037ecc..705dc68 100644 --- a/sui-move-derive/Cargo.toml +++ b/sui-move-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sui-move-derive" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true description = "Procedural macros for sui-move typed structs." readme = "README.md" diff --git a/sui-move-ptb/Cargo.toml b/sui-move-ptb/Cargo.toml index fc994c4..7f30671 100644 --- a/sui-move-ptb/Cargo.toml +++ b/sui-move-ptb/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sui-move-ptb" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true description = "Programmable-transaction builder for typed Move calls on Sui." readme = "README.md" diff --git a/sui-move-runtime/Cargo.toml b/sui-move-runtime/Cargo.toml index f289371..47ab7ae 100644 --- a/sui-move-runtime/Cargo.toml +++ b/sui-move-runtime/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sui-move-runtime" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true description = "Runtime layer for typed Move interactions on Sui (submit, wait, auto-update handles)." readme = "README.md" diff --git a/sui-move/Cargo.toml b/sui-move/Cargo.toml index 2b1af6e..e80f382 100644 --- a/sui-move/Cargo.toml +++ b/sui-move/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sui-move" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true description = "Move-shaped typed layer for Rust, built on top of sui-sdk-types." readme = "README.md" diff --git a/sui-move/src/containers.rs b/sui-move/src/containers.rs index 5f98fd4..5469e50 100644 --- a/sui-move/src/containers.rs +++ b/sui-move/src/containers.rs @@ -87,6 +87,34 @@ impl MoveStr impl HasKey for Table {} impl HasStore for Table {} +/// Move `0x2::table_vec::TableVec`. +/// +/// The Sui framework table vec stores data as a `Table`. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct TableVec { + pub contents: Table, +} + +impl MoveType for TableVec { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) + } +} + +impl MoveStruct for TableVec { + fn struct_tag_static() -> sui_sdk_types::StructTag { + sui_sdk_types::StructTag::new( + parse_address("0x2").expect("address literal"), + parse_identifier("table_vec").expect("module"), + parse_identifier("TableVec").expect("name"), + vec![T::type_tag_static()], + ) + } +} + +impl HasStore for TableVec {} + /// Move `0x2::dynamic_field::Field`. /// /// Dynamic fields are stored under an owning object and addressed by a “name” value. From c7e2b767b540e75079cac51f33c4fc97f5da312e Mon Sep 17 00:00:00 2001 From: David Date: Wed, 4 Feb 2026 16:58:47 +0100 Subject: [PATCH 2/6] apply bounds to struct definition --- sui-move-derive/src/expand.rs | 57 ++++++++++++++++++++--------- sui-move-derive/src/lib.rs | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/sui-move-derive/src/expand.rs b/sui-move-derive/src/expand.rs index ebe9858..c2ff5d0 100644 --- a/sui-move-derive/src/expand.rs +++ b/sui-move-derive/src/expand.rs @@ -224,9 +224,19 @@ pub(crate) fn expand_move_struct( parse_quote!(::sui_move::__private::serde::Deserialize), ]); + // Apply computed bounds to the struct definition itself so that derive macros (e.g. serde) + // see the same constraints as the generated `MoveType`/`MoveStruct` impls. + let mut expanded_generics = generics.clone(); + if !where_bounds.is_empty() { + expanded_generics + .make_where_clause() + .predicates + .extend(where_bounds.iter().cloned()); + } + let mut output_struct = input; output_struct.ident = struct_ident.clone(); - output_struct.generics = generics.clone(); + output_struct.generics = expanded_generics.clone(); output_struct.data = Data::Struct(syn::DataStruct { struct_token: Default::default(), fields: Fields::Named(syn::FieldsNamed { @@ -274,6 +284,31 @@ pub(crate) fn expand_move_struct( found }); + let serde_has_bound_override = serde_attrs.iter().any(|attr| { + let syn::Meta::List(list) = &attr.meta else { + return false; + }; + + let mut found = false; + let parser = syn::meta::parser(|meta| { + if meta.path.is_ident("bound") { + found = true; + } + + if meta.input.peek(syn::Token![=]) { + let _expr: syn::Expr = meta.value()?.parse()?; + } else if meta.input.peek(syn::token::Paren) { + let content; + syn::parenthesized!(content in meta.input); + let _tokens: proc_macro2::TokenStream = content.parse()?; + } + + Ok(()) + }); + let _ = parser.parse2(list.tokens.clone()); + found + }); + output_struct.attrs = other_attrs; output_struct .attrs @@ -283,24 +318,12 @@ pub(crate) fn expand_move_struct( .attrs .push(parse_quote!(#[serde(crate = "sui_move::__private::serde")])); } + if !type_param_idents.is_empty() && !serde_has_bound_override { + output_struct.attrs.push(parse_quote!(#[serde(bound = "")])); + } output_struct.attrs.extend(serde_attrs); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let where_clause = { - let mut where_clause = where_clause.cloned(); - if !where_bounds.is_empty() { - if let Some(ref mut w) = where_clause { - w.predicates.extend(where_bounds.iter().cloned()); - } else { - let preds = where_bounds.iter().cloned(); - where_clause = Some(syn::WhereClause { - where_token: Default::default(), - predicates: preds.collect(), - }); - } - } - where_clause - }; + let (impl_generics, ty_generics, where_clause) = expanded_generics.split_for_impl(); let ability_impls = { let mut impls = Vec::new(); diff --git a/sui-move-derive/src/lib.rs b/sui-move-derive/src/lib.rs index 63b69e2..ea086ff 100644 --- a/sui-move-derive/src/lib.rs +++ b/sui-move-derive/src/lib.rs @@ -99,6 +99,7 @@ pub fn move_struct(attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(test)] mod tests { use super::args::MoveStructArgs; + use proc_macro2::Span; use std::collections::BTreeMap; #[test] @@ -123,4 +124,71 @@ mod tests { ); assert_eq!(args.type_abilities, expected); } + + #[test] + fn expands_struct_with_where_bounds_on_definition() { + let args: MoveStructArgs = syn::parse_quote!( + address = "0x1", + module = "m", + type_abilities = "T0: store" + ); + + let input: syn::DeriveInput = syn::parse_quote!( + pub struct S { + pub v: Vec, + } + ); + + let out = crate::expand::expand_move_struct(args, input).expect("expand"); + let file: syn::File = syn::parse2(out).expect("parse expanded tokens as a file"); + + let struct_item = file + .items + .iter() + .find_map(|item| match item { + syn::Item::Struct(s) if s.ident == syn::Ident::new("S", Span::call_site()) => { + Some(s) + } + _ => None, + }) + .expect("struct S in output"); + + let where_clause = struct_item + .generics + .where_clause + .as_ref() + .expect("where clause on struct definition"); + + let t0_bounds = where_clause + .predicates + .iter() + .find_map(|pred| match pred { + syn::WherePredicate::Type(p) => match &p.bounded_ty { + syn::Type::Path(ty) if ty.qself.is_none() => ty + .path + .get_ident() + .and_then(|ident| (ident == "T0").then_some(&p.bounds)), + _ => None, + }, + _ => None, + }) + .expect("where predicate for T0"); + + fn has_bound( + bounds: &syn::punctuated::Punctuated, + name: &str, + ) -> bool { + bounds.iter().any(|b| match b { + syn::TypeParamBound::Trait(tb) => tb + .path + .segments + .last() + .is_some_and(|seg| seg.ident == name), + _ => false, + }) + } + + assert!(has_bound(t0_bounds, "MoveType")); + assert!(has_bound(t0_bounds, "HasStore")); + } } From 2d9d209975577ed45789ac411c8d2c4d11d31e2d Mon Sep 17 00:00:00 2001 From: David Date: Wed, 4 Feb 2026 19:59:42 +0100 Subject: [PATCH 3/6] fix: primitive types and dependancy graph --- sui-move-codegen/Cargo.toml | 3 + .../examples/localnet_workspace.rs | 114 ++++ sui-move-codegen/src/lib.rs | 3 + sui-move-codegen/src/render/builtins.rs | 8 + sui-move-codegen/src/render/calls.rs | 24 +- sui-move-codegen/src/render/externals.rs | 73 +++ sui-move-codegen/src/render/mod.rs | 191 +++++- sui-move-codegen/src/render/tx_ext.rs | 36 +- sui-move-codegen/src/render/types.rs | 65 +- sui-move-codegen/src/render/util.rs | 25 +- sui-move-codegen/src/workspace.rs | 603 ++++++++++++++++++ sui-move-derive/src/expand.rs | 23 +- sui-move-derive/src/lib.rs | 50 ++ sui-move/src/primitives/mod.rs | 2 + sui-move/src/primitives/priority_queue.rs | 59 ++ sui-move/src/primitives/string.rs | 35 + 16 files changed, 1271 insertions(+), 43 deletions(-) create mode 100644 sui-move-codegen/examples/localnet_workspace.rs create mode 100644 sui-move-codegen/src/render/externals.rs create mode 100644 sui-move-codegen/src/workspace.rs create mode 100644 sui-move/src/primitives/priority_queue.rs create mode 100644 sui-move/src/primitives/string.rs diff --git a/sui-move-codegen/Cargo.toml b/sui-move-codegen/Cargo.toml index 22eccd4..7ef66d5 100644 --- a/sui-move-codegen/Cargo.toml +++ b/sui-move-codegen/Cargo.toml @@ -15,3 +15,6 @@ syn = { workspace = true } prettyplease = { workspace = true } sui-rpc = { workspace = true } sui-sdk-types = { workspace = true } + +[dev-dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/sui-move-codegen/examples/localnet_workspace.rs b/sui-move-codegen/examples/localnet_workspace.rs new file mode 100644 index 0000000..9f462fa --- /dev/null +++ b/sui-move-codegen/examples/localnet_workspace.rs @@ -0,0 +1,114 @@ +//! Generate a bindings workspace for a Move package (plus its dependency closure) on localnet. +//! +//! By default this connects to `http://127.0.0.1:9000` (override with `--grpc` or `SUI_GRPC`) +//! and writes output under `../target/bindings/`. +//! +//! Example: +//! - `cargo run -p sui-move-codegen --example localnet_workspace -- --check` +//! - `cargo run -p sui-move-codegen --example localnet_workspace -- 0x4cc... --check` +//! - `cargo run -p sui-move-codegen --example localnet_workspace -- 0x4cc... --external 0x0aaa...=../my-primitives-crate --check` + +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::process::Command; + +use sui_move_codegen::render::RenderOptions; +use sui_move_codegen::workspace::{generate_bindings_workspace, WorkspaceOptions}; +use sui_move_codegen::{Address, Client}; + +const DEFAULT_PACKAGE: &str = + "0x4cc38b7c23bf14d7555503ab38a9748f9544c2c29c6519df412b4f6fb6971640"; +const DEFAULT_GRPC: &str = "http://127.0.0.1:9000"; + +fn usage() -> ! { + eprintln!( + "Usage: localnet_workspace [package_id] [--grpc ] [--out ] [--external =]... [--check]\n\n\ + Defaults:\n\ + - package_id: {DEFAULT_PACKAGE}\n\ + - grpc: {DEFAULT_GRPC} (or $SUI_GRPC)\n\ + - out: /target/bindings/\n" + ); + std::process::exit(2); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut args = std::env::args().skip(1).peekable(); + + let mut package_id = DEFAULT_PACKAGE.to_string(); + let mut grpc = std::env::var("SUI_GRPC").unwrap_or_else(|_| DEFAULT_GRPC.to_string()); + let mut out_dir: Option = None; + let mut externals: BTreeMap = BTreeMap::new(); + let mut check = false; + + // Optional first positional arg: package id. + if let Some(first) = args.peek() { + if !first.starts_with('-') { + package_id = args.next().unwrap(); + } + } + + while let Some(arg) = args.next() { + match arg.as_str() { + "--grpc" => grpc = args.next().unwrap_or_else(|| usage()), + "--out" => out_dir = Some(PathBuf::from(args.next().unwrap_or_else(|| usage()))), + "--external" => { + let spec = args.next().unwrap_or_else(|| usage()); + let (pkg, path) = spec + .split_once('=') + .ok_or("expected --external =")?; + externals.insert(pkg.to_string(), PathBuf::from(path)); + } + "--check" => check = true, + "--help" | "-h" => usage(), + other => return Err(format!("unknown argument `{other}`").into()), + } + } + + let root_pkg: Address = package_id.parse()?; + let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .canonicalize()?; + let default_out = repo_root + .join("target") + .join("bindings") + .join(package_id.trim_start_matches("0x")); + let out_dir = out_dir.unwrap_or(default_out); + + println!("grpc: {grpc}"); + println!("package: {package_id}"); + println!("out: {}", out_dir.display()); + + let mut client = Client::new(grpc)?; + let render_opts = RenderOptions::default(); + let ws_opts = WorkspaceOptions { + move_binding_root: Some(repo_root), + force_non_flattened: true, + }; + + generate_bindings_workspace( + &mut client, + root_pkg, + &out_dir, + &render_opts, + externals, + ws_opts, + ) + .await?; + + println!("generated: {}", out_dir.display()); + if check { + let status = Command::new("cargo") + .arg("check") + .current_dir(&out_dir) + .status()?; + if !status.success() { + return Err(format!("cargo check failed with status {status}").into()); + } + } else { + println!("next: (optional) cd {} && cargo check", out_dir.display()); + } + + Ok(()) +} + diff --git a/sui-move-codegen/src/lib.rs b/sui-move-codegen/src/lib.rs index 5666034..4e8d89d 100644 --- a/sui-move-codegen/src/lib.rs +++ b/sui-move-codegen/src/lib.rs @@ -11,6 +11,9 @@ pub mod ir; /// Render normalized metadata into Rust source. pub mod render; +/// High-level helpers for generating bindings across packages. +pub mod workspace; + pub use crate::source::fetch_package; pub use crate::source::Address; pub use crate::source::Client; diff --git a/sui-move-codegen/src/render/builtins.rs b/sui-move-codegen/src/render/builtins.rs index 8682cc7..3a69fd9 100644 --- a/sui-move-codegen/src/render/builtins.rs +++ b/sui-move-codegen/src/render/builtins.rs @@ -34,8 +34,16 @@ pub(crate) fn map_builtin(type_name: &TypeName, use_aliases: bool) -> Option (sm(use_aliases, quote! { ascii::String }), false), + ("0x1", "string", "String") => (sm(use_aliases, quote! { string::String }), false), ("0x2", "vec_map", "VecMap") => (sm(use_aliases, quote! { vec_map::VecMap }), false), ("0x2", "vec_set", "VecSet") => (sm(use_aliases, quote! { vec_set::VecSet }), false), + ("0x2", "priority_queue", "Entry") => { + (sm(use_aliases, quote! { priority_queue::Entry }), false) + } + ("0x2", "priority_queue", "PriorityQueue") => ( + sm(use_aliases, quote! { priority_queue::PriorityQueue }), + false, + ), ("0x2", "object_bag", "ObjectBag") => { (sm(use_aliases, quote! { object_bag::ObjectBag }), true) } diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index 5a17ab0..cf4e419 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -10,18 +10,19 @@ use quote::{format_ident, quote}; use crate::ir::{Ability, Function, NormalizedModule, NormalizedPackage, TypeRef, Visibility}; -use super::{builtins, idents, types, RenderOptions}; +use super::{builtins, idents, types, ExternalResolver, RenderOptions}; pub(crate) fn render_functions( module: &NormalizedModule, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> Vec { module .functions .iter() .filter(|f| matches!(f.visibility, Visibility::Public)) - .map(|f| render_function(module, f, pkg, opts)) + .map(|f| render_function(module, f, pkg, opts, resolver)) .collect() } @@ -30,6 +31,7 @@ fn render_function( f: &Function, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { let sm_call = if opts.use_aliases { quote! { sm_call } @@ -53,7 +55,8 @@ fn render_function( .iter() .map(|ty| quote! { spec.push_type_arg::<#ty>(); }); - let (params, pushes, skipped_tx_context) = render_params_and_pushes(module, f, pkg, opts); + let (params, pushes, skipped_tx_context) = + render_params_and_pushes(module, f, pkg, opts, resolver); let signature = move_signature_string(module, f); let doc = doc_lines(&[ @@ -90,6 +93,7 @@ fn render_params_and_pushes( f: &Function, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> (Vec, Vec, bool) { let sm_call = if opts.use_aliases { quote! { sm_call } @@ -112,10 +116,11 @@ fn render_params_and_pushes( arg_idx += 1; let (ref_mutable, inner) = split_ref(&p.ty); - let is_object = is_object_type(inner, f, pkg, opts); + let is_object = is_object_type(inner, f, pkg, opts, resolver); if is_object { - let obj_ty = types::render_type_ref_in_module(inner, &module.name, pkg, opts); + let obj_ty = + types::render_type_ref_in_module(inner, &module.name, pkg, opts, resolver); let param_ty = if ref_mutable { quote! { &mut impl #sm_call::ObjectArg<#obj_ty> } } else { @@ -128,7 +133,8 @@ fn render_params_and_pushes( pushes.push(quote! { spec.push_arg(#arg_ident).expect("encode arg"); }); } } else { - let value_ty = types::render_type_ref_in_module(inner, &module.name, pkg, opts); + let value_ty = + types::render_type_ref_in_module(inner, &module.name, pkg, opts, resolver); params.push(quote! { #arg_ident: #value_ty }); pushes.push(quote! { spec.push_arg(&#arg_ident).expect("encode arg"); }); } @@ -161,12 +167,18 @@ fn is_object_type( f: &Function, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> bool { match ty { TypeRef::Datatype { type_name, .. } => { if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) { return builtin.is_key; } + if let Some(resolver) = resolver { + if let Some(is_key) = resolver.type_has_key(type_name) { + return is_key; + } + } pkg.modules .get(&type_name.module) .and_then(|m| { diff --git a/sui-move-codegen/src/render/externals.rs b/sui-move-codegen/src/render/externals.rs new file mode 100644 index 0000000..bbbb4c4 --- /dev/null +++ b/sui-move-codegen/src/render/externals.rs @@ -0,0 +1,73 @@ +//! External package resolution for rendering cross-package type references. +//! +//! Codegen is deterministic by default: if a type reference points at an unknown external +//! package, rendering emits a `compile_error!` to force the caller to either: +//! - provide an external mapping, or +//! - generate bindings for that package as well. + +use std::collections::BTreeMap; + +use crate::ir::{Ability, NormalizedPackage, TypeName}; + +/// Resolver for packages that are not the one currently being rendered. +/// +/// This lets the renderer: +/// - map external Move types to Rust paths like `dep_crate::module::Type` +/// - determine whether external types have the `key` ability (to generate object-arg signatures) +#[derive(Clone, Debug, Default)] +pub struct ExternalResolver { + crate_by_address: BTreeMap, + has_key_by_type: BTreeMap, +} + +impl ExternalResolver { + /// Create an empty resolver. + pub fn new() -> Self { + Self::default() + } + + /// Register a package under a Rust crate name. + /// + /// Both `storage_id` and `original_id` (if present) are mapped to the same crate name to + /// make resolution robust across package upgrades. + pub fn add_package(&mut self, pkg: &NormalizedPackage, crate_name: impl Into) { + let crate_name = crate_name.into(); + self.crate_by_address + .insert(pkg.storage_id.clone(), crate_name.clone()); + if let Some(orig) = &pkg.original_id { + self.crate_by_address.insert(orig.clone(), crate_name.clone()); + } + + for module in pkg.modules.values() { + for dt in &module.datatypes { + let has_key = dt.abilities.contains(&Ability::Key); + self.has_key_by_type.insert(dt.type_name.clone(), has_key); + + // Be robust to package upgrades: callers may reference either the storage id or + // original id in type signatures. + if let Some(orig) = &pkg.original_id { + if dt.type_name.address != *orig { + let mut alias = dt.type_name.clone(); + alias.address = orig.clone(); + self.has_key_by_type.insert(alias, has_key); + } + } + if dt.type_name.address != pkg.storage_id { + let mut alias = dt.type_name.clone(); + alias.address = pkg.storage_id.clone(); + self.has_key_by_type.insert(alias, has_key); + } + } + } + } + + /// Look up the Rust crate name for a Move package address. + pub fn crate_name_for_address(&self, address: &str) -> Option<&str> { + self.crate_by_address.get(address).map(|s| s.as_str()) + } + + /// Whether a fully-qualified Move type has the `key` ability. + pub fn type_has_key(&self, type_name: &TypeName) -> Option { + self.has_key_by_type.get(type_name).copied() + } +} diff --git a/sui-move-codegen/src/render/mod.rs b/sui-move-codegen/src/render/mod.rs index 249f4b3..c1dedc8 100644 --- a/sui-move-codegen/src/render/mod.rs +++ b/sui-move-codegen/src/render/mod.rs @@ -17,11 +17,14 @@ use crate::ir::NormalizedPackage; mod builtins; mod calls; +mod externals; mod idents; mod tx_ext; mod types; mod util; +pub use externals::ExternalResolver; + /// Options controlling what gets emitted. /// /// These options only affect the *shape* of the rendered Rust source; they do not change the IR. @@ -90,7 +93,20 @@ impl Default for RenderOptions { /// assert!(code.contains("pub const PACKAGE")); /// ``` pub fn render_package(pkg: &NormalizedPackage, opts: &RenderOptions) -> String { - let tokens = util::render_package_tokens(pkg, opts); + let tokens = util::render_package_tokens(pkg, opts, None); + util::prettify(tokens) +} + +/// Render a normalized package into a single Rust source string, resolving external packages. +/// +/// If an external type reference cannot be resolved via `resolver`, the generated code will +/// include a `compile_error!` (same behavior as [`render_package`]). +pub fn render_package_with_resolver( + pkg: &NormalizedPackage, + opts: &RenderOptions, + resolver: &ExternalResolver, +) -> String { + let tokens = util::render_package_tokens(pkg, opts, Some(resolver)); util::prettify(tokens) } @@ -113,13 +129,39 @@ pub fn render_package_split( split_opts.flatten = false; for module in pkg.modules.values() { - let tokens = util::render_module_file(module, pkg, &split_opts); + let tokens = util::render_module_file(module, pkg, &split_opts, None); + let code = util::prettify(tokens); + let filename = format!("{}.rs", module.name); + fs::write(out_dir.join(filename), code)?; + } + + let mod_tokens = util::render_split_mod_rs_tokens(pkg, &split_opts, None); + let mod_code = util::prettify(mod_tokens); + fs::write(out_dir.join("mod.rs"), mod_code)?; + Ok(()) +} + +/// Render a normalized package into multiple files, resolving external packages. +pub fn render_package_split_with_resolver( + pkg: &NormalizedPackage, + opts: &RenderOptions, + resolver: &ExternalResolver, + out_dir: impl AsRef, +) -> std::io::Result<()> { + let out_dir = out_dir.as_ref(); + fs::create_dir_all(out_dir)?; + + let mut split_opts = opts.clone(); + split_opts.flatten = false; + + for module in pkg.modules.values() { + let tokens = util::render_module_file(module, pkg, &split_opts, Some(resolver)); let code = util::prettify(tokens); let filename = format!("{}.rs", module.name); fs::write(out_dir.join(filename), code)?; } - let mod_tokens = util::render_split_mod_rs_tokens(pkg, &split_opts); + let mod_tokens = util::render_split_mod_rs_tokens(pkg, &split_opts, Some(resolver)); let mod_code = util::prettify(mod_tokens); fs::write(out_dir.join("mod.rs"), mod_code)?; Ok(()) @@ -242,4 +284,147 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + + #[test] + fn renders_external_types_via_resolver_as_dep_crate_module_type() { + let dep = NormalizedPackage { + storage_id: "0xb".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([( + "dep".into(), + NormalizedModule { + name: "dep".into(), + datatypes: vec![Datatype { + type_name: TypeName::parse("0xb::dep::Obj").unwrap(), + module: "dep".into(), + name: "Obj".into(), + abilities: vec![Ability::Key, Ability::Store], + type_parameters: vec![], + kind: DatatypeKind::Struct { fields: vec![] }, + }], + functions: vec![], + }, + )]), + }; + + let root = NormalizedPackage { + storage_id: "0xa".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([( + "m".into(), + NormalizedModule { + name: "m".into(), + datatypes: vec![Datatype { + type_name: TypeName::parse("0xa::m::UsesDep").unwrap(), + module: "m".into(), + name: "UsesDep".into(), + abilities: vec![Ability::Store], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "obj".into(), + position: 0, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0xb::dep::Obj").unwrap(), + type_arguments: vec![], + }, + }], + }, + }], + functions: vec![Function { + name: "take".into(), + visibility: Visibility::Public, + is_entry: true, + type_parameters: vec![], + parameters: vec![FunctionParam { + name: "arg0".into(), + ty: TypeRef::Ref { + mutable: true, + inner: Box::new(TypeRef::Datatype { + type_name: TypeName::parse("0xb::dep::Obj").unwrap(), + type_arguments: vec![], + }), + }, + }], + return_types: vec![], + }], + }, + )]), + }; + + let mut resolver = ExternalResolver::new(); + resolver.add_package(&dep, "dep-crate"); + + let code = render_package_with_resolver(&root, &RenderOptions::default(), &resolver); + + assert!(code.contains("dep_crate::dep::Obj")); + assert!(!code.contains("compile_error!")); + assert!(code.contains("arg0: &mut impl sm_call::ObjectArg")); + } + + #[test] + fn external_key_ability_resolution_is_robust_to_upgrades() { + let dep = NormalizedPackage { + storage_id: "0xb01".into(), + original_id: Some("0xb00".into()), + version: 0, + modules: BTreeMap::from([( + "dep".into(), + NormalizedModule { + name: "dep".into(), + datatypes: vec![Datatype { + // Simulate metadata that encodes the storage id. + type_name: TypeName::parse("0xb01::dep::Obj").unwrap(), + module: "dep".into(), + name: "Obj".into(), + abilities: vec![Ability::Key, Ability::Store], + type_parameters: vec![], + kind: DatatypeKind::Struct { fields: vec![] }, + }], + functions: vec![], + }, + )]), + }; + + let root = NormalizedPackage { + storage_id: "0xa".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([( + "m".into(), + NormalizedModule { + name: "m".into(), + datatypes: vec![], + functions: vec![Function { + name: "take".into(), + visibility: Visibility::Public, + is_entry: true, + type_parameters: vec![], + // Simulate a type reference that uses the original id. + parameters: vec![FunctionParam { + name: "arg0".into(), + ty: TypeRef::Ref { + mutable: true, + inner: Box::new(TypeRef::Datatype { + type_name: TypeName::parse("0xb00::dep::Obj").unwrap(), + type_arguments: vec![], + }), + }, + }], + return_types: vec![], + }], + }, + )]), + }; + + let mut resolver = ExternalResolver::new(); + resolver.add_package(&dep, "dep-crate"); + + let code = render_package_with_resolver(&root, &RenderOptions::default(), &resolver); + + // Key ability should still be detected, so the param becomes an object arg. + assert!(code.contains("arg0: &mut impl sm_call::ObjectArg")); + } } diff --git a/sui-move-codegen/src/render/tx_ext.rs b/sui-move-codegen/src/render/tx_ext.rs index 6e8ea9c..b55541c 100644 --- a/sui-move-codegen/src/render/tx_ext.rs +++ b/sui-move-codegen/src/render/tx_ext.rs @@ -8,9 +8,13 @@ use quote::{format_ident, quote}; use crate::ir::{Ability, Function, NormalizedModule, NormalizedPackage, TypeRef, Visibility}; -use super::{builtins, idents, types, RenderOptions}; +use super::{builtins, idents, types, ExternalResolver, RenderOptions}; -pub(crate) fn render_tx_ext(pkg: &NormalizedPackage, opts: &RenderOptions) -> TokenStream { +pub(crate) fn render_tx_ext( + pkg: &NormalizedPackage, + opts: &RenderOptions, + resolver: Option<&ExternalResolver>, +) -> TokenStream { if !opts.emit_calls { // Without call builders, the extension methods would need to inline call construction, // which defeats the layering goal. @@ -26,7 +30,7 @@ pub(crate) fn render_tx_ext(pkg: &NormalizedPackage, opts: &RenderOptions) -> To .iter() .filter(|f| matches!(f.visibility, Visibility::Public)) { - let (trait_method, impl_method) = render_method(module, f, pkg, opts); + let (trait_method, impl_method) = render_method(module, f, pkg, opts, resolver); trait_methods.push(trait_method); impl_methods.push(impl_method); } @@ -63,6 +67,7 @@ fn render_method( f: &Function, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> (TokenStream, TokenStream) { let tx_method_ident = idents::ident(&format!("{}__{}", module.name, f.name)); let call_fn_ident = idents::ident(&f.name); @@ -80,7 +85,13 @@ fn render_method( let bounds = type_param_bounds(f, opts.use_aliases); let where_clause = where_clause(&bounds); - let (params, args, skipped_tx_context) = render_params_and_args(f, pkg, opts); + let sdk = if opts.use_aliases { + quote! { sm::__private::sui_sdk_types } + } else { + quote! { sui_move::__private::sui_sdk_types } + }; + + let (params, args, skipped_tx_context) = render_params_and_args(f, pkg, opts, resolver); let signature = move_signature_string(module, f); let doc = doc_lines(&[ @@ -101,14 +112,14 @@ fn render_method( let signature = quote! { #doc fn #tx_method_ident #fn_generics (&mut self, #(#params),*) - -> Result + -> Result<#sdk::Argument, sui_move_runtime::Error> #where_clause ; }; let implementation = quote! { fn #tx_method_ident #fn_generics (&mut self, #(#params),*) - -> Result + -> Result<#sdk::Argument, sui_move_runtime::Error> #where_clause { self.call(#call_expr) @@ -122,6 +133,7 @@ fn render_params_and_args( f: &Function, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> (Vec, Vec, bool) { let sm_call = if opts.use_aliases { quote! { sm_call } @@ -144,9 +156,9 @@ fn render_params_and_args( arg_idx += 1; let (ref_mutable, inner) = split_ref(&p.ty); - let is_object = is_object_type(inner, f, pkg, opts); + let is_object = is_object_type(inner, f, pkg, opts, resolver); if is_object { - let obj_ty = types::render_type_ref_in_root(inner, pkg, opts); + let obj_ty = types::render_type_ref_in_root(inner, pkg, opts, resolver); let param_ty = if ref_mutable { quote! { &mut impl #sm_call::ObjectArg<#obj_ty> } } else { @@ -154,7 +166,7 @@ fn render_params_and_args( }; params.push(quote! { #arg_ident: #param_ty }); } else { - let value_ty = types::render_type_ref_in_root(inner, pkg, opts); + let value_ty = types::render_type_ref_in_root(inner, pkg, opts, resolver); params.push(quote! { #arg_ident: #value_ty }); } @@ -188,12 +200,18 @@ fn is_object_type( f: &Function, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> bool { match ty { TypeRef::Datatype { type_name, .. } => { if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) { return builtin.is_key; } + if let Some(resolver) = resolver { + if let Some(is_key) = resolver.type_has_key(type_name) { + return is_key; + } + } pkg.modules .get(&type_name.module) .and_then(|m| { diff --git a/sui-move-codegen/src/render/types.rs b/sui-move-codegen/src/render/types.rs index 7543da0..797c2bc 100644 --- a/sui-move-codegen/src/render/types.rs +++ b/sui-move-codegen/src/render/types.rs @@ -11,16 +11,17 @@ use quote::{format_ident, quote}; use crate::ir::{Ability, Datatype, DatatypeKind, Field, NormalizedPackage, TypeName, TypeRef}; -use super::{builtins, idents, RenderOptions}; +use super::{builtins, idents, ExternalResolver, RenderOptions}; pub(crate) fn render_datatype( dt: &Datatype, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { match &dt.kind { - DatatypeKind::Struct { fields } => render_struct(dt, fields, pkg, opts), - DatatypeKind::Enum { variants } => render_enum(dt, variants, pkg, opts), + DatatypeKind::Struct { fields } => render_struct(dt, fields, pkg, opts, resolver), + DatatypeKind::Enum { variants } => render_enum(dt, variants, pkg, opts, resolver), } } @@ -29,21 +30,23 @@ pub(crate) fn render_type_ref_in_module( current_module: &str, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { let current = TypeName { address: pkg.storage_id.clone(), module: current_module.to_string(), name: "".to_string(), }; - render_type_ref(ty, ¤t, pkg, opts) + render_type_ref(ty, ¤t, pkg, opts, resolver) } pub(crate) fn render_type_ref_in_root( ty: &TypeRef, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { - render_type_ref_root(ty, pkg, opts) + render_type_ref_root(ty, pkg, opts, resolver) } fn render_struct( @@ -51,6 +54,7 @@ fn render_struct( fields: &[Field], pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { let type_ident = idents::ident(&dt.name); let type_params = type_params_idents(dt.type_parameters.len()); @@ -85,7 +89,7 @@ fn render_struct( let fields_tokens = fields.iter().map(|f| { let ident = idents::ident(&f.name); - let ty = render_type_ref(&f.ty, &dt.type_name, pkg, opts); + let ty = render_type_ref(&f.ty, &dt.type_name, pkg, opts, resolver); quote! { pub #ident: #ty, } }); @@ -124,6 +128,7 @@ fn render_enum( variants: &[crate::ir::Variant], pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { let type_ident = idents::ident(&dt.name); let type_params = type_params_idents(dt.type_parameters.len()); @@ -154,7 +159,7 @@ fn render_enum( } let fields = v.fields.iter().map(|f| { let field_ident = idents::ident(&f.name); - let field_ty = render_type_ref(&f.ty, &dt.type_name, pkg, opts); + let field_ty = render_type_ref(&f.ty, &dt.type_name, pkg, opts, resolver); quote! { #field_ident: #field_ty, } }); quote! { #variant_ident { #(#fields)* }, } @@ -355,6 +360,7 @@ fn render_type_ref( current_type: &TypeName, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { match ty { TypeRef::Address => prelude_type(opts.use_aliases, quote! { Address }), @@ -369,11 +375,11 @@ fn render_type_ref( prelude_type(opts.use_aliases, quote! { U256 }) } TypeRef::Vector(inner) => { - let inner = render_type_ref(inner, current_type, pkg, opts); + let inner = render_type_ref(inner, current_type, pkg, opts, resolver); quote! { Vec<#inner> } } TypeRef::Ref { mutable, inner } => { - let inner = render_type_ref(inner, current_type, pkg, opts); + let inner = render_type_ref(inner, current_type, pkg, opts, resolver); if *mutable { quote! { &mut #inner } } else { @@ -386,7 +392,7 @@ fn render_type_ref( } => { let mut args = Vec::new(); for a in type_arguments { - args.push(render_type_ref(a, current_type, pkg, opts)); + args.push(render_type_ref(a, current_type, pkg, opts, resolver)); } if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) { @@ -399,6 +405,20 @@ fn render_type_ref( let is_local = is_local_type(type_name, pkg); if !is_local { + if let Some(resolver) = resolver { + if let Some(dep_crate) = resolver.crate_name_for_address(&type_name.address) { + let crate_ident = crate_ident(dep_crate); + let mod_ident = idents::ident(&type_name.module); + let ty_ident = idents::ident(&type_name.name); + let base = quote! { #crate_ident::#mod_ident::#ty_ident }; + return if args.is_empty() { + base + } else { + quote! { #base<#(#args),*> } + }; + } + } + // Keep generation deterministic: unknown external types must be supplied by the // consumer (e.g. another generated package crate). let msg = format!( @@ -434,6 +454,7 @@ fn render_type_ref_root( ty: &TypeRef, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { match ty { TypeRef::Address => prelude_type(opts.use_aliases, quote! { Address }), @@ -445,11 +466,11 @@ fn render_type_ref_root( TypeRef::U128 => quote! { u128 }, TypeRef::U256 => prelude_type(opts.use_aliases, quote! { U256 }), TypeRef::Vector(inner) => { - let inner = render_type_ref_root(inner, pkg, opts); + let inner = render_type_ref_root(inner, pkg, opts, resolver); quote! { Vec<#inner> } } TypeRef::Ref { mutable, inner } => { - let inner = render_type_ref_root(inner, pkg, opts); + let inner = render_type_ref_root(inner, pkg, opts, resolver); if *mutable { quote! { &mut #inner } } else { @@ -462,7 +483,7 @@ fn render_type_ref_root( } => { let mut args = Vec::new(); for a in type_arguments { - args.push(render_type_ref_root(a, pkg, opts)); + args.push(render_type_ref_root(a, pkg, opts, resolver)); } if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) { @@ -475,6 +496,20 @@ fn render_type_ref_root( let is_local = is_local_type(type_name, pkg); if !is_local { + if let Some(resolver) = resolver { + if let Some(dep_crate) = resolver.crate_name_for_address(&type_name.address) { + let crate_ident = crate_ident(dep_crate); + let mod_ident = idents::ident(&type_name.module); + let ty_ident = idents::ident(&type_name.name); + let base = quote! { #crate_ident::#mod_ident::#ty_ident }; + return if args.is_empty() { + base + } else { + quote! { #base<#(#args),*> } + }; + } + } + let msg = format!( "sui-move-codegen: unknown external type `{}`; generate bindings for that package too", display_type_name(type_name) @@ -504,6 +539,10 @@ fn render_type_ref_root( } } +fn crate_ident(name: &str) -> syn::Ident { + idents::ident(&name.replace('-', "_")) +} + fn prelude_type(use_aliases: bool, name: TokenStream) -> TokenStream { if use_aliases { quote! { sm::prelude::#name } diff --git a/sui-move-codegen/src/render/util.rs b/sui-move-codegen/src/render/util.rs index d7ece25..4232031 100644 --- a/sui-move-codegen/src/render/util.rs +++ b/sui-move-codegen/src/render/util.rs @@ -5,9 +5,13 @@ use quote::quote; use crate::ir::{NormalizedModule, NormalizedPackage}; -use super::{calls, idents, tx_ext, types, RenderOptions}; +use super::{calls, idents, tx_ext, types, ExternalResolver, RenderOptions}; -pub(crate) fn render_package_tokens(pkg: &NormalizedPackage, opts: &RenderOptions) -> TokenStream { +pub(crate) fn render_package_tokens( + pkg: &NormalizedPackage, + opts: &RenderOptions, + resolver: Option<&ExternalResolver>, +) -> TokenStream { let root_aliases = if opts.emit_tx_ext && !opts.flatten { aliases(opts) } else { @@ -18,11 +22,11 @@ pub(crate) fn render_package_tokens(pkg: &NormalizedPackage, opts: &RenderOption let mut modules = Vec::new(); for module in pkg.modules.values() { - modules.push(render_module(module, pkg, opts)); + modules.push(render_module(module, pkg, opts, resolver)); } let tx_ext = if opts.emit_tx_ext { - tx_ext::render_tx_ext(pkg, opts) + tx_ext::render_tx_ext(pkg, opts, resolver) } else { quote! {} }; @@ -53,6 +57,7 @@ pub(crate) fn render_package_tokens(pkg: &NormalizedPackage, opts: &RenderOption pub(crate) fn render_split_mod_rs_tokens( pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { let root_aliases = if opts.emit_tx_ext { aliases(opts) @@ -82,7 +87,7 @@ pub(crate) fn render_split_mod_rs_tokens( }; let tx_ext = if opts.emit_tx_ext { - tx_ext::render_tx_ext(pkg, opts) + tx_ext::render_tx_ext(pkg, opts, resolver) } else { quote! {} }; @@ -100,6 +105,7 @@ pub(crate) fn render_module_file( module: &NormalizedModule, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { let aliases = aliases(opts); @@ -109,11 +115,11 @@ pub(crate) fn render_module_file( module .datatypes .iter() - .map(|dt| types::render_datatype(dt, pkg, opts)), + .map(|dt| types::render_datatype(dt, pkg, opts, resolver)), ); } if opts.emit_calls { - items.extend(calls::render_functions(module, pkg, opts)); + items.extend(calls::render_functions(module, pkg, opts, resolver)); } quote! { @@ -127,6 +133,7 @@ pub(crate) fn render_module( module: &NormalizedModule, pkg: &NormalizedPackage, opts: &RenderOptions, + resolver: Option<&ExternalResolver>, ) -> TokenStream { let aliases = aliases(opts); @@ -136,11 +143,11 @@ pub(crate) fn render_module( module .datatypes .iter() - .map(|dt| types::render_datatype(dt, pkg, opts)), + .map(|dt| types::render_datatype(dt, pkg, opts, resolver)), ); } if opts.emit_calls { - items.extend(calls::render_functions(module, pkg, opts)); + items.extend(calls::render_functions(module, pkg, opts, resolver)); } if opts.flatten { diff --git a/sui-move-codegen/src/workspace.rs b/sui-move-codegen/src/workspace.rs new file mode 100644 index 0000000..ebae548 --- /dev/null +++ b/sui-move-codegen/src/workspace.rs @@ -0,0 +1,603 @@ +//! High-level helpers for generating bindings for a package and its dependencies. +//! +//! This module is intentionally small and opinionated: +//! - It discovers external package references by walking type signatures in the IR. +//! - It can fetch missing dependencies recursively over gRPC. +//! - It can optionally write a minimal Cargo workspace with one crate per package. + +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::fs; +use std::path::{Path, PathBuf}; + +use sui_sdk_types::Address; + +use crate::ir::{NormalizedPackage, TypeName, TypeRef}; +use crate::render::{render_package_with_resolver, ExternalResolver, RenderOptions}; +use crate::{fetch_package, Error}; + +/// How to treat an externally-provided bindings crate for a package. +#[derive(Clone, Debug)] +pub struct ExternalCrate { + /// Rust package name from `Cargo.toml` (e.g. `my-bindings`). + pub cargo_name: String, + /// Directory containing `Cargo.toml`. + pub crate_dir: PathBuf, +} + +/// Options for generating a bindings workspace. +#[derive(Clone, Debug)] +pub struct WorkspaceOptions { + /// Write `Cargo.toml` files for generated crates. + /// + /// If set, dependencies on the core `sui-move*` crates are expressed as path dependencies + /// rooted at this directory (typically the `move-binding` repo root). + pub move_binding_root: Option, + + /// Force `RenderOptions::flatten = false` for all generated crates. + /// + /// This keeps generated module paths stable so cross-crate references use + /// `dep_crate::module::Type`. + pub force_non_flattened: bool, +} + +impl Default for WorkspaceOptions { + fn default() -> Self { + Self { + move_binding_root: None, + force_non_flattened: true, + } + } +} + +/// Errors that can occur during dependency discovery/fetching or workspace generation. +#[derive(thiserror::Error, Debug)] +pub enum WorkspaceError { + /// A package id string was not a valid Sui address. + #[error("invalid package id `{0}`")] + InvalidPackageId(String), + + /// Failed to read or parse an external crate's `Cargo.toml`. + #[error("invalid external crate at `{path}`: {message}")] + InvalidExternalCrate { + /// Path to the `Cargo.toml` that was read. + path: String, + /// Human-readable parse or IO error message. + message: String, + }, + + /// Fetching or normalizing package metadata failed. + #[error(transparent)] + Source(#[from] Error), + + /// An IO operation failed while writing the workspace. + #[error(transparent)] + Io(#[from] std::io::Error), +} + +/// Generate bindings for `root_package`, plus any referenced dependency packages. +/// +/// - If a referenced package id exists in `externals`, codegen will reference it via that crate +/// name and *won't* generate a new crate for it. +/// - Otherwise, the dependency is fetched and bindings are generated for it as well. +/// +/// The output directory will contain one folder per generated package crate. If +/// `workspace_opts.move_binding_root` is set, it will also contain a root `Cargo.toml` workspace. +pub async fn generate_bindings_workspace( + client: &mut sui_rpc::Client, + root_package: Address, + out_dir: impl AsRef, + render_opts: &RenderOptions, + externals: BTreeMap, + workspace_opts: WorkspaceOptions, +) -> Result<(), WorkspaceError> { + let out_dir = out_dir.as_ref(); + fs::create_dir_all(out_dir)?; + + let externals = load_external_crates(externals)?; + + let graph = fetch_dependency_closure(client, root_package, &externals).await?; + + // Decide crate names for all fetched packages (generated + external). + let mut crate_by_storage: BTreeMap = BTreeMap::new(); + for (storage_id, pkg) in &graph.packages_by_storage { + if let Some(ext) = external_for_package(&externals, &graph.alias_to_storage, pkg) { + crate_by_storage.insert(storage_id.clone(), ext.cargo_name.clone()); + } else { + let base = pkg + .original_id + .as_deref() + .unwrap_or_else(|| pkg.storage_id.as_str()); + crate_by_storage.insert(storage_id.clone(), default_crate_name(base)); + } + } + + // Build a global resolver so each generated crate can reference every other package. + let mut resolver = ExternalResolver::new(); + for (storage_id, pkg) in &graph.packages_by_storage { + let crate_name = crate_by_storage + .get(storage_id) + .expect("crate name assigned"); + resolver.add_package(pkg, crate_name.clone()); + } + + // Generate crates for packages that are not provided externally. + let mut generated_members = Vec::new(); + for (storage_id, pkg) in &graph.packages_by_storage { + if external_for_package(&externals, &graph.alias_to_storage, pkg).is_some() { + continue; + } + + let crate_name = crate_by_storage + .get(storage_id) + .expect("crate name assigned"); + let crate_dir = out_dir.join(crate_name); + write_generated_crate( + &crate_dir, + pkg, + render_opts, + &resolver, + &crate_by_storage, + &graph.alias_to_storage, + &externals, + &workspace_opts, + )?; + generated_members.push(crate_name.clone()); + } + + if workspace_opts.move_binding_root.is_some() { + write_root_workspace_manifest(out_dir, &generated_members)?; + } + + Ok(()) +} + +struct DependencyGraph { + packages_by_storage: BTreeMap, + alias_to_storage: BTreeMap, +} + +async fn fetch_dependency_closure( + client: &mut sui_rpc::Client, + root: Address, + externals: &BTreeMap, +) -> Result { + let root_norm = normalize_address(&root.to_string()); + let mut queue: VecDeque = VecDeque::from([root_norm]); + + let mut packages_by_storage: BTreeMap = BTreeMap::new(); + let mut alias_to_storage: BTreeMap = BTreeMap::new(); + + while let Some(addr) = queue.pop_front() { + if alias_to_storage.contains_key(&addr) { + continue; + } + + // Fetch this package id (may be a storage id or original id). + let parsed: Address = addr + .parse() + .map_err(|_| WorkspaceError::InvalidPackageId(addr.clone()))?; + let pkg = fetch_package(client, parsed).await?; + + let storage_id = pkg.storage_id.clone(); + let storage_norm = normalize_address(&storage_id); + let original_norm = pkg.original_id.as_deref().map(normalize_address); + + packages_by_storage.insert(storage_norm.clone(), pkg.clone()); + alias_to_storage.insert(storage_norm.clone(), storage_norm.clone()); + if let Some(orig) = &original_norm { + alias_to_storage.insert(orig.clone(), storage_norm.clone()); + } + // Also mark the queried id as resolved (in case it was neither storage nor original + // due to future RPC behavior changes). + alias_to_storage.insert(addr.clone(), storage_norm.clone()); + + // Enqueue dependency package ids referenced by types. + let deps = referenced_external_packages(&pkg); + for dep in deps { + if is_framework_address(&dep) { + continue; + } + if alias_to_storage.contains_key(&dep) { + continue; + } + + // Ensure we have metadata even for externally-provided crates, so we can determine + // object abilities (`key`) for call generation. + let _ = externals; + queue.push_back(dep); + } + } + + Ok(DependencyGraph { + packages_by_storage, + alias_to_storage, + }) +} + +fn referenced_external_packages(pkg: &NormalizedPackage) -> BTreeSet { + let mut out = BTreeSet::new(); + + let mut visit = |type_name: &TypeName| { + if is_local_type(type_name, pkg) { + return; + } + let addr = normalize_address(&type_name.address); + if is_framework_address(&addr) { + return; + } + out.insert(addr); + }; + + for module in pkg.modules.values() { + for dt in &module.datatypes { + match &dt.kind { + crate::ir::DatatypeKind::Struct { fields } => { + for f in fields { + visit_type_ref(&f.ty, &mut visit); + } + } + crate::ir::DatatypeKind::Enum { variants } => { + for v in variants { + for f in &v.fields { + visit_type_ref(&f.ty, &mut visit); + } + } + } + } + } + + for f in &module.functions { + for p in &f.parameters { + visit_type_ref(&p.ty, &mut visit); + } + for r in &f.return_types { + visit_type_ref(r, &mut visit); + } + } + } + + out +} + +fn visit_type_ref(ty: &TypeRef, visit: &mut impl FnMut(&TypeName)) { + match ty { + TypeRef::Vector(inner) => visit_type_ref(inner, visit), + TypeRef::Ref { inner, .. } => visit_type_ref(inner, visit), + TypeRef::Datatype { + type_name, + type_arguments, + } => { + visit(type_name); + for a in type_arguments { + visit_type_ref(a, visit); + } + } + _ => {} + } +} + +fn is_framework_address(addr: &str) -> bool { + addr == "0x1" || addr == "0x2" +} + +fn is_local_type(type_name: &TypeName, pkg: &NormalizedPackage) -> bool { + if normalize_address(&type_name.address) == normalize_address(&pkg.storage_id) { + return true; + } + match &pkg.original_id { + Some(orig) => normalize_address(&type_name.address) == normalize_address(orig), + None => false, + } +} + +fn normalize_address(input: &str) -> String { + let trimmed = input.trim(); + let addr = trimmed + .trim_start_matches("0x") + .trim_start_matches("0X") + .trim_start_matches('0'); + let addr = if addr.is_empty() { "0" } else { addr }; + format!("0x{addr}") +} + +fn default_crate_name(address: &str) -> String { + // Use a short prefix of the address to keep names readable but deterministic. + let hex = address.trim().trim_start_matches("0x"); + let short = &hex[..hex.len().min(16)]; + format!("move_pkg_{short}") +} + +fn load_external_crates( + externals: BTreeMap, +) -> Result, WorkspaceError> { + let mut out = BTreeMap::new(); + for (pkg_id, crate_dir) in externals { + let pkg_id = normalize_address(&pkg_id); + let cargo_name = read_cargo_package_name(&crate_dir)?; + out.insert( + pkg_id, + ExternalCrate { + cargo_name, + crate_dir, + }, + ); + } + Ok(out) +} + +fn external_for_package<'a>( + externals: &'a BTreeMap, + alias_to_storage: &BTreeMap, + pkg: &NormalizedPackage, +) -> Option<&'a ExternalCrate> { + let storage = normalize_address(&pkg.storage_id); + let orig = pkg.original_id.as_deref().map(normalize_address); + + // Prefer a direct match first. + if let Some(ext) = externals.get(&storage) { + return Some(ext); + } + if let Some(orig) = &orig { + if let Some(ext) = externals.get(orig) { + return Some(ext); + } + } + + // Then look for alias matches (if the user specified an original id but the graph resolved + // a storage id, or vice versa). + for (ext_id, ext) in externals { + if let Some(storage_for_ext) = alias_to_storage.get(ext_id) { + if storage_for_ext == &storage { + return Some(ext); + } + } + } + None +} + +fn external_for_address<'a>( + externals: &'a BTreeMap, + alias_to_storage: &BTreeMap, + address: &str, +) -> Option<&'a ExternalCrate> { + if let Some(ext) = externals.get(address) { + return Some(ext); + } + + let storage = alias_to_storage + .get(address) + .map(|s| s.as_str()) + .unwrap_or(address); + if let Some(ext) = externals.get(storage) { + return Some(ext); + } + + externals.iter().find_map(|(ext_id, ext)| { + alias_to_storage + .get(ext_id) + .is_some_and(|resolved| resolved == storage) + .then_some(ext) + }) +} + +fn read_cargo_package_name(crate_dir: &Path) -> Result { + let cargo_toml = crate_dir.join("Cargo.toml"); + let contents = fs::read_to_string(&cargo_toml).map_err(|e| WorkspaceError::InvalidExternalCrate { + path: cargo_toml.display().to_string(), + message: e.to_string(), + })?; + + let mut in_package = false; + for line in contents.lines() { + let trimmed = line.trim(); + if trimmed.starts_with('[') && trimmed.ends_with(']') { + in_package = trimmed == "[package]"; + continue; + } + if !in_package { + continue; + } + if let Some(rest) = trimmed.strip_prefix("name") { + let rest = rest.trim_start(); + if !rest.starts_with('=') { + continue; + } + let value = rest.trim_start_matches('=').trim(); + if let Some(stripped) = value.strip_prefix('"').and_then(|v| v.strip_suffix('"')) { + if !stripped.is_empty() { + return Ok(stripped.to_string()); + } + } + } + } + + Err(WorkspaceError::InvalidExternalCrate { + path: cargo_toml.display().to_string(), + message: "missing [package] name".to_string(), + }) +} + +fn write_generated_crate( + crate_dir: &Path, + pkg: &NormalizedPackage, + render_opts: &RenderOptions, + resolver: &ExternalResolver, + crate_by_storage: &BTreeMap, + alias_to_storage: &BTreeMap, + externals: &BTreeMap, + workspace_opts: &WorkspaceOptions, +) -> Result<(), WorkspaceError> { + fs::create_dir_all(crate_dir.join("src"))?; + + let mut opts = render_opts.clone(); + if workspace_opts.force_non_flattened { + opts.flatten = false; + } + + let code = render_package_with_resolver(pkg, &opts, resolver); + fs::write(crate_dir.join("src").join("lib.rs"), code)?; + + if let Some(move_binding_root) = &workspace_opts.move_binding_root { + let deps = referenced_external_packages(pkg); + let mut dep_specs: BTreeMap = BTreeMap::new(); + + for dep in deps { + // Skip deps that are actually local aliases. + if dep == normalize_address(&pkg.storage_id) + || pkg.original_id.as_deref().is_some_and(|o| dep == normalize_address(o)) + { + continue; + } + + let storage = alias_to_storage + .get(&dep) + .cloned() + .unwrap_or_else(|| dep.clone()); + let dep_crate = crate_by_storage + .get(&storage) + .cloned() + .unwrap_or_else(|| default_crate_name(&dep)); + + let dep_path = if let Some(ext) = external_for_address(externals, alias_to_storage, &dep) + { + ext.crate_dir.display().to_string() + } else { + format!("../{dep_crate}") + }; + dep_specs.insert(dep_crate, dep_path); + } + + let cargo_toml = render_cargo_toml( + crate_dir + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("move_pkg"), + move_binding_root, + &opts, + &dep_specs, + ); + fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?; + } + + Ok(()) +} + +fn render_cargo_toml( + package_name: &str, + move_binding_root: &Path, + opts: &RenderOptions, + deps: &BTreeMap, +) -> String { + let mut out = String::new(); + out.push_str("[package]\n"); + out.push_str(&format!("name = \"{package_name}\"\n")); + out.push_str("version = \"0.1.0\"\n"); + out.push_str("edition = \"2021\"\n\n"); + + out.push_str("[dependencies]\n"); + out.push_str(&format!( + "sui-move = {{ path = \"{}\", features = [\"derive\"] }}\n", + move_binding_root.join("sui-move").display() + )); + out.push_str(&format!( + "sui-move-call = {{ path = \"{}\" }}\n", + move_binding_root.join("sui-move-call").display() + )); + if opts.emit_tx_ext { + out.push_str(&format!( + "sui-move-runtime = {{ path = \"{}\" }}\n", + move_binding_root.join("sui-move-runtime").display() + )); + } + + for (name, path) in deps { + out.push_str(&format!("{name} = {{ path = \"{path}\" }}\n")); + } + + out +} + +fn write_root_workspace_manifest(out_dir: &Path, members: &[String]) -> Result<(), WorkspaceError> { + let mut out = String::new(); + out.push_str("[workspace]\n"); + out.push_str("resolver = \"3\"\n"); + out.push_str("members = [\n"); + for m in members { + out.push_str(&format!(" \"{m}\",\n")); + } + out.push_str("]\n"); + fs::write(out_dir.join("Cargo.toml"), out)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ir::*; + + #[test] + fn referenced_external_packages_skips_framework_and_local() { + let pkg = NormalizedPackage { + storage_id: "0xa".into(), + original_id: Some("0xa0".into()), + version: 0, + modules: BTreeMap::from([( + "m".into(), + NormalizedModule { + name: "m".into(), + datatypes: vec![Datatype { + type_name: TypeName::parse("0xa::m::S").unwrap(), + module: "m".into(), + name: "S".into(), + abilities: vec![Ability::Store], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![ + // framework builtin (skipped) + Field { + name: "uid".into(), + position: 0, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0x2::object::UID").unwrap(), + type_arguments: vec![], + }, + }, + // local type via storage id (skipped) + Field { + name: "local".into(), + position: 1, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0xa::m::S").unwrap(), + type_arguments: vec![], + }, + }, + // local type via original id (skipped) + Field { + name: "local_orig".into(), + position: 2, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0xa0::m::S").unwrap(), + type_arguments: vec![], + }, + }, + // external package (included) + Field { + name: "ext".into(), + position: 3, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0xb::dep::Obj").unwrap(), + type_arguments: vec![], + }, + }, + ], + }, + }], + functions: vec![], + }, + )]), + }; + + let deps = referenced_external_packages(&pkg); + assert_eq!(deps, BTreeSet::from(["0xb".to_string()])); + } +} diff --git a/sui-move-derive/src/expand.rs b/sui-move-derive/src/expand.rs index c2ff5d0..4775993 100644 --- a/sui-move-derive/src/expand.rs +++ b/sui-move-derive/src/expand.rs @@ -213,9 +213,6 @@ pub(crate) fn expand_move_struct( }; let mut derives: Vec = Vec::new(); - if has_copy { - derives.push(parse_quote!(::core::clone::Clone)); - } derives.extend([ parse_quote!(::core::fmt::Debug), parse_quote!(::core::cmp::PartialEq), @@ -325,6 +322,24 @@ pub(crate) fn expand_move_struct( let (impl_generics, ty_generics, where_clause) = expanded_generics.split_for_impl(); + let clone_impl = if has_copy { + let inits = fields.iter().filter_map(|f| { + let ident = f.ident.as_ref()?; + Some(quote! { #ident: ::core::clone::Clone::clone(&self.#ident), }) + }); + quote! { + impl #impl_generics ::core::clone::Clone for #struct_ident #ty_generics #where_clause { + fn clone(&self) -> Self { + Self { + #(#inits)* + } + } + } + } + } else { + quote! {} + }; + let ability_impls = { let mut impls = Vec::new(); if has_key { @@ -353,6 +368,8 @@ pub(crate) fn expand_move_struct( Ok(quote! { #output_struct + #clone_impl + impl #impl_generics ::sui_move::MoveType for #struct_ident #ty_generics #where_clause { fn type_tag_static() -> ::sui_move::__private::sui_sdk_types::TypeTag { ::sui_move::__private::sui_sdk_types::TypeTag::Struct(Box::new( diff --git a/sui-move-derive/src/lib.rs b/sui-move-derive/src/lib.rs index ea086ff..afb9f53 100644 --- a/sui-move-derive/src/lib.rs +++ b/sui-move-derive/src/lib.rs @@ -191,4 +191,54 @@ mod tests { assert!(has_bound(t0_bounds, "MoveType")); assert!(has_bound(t0_bounds, "HasStore")); } + + #[test] + fn expands_copy_phantom_struct_without_clone_bound_on_phantom_params() { + let args: MoveStructArgs = syn::parse_quote!( + address = "0x1", + module = "m", + abilities = "copy", + phantoms = "T0" + ); + + let input: syn::DeriveInput = syn::parse_quote!( + pub struct S { + pub v: Vec, + } + ); + + let out = crate::expand::expand_move_struct(args, input).expect("expand"); + let file: syn::File = syn::parse2(out).expect("parse expanded tokens as a file"); + + let clone_impl = file + .items + .iter() + .find_map(|item| match item { + syn::Item::Impl(imp) => match &imp.trait_ { + Some((_, path, _)) + if path.segments.last().is_some_and(|seg| seg.ident == "Clone") => + { + Some(imp) + } + _ => None, + }, + _ => None, + }) + .expect("Clone impl in output"); + + let t0 = clone_impl + .generics + .params + .iter() + .find_map(|p| match p { + syn::GenericParam::Type(t) if t.ident == "T0" => Some(t), + _ => None, + }) + .expect("T0 generic param in Clone impl"); + + assert!( + t0.bounds.is_empty(), + "Clone impl should not add `T0: Clone` for phantom params" + ); + } } diff --git a/sui-move/src/primitives/mod.rs b/sui-move/src/primitives/mod.rs index 386f19b..4d4ab55 100644 --- a/sui-move/src/primitives/mod.rs +++ b/sui-move/src/primitives/mod.rs @@ -14,6 +14,8 @@ pub mod coin; pub mod linked_table; pub mod object_bag; pub mod object_table; +pub mod priority_queue; +pub mod string; pub mod sui; pub mod tx_context; pub mod type_name; diff --git a/sui-move/src/primitives/priority_queue.rs b/sui-move/src/primitives/priority_queue.rs new file mode 100644 index 0000000..dccbab4 --- /dev/null +++ b/sui-move/src/primitives/priority_queue.rs @@ -0,0 +1,59 @@ +use serde::{Deserialize, Serialize}; + +use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; + +/// Move `0x2::priority_queue::Entry`. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct Entry { + pub priority: u64, + pub value: T, +} + +impl MoveType for Entry { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) + } +} + +impl MoveStruct for Entry { + fn struct_tag_static() -> sui_sdk_types::StructTag { + sui_sdk_types::StructTag::new( + parse_address("0x2").expect("address literal"), + parse_identifier("priority_queue").expect("module"), + parse_identifier("Entry").expect("name"), + vec![T::type_tag_static()], + ) + } +} + +impl crate::HasDrop for Entry {} +impl crate::HasStore for Entry {} + +/// Move `0x2::priority_queue::PriorityQueue`. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct PriorityQueue { + pub entries: Vec>, +} + +impl MoveType for PriorityQueue { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) + } +} + +impl MoveStruct for PriorityQueue { + fn struct_tag_static() -> sui_sdk_types::StructTag { + sui_sdk_types::StructTag::new( + parse_address("0x2").expect("address literal"), + parse_identifier("priority_queue").expect("module"), + parse_identifier("PriorityQueue").expect("name"), + vec![T::type_tag_static()], + ) + } +} + +impl crate::HasDrop for PriorityQueue {} +impl crate::HasStore for PriorityQueue {} + diff --git a/sui-move/src/primitives/string.rs b/sui-move/src/primitives/string.rs new file mode 100644 index 0000000..afaf704 --- /dev/null +++ b/sui-move/src/primitives/string.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; + +/// Move `0x1::string::String`. +/// +/// This is **not** Rust's `String`; it is the Sui Move `string::String` wrapper around UTF-8 +/// bytes. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct String { + /// UTF-8 bytes. + pub bytes: Vec, +} + +impl MoveType for String { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) + } +} + +impl MoveStruct for String { + fn struct_tag_static() -> sui_sdk_types::StructTag { + sui_sdk_types::StructTag::new( + parse_address("0x1").expect("address literal"), + parse_identifier("string").expect("module"), + parse_identifier("String").expect("name"), + vec![], + ) + } +} + +impl crate::HasCopy for String {} +impl crate::HasDrop for String {} +impl crate::HasStore for String {} + From 15b129b265eec1875fe1813efa28d8985ef1827c Mon Sep 17 00:00:00 2001 From: David Date: Wed, 4 Feb 2026 20:12:04 +0100 Subject: [PATCH 4/6] fix: make fmt happy --- .../examples/localnet_workspace.rs | 4 +-- sui-move-codegen/src/render/calls.rs | 3 +- sui-move-codegen/src/render/externals.rs | 3 +- sui-move-codegen/src/workspace.rs | 29 ++++++++------- sui-move-derive/README.md | 34 ++++++++---------- sui-move-derive/src/lib.rs | 35 +++++++------------ sui-move/src/primitives/priority_queue.rs | 1 - sui-move/src/primitives/string.rs | 1 - 8 files changed, 49 insertions(+), 61 deletions(-) diff --git a/sui-move-codegen/examples/localnet_workspace.rs b/sui-move-codegen/examples/localnet_workspace.rs index 9f462fa..32f85d5 100644 --- a/sui-move-codegen/examples/localnet_workspace.rs +++ b/sui-move-codegen/examples/localnet_workspace.rs @@ -16,8 +16,7 @@ use sui_move_codegen::render::RenderOptions; use sui_move_codegen::workspace::{generate_bindings_workspace, WorkspaceOptions}; use sui_move_codegen::{Address, Client}; -const DEFAULT_PACKAGE: &str = - "0x4cc38b7c23bf14d7555503ab38a9748f9544c2c29c6519df412b4f6fb6971640"; +const DEFAULT_PACKAGE: &str = "0x4cc38b7c23bf14d7555503ab38a9748f9544c2c29c6519df412b4f6fb6971640"; const DEFAULT_GRPC: &str = "http://127.0.0.1:9000"; fn usage() -> ! { @@ -111,4 +110,3 @@ async fn main() -> Result<(), Box> { Ok(()) } - diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index cf4e419..98b1320 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -119,8 +119,7 @@ fn render_params_and_pushes( let is_object = is_object_type(inner, f, pkg, opts, resolver); if is_object { - let obj_ty = - types::render_type_ref_in_module(inner, &module.name, pkg, opts, resolver); + let obj_ty = types::render_type_ref_in_module(inner, &module.name, pkg, opts, resolver); let param_ty = if ref_mutable { quote! { &mut impl #sm_call::ObjectArg<#obj_ty> } } else { diff --git a/sui-move-codegen/src/render/externals.rs b/sui-move-codegen/src/render/externals.rs index bbbb4c4..f083d0b 100644 --- a/sui-move-codegen/src/render/externals.rs +++ b/sui-move-codegen/src/render/externals.rs @@ -35,7 +35,8 @@ impl ExternalResolver { self.crate_by_address .insert(pkg.storage_id.clone(), crate_name.clone()); if let Some(orig) = &pkg.original_id { - self.crate_by_address.insert(orig.clone(), crate_name.clone()); + self.crate_by_address + .insert(orig.clone(), crate_name.clone()); } for module in pkg.modules.values() { diff --git a/sui-move-codegen/src/workspace.rs b/sui-move-codegen/src/workspace.rs index ebae548..7d86123 100644 --- a/sui-move-codegen/src/workspace.rs +++ b/sui-move-codegen/src/workspace.rs @@ -106,7 +106,7 @@ pub async fn generate_bindings_workspace( let base = pkg .original_id .as_deref() - .unwrap_or_else(|| pkg.storage_id.as_str()); + .unwrap_or(pkg.storage_id.as_str()); crate_by_storage.insert(storage_id.clone(), default_crate_name(base)); } } @@ -382,10 +382,11 @@ fn external_for_address<'a>( fn read_cargo_package_name(crate_dir: &Path) -> Result { let cargo_toml = crate_dir.join("Cargo.toml"); - let contents = fs::read_to_string(&cargo_toml).map_err(|e| WorkspaceError::InvalidExternalCrate { - path: cargo_toml.display().to_string(), - message: e.to_string(), - })?; + let contents = + fs::read_to_string(&cargo_toml).map_err(|e| WorkspaceError::InvalidExternalCrate { + path: cargo_toml.display().to_string(), + message: e.to_string(), + })?; let mut in_package = false; for line in contents.lines() { @@ -417,6 +418,7 @@ fn read_cargo_package_name(crate_dir: &Path) -> Result { }) } +#[allow(clippy::too_many_arguments)] fn write_generated_crate( crate_dir: &Path, pkg: &NormalizedPackage, @@ -444,7 +446,10 @@ fn write_generated_crate( for dep in deps { // Skip deps that are actually local aliases. if dep == normalize_address(&pkg.storage_id) - || pkg.original_id.as_deref().is_some_and(|o| dep == normalize_address(o)) + || pkg + .original_id + .as_deref() + .is_some_and(|o| dep == normalize_address(o)) { continue; } @@ -458,12 +463,12 @@ fn write_generated_crate( .cloned() .unwrap_or_else(|| default_crate_name(&dep)); - let dep_path = if let Some(ext) = external_for_address(externals, alias_to_storage, &dep) - { - ext.crate_dir.display().to_string() - } else { - format!("../{dep_crate}") - }; + let dep_path = + if let Some(ext) = external_for_address(externals, alias_to_storage, &dep) { + ext.crate_dir.display().to_string() + } else { + format!("../{dep_crate}") + }; dep_specs.insert(dep_crate, dep_path); } diff --git a/sui-move-derive/README.md b/sui-move-derive/README.md index 46bbc33..c93c33f 100644 --- a/sui-move-derive/README.md +++ b/sui-move-derive/README.md @@ -30,15 +30,13 @@ pub struct Point { pub y: u64, } -fn main() { - let tag = ::type_tag_static(); - match tag { - TypeTag::Struct(struct_tag) => { - assert_eq!(struct_tag.module().to_string(), "demo"); - assert_eq!(struct_tag.name().to_string(), "Point"); - } - other => panic!("expected struct type tag, got {other:?}"), +let tag = ::type_tag_static(); +match tag { + TypeTag::Struct(struct_tag) => { + assert_eq!(struct_tag.module().to_string(), "demo"); + assert_eq!(struct_tag.name().to_string(), "Point"); } + other => panic!("expected struct type tag, got {other:?}"), } ``` @@ -127,18 +125,16 @@ pub struct Vault { pub balance: Vec, } -fn main() { - let _tag = as sui_move::MoveType>::type_tag_static(); - let _value = Vault:: { - id: UID { - id: ID { - bytes: Address::new([0u8; 32]), - }, +let _tag = as sui_move::MoveType>::type_tag_static(); +let _value = Vault:: { + id: UID { + id: ID { + bytes: Address::new([0u8; 32]), }, - balance: vec![1, 2, 3], - phantom_t: PhantomData, - }; -} + }, + balance: vec![1, 2, 3], + phantom_t: PhantomData, +}; ``` If you try to declare a `key` struct without an `id` field, it fails at compile time: diff --git a/sui-move-derive/src/lib.rs b/sui-move-derive/src/lib.rs index afb9f53..b9c8983 100644 --- a/sui-move-derive/src/lib.rs +++ b/sui-move-derive/src/lib.rs @@ -23,8 +23,6 @@ mod util; /// pub value: u64, /// } /// } -/// -/// fn main() {} /// ``` #[proc_macro_attribute] pub fn move_module(_args: TokenStream, input: TokenStream) -> TokenStream { @@ -73,17 +71,15 @@ pub fn move_module(_args: TokenStream, input: TokenStream) -> TokenStream { /// pub balance: Vec, /// } /// -/// fn main() { -/// let _value = Vault:: { -/// id: UID { -/// id: ID { -/// bytes: Address::new([0u8; 32]), -/// }, +/// let _value = Vault:: { +/// id: UID { +/// id: ID { +/// bytes: Address::new([0u8; 32]), /// }, -/// balance: vec![1, 2, 3], -/// phantom_t: PhantomData, -/// }; -/// } +/// }, +/// balance: vec![1, 2, 3], +/// phantom_t: PhantomData, +/// }; /// ``` #[proc_macro_attribute] pub fn move_struct(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -127,11 +123,8 @@ mod tests { #[test] fn expands_struct_with_where_bounds_on_definition() { - let args: MoveStructArgs = syn::parse_quote!( - address = "0x1", - module = "m", - type_abilities = "T0: store" - ); + let args: MoveStructArgs = + syn::parse_quote!(address = "0x1", module = "m", type_abilities = "T0: store"); let input: syn::DeriveInput = syn::parse_quote!( pub struct S { @@ -179,11 +172,9 @@ mod tests { name: &str, ) -> bool { bounds.iter().any(|b| match b { - syn::TypeParamBound::Trait(tb) => tb - .path - .segments - .last() - .is_some_and(|seg| seg.ident == name), + syn::TypeParamBound::Trait(tb) => { + tb.path.segments.last().is_some_and(|seg| seg.ident == name) + } _ => false, }) } diff --git a/sui-move/src/primitives/priority_queue.rs b/sui-move/src/primitives/priority_queue.rs index dccbab4..2c4b1a4 100644 --- a/sui-move/src/primitives/priority_queue.rs +++ b/sui-move/src/primitives/priority_queue.rs @@ -56,4 +56,3 @@ impl MoveStruct for PriorityQueue { impl crate::HasDrop for PriorityQueue {} impl crate::HasStore for PriorityQueue {} - diff --git a/sui-move/src/primitives/string.rs b/sui-move/src/primitives/string.rs index afaf704..2890fdb 100644 --- a/sui-move/src/primitives/string.rs +++ b/sui-move/src/primitives/string.rs @@ -32,4 +32,3 @@ impl MoveStruct for String { impl crate::HasCopy for String {} impl crate::HasDrop for String {} impl crate::HasStore for String {} - From 8e0bd80ee0f2c068d0e8452b4a7d6d922e8f4b9c Mon Sep 17 00:00:00 2001 From: Pavel Koch Date: Fri, 19 Jun 2026 14:38:19 +0200 Subject: [PATCH 5/6] draft: feat: move binding idents --- Cargo.toml | 6 +++--- sui-move-codegen/examples/localnet_workspace.rs | 5 +++-- sui-move-codegen/src/lib.rs | 2 -- sui-move-codegen/src/source.rs | 1 - 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d40798b..ecea5af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" bcs = "0.1.6" -sui-sdk-types = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["rand", "serde"] } -sui-rpc = { git = "https://github.com/mystenlabs/sui-rust-sdk" } -sui-crypto = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["ed25519", "pem"] } +sui-sdk-types = { version = "0.3.1", features = ["serde"] } +sui-rpc = { version = "0.3.1" } +sui-crypto = { version = "0.3.0" } proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full"] } diff --git a/sui-move-codegen/examples/localnet_workspace.rs b/sui-move-codegen/examples/localnet_workspace.rs index 32f85d5..77dc99d 100644 --- a/sui-move-codegen/examples/localnet_workspace.rs +++ b/sui-move-codegen/examples/localnet_workspace.rs @@ -14,7 +14,8 @@ use std::process::Command; use sui_move_codegen::render::RenderOptions; use sui_move_codegen::workspace::{generate_bindings_workspace, WorkspaceOptions}; -use sui_move_codegen::{Address, Client}; +use sui_rpc::Client; +use sui_sdk_types::Address; const DEFAULT_PACKAGE: &str = "0x4cc38b7c23bf14d7555503ab38a9748f9544c2c29c6519df412b4f6fb6971640"; const DEFAULT_GRPC: &str = "http://127.0.0.1:9000"; @@ -38,7 +39,7 @@ async fn main() -> Result<(), Box> { let mut grpc = std::env::var("SUI_GRPC").unwrap_or_else(|_| DEFAULT_GRPC.to_string()); let mut out_dir: Option = None; let mut externals: BTreeMap = BTreeMap::new(); - let mut check = false; + let mut check: bool = false; // Optional first positional arg: package id. if let Some(first) = args.peek() { diff --git a/sui-move-codegen/src/lib.rs b/sui-move-codegen/src/lib.rs index 4e8d89d..f210258 100644 --- a/sui-move-codegen/src/lib.rs +++ b/sui-move-codegen/src/lib.rs @@ -15,8 +15,6 @@ pub mod render; pub mod workspace; pub use crate::source::fetch_package; -pub use crate::source::Address; -pub use crate::source::Client; /// Errors from sourcing or normalizing package metadata. #[derive(thiserror::Error, Debug)] diff --git a/sui-move-codegen/src/source.rs b/sui-move-codegen/src/source.rs index bdca648..eb78f6b 100644 --- a/sui-move-codegen/src/source.rs +++ b/sui-move-codegen/src/source.rs @@ -4,7 +4,6 @@ use std::collections::BTreeMap; use sui_rpc::proto::sui::rpc::v2 as proto; -pub use sui_rpc::Client; pub use sui_sdk_types::Address; use crate::ir::{ From ac7219597272547ce5c25868c75d01e18e8918a5 Mon Sep 17 00:00:00 2001 From: Pavel Koch Date: Mon, 22 Jun 2026 14:51:21 +0200 Subject: [PATCH 6/6] add back workspace version and edition --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 167edb2..9d61ce6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ members = [ [workspace.package] license = "Apache-2.0" +edition = "2021" +version = "0.1.0" repository = "https://github.com/Talus-Network/move-binding" homepage = "https://github.com/Talus-Network/move-binding"