diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e8453..ad915cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,5 +22,10 @@ The format is based on [Keep a Changelog], and this project adheres to - ergonomic transaction macro (`sui_move_runtime::tx!`). - Add `sui-move-codegen`: fetch + normalize package metadata (`NormalizedPackage`) and render typed Rust bindings (Move-shaped types + `CallSpec` builders). +### Changed + +- Generated package scopes now resolve type identity by module and datatype while calls continue to + use the current package address. + [Keep a Changelog]: https://keepachangelog.com/en/1.1.0/ [Semantic Versioning]: https://semver.org/spec/v2.0.0.html diff --git a/Cargo.toml b/Cargo.toml index cfd39d9..4020b3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,9 @@ serde_json = "1.0" thiserror = "1.0" bcs = "0.1.6" ethnum = "1.5.3" -sui-sdk-types = { version = "0.3.1", features = ["serde"] } -sui-rpc = { version = "0.3.1" } -sui-crypto = { version = "0.3.0" } +sui-sdk-types = { version = "0.3.2", features = ["serde"] } +sui-rpc = { version = "0.3.2" } +sui-crypto = { version = "0.3.1" } proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full"] } diff --git a/sui-move-codegen/README.md b/sui-move-codegen/README.md index e1ab513..777a6cc 100644 --- a/sui-move-codegen/README.md +++ b/sui-move-codegen/README.md @@ -68,7 +68,8 @@ documentation and converts reserved Rust identifiers safely in generated APIs. Given a `NormalizedPackage` (either fetched from gRPC or loaded from JSON), this crate can render: - `CALL_PACKAGE` / `TYPE_PACKAGE` constants for generated calls and type identity -- `call_package()` / `type_package()` / `with_packages(...)` helpers for scoped package-id overrides +- `call_package()` / `type_package()` / `with_packages(...)` helpers for scoped package address overrides +- `type_package_for(...)` / `with_package_context(...)` helpers for exact datatype origins - One Rust module per Move module (or a flat layout via `RenderOptions::flatten`) - Move datatypes as Rust types (structs use `#[sui_move::move_struct]` via `sui-move`’s `derive` feature) @@ -102,6 +103,94 @@ bindings::with_packages(localnet_package, localnet_package, || { }); ``` +An upgraded package can contain datatypes that originate in different package versions. Calls must +target the current package, while each type tag must retain the package where that datatype was +first defined. Generated call builders resolve their target through `call_package()`, and each +generated datatype resolves its type identity through `type_package_for(module, datatype)`. Use +`with_package_context` to scope both lookups to one release: + +```rust +# use std::collections::BTreeMap; +# use sui_sdk_types::Address; +# mod bindings { +# use std::collections::BTreeMap; +# use sui_sdk_types::Address; +# pub type TypeOrigins = BTreeMap>; +# #[derive(Clone)] +# struct PackageContext { +# call_package: Address, +# fallback_type_package: Address, +# origins: TypeOrigins, +# } +# std::thread_local! { +# static CONTEXT: std::cell::RefCell> = +# const { std::cell::RefCell::new(None) }; +# } +# pub fn with_package_context( +# call_package: Address, +# fallback_type_package: Address, +# origins: &TypeOrigins, +# f: impl FnOnce() -> R, +# ) -> R { +# struct Reset(Option); +# impl Drop for Reset { +# fn drop(&mut self) { +# CONTEXT.with(|slot| { +# slot.replace(self.0.take()); +# }); +# } +# } +# let previous = CONTEXT.with(|slot| { +# slot.replace(Some(PackageContext { +# call_package, +# fallback_type_package, +# origins: origins.clone(), +# })) +# }); +# let _reset = Reset(previous); +# f() +# } +# pub fn call_package() -> Address { +# CONTEXT.with(|slot| slot.borrow().as_ref().unwrap().call_package) +# } +# pub fn type_package_for(module: &str, datatype: &str) -> Address { +# CONTEXT.with(|slot| { +# let context = slot.borrow(); +# let context = context.as_ref().unwrap(); +# context +# .origins +# .get(module) +# .and_then(|datatypes| datatypes.get(datatype)) +# .copied() +# .unwrap_or(context.fallback_type_package) +# }) +# } +# } +let initial_package = Address::from_static("0xa1"); +let upgraded_package = Address::from_static("0xa2"); +let current_package = Address::from_static("0xa3"); +let origins = BTreeMap::from([( + "agent".to_owned(), + BTreeMap::from([ + ("Agent".to_owned(), initial_package), + ("AgentStateV2".to_owned(), upgraded_package), + ]), +)]); + +let (call_package, agent_type_package, state_v2_type_package) = + bindings::with_package_context(current_package, initial_package, &origins, || { + ( + bindings::call_package(), + bindings::type_package_for("agent", "Agent"), + bindings::type_package_for("agent", "AgentStateV2"), + ) + }); + +assert_eq!(call_package, current_package); +assert_eq!(agent_type_package, initial_package); +assert_eq!(state_v2_type_package, upgraded_package); +``` + ## Example: render from an in-memory IR ```rust diff --git a/sui-move-codegen/src/render/mod.rs b/sui-move-codegen/src/render/mod.rs index 3e68f78..112c9d3 100644 --- a/sui-move-codegen/src/render/mod.rs +++ b/sui-move-codegen/src/render/mod.rs @@ -733,7 +733,7 @@ mod tests { assert!(!code.contains("pub fn package()")); assert!(!code.contains("pub fn with_package")); assert!(code.contains("CallTarget::new(call_package(), \"m\", \"mutate\")")); - assert!(code.contains("address_fn = \"super::type_package\"")); + assert!(code.contains("address_fn = \"__type_package_for_Obj\"")); } #[test] @@ -746,7 +746,18 @@ mod tests { assert!(code.contains("pub fn type_package() -> sui_move::prelude::Address")); assert!(code.contains("pub fn with_packages")); assert!(code.contains("CallTarget::new(call_package(), \"m\", \"mutate\")")); - assert!(code.contains("address_fn = \"super::type_package\"")); + assert!(code.contains("address_fn = \"__type_package_for_Obj\"")); + } + + #[test] + fn generated_bindings_resolve_each_datatype_origin() { + let code = render_package(&demo_pkg(), &RenderOptions::default()); + + assert!(code.contains("pub type TypeOrigins")); + assert!(code.contains("pub fn type_package_for(")); + assert!(code.contains("pub fn with_package_context")); + assert!(code.contains("type_package_for(\"m\", \"Obj\")")); + assert!(code.contains("type_package_for(\"m\", \"Pair\")")); } #[test] @@ -973,7 +984,7 @@ mod tests { let module = parts.modules.get("m").expect("module body"); assert!(module.contains("pub mod m")); - assert!(module.contains("use super::{call_package, type_package};")); + assert!(module.contains("use super::{call_package, type_package_for};")); assert!(module.contains("pub struct Obj")); assert!(module.contains("pub fn mutate")); } diff --git a/sui-move-codegen/src/render/types.rs b/sui-move-codegen/src/render/types.rs index b25836b..cefa266 100644 --- a/sui-move-codegen/src/render/types.rs +++ b/sui-move-codegen/src/render/types.rs @@ -20,17 +20,37 @@ pub(crate) fn render_datatype( pkg: &NormalizedPackage, opts: &RenderOptions, ) -> TokenStream { + let address_function = render_datatype_address_function(dt); let datatype = match &dt.kind { DatatypeKind::Struct { fields } => render_struct(dt, fields, pkg, opts), DatatypeKind::Enum { variants } => render_enum(dt, variants, pkg, opts), }; let helpers = render_canonical_helpers(dt, pkg, opts); quote! { + #address_function #datatype #helpers } } +fn render_datatype_address_function(dt: &Datatype) -> TokenStream { + let function = datatype_address_function_ident(dt); + let module = syn::LitStr::new(&dt.type_name.module, proc_macro2::Span::call_site()); + let datatype = syn::LitStr::new(&dt.type_name.name, proc_macro2::Span::call_site()); + + quote! { + #[doc(hidden)] + #[allow(non_snake_case)] + fn #function() -> sui_move::prelude::Address { + type_package_for(#module, #datatype) + } + } +} + +fn datatype_address_function_ident(dt: &Datatype) -> syn::Ident { + format_ident!("__type_package_for_{}", dt.name) +} + pub(crate) fn render_type_ref_in_module( ty: &TypeRef, current_module: &str, @@ -89,11 +109,10 @@ fn render_struct( let abilities_arg = abilities_lit.map(|lit| quote! { abilities = #lit, }); let phantoms_arg = phantoms_lit.map(|lit| quote! { phantoms = #lit, }); let type_abilities_arg = type_abilities_lit.map(|lit| quote! { type_abilities = #lit, }); - let address_fn = if opts.flatten { - syn::LitStr::new("type_package", proc_macro2::Span::call_site()) - } else { - syn::LitStr::new("super::type_package", proc_macro2::Span::call_site()) - }; + let address_fn = syn::LitStr::new( + &datatype_address_function_ident(dt).to_string(), + proc_macro2::Span::call_site(), + ); let fields_tokens = fields.iter().map(|f| { let ident = idents::ident(&f.name); @@ -973,6 +992,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { 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()); + let address_function = datatype_address_function_ident(dt); let type_params = type_params_idents(dt.type_parameters.len()); let ty_params_for_tag = type_params @@ -981,7 +1001,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { quote! { #sm::__private::sui_sdk_types::StructTag::new( - type_package(), + #address_function(), #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 e98a2ea..75f9c1d 100644 --- a/sui-move-codegen/src/render/util.rs +++ b/sui-move-codegen/src/render/util.rs @@ -104,7 +104,7 @@ pub(crate) fn render_module_file( quote! { #aliases - use super::{call_package, type_package}; + use super::{call_package, type_package_for}; #(#items)* } } @@ -136,7 +136,7 @@ pub(crate) fn render_module( quote! { pub mod #module_ident { #aliases - use super::{call_package, type_package}; + use super::{call_package, type_package_for}; #(#items)* } } @@ -164,11 +164,26 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream { let _ = opts; let address_ty = quote! { sui_move::prelude::Address }; quote! { + /// Exact datatype origins grouped by module and datatype name. + pub type TypeOrigins = std::collections::BTreeMap< + String, + std::collections::BTreeMap, + >; + + /// Scoped datatype identity for one generated package. + /// + /// A single package address cannot represent datatypes introduced by + /// different versions of the same Sui package. + struct TypeScope { + fallback: #address_ty, + origins: TypeOrigins, + } + std::thread_local! { static CALL_PACKAGE_OVERRIDE: std::cell::Cell> = std::cell::Cell::new(None); - static TYPE_PACKAGE_OVERRIDE: std::cell::Cell> = - std::cell::Cell::new(None); + static TYPE_SCOPE_OVERRIDE: std::cell::RefCell> = + const { std::cell::RefCell::new(None) }; } /// Current call package address for this generated binding. @@ -184,7 +199,30 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream { /// Returns the scoped override set by [`with_type_package`] or [`with_packages`], or /// [`TYPE_PACKAGE`] when no override is active. pub fn type_package() -> #address_ty { - TYPE_PACKAGE_OVERRIDE.with(|slot| slot.get().unwrap_or(TYPE_PACKAGE)) + TYPE_SCOPE_OVERRIDE.with(|slot| { + slot.borrow() + .as_ref() + .map(|scope| scope.fallback) + .unwrap_or(TYPE_PACKAGE) + }) + } + + /// Resolve the defining package for one Move datatype. + /// + /// The exact origin set by [`with_type_origins`] takes precedence over + /// the current fallback returned by [`type_package`]. + pub fn type_package_for(module: &str, datatype: &str) -> #address_ty { + TYPE_SCOPE_OVERRIDE.with(|slot| { + let scope = slot.borrow(); + let Some(scope) = scope.as_ref() else { + return TYPE_PACKAGE; + }; + scope.origins + .get(module) + .and_then(|datatypes| datatypes.get(datatype)) + .copied() + .unwrap_or(scope.fallback) + }) } /// Run a closure with this generated binding scoped to `package` for Move calls. @@ -212,18 +250,32 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream { /// /// The previous type package override is restored when the closure returns or unwinds. pub fn with_type_package(package: #address_ty, f: impl FnOnce() -> R) -> R { - struct Reset(Option<#address_ty>); + with_type_origins(package, &TypeOrigins::new(), f) + } + + /// Run a closure with a fallback package and exact datatype origins. + /// + /// The previous type scope is restored when the closure returns or unwinds. + pub fn with_type_origins( + fallback: #address_ty, + origins: &TypeOrigins, + f: impl FnOnce() -> R, + ) -> R { + struct Reset(Option); impl Drop for Reset { fn drop(&mut self) { - TYPE_PACKAGE_OVERRIDE.with(|slot| slot.set(self.0)); + TYPE_SCOPE_OVERRIDE.with(|slot| { + slot.replace(self.0.take()); + }); } } - let previous = TYPE_PACKAGE_OVERRIDE.with(|slot| { - let previous = slot.get(); - slot.set(Some(package)); - previous + let previous = TYPE_SCOPE_OVERRIDE.with(|slot| { + slot.replace(Some(TypeScope { + fallback, + origins: origins.clone(), + })) }); let _reset = Reset(previous); f() @@ -240,6 +292,21 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream { ) -> R { with_call_package(call_package, || with_type_package(type_package, f)) } + + /// Run a closure with the current call package and exact datatype origins. + /// + /// Use this when one package contains datatypes introduced by several + /// package versions. + pub fn with_package_context( + call_package: #address_ty, + fallback_type_package: #address_ty, + origins: &TypeOrigins, + f: impl FnOnce() -> R, + ) -> R { + with_call_package(call_package, || { + with_type_origins(fallback_type_package, origins, f) + }) + } } }