From 1b822ecd72318ad6a8ce7f30362967b74277c362 Mon Sep 17 00:00:00 2001 From: Pavel Koch Date: Wed, 4 Feb 2026 16:19:31 +0100 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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 a7413c1569caa675a654ad385ba60112a913354a Mon Sep 17 00:00:00 2001 From: David Date: Thu, 5 Feb 2026 13:33:47 +0100 Subject: [PATCH 5/9] feat: recursive codegen --- CHANGELOG.md | 2 +- sui-move-call/README.md | 24 ++- sui-move-call/src/lib.rs | 48 ++++- sui-move-call/tests/basic.rs | 12 +- sui-move-codegen/src/render/builtins.rs | 80 ------- sui-move-codegen/src/render/callable.rs | 32 +++ sui-move-codegen/src/render/calls.rs | 13 +- sui-move-codegen/src/render/mod.rs | 2 +- sui-move-codegen/src/render/tx_ext.rs | 11 +- sui-move-codegen/src/render/types.rs | 24 +-- sui-move-codegen/src/workspace.rs | 14 +- sui-move-derive/README.md | 19 +- sui-move-derive/src/args.rs | 127 ++++++----- sui-move-derive/src/expand.rs | 11 +- sui-move-derive/src/lib.rs | 21 +- sui-move-ptb/README.md | 48 ++++- sui-move-ptb/tests/basic.rs | 12 +- sui-move-runtime/README.md | 20 +- sui-move-runtime/src/handles.rs | 12 +- sui-move/Cargo.toml | 1 - sui-move/README.md | 35 ++- sui-move/examples/derive_struct.rs | 13 +- sui-move/examples/tag_checked_decode.rs | 71 +++++-- sui-move/examples/type_tags.rs | 30 ++- sui-move/src/builtins.rs | 10 +- sui-move/src/containers.rs | 187 ---------------- sui-move/src/decode.rs | 88 +++++++- sui-move/src/lib.rs | 52 +++-- sui-move/src/primitives/ascii.rs | 39 ---- sui-move/src/primitives/bag.rs | 39 ---- sui-move/src/primitives/balance.rs | 39 ---- sui-move/src/primitives/clock.rs | 39 ---- sui-move/src/primitives/coin.rs | 42 ---- sui-move/src/primitives/linked_table.rs | 106 ---------- sui-move/src/primitives/mod.rs | 23 -- sui-move/src/primitives/object_bag.rs | 39 ---- sui-move/src/primitives/object_table.rs | 68 ------ sui-move/src/primitives/priority_queue.rs | 58 ----- sui-move/src/primitives/string.rs | 34 --- sui-move/src/primitives/sui.rs | 37 ---- sui-move/src/primitives/tx_context.rs | 34 --- sui-move/src/primitives/type_name.rs | 41 ---- sui-move/src/primitives/vec_map.rs | 72 ------- sui-move/src/primitives/vec_set.rs | 39 ---- sui-move/src/types.rs | 81 ------- sui-move/tests/basic.rs | 247 +++------------------- sui-move/tests/derive.rs | 50 ++--- 47 files changed, 589 insertions(+), 1557 deletions(-) delete mode 100644 sui-move-codegen/src/render/builtins.rs create mode 100644 sui-move-codegen/src/render/callable.rs delete mode 100644 sui-move/src/containers.rs delete mode 100644 sui-move/src/primitives/ascii.rs delete mode 100644 sui-move/src/primitives/bag.rs delete mode 100644 sui-move/src/primitives/balance.rs delete mode 100644 sui-move/src/primitives/clock.rs delete mode 100644 sui-move/src/primitives/coin.rs delete mode 100644 sui-move/src/primitives/linked_table.rs delete mode 100644 sui-move/src/primitives/mod.rs delete mode 100644 sui-move/src/primitives/object_bag.rs delete mode 100644 sui-move/src/primitives/object_table.rs delete mode 100644 sui-move/src/primitives/priority_queue.rs delete mode 100644 sui-move/src/primitives/string.rs delete mode 100644 sui-move/src/primitives/sui.rs delete mode 100644 sui-move/src/primitives/tx_context.rs delete mode 100644 sui-move/src/primitives/type_name.rs delete mode 100644 sui-move/src/primitives/vec_map.rs delete mode 100644 sui-move/src/primitives/vec_set.rs delete mode 100644 sui-move/src/types.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6f29d..6d6f3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog], and this project adheres to ### Added - Add `sui-move`: core Move-shaped type layer (traits, abilities, decoding). -- Add Move framework primitives under `sui_move::primitives` (e.g. `coin`, `balance`, `vec_map`) with BCS-accurate tags/layouts. +- Add `MoveType` + ability-marker impls for Move builtins (`u8/u16/u32/u64/u128`, `bool`, `Address`, `U256`, `Vec`). - Add `sui-move-derive` and the `sui-move` `derive` feature for defining Move-shaped structs via macros. - Add `sui-move-call`: typed Move call descriptions (`CallSpec`) plus typed wrappers for Sui `Input` kinds (pure, immutable/owned, shared, receiving). - Add `sui-move-ptb`: minimal PTB builder that consumes `CallSpec` and produces `ProgrammableTransaction`. diff --git a/sui-move-call/README.md b/sui-move-call/README.md index 59b20c9..ddb6ab3 100644 --- a/sui-move-call/README.md +++ b/sui-move-call/README.md @@ -73,9 +73,19 @@ use sui_move::prelude::*; use sui_move_call::{CallSpec, MoveObject}; use sui_sdk_types::{Address, Digest, ObjectReference, TypeTag}; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +pub struct ID { + pub bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +pub struct UID { + pub id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "vault", abilities = "key")] pub struct Vault { - pub id: sui_move::types::UID, + pub id: UID, } pub fn withdraw(vault: &MoveObject, amount: u64) -> CallSpec { @@ -112,9 +122,19 @@ use std::str::FromStr; use sui_move_call::{CallArg, CallSpec, ReceivingMoveObject, SharedMoveObject}; use sui_sdk_types::{Address, Digest, ObjectReference}; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +struct ID { + bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] struct Thing { - id: sui_move::types::UID, + id: UID, } let package = Address::from_str("0x1").unwrap(); diff --git a/sui-move-call/src/lib.rs b/sui-move-call/src/lib.rs index 80b1622..157a0b5 100644 --- a/sui-move-call/src/lib.rs +++ b/sui-move-call/src/lib.rs @@ -31,9 +31,19 @@ pub use sui_sdk_types::Input as CallArg; /// use sui_move_call::MoveObject; /// use sui_sdk_types::{Address, Digest, ObjectReference}; /// +/// #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +/// struct ID { +/// bytes: Address, +/// } +/// +/// #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// struct UID { +/// id: ID, +/// } +/// /// #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] /// struct Demo { -/// id: sui_move::types::UID, +/// id: UID, /// } /// /// let id = Address::from_str("0x1").unwrap(); @@ -82,9 +92,19 @@ impl MoveObject { /// use sui_move_call::SharedMoveObject; /// use sui_sdk_types::Address; /// +/// #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +/// struct ID { +/// bytes: Address, +/// } +/// +/// #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// struct UID { +/// id: ID, +/// } +/// /// #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] /// struct SharedThing { -/// id: sui_move::types::UID, +/// id: UID, /// } /// /// let object_id = Address::from_str("0x1").unwrap(); @@ -168,9 +188,19 @@ impl SharedMoveObject { /// use sui_move_call::ReceivingMoveObject; /// use sui_sdk_types::{Address, Digest, ObjectReference}; /// +/// #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +/// struct ID { +/// bytes: Address, +/// } +/// +/// #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// struct UID { +/// id: ID, +/// } +/// /// #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] /// struct ReceivingThing { -/// id: sui_move::types::UID, +/// id: UID, /// } /// /// let id = Address::from_str("0x1").unwrap(); @@ -369,9 +399,19 @@ pub enum CallSpecError { /// use sui_move_call::{CallSpec, MoveObject}; /// use sui_sdk_types::{Address, Digest, ObjectReference, TypeTag}; /// +/// #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +/// struct ID { +/// bytes: Address, +/// } +/// +/// #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// struct UID { +/// id: ID, +/// } +/// /// #[sui_move::move_struct(address = "0x1", module = "vault", abilities = "key")] /// struct Vault { -/// id: sui_move::types::UID, +/// id: UID, /// } /// /// let package = Address::from_str("0x1").unwrap(); diff --git a/sui-move-call/tests/basic.rs b/sui-move-call/tests/basic.rs index 8251769..1b35812 100644 --- a/sui-move-call/tests/basic.rs +++ b/sui-move-call/tests/basic.rs @@ -6,9 +6,19 @@ use sui_move_call::{ }; use sui_sdk_types::{Address, Digest, FundsWithdrawal, ObjectReference, TypeTag, WithdrawFrom}; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +struct ID { + bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] struct Demo { - id: sui_move::types::UID, + id: UID, } #[test] diff --git a/sui-move-codegen/src/render/builtins.rs b/sui-move-codegen/src/render/builtins.rs deleted file mode 100644 index 3a69fd9..0000000 --- a/sui-move-codegen/src/render/builtins.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Mapping for common Sui framework types. - -use proc_macro2::TokenStream; -use quote::quote; - -use crate::ir::TypeName; - -pub(crate) struct BuiltinDatatype { - pub(crate) path: TokenStream, - /// Whether this builtin has the Move `key` ability (i.e. is an object type). - /// - /// This flag is used by call generation to decide whether a parameter should be typed as an - /// object argument (`&impl ObjectArg`) rather than a pure value. - pub(crate) is_key: bool, -} - -pub(crate) fn map_builtin(type_name: &TypeName, use_aliases: bool) -> Option { - let address = type_name.address.as_str(); - let module = type_name.module.as_str(); - let name = type_name.name.as_str(); - - let (path, is_key) = match (address, module, name) { - ("0x2", "object", "UID") => (sm(use_aliases, quote! { types::UID }), false), - ("0x2", "object", "ID") => (sm(use_aliases, quote! { types::ID }), false), - ("0x2", "sui", "SUI") => (sm(use_aliases, quote! { sui::SUI }), false), - ("0x2", "bag", "Bag") => (sm(use_aliases, quote! { bag::Bag }), true), - ("0x2", "balance", "Balance") => (sm(use_aliases, quote! { balance::Balance }), false), - ("0x2", "coin", "Coin") => (sm(use_aliases, quote! { coin::Coin }), true), - ("0x2", "clock", "Clock") => (sm(use_aliases, quote! { clock::Clock }), true), - ("0x2", "tx_context", "TxContext") => { - (sm(use_aliases, quote! { tx_context::TxContext }), false) - } - ("0x1", "type_name", "TypeName") => { - (sm(use_aliases, quote! { type_name::TypeName }), false) - } - ("0x1", "ascii", "String") => (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) - } - ("0x2", "linked_table", "LinkedTable") => { - (sm(use_aliases, quote! { linked_table::LinkedTable }), false) - } - ("0x2", "object_table", "ObjectTable") => { - (sm(use_aliases, quote! { object_table::ObjectTable }), false) - } - ("0x1", "option", "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) - } - ("0x2", "dynamic_object_field", "DynamicField") => ( - sm(use_aliases, quote! { containers::DynamicObjectField }), - false, - ), - _ => return None, - }; - - Some(BuiltinDatatype { path, is_key }) -} - -fn sm(use_aliases: bool, path: TokenStream) -> TokenStream { - if use_aliases { - quote! { sm::#path } - } else { - quote! { sui_move::#path } - } -} diff --git a/sui-move-codegen/src/render/callable.rs b/sui-move-codegen/src/render/callable.rs new file mode 100644 index 0000000..dfa7139 --- /dev/null +++ b/sui-move-codegen/src/render/callable.rs @@ -0,0 +1,32 @@ +use crate::ir::{Function, TypeRef, Visibility}; + +/// Whether a Move function can be called via a Sui `MoveCall` command. +/// +/// Based on `sui-sdk-types` docs: callable functions are `entry` or `public` and must not have +/// reference return types. +pub(crate) fn is_callable(f: &Function) -> bool { + if !(f.is_entry || matches!(f.visibility, Visibility::Public)) { + return false; + } + !f.return_types.iter().any(type_contains_ref) +} + +fn type_contains_ref(ty: &TypeRef) -> bool { + match ty { + TypeRef::Ref { .. } => true, + TypeRef::Vector(inner) => type_contains_ref(inner), + TypeRef::Datatype { + type_arguments, .. + } => type_arguments.iter().any(type_contains_ref), + TypeRef::Address + | TypeRef::Bool + | TypeRef::U8 + | TypeRef::U16 + | TypeRef::U32 + | TypeRef::U64 + | TypeRef::U128 + | TypeRef::U256 + | TypeRef::TypeParameter(_) => false, + } +} + diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index 98b1320..0bd35dd 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -8,9 +8,9 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use crate::ir::{Ability, Function, NormalizedModule, NormalizedPackage, TypeRef, Visibility}; +use crate::ir::{Ability, Function, NormalizedModule, NormalizedPackage, TypeRef}; -use super::{builtins, idents, types, ExternalResolver, RenderOptions}; +use super::{callable, idents, types, ExternalResolver, RenderOptions}; pub(crate) fn render_functions( module: &NormalizedModule, @@ -21,7 +21,7 @@ pub(crate) fn render_functions( module .functions .iter() - .filter(|f| matches!(f.visibility, Visibility::Public)) + .filter(|f| callable::is_callable(f)) .map(|f| render_function(module, f, pkg, opts, resolver)) .collect() } @@ -69,7 +69,7 @@ fn render_function( if f.is_entry { String::new() } else { - "Note: this function is not marked `entry`.".to_string() + "Note: callable as `public` (not `entry`).".to_string() }, ]); @@ -165,14 +165,11 @@ fn is_object_type( ty: &TypeRef, f: &Function, pkg: &NormalizedPackage, - opts: &RenderOptions, + _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; diff --git a/sui-move-codegen/src/render/mod.rs b/sui-move-codegen/src/render/mod.rs index c1dedc8..33c1742 100644 --- a/sui-move-codegen/src/render/mod.rs +++ b/sui-move-codegen/src/render/mod.rs @@ -15,8 +15,8 @@ use std::path::Path; use crate::ir::NormalizedPackage; -mod builtins; mod calls; +mod callable; mod externals; mod idents; mod tx_ext; diff --git a/sui-move-codegen/src/render/tx_ext.rs b/sui-move-codegen/src/render/tx_ext.rs index b55541c..da0b8c1 100644 --- a/sui-move-codegen/src/render/tx_ext.rs +++ b/sui-move-codegen/src/render/tx_ext.rs @@ -6,9 +6,9 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use crate::ir::{Ability, Function, NormalizedModule, NormalizedPackage, TypeRef, Visibility}; +use crate::ir::{Ability, Function, NormalizedModule, NormalizedPackage, TypeRef}; -use super::{builtins, idents, types, ExternalResolver, RenderOptions}; +use super::{callable, idents, types, ExternalResolver, RenderOptions}; pub(crate) fn render_tx_ext( pkg: &NormalizedPackage, @@ -28,7 +28,7 @@ pub(crate) fn render_tx_ext( for f in module .functions .iter() - .filter(|f| matches!(f.visibility, Visibility::Public)) + .filter(|f| callable::is_callable(f)) { let (trait_method, impl_method) = render_method(module, f, pkg, opts, resolver); trait_methods.push(trait_method); @@ -199,14 +199,11 @@ fn is_object_type( ty: &TypeRef, f: &Function, pkg: &NormalizedPackage, - opts: &RenderOptions, + _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; diff --git a/sui-move-codegen/src/render/types.rs b/sui-move-codegen/src/render/types.rs index 797c2bc..d00958a 100644 --- a/sui-move-codegen/src/render/types.rs +++ b/sui-move-codegen/src/render/types.rs @@ -11,7 +11,7 @@ use quote::{format_ident, quote}; use crate::ir::{Ability, Datatype, DatatypeKind, Field, NormalizedPackage, TypeName, TypeRef}; -use super::{builtins, idents, ExternalResolver, RenderOptions}; +use super::{idents, ExternalResolver, RenderOptions}; pub(crate) fn render_datatype( dt: &Datatype, @@ -68,7 +68,6 @@ fn render_struct( let phantoms = phantom_params_string(dt); let type_abilities = type_abilities_string(dt); - let address_lit = syn::LitStr::new(address, proc_macro2::Span::call_site()); let module_lit = syn::LitStr::new(module, proc_macro2::Span::call_site()); let name_lit = syn::LitStr::new(move_name, proc_macro2::Span::call_site()); let abilities_lit = abilities @@ -110,7 +109,7 @@ fn render_struct( quote! { #doc #[#macro_path( - address = #address_lit, + address = PACKAGE, module = #module_lit, #name_arg #abilities_arg @@ -269,7 +268,6 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { quote! { sui_move } }; - let address = syn::LitStr::new(&dt.type_name.address, proc_macro2::Span::call_site()); let module = syn::LitStr::new(&dt.type_name.module, proc_macro2::Span::call_site()); let name = syn::LitStr::new(&dt.type_name.name, proc_macro2::Span::call_site()); @@ -280,7 +278,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { quote! { #sm::__private::sui_sdk_types::StructTag::new( - #sm::parse_address(#address).expect("invalid address literal"), + PACKAGE, #sm::parse_identifier(#module).expect("invalid module"), #sm::parse_identifier(#name).expect("invalid struct name"), vec![#(#ty_params_for_tag),*], @@ -395,14 +393,6 @@ fn render_type_ref( args.push(render_type_ref(a, current_type, pkg, opts, resolver)); } - if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) { - if args.is_empty() { - return builtin.path; - } - let path = builtin.path; - return quote! { #path<#(#args),*> }; - } - let is_local = is_local_type(type_name, pkg); if !is_local { if let Some(resolver) = resolver { @@ -486,14 +476,6 @@ fn render_type_ref_root( args.push(render_type_ref_root(a, pkg, opts, resolver)); } - if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) { - if args.is_empty() { - return builtin.path; - } - let path = builtin.path; - return quote! { #path<#(#args),*> }; - } - let is_local = is_local_type(type_name, pkg); if !is_local { if let Some(resolver) = resolver { diff --git a/sui-move-codegen/src/workspace.rs b/sui-move-codegen/src/workspace.rs index 7d86123..c6cc158 100644 --- a/sui-move-codegen/src/workspace.rs +++ b/sui-move-codegen/src/workspace.rs @@ -194,9 +194,6 @@ async fn fetch_dependency_closure( // 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; } @@ -222,9 +219,6 @@ fn referenced_external_packages(pkg: &NormalizedPackage) -> BTreeSet { return; } let addr = normalize_address(&type_name.address); - if is_framework_address(&addr) { - return; - } out.insert(addr); }; @@ -276,10 +270,6 @@ fn visit_type_ref(ty: &TypeRef, visit: &mut impl FnMut(&TypeName)) { } } -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; @@ -541,7 +531,7 @@ mod tests { use crate::ir::*; #[test] - fn referenced_external_packages_skips_framework_and_local() { + fn referenced_external_packages_includes_framework_and_skips_local() { let pkg = NormalizedPackage { storage_id: "0xa".into(), original_id: Some("0xa0".into()), @@ -603,6 +593,6 @@ mod tests { }; let deps = referenced_external_packages(&pkg); - assert_eq!(deps, BTreeSet::from(["0xb".to_string()])); + assert_eq!(deps, BTreeSet::from(["0x2".to_string(), "0xb".to_string()])); } } diff --git a/sui-move-derive/README.md b/sui-move-derive/README.md index c93c33f..2885d3e 100644 --- a/sui-move-derive/README.md +++ b/sui-move-derive/README.md @@ -110,9 +110,18 @@ You can satisfy those requirements either by: ```rust,no_run use std::marker::PhantomData; use sui_move::prelude::Address; -use sui_move::types::{ID, UID}; use sui_move_derive::move_struct; +#[move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +pub struct ID { + pub bytes: Address, +} + +#[move_struct(address = "0x2", module = "object", abilities = "store")] +pub struct UID { + pub id: ID, +} + #[move_struct( address = "0x1", module = "vault", @@ -152,10 +161,16 @@ Similarly, invalid ability combinations are rejected: ```rust,compile_fail use sui_move_derive::move_struct; +use sui_move::prelude::Address; // A struct cannot be both `key` and `copy`. +#[move_struct(address = "0x2", module = "object", abilities = "store")] +pub struct UID { + pub id: Address, +} + #[move_struct(address = "0x1", module = "broken", abilities = "key, store, copy")] pub struct KeyAndCopy { - pub id: sui_move::types::UID, + pub id: UID, } ``` diff --git a/sui-move-derive/src/args.rs b/sui-move-derive/src/args.rs index c22d264..7b5456b 100644 --- a/sui-move-derive/src/args.rs +++ b/sui-move-derive/src/args.rs @@ -9,12 +9,17 @@ use std::collections::BTreeMap; use syn::parse::{Parse, ParseStream, Parser}; use syn::spanned::Spanned; -use syn::Lit; + +#[derive(Clone)] +pub(crate) enum AddressArg { + Literal(String), + Expr(syn::Expr), +} /// Parsed arguments for `#[move_struct(...)]`. #[derive(Default)] pub(crate) struct MoveStructArgs { - pub(crate) address: Option, + pub(crate) address: Option, pub(crate) module: Option, pub(crate) name: Option, pub(crate) abilities: Vec, @@ -33,85 +38,61 @@ impl Parse for MoveStructArgs { .get_ident() .ok_or_else(|| syn::Error::new(meta.path.span(), "expected identifier key"))? .to_string(); - let lit: Lit = meta.value()?.parse()?; + let expr: syn::Expr = meta.value()?.parse()?; match ident.as_str() { "address" => { - if let Lit::Str(s) = lit { - args.address = Some(s.value()); - } else { - return Err(syn::Error::new(lit.span(), "address must be a string")); + match expr { + syn::Expr::Lit(expr_lit) => match expr_lit.lit { + syn::Lit::Str(s) => args.address = Some(AddressArg::Literal(s.value())), + other => { + return Err(syn::Error::new(other.span(), "address must be a string literal or an Address expression")); + } + }, + other => { + args.address = Some(AddressArg::Expr(other)); + } } } "module" => { - if let Lit::Str(s) = lit { - args.module = Some(s.value()); - } else { - return Err(syn::Error::new(lit.span(), "module must be a string")); - } + let s = expect_lit_str(expr, "module")?; + args.module = Some(s.value()); } "name" => { - if let Lit::Str(s) = lit { - args.name = Some(s.value()); - } else { - return Err(syn::Error::new(lit.span(), "name must be a string")); - } + let s = expect_lit_str(expr, "name")?; + args.name = Some(s.value()); } "abilities" => { - if let Lit::Str(s) = lit { - args.abilities = s - .value() - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } else { - return Err(syn::Error::new( - lit.span(), - "abilities must be a string literal, e.g., \"key, store\"", - )); - } + let s = expect_lit_str(expr, "abilities")?; + args.abilities = s + .value() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); } "phantoms" => { - if let Lit::Str(s) = lit { - args.phantoms = s - .value() - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } else { - return Err(syn::Error::new( - lit.span(), - "phantoms must be a string literal, e.g., \"T, U\"", - )); - } + let s = expect_lit_str(expr, "phantoms")?; + args.phantoms = s + .value() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); } "type_abilities" => { - if let Lit::Str(ref s) = lit { - args.type_abilities = parse_type_abilities(&s.value(), lit.span())?; - } else { - return Err(syn::Error::new( - lit.span(), - "type_abilities must be a string literal, e.g., \"T: store, copy; U: drop\"", - )); - } + let s = expect_lit_str(expr, "type_abilities")?; + args.type_abilities = parse_type_abilities(&s.value(), s.span())?; } "uid_type" => { - if let Lit::Str(ref s) = lit { - let ty: syn::Type = syn::parse_str(&s.value()).map_err(|_| { - syn::Error::new( - s.span(), - "uid_type must be a valid Rust type path, e.g., \"sui_move::types::UID\"", - ) - })?; - args.uid_type = Some(ty); - } else { - return Err(syn::Error::new( - lit.span(), - "uid_type must be a string literal path", - )); - } + let s = expect_lit_str(expr, "uid_type")?; + let ty: syn::Type = syn::parse_str(&s.value()).map_err(|_| { + syn::Error::new( + s.span(), + "uid_type must be a valid Rust type path, e.g., \"crate::UID\"", + ) + })?; + args.uid_type = Some(ty); } other => { return Err(syn::Error::new( @@ -131,6 +112,22 @@ impl Parse for MoveStructArgs { } } +fn expect_lit_str(expr: syn::Expr, key: &'static str) -> syn::Result { + match expr { + syn::Expr::Lit(expr_lit) => match expr_lit.lit { + syn::Lit::Str(s) => Ok(s), + other => Err(syn::Error::new( + other.span(), + format!("{key} must be a string literal"), + )), + }, + other => Err(syn::Error::new( + other.span(), + format!("{key} must be a string literal"), + )), + } +} + fn parse_type_abilities( raw: &str, span: proc_macro2::Span, diff --git a/sui-move-derive/src/expand.rs b/sui-move-derive/src/expand.rs index 4775993..e2d21ab 100644 --- a/sui-move-derive/src/expand.rs +++ b/sui-move-derive/src/expand.rs @@ -19,7 +19,7 @@ use syn::spanned::Spanned; use syn::{Data, DeriveInput, Field, FieldMutability, Fields, GenericParam, TypeParam}; use crate::abilities::{parse_inline_abilities, AbilityFlags}; -use crate::args::MoveStructArgs; +use crate::args::{AddressArg, MoveStructArgs}; use crate::util::{has_phantom_attr, is_phantom_field_type, is_uid_field, parse_serde_attr}; pub(crate) fn expand_move_struct( @@ -195,6 +195,13 @@ pub(crate) fn expand_move_struct( .ok_or_else(|| syn::Error::new(span, "module is required in #[move_struct]"))?; let struct_name = args.name.unwrap_or_else(|| struct_ident.to_string()); + let address_expr = match address { + AddressArg::Literal(addr) => quote! { + ::sui_move::parse_address(#addr).expect("invalid address literal") + }, + AddressArg::Expr(expr) => quote! { #expr }, + }; + let ty_params_for_tag = type_param_idents .iter() .map(|p| { @@ -205,7 +212,7 @@ pub(crate) fn expand_move_struct( let struct_tag_builder = quote! { ::sui_move::__private::sui_sdk_types::StructTag::new( - ::sui_move::parse_address(#address).expect("invalid address literal"), + #address_expr, ::sui_move::parse_identifier(#module_name).expect("invalid module"), ::sui_move::parse_identifier(#struct_name).expect("invalid struct name"), vec![#(#ty_params_for_tag),*], diff --git a/sui-move-derive/src/lib.rs b/sui-move-derive/src/lib.rs index b9c8983..142697d 100644 --- a/sui-move-derive/src/lib.rs +++ b/sui-move-derive/src/lib.rs @@ -44,7 +44,8 @@ pub fn move_module(_args: TokenStream, input: TokenStream) -> TokenStream { /// - Named-field structs only (`struct X { ... }`) /// /// # Arguments -/// - `address = "0x..."` (required): Move address +/// - `address = "0x..."` (required): Move address (string literal) or a Rust expression that +/// evaluates to `sui_sdk_types::Address` (e.g. `PACKAGE`) /// - `module = "..."` (required): Move module name /// - `name = "..."` (optional): override Move struct name (defaults to Rust name) /// - `abilities = "key, store, copy, drop"` (optional): comma-separated Move abilities @@ -56,9 +57,18 @@ pub fn move_module(_args: TokenStream, input: TokenStream) -> TokenStream { /// ```rust,no_run /// use std::marker::PhantomData; /// use sui_move::prelude::Address; -/// use sui_move::types::{ID, UID}; /// use sui_move_derive::move_struct; /// +/// #[move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +/// pub struct ID { +/// pub bytes: Address, +/// } +/// +/// #[move_struct(address = "0x2", module = "object", abilities = "store")] +/// pub struct UID { +/// pub id: ID, +/// } +/// /// #[move_struct( /// address = "0x1", /// module = "vault", @@ -94,7 +104,7 @@ pub fn move_struct(attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(test)] mod tests { - use super::args::MoveStructArgs; + use super::args::{AddressArg, MoveStructArgs}; use proc_macro2::Span; use std::collections::BTreeMap; @@ -107,7 +117,10 @@ mod tests { phantoms = "T", type_abilities = "T: store, copy" ); - assert_eq!(args.address.as_deref(), Some("0x1")); + assert!(matches!( + args.address.as_ref(), + Some(AddressArg::Literal(s)) if s == "0x1" + )); assert_eq!(args.module.as_deref(), Some("vault")); assert_eq!(args.name, None); assert_eq!(args.abilities, vec!["key".to_string(), "store".to_string()]); diff --git a/sui-move-ptb/README.md b/sui-move-ptb/README.md index 777b072..2b6baf1 100644 --- a/sui-move-ptb/README.md +++ b/sui-move-ptb/README.md @@ -71,9 +71,19 @@ use sui_move_call::{CallSpec, MoveObject}; use sui_move_ptb::ptb; use sui_sdk_types::{Address, Digest, ObjectReference}; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +struct ID { + bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "vault", abilities = "key")] struct Vault { - id: sui_move::types::UID, + id: UID, } fn withdraw(package: Address, vault: &MoveObject, amount: u64) -> CallSpec { @@ -108,9 +118,19 @@ use sui_move_call::{CallSpec, MoveObject}; use sui_move_ptb::ptb; use sui_sdk_types::{Address, Digest, ObjectReference}; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +struct ID { + bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "vault", abilities = "key")] struct Vault { - id: sui_move::types::UID, + id: UID, } fn touch(package: Address, vault: &MoveObject, amount: u64) -> CallSpec { @@ -148,9 +168,19 @@ use sui_move_call::{CallArg, SharedMoveObject}; use sui_move_ptb::PtbBuilder; use sui_sdk_types::{Address, Argument, Mutability}; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +struct ID { + bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] struct Thing { - id: sui_move::types::UID, + id: UID, } let object_id = Address::from_str("0x2").unwrap(); @@ -187,9 +217,19 @@ use sui_move_call::{CallArg, MoveObject, ReceivingMoveObject}; use sui_move_ptb::{BuildError, PtbBuilder}; use sui_sdk_types::{Address, Digest, ObjectReference}; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +struct ID { + bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] struct Thing { - id: sui_move::types::UID, + id: UID, } let object_id = Address::from_str("0x2").unwrap(); diff --git a/sui-move-ptb/tests/basic.rs b/sui-move-ptb/tests/basic.rs index dec3054..e8ad078 100644 --- a/sui-move-ptb/tests/basic.rs +++ b/sui-move-ptb/tests/basic.rs @@ -7,9 +7,19 @@ use sui_sdk_types::{ WithdrawFrom, }; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +struct ID { + bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] struct Thing { - id: sui_move::types::UID, + id: UID, } fn mk_obj(id: &str, version: u64) -> ObjectReference { diff --git a/sui-move-runtime/README.md b/sui-move-runtime/README.md index 9c4beab..9121001 100644 --- a/sui-move-runtime/README.md +++ b/sui-move-runtime/README.md @@ -97,7 +97,13 @@ If you want a one-shot action, use the `tx!` macro variants: use sui_move_runtime::prelude::*; use sui_sdk_types::Address; -# async fn demo(mut rt: Runtime, sender: Address, coin: Object>) -> Result<(), Error> { +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +# struct ID { bytes: Address } +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +# struct UID { id: ID } +# #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +# struct DemoCoin { id: UID } +# async fn demo(mut rt: Runtime, sender: Address, coin: Object) -> Result<(), Error> { let _sim = sui_move_runtime::tx!(simulate, &mut rt, sender => { CallSpec::new("0x1".parse().unwrap(), "m", "f").unwrap(); }) @@ -171,9 +177,19 @@ If you need a specific input mode, derive an explicit view at the moment it matt ```rust,no_run use sui_move_runtime::prelude::*; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +struct ID { + bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: ID, +} + #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] struct Demo { - id: sui_move::types::UID, + id: UID, } fn views(obj: Object) -> Result<(), sui_move_call::CallArgError> { diff --git a/sui-move-runtime/src/handles.rs b/sui-move-runtime/src/handles.rs index 3fe52e3..3cdda4c 100644 --- a/sui-move-runtime/src/handles.rs +++ b/sui-move-runtime/src/handles.rs @@ -629,9 +629,19 @@ mod tests { ObjectOut, Owner, TransactionEffects, TransactionEffectsV2, }; + #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] + struct ID { + bytes: Address, + } + + #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] + struct UID { + id: ID, + } + #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] struct Demo { - id: sui_move::types::UID, + id: UID, } #[test] diff --git a/sui-move/Cargo.toml b/sui-move/Cargo.toml index e80f382..aa177b4 100644 --- a/sui-move/Cargo.toml +++ b/sui-move/Cargo.toml @@ -10,7 +10,6 @@ serde = { workspace = true, features = ["derive"] } bcs = { workspace = true } sui-sdk-types = { workspace = true } thiserror = { workspace = true } -serde_json = { workspace = true } sui-move-derive = { path = "../sui-move-derive", optional = true } [features] diff --git a/sui-move/README.md b/sui-move/README.md index 6403660..2188b85 100644 --- a/sui-move/README.md +++ b/sui-move/README.md @@ -75,10 +75,21 @@ structs. #[cfg(feature = "derive")] mod example { use sui_move::move_struct; + use sui_move::prelude::Address; + + #[move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] + pub struct ID { + pub bytes: Address, + } + + #[move_struct(address = "0x2", module = "object", abilities = "store")] + pub struct UID { + pub id: ID, + } #[move_struct(address = "0x1", module = "vault", abilities = "key, store")] pub struct Vault { - pub id: sui_move::types::UID, + pub id: UID, pub value: u64, } } @@ -130,31 +141,11 @@ assert_eq!(inst.value, vec![1u8, 2, 3]); The crate implements `MoveType` (and ability markers) for: -- `u8`, `u16`, `u32`, `u64`, `u128`, `bool` +- `u8`, `u16`, `u32`, `u64`, `u128`, `bool`, `sui_move::U256` - `sui_sdk_types::Address` - `Vec` where `T: MoveType` -### Sui framework types (`sui_move::primitives`) - -`sui_move::primitives` contains minimal Rust mirrors of common Sui Move framework structs -so they can be referenced in `TypeTag`s and decoded in a typed way (e.g. `coin::Coin`, -`balance::Balance`, `vec_map::VecMap`, `vec_set::VecSet`, etc). - -```rust -use sui_move::{coin::Coin, sui::SUI, MoveType}; - -let _tag = as MoveType>::type_tag_static(); -``` - -### Framework containers (`sui_move::containers`) - -`sui_move::containers` includes widely-used container structs such as `MoveOption`, -`Table`, and dynamic field shapes, represented in a way that preserves their tags. - ## Module guide - `sui_move::prelude`: convenient imports for common traits/types -- `sui_move::types`: core Sui object types (`ID`, `UID`) -- `sui_move::containers`: Move framework container shapes -- `sui_move::primitives`: Sui framework “primitive” structs - `sui_move::decode`: ability-aware decode helpers diff --git a/sui-move/examples/derive_struct.rs b/sui-move/examples/derive_struct.rs index 6830e35..0f5c6fe 100644 --- a/sui-move/examples/derive_struct.rs +++ b/sui-move/examples/derive_struct.rs @@ -1,8 +1,19 @@ use sui_move::move_struct; +use sui_move::prelude::Address; + +#[move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +pub struct ID { + pub bytes: Address, +} + +#[move_struct(address = "0x2", module = "object", abilities = "store")] +pub struct UID { + pub id: ID, +} #[move_struct(address = "0x1", module = "vault", abilities = "key, store")] pub struct Vault { - pub id: sui_move::types::UID, + pub id: UID, pub value: u64, } diff --git a/sui-move/examples/tag_checked_decode.rs b/sui-move/examples/tag_checked_decode.rs index 61583f7..c0b0f2e 100644 --- a/sui-move/examples/tag_checked_decode.rs +++ b/sui-move/examples/tag_checked_decode.rs @@ -1,29 +1,66 @@ -use std::marker::PhantomData; +use sui_move::{decode_keyed, parse_address, parse_identifier, HasKey, MoveStruct, MoveType}; +use sui_sdk_types::{Address, StructTag, TypeTag}; -use sui_move::MoveType; -use sui_move::{balance::Balance, coin::Coin, decode_keyed, sui::SUI, types::ID, types::UID}; -use sui_sdk_types::Address; +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +struct UID { + bytes: Address, +} + +impl MoveType for UID { + fn type_tag_static() -> TypeTag { + TypeTag::Struct(Box::new(::struct_tag_static())) + } +} + +impl MoveStruct for UID { + fn struct_tag_static() -> StructTag { + StructTag::new( + parse_address("0x2").expect("address"), + parse_identifier("object").expect("module"), + parse_identifier("UID").expect("name"), + vec![], + ) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +struct DemoCoin { + id: UID, + balance: u64, +} + +impl MoveType for DemoCoin { + fn type_tag_static() -> TypeTag { + TypeTag::Struct(Box::new(::struct_tag_static())) + } +} + +impl MoveStruct for DemoCoin { + fn struct_tag_static() -> StructTag { + StructTag::new( + parse_address("0x1").expect("address"), + parse_identifier("demo").expect("module"), + parse_identifier("Coin").expect("name"), + vec![], + ) + } +} + +impl HasKey for DemoCoin {} fn main() { - let coin = Coin:: { + let coin = DemoCoin { id: UID { - id: ID { - bytes: Address::new([7u8; 32]), - }, - }, - balance: Balance:: { - value: 10, - phantom: PhantomData, + bytes: Address::new([7u8; 32]), }, + balance: 10, }; let bytes = coin.to_bcs().unwrap(); - let inst = - decode_keyed::>( as sui_move::MoveType>::type_tag_static(), &bytes) - .unwrap(); - assert_eq!(inst.value.balance.value, 10); + let inst = decode_keyed::(::type_tag_static(), &bytes).unwrap(); + assert_eq!(inst.value.balance, 10); - let err = decode_keyed::>(sui_sdk_types::TypeTag::U8, &bytes).unwrap_err(); + let err = decode_keyed::(TypeTag::U8, &bytes).unwrap_err(); assert!(matches!(err, sui_move::DecodeError::TypeTagMismatch { .. })); } diff --git a/sui-move/examples/type_tags.rs b/sui-move/examples/type_tags.rs index 4b617c1..9ff49df 100644 --- a/sui-move/examples/type_tags.rs +++ b/sui-move/examples/type_tags.rs @@ -1,13 +1,35 @@ use sui_move::prelude::*; -use sui_move::{coin::Coin, sui::SUI}; fn main() { assert_eq!(sui_move::type_tag_of::(), TypeTag::U64); - match as MoveType>::type_tag_static() { + let _u256 = U256([0u8; 32]); + assert!(matches!(::type_tag_static(), TypeTag::U256)); + + #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + struct Demo; + + impl MoveType for Demo { + fn type_tag_static() -> TypeTag { + TypeTag::Struct(Box::new(::struct_tag_static())) + } + } + + impl MoveStruct for Demo { + fn struct_tag_static() -> StructTag { + StructTag::new( + parse_address("0x1").expect("address"), + parse_identifier("demo").expect("module"), + parse_identifier("Demo").expect("name"), + vec![], + ) + } + } + + match ::type_tag_static() { TypeTag::Struct(tag) => { - assert_eq!(tag.module().to_string(), "coin"); - assert_eq!(tag.name().to_string(), "Coin"); + assert_eq!(tag.module().to_string(), "demo"); + assert_eq!(tag.name().to_string(), "Demo"); } other => panic!("expected struct type tag, got {other:?}"), } diff --git a/sui-move/src/builtins.rs b/sui-move/src/builtins.rs index 0287130..61abed6 100644 --- a/sui-move/src/builtins.rs +++ b/sui-move/src/builtins.rs @@ -3,9 +3,10 @@ //! This module provides mappings for: //! - integer and boolean primitives (`u8`, `u16`, `u32`, `u64`, `u128`, `bool`) //! - `sui_sdk_types::Address` +//! - Move `u256` via [`crate::U256`] //! - `Vec` as Move `vector` -use crate::{HasCopy, HasDrop, HasStore, MoveType}; +use crate::{HasCopy, HasDrop, HasStore, MoveType, U256}; macro_rules! impl_primitive { ($ty:ty, $variant:ident) => { @@ -30,6 +31,12 @@ impl MoveType for sui_sdk_types::Address { } } +impl MoveType for U256 { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::U256 + } +} + impl MoveType for Vec { fn type_tag_static() -> sui_sdk_types::TypeTag { sui_sdk_types::TypeTag::Vector(Box::new(T::type_tag_static())) @@ -51,6 +58,7 @@ impl_ability_markers_primitive!(u64); impl_ability_markers_primitive!(u128); impl_ability_markers_primitive!(bool); impl_ability_markers_primitive!(sui_sdk_types::Address); +impl_ability_markers_primitive!(U256); impl HasCopy for Vec {} impl HasDrop for Vec {} diff --git a/sui-move/src/containers.rs b/sui-move/src/containers.rs deleted file mode 100644 index 5469e50..0000000 --- a/sui-move/src/containers.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Common Move framework container types. -//! -//! These are “shape” types: they exist to preserve and compute correct Move type tags and to -//! support typed decoding. They do not implement any on-chain behavior. - -use serde::{Deserialize, Serialize}; - -use crate::{ - parse_address, parse_identifier, HasCopy, HasDrop, HasKey, HasStore, MoveStruct, MoveType, -}; - -/// Move `0x1::option::Option`. -/// -/// In Move, `Option` is represented as a `vector` with length `0` (none) or `1` (some). -/// -/// # Example -/// ``` -/// use sui_move::prelude::*; -/// use sui_move::containers::MoveOption; -/// -/// match as MoveType>::type_tag_static() { -/// TypeTag::Struct(tag) => { -/// assert_eq!(tag.module().to_string(), "option"); -/// assert_eq!(tag.name().to_string(), "Option"); -/// } -/// other => panic!("expected struct type tag, got {other:?}"), -/// } -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct MoveOption { - pub vec: Vec, -} - -impl MoveType for MoveOption { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for MoveOption { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x1").expect("address literal"), - parse_identifier("option").expect("module"), - parse_identifier("Option").expect("name"), - vec![T::type_tag_static()], - ) - } -} - -impl HasCopy for MoveOption {} -impl HasDrop for MoveOption {} -impl HasStore for MoveOption {} - -/// Move `0x2::table::Table`. -/// -/// The Sui framework table stores data under a `UID`. In Rust this struct carries the ID and a -/// size, plus a phantom to preserve type parameters. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct Table { - pub id: crate::types::UID, - pub size: u64, - #[serde(skip, default)] - pub phantom: std::marker::PhantomData<(K, V)>, -} - -impl MoveType for Table { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct - for Table -{ - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("table").expect("module"), - parse_identifier("Table").expect("name"), - vec![K::type_tag_static(), V::type_tag_static()], - ) - } -} - -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. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct DynamicField { - pub id: crate::types::UID, - pub name: Name, - pub value: Value, -} - -impl MoveType - for DynamicField -{ - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct - for DynamicField -{ - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("dynamic_field").expect("module"), - parse_identifier("Field").expect("name"), - vec![Name::type_tag_static(), Value::type_tag_static()], - ) - } -} - -impl HasKey - for DynamicField -{ -} - -/// Move `0x2::dynamic_object_field::Wrapper`. -/// -/// Dynamic object fields are stored as a `dynamic_field::Field, ID>`, where the -/// field's `value` stores the child's `object::ID`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct DynamicObjectFieldWrapper { - pub name: Name, -} - -impl MoveType for DynamicObjectFieldWrapper { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for DynamicObjectFieldWrapper { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("dynamic_object_field").expect("module"), - parse_identifier("Wrapper").expect("name"), - vec![Name::type_tag_static()], - ) - } -} - -impl HasCopy for DynamicObjectFieldWrapper {} -impl HasDrop for DynamicObjectFieldWrapper {} -impl HasStore for DynamicObjectFieldWrapper {} - -/// Move `0x2::dynamic_field::Field, object::ID>`. -pub type DynamicObjectField = DynamicField, crate::types::ID>; diff --git a/sui-move/src/decode.rs b/sui-move/src/decode.rs index 4223929..1e8bc38 100644 --- a/sui-move/src/decode.rs +++ b/sui-move/src/decode.rs @@ -21,25 +21,91 @@ pub fn decode_copyable(bytes: &[u8]) -> Result { +/// #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// struct ID { +/// bytes: sui_sdk_types::Address, +/// } +/// +/// impl MoveType for ID { +/// fn type_tag_static() -> TypeTag { +/// TypeTag::Struct(Box::new(::struct_tag_static())) +/// } +/// } +/// +/// impl MoveStruct for ID { +/// fn struct_tag_static() -> StructTag { +/// StructTag::new( +/// parse_address("0x2").unwrap(), +/// parse_identifier("object").unwrap(), +/// parse_identifier("ID").unwrap(), +/// vec![], +/// ) +/// } +/// } +/// +/// #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// struct UID { +/// id: ID, +/// } +/// +/// impl MoveType for UID { +/// fn type_tag_static() -> TypeTag { +/// TypeTag::Struct(Box::new(::struct_tag_static())) +/// } +/// } +/// +/// impl MoveStruct for UID { +/// fn struct_tag_static() -> StructTag { +/// StructTag::new( +/// parse_address("0x2").unwrap(), +/// parse_identifier("object").unwrap(), +/// parse_identifier("UID").unwrap(), +/// vec![], +/// ) +/// } +/// } +/// +/// #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// struct DemoCoin { +/// id: UID, +/// value: u64, +/// } +/// +/// impl HasKey for DemoCoin {} +/// +/// impl MoveType for DemoCoin { +/// fn type_tag_static() -> TypeTag { +/// TypeTag::Struct(Box::new(::struct_tag_static())) +/// } +/// } +/// +/// impl MoveStruct for DemoCoin { +/// fn struct_tag_static() -> StructTag { +/// StructTag::new( +/// parse_address("0x1").unwrap(), +/// parse_identifier("demo").unwrap(), +/// parse_identifier("DemoCoin").unwrap(), +/// vec![], +/// ) +/// } +/// } +/// +/// let coin = DemoCoin { /// id: UID { /// id: ID { -/// bytes: Address::new([0u8; 32]), +/// bytes: sui_sdk_types::Address::new([0u8; 32]), /// }, /// }, -/// balance: Balance:: { -/// value: 10, -/// phantom: PhantomData, -/// }, +/// value: 10, /// }; /// /// let bytes = coin.to_bcs().unwrap(); -/// let inst = decode_keyed::>( as MoveType>::type_tag_static(), &bytes).unwrap(); -/// assert_eq!(inst.value.balance.value, 10); +/// let inst = +/// decode_keyed::(::type_tag_static(), &bytes).unwrap(); +/// assert_eq!(inst.value.value, 10); /// ``` pub fn decode_keyed( type_tag: sui_sdk_types::TypeTag, diff --git a/sui-move/src/lib.rs b/sui-move/src/lib.rs index 55d9160..9a67342 100644 --- a/sui-move/src/lib.rs +++ b/sui-move/src/lib.rs @@ -13,10 +13,9 @@ pub mod prelude { //! //! Intended for end-user code and examples. pub use crate::{ - containers::DynamicField, containers::DynamicObjectField, - containers::DynamicObjectFieldWrapper, containers::MoveOption, containers::Table, - types::ID, types::UID, Copyable, Droppable, HasCopy, HasDrop, HasKey, HasStore, - MoveInstance, MoveStruct, MoveType, Storable, + decode_copyable, decode_keyed, decode_storable, parse_address, parse_identifier, Copyable, + DecodeError, Droppable, HasCopy, HasDrop, HasKey, HasStore, MoveInstance, MoveStruct, + MoveType, Storable, U256, }; #[cfg(feature = "derive")] pub use crate::{move_module, move_struct}; @@ -30,12 +29,14 @@ pub mod __private { } mod builtins; -pub mod containers; pub mod decode; -pub mod primitives; -pub mod types; pub use decode::{decode_copyable, decode_keyed, decode_storable}; -pub use primitives::*; + +/// Move `u256`. +/// +/// This is a minimal representation that BCS-encodes as 32 little-endian bytes. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct U256(pub [u8; 32]); /// A Rust type that corresponds to a Move type. /// @@ -71,10 +72,6 @@ pub trait MoveType: Serialize + for<'de> Deserialize<'de> + fmt::Debug + Partial bcs::from_bytes(bytes) } - /// Convert this value into JSON using `serde_json`. - fn to_json(&self) -> serde_json::Value { - serde_json::to_value(self).expect("serialization should not fail") - } } /// A Move struct type (including any type parameters). @@ -84,11 +81,32 @@ pub trait MoveType: Serialize + for<'de> Deserialize<'de> + fmt::Debug + Partial /// /// # Example /// ``` -/// use sui_move::prelude::*; -/// -/// let tag = sui_move::types::UID::struct_tag_static(); -/// assert_eq!(tag.module().to_string(), "object"); -/// assert_eq!(tag.name().to_string(), "UID"); +/// use sui_move::{MoveStruct, MoveType}; +/// use sui_sdk_types::{StructTag, TypeTag}; +/// +/// #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// struct Demo; +/// +/// impl MoveType for Demo { +/// fn type_tag_static() -> TypeTag { +/// TypeTag::Struct(Box::new(::struct_tag_static())) +/// } +/// } +/// +/// impl MoveStruct for Demo { +/// fn struct_tag_static() -> StructTag { +/// StructTag::new( +/// sui_move::parse_address("0x1").unwrap(), +/// sui_move::parse_identifier("m").unwrap(), +/// sui_move::parse_identifier("Demo").unwrap(), +/// vec![], +/// ) +/// } +/// } +/// +/// let tag = Demo::struct_tag_static(); +/// assert_eq!(tag.module().to_string(), "m"); +/// assert_eq!(tag.name().to_string(), "Demo"); /// ``` pub trait MoveStruct: MoveType { /// Construct the static struct tag (including type arguments). diff --git a/sui-move/src/primitives/ascii.rs b/sui-move/src/primitives/ascii.rs deleted file mode 100644 index 360376c..0000000 --- a/sui-move/src/primitives/ascii.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; - -/// Move `0x1::ascii::String`. -/// -/// This is **not** Rust's `String`; it is the Sui Move `ascii::String` wrapper around bytes. -/// -/// # Example -/// ``` -/// use sui_move::{ascii, MoveStruct}; -/// -/// let tag = ascii::String::struct_tag_static(); -/// assert_eq!(tag.module().to_string(), "ascii"); -/// assert_eq!(tag.name().to_string(), "String"); -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct String(pub 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("ascii").expect("module"), - parse_identifier("String").expect("name"), - vec![], - ) - } -} - -impl crate::HasCopy for String {} -impl crate::HasDrop for String {} -impl crate::HasStore for String {} diff --git a/sui-move/src/primitives/bag.rs b/sui-move/src/primitives/bag.rs deleted file mode 100644 index e145a2e..0000000 --- a/sui-move/src/primitives/bag.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, types::UID, HasKey, HasStore, MoveStruct, MoveType}; - -/// Move `0x2::bag::Bag`. -/// -/// A `key` object representing a heterogeneous container in the Sui framework. -/// -/// # Example -/// ``` -/// use sui_move::{bag::Bag, MoveType}; -/// -/// let _tag = ::type_tag_static(); -/// ``` -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Bag { - pub id: UID, - pub size: u64, -} - -impl MoveType for Bag { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for Bag { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("bag").expect("module"), - parse_identifier("Bag").expect("name"), - vec![], - ) - } -} - -impl HasKey for Bag {} -impl HasStore for Bag {} diff --git a/sui-move/src/primitives/balance.rs b/sui-move/src/primitives/balance.rs deleted file mode 100644 index 11cbe57..0000000 --- a/sui-move/src/primitives/balance.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, HasStore, MoveStruct, MoveType}; - -/// Move `0x2::balance::Balance`. -/// -/// A `store`-only value used by many Sui framework structs. -/// -/// # Example -/// ``` -/// use sui_move::{balance::Balance, sui::SUI, MoveType}; -/// -/// let _tag = as MoveType>::type_tag_static(); -/// ``` -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Balance { - pub value: u64, - #[serde(skip, default)] - pub phantom: std::marker::PhantomData, -} - -impl MoveType for Balance { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for Balance { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("balance").expect("module"), - parse_identifier("Balance").expect("name"), - vec![T::type_tag_static()], - ) - } -} - -impl HasStore for Balance {} diff --git a/sui-move/src/primitives/clock.rs b/sui-move/src/primitives/clock.rs deleted file mode 100644 index c571c18..0000000 --- a/sui-move/src/primitives/clock.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, types::UID, HasKey, HasStore, MoveStruct, MoveType}; - -/// Move `0x2::clock::Clock`. -/// -/// A `key` object that stores the current on-chain timestamp (milliseconds since epoch). -/// -/// # Example -/// ``` -/// use sui_move::{clock::Clock, MoveType}; -/// -/// let _tag = ::type_tag_static(); -/// ``` -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Clock { - pub id: UID, - pub timestamp_ms: u64, -} - -impl MoveType for Clock { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for Clock { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("clock").expect("module"), - parse_identifier("Clock").expect("name"), - vec![], - ) - } -} - -impl HasKey for Clock {} -impl HasStore for Clock {} diff --git a/sui-move/src/primitives/coin.rs b/sui-move/src/primitives/coin.rs deleted file mode 100644 index 6616c81..0000000 --- a/sui-move/src/primitives/coin.rs +++ /dev/null @@ -1,42 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{ - balance::Balance, parse_address, parse_identifier, types::UID, HasKey, HasStore, MoveStruct, - MoveType, -}; - -/// Move `0x2::coin::Coin`. -/// -/// A `key` object holding a [`Balance`](crate::balance::Balance). -/// -/// # Example -/// ``` -/// use sui_move::{coin::Coin, sui::SUI, MoveType}; -/// -/// let _tag = as MoveType>::type_tag_static(); -/// ``` -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Coin { - pub id: UID, - pub balance: Balance, -} - -impl MoveType for Coin { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for Coin { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("coin").expect("module"), - parse_identifier("Coin").expect("name"), - vec![T::type_tag_static()], - ) - } -} - -impl HasKey for Coin {} -impl HasStore for Coin {} diff --git a/sui-move/src/primitives/linked_table.rs b/sui-move/src/primitives/linked_table.rs deleted file mode 100644 index 81cb3eb..0000000 --- a/sui-move/src/primitives/linked_table.rs +++ /dev/null @@ -1,106 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{ - containers::MoveOption, parse_address, parse_identifier, HasKey, HasStore, MoveStruct, MoveType, -}; - -/// Move `0x2::linked_table::LinkedTable`. -/// -/// A key object representing an ordered, table-like container in the Sui framework. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct LinkedTable< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, -> { - pub id: crate::types::UID, - pub size: u64, - pub head: MoveOption, - pub tail: MoveOption, - #[serde(skip, default)] - pub phantom_v: std::marker::PhantomData, -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, - > MoveType for LinkedTable -{ - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, - > MoveStruct for LinkedTable -{ - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("linked_table").expect("module"), - parse_identifier("LinkedTable").expect("name"), - vec![K::type_tag_static(), V::type_tag_static()], - ) - } -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, - > HasKey for LinkedTable -{ -} -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, - > HasStore for LinkedTable -{ -} - -/// Move `0x2::linked_table::Node`. -/// -/// A store-only node value stored under a `LinkedTable`. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct Node< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, -> { - pub prev: MoveOption, - pub next: MoveOption, - pub value: V, -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, - > MoveType for Node -{ - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, - > MoveStruct for Node -{ - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("linked_table").expect("module"), - parse_identifier("Node").expect("name"), - vec![K::type_tag_static(), V::type_tag_static()], - ) - } -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasStore, - > HasStore for Node -{ -} diff --git a/sui-move/src/primitives/mod.rs b/sui-move/src/primitives/mod.rs deleted file mode 100644 index 4d4ab55..0000000 --- a/sui-move/src/primitives/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Minimal mirrors of common Sui Move framework structs. -//! -//! These types exist to: -//! - construct correct Move `TypeTag`/`StructTag` values, and -//! - enable typed decoding of on-chain values into Rust. -//! -//! They are intentionally small “shape” types and do not provide any on-chain behavior. - -pub mod ascii; -pub mod bag; -pub mod balance; -pub mod clock; -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; -pub mod vec_map; -pub mod vec_set; diff --git a/sui-move/src/primitives/object_bag.rs b/sui-move/src/primitives/object_bag.rs deleted file mode 100644 index 893d3f8..0000000 --- a/sui-move/src/primitives/object_bag.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; - -/// Move `0x2::object_bag::ObjectBag`. -/// -/// A heterogeneous container which stores objects (`key` values). -/// -/// # Example -/// ``` -/// use sui_move::{object_bag::ObjectBag, MoveType}; -/// -/// let _tag = ::type_tag_static(); -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct ObjectBag { - pub id: crate::types::UID, - pub size: u64, -} - -impl MoveType for ObjectBag { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for ObjectBag { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("object_bag").expect("module"), - parse_identifier("ObjectBag").expect("name"), - vec![], - ) - } -} - -impl crate::HasStore for ObjectBag {} -impl crate::HasKey for ObjectBag {} diff --git a/sui-move/src/primitives/object_table.rs b/sui-move/src/primitives/object_table.rs deleted file mode 100644 index b2d8b2e..0000000 --- a/sui-move/src/primitives/object_table.rs +++ /dev/null @@ -1,68 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; - -/// Move `0x2::object_table::ObjectTable`. -/// -/// This is a framework table where the value type is itself an object (`key`). -/// -/// The type bounds mirror the Move framework constraints: -/// - `K` is `copy + drop + store` -/// - `V` is `key + store` -/// -/// # Example -/// ``` -/// use sui_move::{coin::Coin, object_table::ObjectTable, prelude::*, sui::SUI}; -/// -/// let _tag = > as MoveType>::type_tag_static(); -/// ``` -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct ObjectTable< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasKey + crate::HasStore, -> { - pub id: crate::types::UID, - pub size: u64, - #[serde(skip, default)] - pub phantom: std::marker::PhantomData<(K, V)>, -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasKey + crate::HasStore, - > MoveType for ObjectTable -{ - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasKey + crate::HasStore, - > MoveStruct for ObjectTable -{ - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("object_table").expect("module"), - parse_identifier("ObjectTable").expect("name"), - vec![K::type_tag_static(), V::type_tag_static()], - ) - } -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasKey + crate::HasStore, - > crate::HasStore for ObjectTable -{ -} - -impl< - K: MoveType + crate::HasCopy + crate::HasDrop + crate::HasStore, - V: MoveType + crate::HasKey + crate::HasStore, - > crate::HasKey for ObjectTable -{ -} diff --git a/sui-move/src/primitives/priority_queue.rs b/sui-move/src/primitives/priority_queue.rs deleted file mode 100644 index 2c4b1a4..0000000 --- a/sui-move/src/primitives/priority_queue.rs +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index 2890fdb..0000000 --- a/sui-move/src/primitives/string.rs +++ /dev/null @@ -1,34 +0,0 @@ -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 {} diff --git a/sui-move/src/primitives/sui.rs b/sui-move/src/primitives/sui.rs deleted file mode 100644 index 64cf38a..0000000 --- a/sui-move/src/primitives/sui.rs +++ /dev/null @@ -1,37 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, HasDrop, MoveStruct, MoveType}; - -/// Move `0x2::sui::SUI` (the Sui coin type). -/// -/// This is the type argument used for `coin::Coin` and `balance::Balance`. -/// -/// # Example -/// ``` -/// use sui_move::{sui::SUI, MoveStruct}; -/// -/// let tag = SUI::struct_tag_static(); -/// assert_eq!(tag.module().to_string(), "sui"); -/// assert_eq!(tag.name().to_string(), "SUI"); -/// ``` -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct SUI; - -impl MoveType for SUI { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for SUI { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("sui").expect("module"), - parse_identifier("SUI").expect("name"), - vec![], - ) - } -} - -impl HasDrop for SUI {} diff --git a/sui-move/src/primitives/tx_context.rs b/sui-move/src/primitives/tx_context.rs deleted file mode 100644 index b40eeec..0000000 --- a/sui-move/src/primitives/tx_context.rs +++ /dev/null @@ -1,34 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; - -/// Phantom placeholder for `0x2::tx_context::TxContext` so it can appear in type tags. -/// -/// This type is not meant to be instantiated; it exists to build tags for entry function -/// signatures that reference `TxContext`. -/// -/// # Example -/// ``` -/// use sui_move::{tx_context::TxContext, MoveType}; -/// -/// let _tag = ::type_tag_static(); -/// ``` -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct TxContext; - -impl MoveType for TxContext { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for TxContext { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("tx_context").expect("module"), - parse_identifier("TxContext").expect("name"), - vec![], - ) - } -} diff --git a/sui-move/src/primitives/type_name.rs b/sui-move/src/primitives/type_name.rs deleted file mode 100644 index e077bc4..0000000 --- a/sui-move/src/primitives/type_name.rs +++ /dev/null @@ -1,41 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; - -/// Move `0x1::type_name::TypeName`. -/// -/// This is used in the Sui framework to carry a runtime type name. -/// -/// # Example -/// ``` -/// use sui_move::{type_name::TypeName, MoveStruct}; -/// -/// let tag = TypeName::struct_tag_static(); -/// assert_eq!(tag.module().to_string(), "type_name"); -/// assert_eq!(tag.name().to_string(), "TypeName"); -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct TypeName { - pub name: String, -} - -impl MoveType for TypeName { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl crate::MoveStruct for TypeName { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x1").expect("address literal"), - parse_identifier("type_name").expect("module"), - parse_identifier("TypeName").expect("name"), - vec![], - ) - } -} - -impl crate::HasCopy for TypeName {} -impl crate::HasDrop for TypeName {} -impl crate::HasStore for TypeName {} diff --git a/sui-move/src/primitives/vec_map.rs b/sui-move/src/primitives/vec_map.rs deleted file mode 100644 index 26155f8..0000000 --- a/sui-move/src/primitives/vec_map.rs +++ /dev/null @@ -1,72 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; - -/// Move `0x2::vec_map::Entry`. -/// -/// The key type must be `copy` in the framework. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct Entry { - pub key: K, - pub value: V, -} - -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("vec_map").expect("module"), - parse_identifier("Entry").expect("name"), - vec![K::type_tag_static(), V::type_tag_static()], - ) - } -} - -impl crate::HasCopy for Entry {} -impl crate::HasDrop for Entry {} -impl crate::HasStore for Entry {} - -/// Move `0x2::vec_map::VecMap`. -/// -/// A small ordered map implementation backed by a vector of entries. The key type must be -/// `copy` in the framework. -/// -/// # Example -/// ``` -/// use sui_move::{prelude::*, vec_map::VecMap}; -/// -/// let _tag = as MoveType>::type_tag_static(); -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct VecMap { - pub contents: Vec>, -} - -impl MoveType for VecMap { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for VecMap { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("vec_map").expect("module"), - parse_identifier("VecMap").expect("name"), - vec![K::type_tag_static(), V::type_tag_static()], - ) - } -} - -impl crate::HasCopy for VecMap {} -impl crate::HasDrop for VecMap {} -impl crate::HasStore for VecMap {} diff --git a/sui-move/src/primitives/vec_set.rs b/sui-move/src/primitives/vec_set.rs deleted file mode 100644 index fe15517..0000000 --- a/sui-move/src/primitives/vec_set.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, MoveStruct, MoveType}; - -/// Move `0x2::vec_set::VecSet`. -/// -/// A small set implementation backed by a vector. In the Move framework, the type parameter must -/// be `copy`. -/// -/// # Example -/// ``` -/// use sui_move::{prelude::*, vec_set::VecSet}; -/// -/// let _tag = as MoveType>::type_tag_static(); -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct VecSet(pub Vec); - -impl MoveType for VecSet { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for VecSet { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("vec_set").expect("module"), - parse_identifier("VecSet").expect("name"), - vec![T::type_tag_static()], - ) - } -} - -impl crate::HasCopy for VecSet {} -impl crate::HasDrop for VecSet {} -impl crate::HasStore for VecSet {} diff --git a/sui-move/src/types.rs b/sui-move/src/types.rs deleted file mode 100644 index 0d59bce..0000000 --- a/sui-move/src/types.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Core Sui object types from the Move framework. -//! -//! These types are widely used by other Sui framework structs (e.g. `coin::Coin`). - -use serde::{Deserialize, Serialize}; - -use crate::{parse_address, parse_identifier, HasCopy, HasDrop, HasStore, MoveStruct, MoveType}; - -/// Move `0x2::object::ID`. -/// -/// In the Sui framework this wraps a Move `address` for compact BCS encoding. -/// -/// # Example -/// ``` -/// use sui_move::prelude::*; -/// -/// let tag = sui_move::types::ID::struct_tag_static(); -/// assert_eq!(tag.module().to_string(), "object"); -/// assert_eq!(tag.name().to_string(), "ID"); -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct ID { - pub bytes: sui_sdk_types::Address, -} - -impl MoveType for ID { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for ID { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("object").expect("module"), - parse_identifier("ID").expect("name"), - vec![], - ) - } -} - -impl HasCopy for ID {} -impl HasDrop for ID {} -impl HasStore for ID {} - -/// Move `0x2::object::UID`. -/// -/// This is the “unique ID” embedded in `key` objects. -/// -/// # Example -/// ``` -/// use sui_move::prelude::*; -/// -/// let tag = sui_move::types::UID::struct_tag_static(); -/// assert_eq!(tag.module().to_string(), "object"); -/// assert_eq!(tag.name().to_string(), "UID"); -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UID { - pub id: ID, -} - -impl MoveType for UID { - fn type_tag_static() -> sui_sdk_types::TypeTag { - sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) - } -} - -impl MoveStruct for UID { - fn struct_tag_static() -> sui_sdk_types::StructTag { - sui_sdk_types::StructTag::new( - parse_address("0x2").expect("address literal"), - parse_identifier("object").expect("module"), - parse_identifier("UID").expect("name"), - vec![], - ) - } -} - -impl HasStore for UID {} diff --git a/sui-move/tests/basic.rs b/sui-move/tests/basic.rs index 77d21cc..dca541b 100644 --- a/sui-move/tests/basic.rs +++ b/sui-move/tests/basic.rs @@ -1,242 +1,51 @@ use std::str::FromStr; + use sui_move::prelude::*; -use sui_move::{containers::MoveOption, decode_keyed, MoveInstance}; #[test] fn primitive_type_tags_are_correct() { - assert!(matches!( - ::type_tag_static(), - sui_sdk_types::TypeTag::U8 - )); - assert!(matches!( - ::type_tag_static(), - sui_sdk_types::TypeTag::U16 - )); - assert!(matches!( - ::type_tag_static(), - sui_sdk_types::TypeTag::U32 - )); - assert!(matches!( - ::type_tag_static(), - sui_sdk_types::TypeTag::U64 - )); - assert!(matches!( - ::type_tag_static(), - sui_sdk_types::TypeTag::U128 - )); - assert!(matches!( - ::type_tag_static(), - sui_sdk_types::TypeTag::Bool - )); - assert!(matches!( - ::type_tag_static(), - sui_sdk_types::TypeTag::Address - )); + assert!(matches!(::type_tag_static(), TypeTag::U8)); + assert!(matches!(::type_tag_static(), TypeTag::U16)); + assert!(matches!(::type_tag_static(), TypeTag::U32)); + assert!(matches!(::type_tag_static(), TypeTag::U64)); + assert!(matches!(::type_tag_static(), TypeTag::U128)); + assert!(matches!(::type_tag_static(), TypeTag::U256)); + assert!(matches!(::type_tag_static(), TypeTag::Bool)); + assert!(matches!(
::type_tag_static(), TypeTag::Address)); match as MoveType>::type_tag_static() { - sui_sdk_types::TypeTag::Vector(inner) => { - assert!(matches!(*inner, sui_sdk_types::TypeTag::U64)); - } + TypeTag::Vector(inner) => assert!(matches!(*inner, TypeTag::U64)), other => panic!("expected vector type tag, got {other:?}"), } } #[test] -fn core_types_have_correct_tags() { - match ::type_tag_static() { - sui_sdk_types::TypeTag::Struct(tag) => { - assert_eq!( - *tag.address(), - sui_sdk_types::Address::from_str("0x2").unwrap() - ); - assert_eq!(tag.module().to_string(), "object"); - assert_eq!(tag.name().to_string(), "ID"); - } - other => panic!("expected struct tag, got {other:?}"), - } - - match ::type_tag_static() { - sui_sdk_types::TypeTag::Struct(tag) => { - assert_eq!( - *tag.address(), - sui_sdk_types::Address::from_str("0x2").unwrap() - ); - assert_eq!(tag.module().to_string(), "object"); - assert_eq!(tag.name().to_string(), "UID"); - } - other => panic!("expected struct tag, got {other:?}"), - } +fn parse_helpers_work() { + assert_eq!( + parse_address("0x2").unwrap(), + Address::from_str("0x2").unwrap() + ); + assert_eq!(parse_identifier("coin").unwrap().to_string(), "coin"); } #[test] -fn tag_verification_and_bcs_roundtrip() { - let value = MoveOption:: { vec: vec![1, 2, 3] }; +fn move_instance_verifies_type_tag_and_roundtrips_bcs() { + let value = vec![1u8, 2, 3]; let bytes = value.to_bcs().unwrap(); - let inst = MoveInstance::>::from_raw_type( - as MoveType>::type_tag_static(), - &bytes, - ) - .unwrap(); - assert_eq!(inst.value.vec, vec![1, 2, 3]); - - let err = MoveInstance::>::from_raw_type(sui_sdk_types::TypeTag::U8, &bytes) - .unwrap_err(); - assert!(matches!(err, sui_move::DecodeError::TypeTagMismatch { .. })); -} - -fn require_store(_: &T) {} -fn require_copy(_: &T) {} - -#[test] -fn type_abilities_are_respected() { - let value = 5u64; - require_store(&value); - require_copy(&value); + let inst = MoveInstance::>::from_raw_type( as MoveType>::type_tag_static(), &bytes) + .unwrap(); + assert_eq!(inst.value, value); - let uid = uid_with_byte(9); - require_store(&uid); + let err = MoveInstance::>::from_raw_type(TypeTag::U64, &bytes).unwrap_err(); + assert!(matches!(err, DecodeError::TypeTagMismatch { .. })); } #[test] -fn move_option_and_containers_have_correct_tags() { - let opt = MoveOption:: { vec: vec![5] }; - let bytes = opt.to_bcs().unwrap(); - let decoded = MoveOption::::from_bcs(&bytes).unwrap(); - assert_eq!(decoded.vec, vec![5]); - match MoveOption::::type_tag_static() { - sui_sdk_types::TypeTag::Struct(tag) => { - assert_eq!(tag.module().to_string(), "option"); - assert_eq!(tag.name().to_string(), "Option"); - } - _ => panic!("expected struct tag"), - } - - let table = sui_move::containers::Table:: { - id: uid_with_byte(0), - size: 0, - phantom: std::marker::PhantomData, - }; - let table_bytes = table.to_bcs().unwrap(); - let decoded_table = sui_move::containers::Table::::from_bcs(&table_bytes).unwrap(); - assert_eq!(decoded_table.id.id.bytes, Address::new([0u8; 32])); - assert_eq!(decoded_table.size, 0); - - let df = sui_move::containers::DynamicField:: { - id: uid_with_byte(1), - name: 9, - value: 8, - }; - let df_bytes = df.to_bcs().unwrap(); - let decoded_df = sui_move::containers::DynamicField::::from_bcs(&df_bytes).unwrap(); - assert_eq!(decoded_df.value, 8); - - let dof = sui_move::containers::DynamicObjectField:: { - id: uid_with_byte(2), - name: sui_move::containers::DynamicObjectFieldWrapper { name: 1u64 }, - value: sui_move::types::ID { - bytes: Address::new([2u8; 32]), - }, - }; - let dof_bytes = dof.to_bcs().unwrap(); - let decoded_dof = - sui_move::containers::DynamicObjectField::::from_bcs(&dof_bytes).unwrap(); - assert_eq!(decoded_dof.name.name, 1); -} - -#[test] -fn primitives_can_decode_keyed_values() { - let coin = sui_move::coin::Coin:: { - id: uid_with_byte(7), - balance: sui_move::balance::Balance:: { - value: 123, - phantom: std::marker::PhantomData, - }, - }; - let bytes = coin.to_bcs().unwrap(); - - let decoded = decode_keyed::>( - as MoveType>::type_tag_static(), - &bytes, - ) - .unwrap(); - assert_eq!(decoded.value.balance.value, 123); -} - -#[test] -fn object_id_uses_address_bcs_layout() { - let addr = Address::new([9u8; 32]); - let id = sui_move::types::ID { bytes: addr }; - let bytes = id.to_bcs().unwrap(); +fn u256_bcs_is_32_bytes() { + let value = U256([9u8; 32]); + let bytes = value.to_bcs().unwrap(); assert_eq!(bytes.len(), 32); - assert_eq!(bytes.as_slice(), addr.as_bytes()); -} - -#[test] -fn table_and_dynamic_object_field_match_framework_layouts() { - let table = sui_move::containers::Table:: { - id: uid_with_byte(1), - size: 7, - phantom: std::marker::PhantomData, - }; - let bytes = table.to_bcs().unwrap(); - assert_eq!(bytes.len(), 40); - let decoded = sui_move::containers::Table::::from_bcs(&bytes).unwrap(); - assert_eq!(decoded.size, 7); - - let wrapper_tag = sui_move::containers::DynamicObjectFieldWrapper::::struct_tag_static(); - assert_eq!(wrapper_tag.module().to_string(), "dynamic_object_field"); - assert_eq!(wrapper_tag.name().to_string(), "Wrapper"); - - let dof_tag = sui_move::containers::DynamicObjectField::::struct_tag_static(); - assert_eq!(dof_tag.module().to_string(), "dynamic_field"); - assert_eq!(dof_tag.name().to_string(), "Field"); - assert_eq!(dof_tag.type_params().len(), 2); - - match &dof_tag.type_params()[0] { - TypeTag::Struct(inner) => { - assert_eq!(inner.module().to_string(), "dynamic_object_field"); - assert_eq!(inner.name().to_string(), "Wrapper"); - } - other => panic!("expected wrapper struct type tag, got {other:?}"), - } - - assert_eq!( - dof_tag.type_params()[1], - sui_move::types::ID::type_tag_static() - ); -} - -#[test] -fn framework_containers_store_sizes() { - let object_bag = sui_move::object_bag::ObjectBag { - id: uid_with_byte(5), - size: 0, - }; - assert_eq!(object_bag.to_bcs().unwrap().len(), 40); - - let object_table = - sui_move::object_table::ObjectTable::> { - id: uid_with_byte(6), - size: 0, - phantom: std::marker::PhantomData, - }; - assert_eq!(object_table.to_bcs().unwrap().len(), 40); - - let linked_table = sui_move::linked_table::LinkedTable:: { - id: uid_with_byte(7), - size: 0, - head: MoveOption { vec: vec![] }, - tail: MoveOption { vec: vec![] }, - phantom_v: std::marker::PhantomData, - }; - assert_eq!(linked_table.to_bcs().unwrap().len(), 42); -} - -fn uid_with_byte(byte: u8) -> sui_move::types::UID { - sui_move::types::UID { - id: sui_move::types::ID { - bytes: Address::new([byte; 32]), - }, - } + let decoded = ::from_bcs(&bytes).unwrap(); + assert_eq!(decoded, value); } diff --git a/sui-move/tests/derive.rs b/sui-move/tests/derive.rs index 04cc759..cfc9cb2 100644 --- a/sui-move/tests/derive.rs +++ b/sui-move/tests/derive.rs @@ -2,7 +2,16 @@ use std::str::FromStr; use sui_move::prelude::*; -use sui_move::{containers::MoveOption, Copyable, MoveInstance, Storable}; + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +pub struct ID { + pub bytes: Address, +} + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +pub struct UID { + pub id: ID, +} #[sui_move::move_module(address = "0x1", name = "vault")] mod vault { @@ -11,10 +20,9 @@ mod vault { module = "vault", abilities = "key, store", phantoms = "T", - uid_type = "sui_move::types::UID" - )] + )] pub struct Vault { - pub id: sui_move::types::UID, + pub id: crate::UID, pub balance: Vec, } } @@ -23,7 +31,7 @@ mod vault { mod wrapper { #[sui_move::move_struct(address = "0x1", module = "wrapper", abilities = "key, store")] pub struct VaultWrapper { - pub id: sui_move::types::UID, + pub id: crate::UID, pub inner: crate::vault::Vault, } } @@ -63,14 +71,14 @@ fn type_tag_matches_move_definition() { #[test] fn nested_structs_are_supported() { let wrapper = wrapper::VaultWrapper { - id: sui_move::types::UID { - id: sui_move::types::ID { + id: UID { + id: ID { bytes: Address::new([0u8; 32]), }, }, inner: vault::Vault:: { - id: sui_move::types::UID { - id: sui_move::types::ID { + id: UID { + id: ID { bytes: Address::new([42u8; 32]), }, }, @@ -96,8 +104,8 @@ fn nested_structs_are_supported() { #[test] fn tag_verification_and_bcs_roundtrip() { let value = vault::Vault:: { - id: sui_move::types::UID { - id: sui_move::types::ID { + id: UID { + id: ID { bytes: Address::new([7u8; 32]), }, }, @@ -130,8 +138,8 @@ fn type_abilities_are_respected() { require_copy(&boxed.value); let nested = vault::Vault { - id: sui_move::types::UID { - id: sui_move::types::ID { + id: UID { + id: ID { bytes: Address::new([9u8; 32]), }, }, @@ -140,19 +148,3 @@ fn type_abilities_are_respected() { }; assert_eq!(nested.balance[0].value, 7); } - -#[test] -fn move_option_and_containers_have_correct_tags() { - let opt = MoveOption:: { vec: vec![5] }; - let bytes = opt.to_bcs().unwrap(); - let decoded = MoveOption::::from_bcs(&bytes).unwrap(); - assert_eq!(decoded.vec, vec![5]); - - match MoveOption::::type_tag_static() { - sui_sdk_types::TypeTag::Struct(tag) => { - assert_eq!(tag.module().to_string(), "option"); - assert_eq!(tag.name().to_string(), "Option"); - } - _ => panic!("expected struct tag"), - } -} From 026305008f8544cb948269fec8cc441088066d1e Mon Sep 17 00:00:00 2001 From: David Date: Thu, 5 Feb 2026 13:37:23 +0100 Subject: [PATCH 6/9] fix: doc test --- sui-move-runtime/README.md | 35 +++++++++++++++++++++++++-------- sui-move-runtime/src/handles.rs | 20 +++++++++++++++---- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/sui-move-runtime/README.md b/sui-move-runtime/README.md index 9121001..c2c5c9a 100644 --- a/sui-move-runtime/README.md +++ b/sui-move-runtime/README.md @@ -23,9 +23,15 @@ truthful to Sui’s “versioned objects + effects” model (`MODEL.md`): ```rust,no_run use sui_move_runtime::prelude::*; -use sui_move::{coin::Coin, sui::SUI}; use sui_sdk_types::{Address, PersonalMessage, Transaction, UserSignature}; +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +# struct ID { bytes: Address } +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +# struct UID { id: ID } +# #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +# struct DemoCoin { id: UID } +# # #[derive(Clone)] # struct DummySigner; # impl sui_crypto::SuiSigner for DummySigner { @@ -54,7 +60,7 @@ async fn demo() -> Result<(), Error> { let mut rt = Runtime::new(client, signer); // Read: fetch a typed runtime-owned handle. - let coin: Object> = rt.read().object::>(coin_id).await?; + let coin: Object = rt.read().object::(coin_id).await?; // Tx: one-shot build + commit with the `tx!` macro. let receipt = sui_move_runtime::tx!(&mut rt, sender => { @@ -339,15 +345,21 @@ BCS layout expects Y” becomes an explicit error instead of a silent footgun. ```rust,no_run use sui_move_runtime::prelude::*; -use sui_move::{coin::Coin, sui::SUI}; use sui_sdk_types::Address; # async fn demo(mut rt: Runtime) -> Result<(), Error> { let coin_id: Address = "0x2".parse().unwrap(); -let (coin, value): (Object>, Coin) = rt.read().get(coin_id).await?; -let _latest: Coin = rt.read().decode(&coin).await?; -let _unchecked: Coin = rt.read().decode_unchecked(&coin).await?; +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +# struct ID { bytes: Address } +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +# struct UID { id: ID } +# #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +# struct DemoCoin { id: UID } + +let (coin, value): (Object, DemoCoin) = rt.read().get(coin_id).await?; +let _latest: DemoCoin = rt.read().decode(&coin).await?; +let _unchecked: DemoCoin = rt.read().decode_unchecked(&coin).await?; # let _ = value; # Ok(()) # } @@ -406,11 +418,18 @@ threading `&mut ObjectReference` everywhere. ```rust,no_run use sui_move_runtime::prelude::*; -use sui_move::{coin::Coin, sui::SUI}; +use sui_sdk_types::Address; + +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +# struct ID { bytes: Address } +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +# struct UID { id: ID } +# #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +# struct DemoCoin { id: UID } #[derive(Clone)] struct Wallet { - coin: Object>, + coin: Object, } # async fn demo(mut rt: Runtime, sender: sui_sdk_types::Address) -> Result<(), Error> { diff --git a/sui-move-runtime/src/handles.rs b/sui-move-runtime/src/handles.rs index 3cdda4c..7b47798 100644 --- a/sui-move-runtime/src/handles.rs +++ b/sui-move-runtime/src/handles.rs @@ -104,7 +104,14 @@ struct TrackedObjectSnapshot { /// # Example /// ```rust,no_run /// use sui_move_runtime::prelude::*; -/// use sui_move::{coin::Coin, sui::SUI}; +/// use sui_sdk_types::Address; +/// +/// # #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +/// # struct ID { bytes: Address } +/// # #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// # struct UID { id: ID } +/// # #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +/// # struct DemoCoin { id: UID } /// /// fn touch(coin: &impl ToCallArg) -> CallSpec { /// let package: sui_sdk_types::Address = "0x1".parse().unwrap(); @@ -114,7 +121,7 @@ struct TrackedObjectSnapshot { /// } /// /// # async fn demo(mut rt: Runtime, sender: sui_sdk_types::Address) -> Result<(), Error> { -/// let coin: Object> = rt.read().object("0x2".parse().unwrap()).await?; +/// let coin: Object = rt.read().object("0x2".parse().unwrap()).await?; /// let mut tx = rt.tx(sender); /// tx.call(touch(&coin))?; /// tx.commit().await?; @@ -383,13 +390,18 @@ impl ToCallArgMut for ReceivingObjec /// /// # Example /// ``` -/// use sui_move::{coin::Coin, sui::SUI}; /// use sui_move_call::{CallArg, CallSpec}; /// use sui_move_runtime::SharedObject; /// use sui_sdk_types::Address; /// /// let package: Address = "0x1".parse().unwrap(); -/// let shared = SharedObject::>::mutable("0x2".parse().unwrap(), 1); +/// # #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +/// # struct ID { bytes: Address } +/// # #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// # struct UID { id: ID } +/// # #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +/// # struct DemoCoin { id: UID } +/// let shared = SharedObject::::mutable("0x2".parse().unwrap(), 1); /// /// let mut spec = CallSpec::new(package, "m", "f").unwrap(); /// spec.push_arg(&shared).unwrap(); From 949a6c323e896d540ffc003ae9071f1516d50da4 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 5 Feb 2026 14:13:25 +0100 Subject: [PATCH 7/9] fix: rendering --- sui-move-codegen/src/render/calls.rs | 26 ++++-- sui-move-codegen/src/render/tx_ext.rs | 26 ++++-- sui-move-codegen/src/render/types.rs | 24 ++++-- sui-move-codegen/src/render/util.rs | 10 +++ sui-move-derive/src/expand.rs | 117 +++++++++++++++----------- sui-move-derive/src/lib.rs | 31 +++++-- 6 files changed, 154 insertions(+), 80 deletions(-) diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index 0bd35dd..c0facb4 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -207,20 +207,30 @@ fn type_param_bounds(f: &Function, use_aliases: bool) -> Vec { .map(|(idx, p)| { let ty = format_ident!("T{idx}"); - let base = if p.constraints.contains(&Ability::Key) { + let has_key = p.constraints.contains(&Ability::Key); + let has_store = p.constraints.contains(&Ability::Store); + let has_copy = p.constraints.contains(&Ability::Copy); + // Move rule: `copy` implies `drop`. + let has_drop = p.constraints.contains(&Ability::Drop) || has_copy; + + let base = if has_key { quote! { #sm::MoveStruct } } else { quote! { #sm::MoveType } }; let mut bounds: Vec = vec![base]; - for a in &p.constraints { - bounds.push(match a { - Ability::Copy => quote! { #sm::HasCopy }, - Ability::Drop => quote! { #sm::HasDrop }, - Ability::Store => quote! { #sm::HasStore }, - Ability::Key => quote! { #sm::HasKey }, - }); + if has_store { + bounds.push(quote! { #sm::HasStore }); + } + if has_copy { + bounds.push(quote! { #sm::HasCopy }); + } + if has_drop { + bounds.push(quote! { #sm::HasDrop }); + } + if has_key { + bounds.push(quote! { #sm::HasKey }); } quote! { #ty: #(#bounds)+* } }) diff --git a/sui-move-codegen/src/render/tx_ext.rs b/sui-move-codegen/src/render/tx_ext.rs index da0b8c1..9e9e51b 100644 --- a/sui-move-codegen/src/render/tx_ext.rs +++ b/sui-move-codegen/src/render/tx_ext.rs @@ -241,20 +241,30 @@ fn type_param_bounds(f: &Function, use_aliases: bool) -> Vec { .map(|(idx, p)| { let ty = format_ident!("T{idx}"); - let base = if p.constraints.contains(&Ability::Key) { + let has_key = p.constraints.contains(&Ability::Key); + let has_store = p.constraints.contains(&Ability::Store); + let has_copy = p.constraints.contains(&Ability::Copy); + // Move rule: `copy` implies `drop`. + let has_drop = p.constraints.contains(&Ability::Drop) || has_copy; + + let base = if has_key { quote! { #sm::MoveStruct } } else { quote! { #sm::MoveType } }; let mut bounds: Vec = vec![base]; - for a in &p.constraints { - bounds.push(match a { - Ability::Copy => quote! { #sm::HasCopy }, - Ability::Drop => quote! { #sm::HasDrop }, - Ability::Store => quote! { #sm::HasStore }, - Ability::Key => quote! { #sm::HasKey }, - }); + if has_store { + bounds.push(quote! { #sm::HasStore }); + } + if has_copy { + bounds.push(quote! { #sm::HasCopy }); + } + if has_drop { + bounds.push(quote! { #sm::HasDrop }); + } + if has_key { + bounds.push(quote! { #sm::HasKey }); } quote! { #ty: #(#bounds)+* } }) diff --git a/sui-move-codegen/src/render/types.rs b/sui-move-codegen/src/render/types.rs index d00958a..6c30023 100644 --- a/sui-move-codegen/src/render/types.rs +++ b/sui-move-codegen/src/render/types.rs @@ -331,14 +331,24 @@ fn type_param_bounds(dt: &Datatype, use_aliases: bool) -> Vec { .enumerate() .map(|(idx, p)| { let ty = format_ident!("T{idx}"); + let has_key = p.constraints.contains(&Ability::Key); + let has_store = p.constraints.contains(&Ability::Store); + let has_copy = p.constraints.contains(&Ability::Copy); + // Move rule: `copy` implies `drop`. + let has_drop = p.constraints.contains(&Ability::Drop) || has_copy; + let mut bounds: Vec = vec![quote! { #sm::MoveType }]; - for a in &p.constraints { - bounds.push(match a { - Ability::Copy => quote! { #sm::HasCopy }, - Ability::Drop => quote! { #sm::HasDrop }, - Ability::Store => quote! { #sm::HasStore }, - Ability::Key => quote! { #sm::HasKey }, - }); + if has_store { + bounds.push(quote! { #sm::HasStore }); + } + if has_copy { + bounds.push(quote! { #sm::HasCopy }); + } + if has_drop { + bounds.push(quote! { #sm::HasDrop }); + } + if has_key { + bounds.push(quote! { #sm::HasKey }); } quote! { #ty: #(#bounds)+* } }) diff --git a/sui-move-codegen/src/render/util.rs b/sui-move-codegen/src/render/util.rs index 4232031..21a45a1 100644 --- a/sui-move-codegen/src/render/util.rs +++ b/sui-move-codegen/src/render/util.rs @@ -3,6 +3,8 @@ use proc_macro2::TokenStream; use quote::quote; +use std::collections::BTreeSet; + use crate::ir::{NormalizedModule, NormalizedPackage}; use super::{calls, idents, tx_ext, types, ExternalResolver, RenderOptions}; @@ -34,11 +36,15 @@ pub(crate) fn render_package_tokens( let reexports = if opts.flatten || !opts.emit_types { quote! {} } else { + let mut seen: BTreeSet = BTreeSet::new(); let mut reexp = Vec::new(); for module in pkg.modules.values() { let module_ident = idents::ident(&module.name); for dt in &module.datatypes { let ty_ident = idents::ident(&dt.name); + if !seen.insert(ty_ident.to_string()) { + continue; + } reexp.push(quote! { pub use #module_ident::#ty_ident; }); } } @@ -75,11 +81,15 @@ pub(crate) fn render_split_mod_rs_tokens( let reexports = if !opts.emit_types { quote! {} } else { + let mut seen: BTreeSet = BTreeSet::new(); let mut out = Vec::new(); for module in pkg.modules.values() { let module_ident = idents::ident(&module.name); for dt in &module.datatypes { let ty_ident = idents::ident(&dt.name); + if !seen.insert(ty_ident.to_string()) { + continue; + } out.push(quote! { pub use #module_ident::#ty_ident; }); } } diff --git a/sui-move-derive/src/expand.rs b/sui-move-derive/src/expand.rs index e2d21ab..6f5da88 100644 --- a/sui-move-derive/src/expand.rs +++ b/sui-move-derive/src/expand.rs @@ -137,7 +137,10 @@ pub(crate) fn expand_move_struct( } } - let mut where_bounds: Vec = Vec::new(); + let mut type_param_where_bounds: Vec = Vec::new(); + let mut copy_where_bounds: Vec = Vec::new(); + let mut drop_where_bounds: Vec = Vec::new(); + let mut store_where_bounds: Vec = Vec::new(); for param in &type_param_idents { let ident = ¶m.ident; let mut bounds: Vec = vec![parse_quote!(::sui_move::MoveType)]; @@ -157,7 +160,7 @@ pub(crate) fn expand_move_struct( } } - where_bounds.push(parse_quote!(#ident: #(#bounds)+*)); + type_param_where_bounds.push(parse_quote!(#ident: #(#bounds)+*)); } if has_key @@ -177,13 +180,13 @@ pub(crate) fn expand_move_struct( } let ty = &field.ty; if has_copy { - where_bounds.push(parse_quote!(#ty: ::sui_move::HasCopy)); + copy_where_bounds.push(parse_quote!(#ty: ::sui_move::HasCopy)); } if has_drop { - where_bounds.push(parse_quote!(#ty: ::sui_move::HasDrop)); + drop_where_bounds.push(parse_quote!(#ty: ::sui_move::HasDrop)); } if has_store { - where_bounds.push(parse_quote!(#ty: ::sui_move::HasStore)); + store_where_bounds.push(parse_quote!(#ty: ::sui_move::HasStore)); } } @@ -228,19 +231,16 @@ 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()); - } + // Keep the struct definition's generics unchanged. + // + // We intentionally do not inject `MoveType`/ability bounds onto the type itself. In Move, + // a generic struct type is well-formed for any type arguments; abilities are conditional and + // gate what you can do with that instantiation. We model that by putting bounds on the + // generated impl blocks, not on the struct definition. let mut output_struct = input; output_struct.ident = struct_ident.clone(); - output_struct.generics = expanded_generics.clone(); + output_struct.generics = generics.clone(); output_struct.data = Data::Struct(syn::DataStruct { struct_token: Default::default(), fields: Fields::Named(syn::FieldsNamed { @@ -288,31 +288,6 @@ 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 @@ -322,20 +297,32 @@ 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) = expanded_generics.split_for_impl(); + let mut move_generics = generics.clone(); + if !type_param_where_bounds.is_empty() { + move_generics + .make_where_clause() + .predicates + .extend(type_param_where_bounds.iter().cloned()); + } + let (impl_generics, ty_generics, where_clause) = move_generics.split_for_impl(); let clone_impl = if has_copy { + let mut clone_generics = move_generics.clone(); + if !copy_where_bounds.is_empty() { + clone_generics + .make_where_clause() + .predicates + .extend(copy_where_bounds.iter().cloned()); + } + let (clone_impl_generics, _, clone_where_clause) = clone_generics.split_for_impl(); 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 { + impl #clone_impl_generics ::core::clone::Clone for #struct_ident #ty_generics #clone_where_clause { fn clone(&self) -> Self { Self { #(#inits)* @@ -350,23 +337,55 @@ pub(crate) fn expand_move_struct( let ability_impls = { let mut impls = Vec::new(); if has_key { + let mut key_generics = move_generics.clone(); + if !store_where_bounds.is_empty() { + key_generics + .make_where_clause() + .predicates + .extend(store_where_bounds.iter().cloned()); + } + let (key_impl_generics, _, key_where_clause) = key_generics.split_for_impl(); impls.push(quote! { - impl #impl_generics ::sui_move::HasKey for #struct_ident #ty_generics #where_clause {} + impl #key_impl_generics ::sui_move::HasKey for #struct_ident #ty_generics #key_where_clause {} }); } if has_store { + let mut store_generics = move_generics.clone(); + if !store_where_bounds.is_empty() { + store_generics + .make_where_clause() + .predicates + .extend(store_where_bounds.iter().cloned()); + } + let (store_impl_generics, _, store_where_clause) = store_generics.split_for_impl(); impls.push(quote! { - impl #impl_generics ::sui_move::HasStore for #struct_ident #ty_generics #where_clause {} + impl #store_impl_generics ::sui_move::HasStore for #struct_ident #ty_generics #store_where_clause {} }); } if has_copy { + let mut copy_generics = move_generics.clone(); + if !copy_where_bounds.is_empty() { + copy_generics + .make_where_clause() + .predicates + .extend(copy_where_bounds.iter().cloned()); + } + let (copy_impl_generics, _, copy_where_clause) = copy_generics.split_for_impl(); impls.push(quote! { - impl #impl_generics ::sui_move::HasCopy for #struct_ident #ty_generics #where_clause {} + impl #copy_impl_generics ::sui_move::HasCopy for #struct_ident #ty_generics #copy_where_clause {} }); } if has_drop { + let mut drop_generics = move_generics.clone(); + if !drop_where_bounds.is_empty() { + drop_generics + .make_where_clause() + .predicates + .extend(drop_where_bounds.iter().cloned()); + } + let (drop_impl_generics, _, drop_where_clause) = drop_generics.split_for_impl(); impls.push(quote! { - impl #impl_generics ::sui_move::HasDrop for #struct_ident #ty_generics #where_clause {} + impl #drop_impl_generics ::sui_move::HasDrop for #struct_ident #ty_generics #drop_where_clause {} }); } quote! { #(#impls)* } diff --git a/sui-move-derive/src/lib.rs b/sui-move-derive/src/lib.rs index 142697d..fc73a26 100644 --- a/sui-move-derive/src/lib.rs +++ b/sui-move-derive/src/lib.rs @@ -105,7 +105,6 @@ pub fn move_struct(attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(test)] mod tests { use super::args::{AddressArg, MoveStructArgs}; - use proc_macro2::Span; use std::collections::BTreeMap; #[test] @@ -135,7 +134,7 @@ mod tests { } #[test] - fn expands_struct_with_where_bounds_on_definition() { + fn expands_type_abilities_into_impl_where_bounds() { let args: MoveStructArgs = syn::parse_quote!(address = "0x1", module = "m", type_abilities = "T0: store"); @@ -148,22 +147,38 @@ mod tests { 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 + let move_struct_impl = file .items .iter() .find_map(|item| match item { - syn::Item::Struct(s) if s.ident == syn::Ident::new("S", Span::call_site()) => { - Some(s) + syn::Item::Impl(imp) => { + let Some((_, trait_path, _)) = &imp.trait_ else { + return None; + }; + if !trait_path + .segments + .last() + .is_some_and(|seg| seg.ident == "MoveStruct") + { + return None; + } + let syn::Type::Path(self_ty) = imp.self_ty.as_ref() else { + return None; + }; + if self_ty.path.segments.last()?.ident != "S" { + return None; + } + Some(imp) } _ => None, }) - .expect("struct S in output"); + .expect("impl MoveStruct for S in output"); - let where_clause = struct_item + let where_clause = move_struct_impl .generics .where_clause .as_ref() - .expect("where clause on struct definition"); + .expect("where clause on impl"); let t0_bounds = where_clause .predicates From 46f0766b303ac4d946e68a6684c364b2ed278c69 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 5 Feb 2026 14:18:57 +0100 Subject: [PATCH 8/9] fix: small fix in pacakge ID --- sui-move-codegen/src/render/types.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sui-move-codegen/src/render/types.rs b/sui-move-codegen/src/render/types.rs index 6c30023..eb5219d 100644 --- a/sui-move-codegen/src/render/types.rs +++ b/sui-move-codegen/src/render/types.rs @@ -68,6 +68,7 @@ fn render_struct( let phantoms = phantom_params_string(dt); let type_abilities = type_abilities_string(dt); + let address_lit = syn::LitStr::new(address, proc_macro2::Span::call_site()); let module_lit = syn::LitStr::new(module, proc_macro2::Span::call_site()); let name_lit = syn::LitStr::new(move_name, proc_macro2::Span::call_site()); let abilities_lit = abilities @@ -109,7 +110,7 @@ fn render_struct( quote! { #doc #[#macro_path( - address = PACKAGE, + address = #address_lit, module = #module_lit, #name_arg #abilities_arg @@ -268,6 +269,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { quote! { sui_move } }; + let address = syn::LitStr::new(&dt.type_name.address, proc_macro2::Span::call_site()); let module = syn::LitStr::new(&dt.type_name.module, proc_macro2::Span::call_site()); let name = syn::LitStr::new(&dt.type_name.name, proc_macro2::Span::call_site()); @@ -278,7 +280,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { quote! { #sm::__private::sui_sdk_types::StructTag::new( - PACKAGE, + #sm::parse_address(#address).expect("invalid address literal"), #sm::parse_identifier(#module).expect("invalid module"), #sm::parse_identifier(#name).expect("invalid struct name"), vec![#(#ty_params_for_tag),*], From d335f36e5803d1152a2092af3b78f306c8cbc638 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 5 Feb 2026 14:57:34 +0100 Subject: [PATCH 9/9] feat: runtime pacakge update --- sui-move-codegen/src/render/calls.rs | 8 +++- sui-move-codegen/src/render/types.rs | 20 ++++++++-- sui-move-codegen/src/render/util.rs | 60 +++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index c0facb4..7054315 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -51,6 +51,12 @@ fn render_function( let module_name = syn::LitStr::new(&module.name, proc_macro2::Span::call_site()); let function_name = syn::LitStr::new(&f.name, proc_macro2::Span::call_site()); + let package_expr = if opts.flatten { + quote! { __sui_move_bindings::call_package() } + } else { + quote! { super::__sui_move_bindings::call_package() } + }; + let push_type_args = type_params .iter() .map(|ty| quote! { spec.push_type_arg::<#ty>(); }); @@ -79,7 +85,7 @@ fn render_function( pub fn #fn_ident #fn_generics ( #(#params),* ) -> #sm_call::CallSpec #where_clause { - let mut spec = #sm_call::CallSpec::new(PACKAGE, #module_name, #function_name) + let mut spec = #sm_call::CallSpec::new(#package_expr, #module_name, #function_name) .expect("valid Move identifiers"); #(#push_type_args)* #(#pushes)* diff --git a/sui-move-codegen/src/render/types.rs b/sui-move-codegen/src/render/types.rs index eb5219d..4496c84 100644 --- a/sui-move-codegen/src/render/types.rs +++ b/sui-move-codegen/src/render/types.rs @@ -107,10 +107,16 @@ fn render_struct( quote! { sui_move::move_struct } }; + let address_expr = if opts.flatten { + quote! { __sui_move_bindings::local_type_address(::sui_move::prelude::Address::from_static(#address_lit)) } + } else { + quote! { super::__sui_move_bindings::local_type_address(::sui_move::prelude::Address::from_static(#address_lit)) } + }; + quote! { #doc #[#macro_path( - address = #address_lit, + address = #address_expr, module = #module_lit, #name_arg #abilities_arg @@ -146,7 +152,7 @@ fn render_enum( } else { quote! { sui_move } }; - let struct_tag_builder = struct_tag_builder_tokens(dt, opts.use_aliases); + let struct_tag_builder = struct_tag_builder_tokens(dt, opts.use_aliases, opts.flatten); let where_clause = where_clause(&bounds); let derives = enum_derives(&dt.abilities, opts.use_aliases); @@ -262,13 +268,19 @@ fn ability_impls_for_datatype( quote! { #(#out)* } } -fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { +fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool, flatten: bool) -> TokenStream { let sm = if use_aliases { quote! { sm } } else { quote! { sui_move } }; + let helper = if flatten { + quote! { __sui_move_bindings::local_type_address } + } else { + quote! { super::__sui_move_bindings::local_type_address } + }; + let address = syn::LitStr::new(&dt.type_name.address, proc_macro2::Span::call_site()); let module = syn::LitStr::new(&dt.type_name.module, proc_macro2::Span::call_site()); let name = syn::LitStr::new(&dt.type_name.name, proc_macro2::Span::call_site()); @@ -280,7 +292,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { quote! { #sm::__private::sui_sdk_types::StructTag::new( - #sm::parse_address(#address).expect("invalid address literal"), + #helper(#sm::prelude::Address::from_static(#address)), #sm::parse_identifier(#module).expect("invalid module"), #sm::parse_identifier(#name).expect("invalid struct name"), vec![#(#ty_params_for_tag),*], diff --git a/sui-move-codegen/src/render/util.rs b/sui-move-codegen/src/render/util.rs index 21a45a1..dc12dc4 100644 --- a/sui-move-codegen/src/render/util.rs +++ b/sui-move-codegen/src/render/util.rs @@ -134,7 +134,6 @@ pub(crate) fn render_module_file( quote! { #aliases - use super::PACKAGE; #(#items)* } } @@ -167,7 +166,6 @@ pub(crate) fn render_module( quote! { pub mod #module_ident { #aliases - use super::PACKAGE; #(#items)* } } @@ -181,6 +179,64 @@ fn package_const_tokens(pkg: &NormalizedPackage, opts: &RenderOptions) -> TokenS quote! { /// Package address (the on-chain package object id). pub const PACKAGE: #address_ty = #address_ty::from_static(#addr); + + /// Internal address helpers for this bindings module. + /// + /// This is a small escape hatch for two common deployment patterns: + /// - upgrade: new package object id (`storage_id`), but types keep their defining ids → + /// override only [`__sui_move_bindings::call_package`]. + /// - republish: same code published as a brand-new package (new `original_id`) → + /// override both calls and local type addresses via [`__sui_move_bindings::republish_to`]. + #[doc(hidden)] + pub mod __sui_move_bindings { + use super::PACKAGE; + + type Address = #address_ty; + + static CALL_PACKAGE_OVERRIDE: ::std::sync::OnceLock
= + ::std::sync::OnceLock::new(); + static LOCAL_TYPE_ADDRESS_OVERRIDE: ::std::sync::OnceLock
= + ::std::sync::OnceLock::new(); + + /// Package address used for generated call stubs. + /// + /// Defaults to [`PACKAGE`]. Can be overridden once via [`set_call_package`]. + #[must_use] + pub fn call_package() -> Address { + CALL_PACKAGE_OVERRIDE.get().copied().unwrap_or(PACKAGE) + } + + /// Override the package address used for generated call stubs. + /// + /// Use this for upgrades (same `original_id`, new `storage_id`). + pub fn set_call_package(addr: Address) -> Result<(), Address> { + CALL_PACKAGE_OVERRIDE.set(addr) + } + + /// Override the address used for local type tags. + /// + /// Use this for republishing the same code as a new package id. + pub fn set_local_type_address_override(addr: Address) -> Result<(), Address> { + LOCAL_TYPE_ADDRESS_OVERRIDE.set(addr) + } + + /// Convenience for the republish case: retarget both calls and local type tags. + pub fn republish_to(addr: Address) -> Result<(), Address> { + set_call_package(addr)?; + set_local_type_address_override(addr)?; + Ok(()) + } + + /// Resolve the defining address for a local type. + /// + /// When republishing the same code, you can override all local type addresses by + /// calling [`set_local_type_address_override`]. For upgrades, keep the default per-type + /// defining id. + #[must_use] + pub fn local_type_address(default: Address) -> Address { + LOCAL_TYPE_ADDRESS_OVERRIDE.get().copied().unwrap_or(default) + } + } } }