From ecea8ac4fe4b6799cbdf9f11757d8b8a64de3499 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2026 18:53:34 +0200 Subject: [PATCH 1/6] feat: boostraped move --- CHANGELOG.md | 3 +- sui-move-call/README.md | 14 +- sui-move-call/src/lib.rs | 20 +- sui-move-call/tests/basic.rs | 12 +- sui-move-codegen/src/render/builtins.rs | 60 +---- sui-move-codegen/src/render/mod.rs | 7 + sui-move-derive/README.md | 28 ++- sui-move-derive/src/args.rs | 2 +- sui-move-derive/src/lib.rs | 22 +- sui-move-derive/src/util.rs | 7 +- sui-move-ptb/README.md | 28 ++- sui-move-ptb/tests/basic.rs | 12 +- sui-move-runtime/README.md | 79 +++++-- sui-move-runtime/src/handles.rs | 42 +++- sui-move/README.md | 35 +-- sui-move/examples/derive_struct.rs | 25 ++- sui-move/examples/tag_checked_decode.rs | 60 +++-- sui-move/examples/type_tags.rs | 37 +++- sui-move/src/containers.rs | 159 -------------- sui-move/src/decode.rs | 48 ++-- sui-move/src/lib.rs | 22 +- 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 | 21 -- sui-move/src/primitives/object_bag.rs | 39 ---- sui-move/src/primitives/object_table.rs | 68 ------ 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 | 279 +++++++----------------- sui-move/tests/derive.rs | 97 ++++---- 38 files changed, 506 insertions(+), 1328 deletions(-) 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/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..82e8453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ 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. +- Keep Move framework declarations out of the `sui-move` core so generated package bindings define + `UID`, `Coin`, `Balance`, containers, and other framework shapes. - 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..e4e58ca 100644 --- a/sui-move-call/README.md +++ b/sui-move-call/README.md @@ -73,9 +73,14 @@ 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 = "store")] +pub struct UID { + pub id: u64, +} + #[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 +117,14 @@ 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 = "store")] +struct UID { + id: u64, +} + #[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..26568dd 100644 --- a/sui-move-call/src/lib.rs +++ b/sui-move-call/src/lib.rs @@ -31,9 +31,12 @@ 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 = "store")] +/// # struct UID { id: u64 } +/// # /// #[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 +85,12 @@ impl MoveObject { /// use sui_move_call::SharedMoveObject; /// use sui_sdk_types::Address; /// +/// # #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// # struct UID { id: u64 } +/// # /// #[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 +174,12 @@ impl SharedMoveObject { /// use sui_move_call::ReceivingMoveObject; /// use sui_sdk_types::{Address, Digest, ObjectReference}; /// +/// # #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// # struct UID { id: u64 } +/// # /// #[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 +378,12 @@ 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 = "store")] +/// # struct UID { id: u64 } +/// # /// #[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..f0443f3 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, 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 index d9efe4f..784f7d0 100644 --- a/sui-move-codegen/src/render/builtins.rs +++ b/sui-move-codegen/src/render/builtins.rs @@ -1,7 +1,10 @@ -//! Mapping for common Sui framework types. +//! Mapping for irreducible built-in Move types. +//! +//! Sui framework types such as `0x2::object::UID`, `0x2::coin::Coin`, and +//! `0x1::option::Option` are deliberately not mapped here. They are package-defined datatypes and +//! must be generated from package metadata like any other Move package. use proc_macro2::TokenStream; -use quote::quote; use crate::ir::TypeName; @@ -15,55 +18,6 @@ pub(crate) struct BuiltinDatatype { } 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), - ("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", "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", "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 } - } + let _ = (type_name, use_aliases); + None } diff --git a/sui-move-codegen/src/render/mod.rs b/sui-move-codegen/src/render/mod.rs index 249f4b3..0857803 100644 --- a/sui-move-codegen/src/render/mod.rs +++ b/sui-move-codegen/src/render/mod.rs @@ -209,6 +209,13 @@ mod tests { assert!(code.contains("#[sui_move::move_struct")); } + #[test] + fn external_framework_types_are_not_mapped_to_sui_move_core() { + let code = render_package(&demo_pkg(), &RenderOptions::default()); + assert!(!code.contains("sm::types::UID")); + assert!(code.contains("unknown external type `0x2::object::UID`")); + } + #[test] fn renders_tx_ext_trait_when_enabled() { let opts = RenderOptions { diff --git a/sui-move-derive/README.md b/sui-move-derive/README.md index 46bbc33..00483a7 100644 --- a/sui-move-derive/README.md +++ b/sui-move-derive/README.md @@ -107,14 +107,30 @@ You can satisfy those requirements either by: ## Examples -### `key` objects require `id: UID` +### `key` objects require package-defined `id: UID` ```rust,no_run use std::marker::PhantomData; use sui_move::prelude::Address; -use sui_move::types::{ID, UID}; use sui_move_derive::move_struct; +/// Local package declaration for `0x2::object::ID`. +/// +/// Framework types are ordinary Move declarations from the type-kernel perspective. In production +/// this shape should come from generated package bindings rather than from `sui-move` itself. +#[move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +pub struct ID { + pub bytes: Address, +} + +/// Local package declaration for `0x2::object::UID`. +/// +/// A `key` object is recognized by an `id` field whose type is a package-defined `UID` shape. +#[move_struct(address = "0x2", module = "object", abilities = "store")] +pub struct UID { + pub id: ID, +} + #[move_struct( address = "0x1", module = "vault", @@ -157,9 +173,15 @@ Similarly, invalid ability combinations are rejected: ```rust,compile_fail use sui_move_derive::move_struct; +/// Minimal package-defined UID fixture for the compile-fail example. +#[move_struct(address = "0x2", module = "object", abilities = "store")] +pub struct UID { + pub id: u64, +} + // A struct cannot be both `key` and `copy`. #[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..f620f20 100644 --- a/sui-move-derive/src/args.rs +++ b/sui-move-derive/src/args.rs @@ -102,7 +102,7 @@ impl Parse for MoveStructArgs { 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\"", + "uid_type must be a valid Rust type path, e.g., \"crate::object::UID\"", ) })?; args.uid_type = Some(ty); diff --git a/sui-move-derive/src/lib.rs b/sui-move-derive/src/lib.rs index 63b69e2..1bee176 100644 --- a/sui-move-derive/src/lib.rs +++ b/sui-move-derive/src/lib.rs @@ -52,15 +52,33 @@ pub fn move_module(_args: TokenStream, input: TokenStream) -> TokenStream { /// - `abilities = "key, store, copy, drop"` (optional): comma-separated Move abilities /// - `phantoms = "T, U"` (optional): comma-separated phantom type params /// - `type_abilities = "T: store, copy; U: drop"` (optional): ability expectations for type params -/// - `uid_type = "path::to::UID"` (optional): override what counts as `UID` for `key` enforcement +/// - `uid_type = "path::to::UID"` (optional): override what package-defined type counts as `UID` +/// for `key` enforcement /// /// # Example /// ```rust,no_run /// use std::marker::PhantomData; /// use sui_move::prelude::Address; -/// use sui_move::types::{ID, UID}; /// use sui_move_derive::move_struct; /// +/// /// Local package declaration for `0x2::object::ID`. +/// /// +/// /// Framework types are ordinary Move declarations from the type-kernel perspective. In +/// /// production this shape should come from generated package bindings rather than from +/// /// `sui-move` itself. +/// #[move_struct(address = "0x2", module = "object", abilities = "copy, drop, store")] +/// pub struct ID { +/// pub bytes: Address, +/// } +/// +/// /// Local package declaration for `0x2::object::UID`. +/// /// +/// /// A `key` object is recognized by an `id` field whose type is a package-defined `UID` shape. +/// #[move_struct(address = "0x2", module = "object", abilities = "store")] +/// pub struct UID { +/// pub id: ID, +/// } +/// /// #[move_struct( /// address = "0x1", /// module = "vault", diff --git a/sui-move-derive/src/util.rs b/sui-move-derive/src/util.rs index c2780bc..1efb2bb 100644 --- a/sui-move-derive/src/util.rs +++ b/sui-move-derive/src/util.rs @@ -32,7 +32,12 @@ pub(crate) fn has_phantom_attr(attrs: &[Attribute]) -> bool { }) } -/// Whether a field is the `id: UID` field used for `key` objects. +/// Whether a field is the package-defined `id: UID` field required by `key` objects. +/// +/// `UID` is intentionally recognized structurally, by field name and final path segment, because +/// the framework declaration is not a core `sui-move` type. Generated framework bindings and local +/// package fixtures should therefore satisfy the default rule without coupling this derive crate to a +/// handwritten core UID mirror. pub(crate) fn is_uid_field(field: &Field, uid_override: Option<&syn::Type>) -> bool { let has_id_name = field.ident.as_ref().map(|i| i == "id").unwrap_or(false); if !has_id_name { diff --git a/sui-move-ptb/README.md b/sui-move-ptb/README.md index 777b072..a943366 100644 --- a/sui-move-ptb/README.md +++ b/sui-move-ptb/README.md @@ -71,9 +71,14 @@ 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 = "store")] +struct UID { + id: u64, +} + #[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 +113,14 @@ 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 = "store")] +struct UID { + id: u64, +} + #[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 +158,14 @@ 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 = "store")] +struct UID { + id: u64, +} + #[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 +202,14 @@ 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 = "store")] +struct UID { + id: u64, +} + #[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..e6b4e6f 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, 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..d9dabec 100644 --- a/sui-move-runtime/README.md +++ b/sui-move-runtime/README.md @@ -23,7 +23,6 @@ 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}; # #[derive(Clone)] @@ -36,11 +35,19 @@ use sui_sdk_types::{Address, PersonalMessage, Transaction, UserSignature}; # unimplemented!("provide a real signer (keypair, wallet, kms, ...)") # } # } +# +# #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +# struct UID { id: u64 } +# +# #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +# struct Demo { +# id: UID, +# } -fn touch_coin(coin: &impl ToCallArg, amount: u64) -> CallSpec { +fn touch_object(object: &impl ToCallArg, amount: u64) -> CallSpec { let package: Address = "0x1".parse().unwrap(); let mut spec = CallSpec::new(package, "demo", "touch").unwrap(); - spec.push_arg(coin).unwrap(); + spec.push_arg(object).unwrap(); spec.push_arg(&amount).unwrap(); spec } @@ -49,24 +56,24 @@ async fn demo() -> Result<(), Error> { let client = sui_rpc::Client::new(sui_rpc::Client::TESTNET_FULLNODE).unwrap(); let signer = DummySigner; let sender: Address = "0x123".parse().unwrap(); - let coin_id: Address = "0x2".parse().unwrap(); + let object_id: Address = "0x2".parse().unwrap(); let mut rt = Runtime::new(client, signer); // Read: fetch a typed runtime-owned handle. - let coin: Object> = rt.read().object::>(coin_id).await?; + let object: Object = rt.read().object::(object_id).await?; // Tx: one-shot build + commit with the `tx!` macro. let receipt = sui_move_runtime::tx!(&mut rt, sender => { - touch_coin(&coin, 10); + touch_object(&object, 10); }) .await?; // On-chain execution failures are recorded in the receipt (they are not transport errors). receipt.ensure_success()?; - // Back in Read: `coin`'s `ObjectReference` has been updated internally. - let _latest_ref = coin.reference(); + // Back in Read: `object`'s `ObjectReference` has been updated internally. + let _latest_ref = object.reference(); Ok(()) } # let _ = demo; @@ -97,7 +104,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 = "store")] +# struct UID { id: u64 } +# +# #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +# struct Demo { id: UID } +# +# async fn demo(mut rt: Runtime, sender: Address, object: Object) -> Result<(), Error> { let _sim = sui_move_runtime::tx!(simulate, &mut rt, sender => { CallSpec::new("0x1".parse().unwrap(), "m", "f").unwrap(); }) @@ -107,7 +120,7 @@ let _dbg = sui_move_runtime::tx!(inspect, &mut rt, sender => { CallSpec::new("0x1".parse().unwrap(), "m", "f").unwrap(); }) .await?; -# let _ = coin; +# let _ = object; # Ok(()) # } ``` @@ -171,9 +184,14 @@ 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 = "store")] +struct UID { + id: u64, +} + #[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> { @@ -323,15 +341,25 @@ 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; +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: u64, +} + +#[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +struct Demo { + id: UID, + value: u64, +} + # async fn demo(mut rt: Runtime) -> Result<(), Error> { -let coin_id: Address = "0x2".parse().unwrap(); +let object_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?; +let (object, value): (Object, Demo) = rt.read().get(object_id).await?; +let _latest: Demo = rt.read().decode(&object).await?; +let _unchecked: Demo = rt.read().decode_unchecked(&object).await?; # let _ = value; # Ok(()) # } @@ -390,26 +418,35 @@ threading `&mut ObjectReference` everywhere. ```rust,no_run use sui_move_runtime::prelude::*; -use sui_move::{coin::Coin, sui::SUI}; + +#[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +struct UID { + id: u64, +} + +#[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key")] +struct Demo { + id: UID, +} #[derive(Clone)] struct Wallet { - coin: Object>, + object: Object, } # async fn demo(mut rt: Runtime, sender: sui_sdk_types::Address) -> Result<(), Error> { let wallet = Wallet { - coin: rt.read().object("0x2".parse().unwrap()).await?, + object: rt.read().object("0x2".parse().unwrap()).await?, }; let ptb = sui_move_ptb::ptb! { - // any call that mutates `wallet.coin` on-chain + // any call that mutates `wallet.object` on-chain CallSpec::new("0x1".parse().unwrap(), "m", "f").unwrap(); }?; rt.tx(sender).commit_ptb(ptb).await?; // The `ObjectReference` is refreshed internally after commit. -let _latest = wallet.coin.reference(); +let _latest = wallet.object.reference(); # Ok(()) # } ``` diff --git a/sui-move-runtime/src/handles.rs b/sui-move-runtime/src/handles.rs index 3fe52e3..7dcf9c5 100644 --- a/sui-move-runtime/src/handles.rs +++ b/sui-move-runtime/src/handles.rs @@ -104,19 +104,27 @@ struct TrackedObjectSnapshot { /// # Example /// ```rust,no_run /// use sui_move_runtime::prelude::*; -/// use sui_move::{coin::Coin, sui::SUI}; /// -/// fn touch(coin: &impl ToCallArg) -> CallSpec { +/// # #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// # struct UID { +/// # id: u64, +/// # } +/// # #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key, store")] +/// # struct Demo { +/// # id: UID, +/// # } +/// +/// fn touch(object: &impl ToCallArg) -> CallSpec { /// let package: sui_sdk_types::Address = "0x1".parse().unwrap(); /// let mut spec = CallSpec::new(package, "demo", "touch").unwrap(); -/// spec.push_arg(coin).unwrap(); +/// spec.push_arg(object).unwrap(); /// spec /// } /// /// # async fn demo(mut rt: Runtime, sender: sui_sdk_types::Address) -> Result<(), Error> { -/// let coin: Object> = rt.read().object("0x2".parse().unwrap()).await?; +/// let object: Object = rt.read().object("0x2".parse().unwrap()).await?; /// let mut tx = rt.tx(sender); -/// tx.call(touch(&coin))?; +/// tx.call(touch(&object))?; /// tx.commit().await?; /// # Ok(()) /// # } @@ -383,13 +391,21 @@ 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; /// +/// # #[sui_move::move_struct(address = "0x2", module = "object", abilities = "store")] +/// # struct UID { +/// # id: u64, +/// # } +/// # #[sui_move::move_struct(address = "0x1", module = "demo", abilities = "key, store")] +/// # struct Demo { +/// # id: UID, +/// # } +/// /// let package: Address = "0x1".parse().unwrap(); -/// let shared = SharedObject::>::mutable("0x2".parse().unwrap(), 1); +/// let shared = SharedObject::::mutable("0x2".parse().unwrap(), 1); /// /// let mut spec = CallSpec::new(package, "m", "f").unwrap(); /// spec.push_arg(&shared).unwrap(); @@ -629,9 +645,19 @@ mod tests { ObjectOut, Owner, TransactionEffects, TransactionEffectsV2, }; + #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, 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/README.md b/sui-move/README.md index 6403660..431403f 100644 --- a/sui-move/README.md +++ b/sui-move/README.md @@ -76,9 +76,14 @@ structs. mod example { use sui_move::move_struct; + #[move_struct(address = "0x2", module = "object", abilities = "store")] + pub struct UID { + pub id: u64, + } + #[move_struct(address = "0x1", module = "vault", abilities = "key, store")] pub struct Vault { - pub id: sui_move::types::UID, + pub id: UID, pub value: u64, } } @@ -134,27 +139,25 @@ The crate implements `MoveType` (and ability markers) for: - `sui_sdk_types::Address` - `Vec` where `T: MoveType` -### Sui framework types (`sui_move::primitives`) +### Framework types are not core -`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). +`sui-move` intentionally does not export handwritten mirrors for Sui framework packages such as +`0x2::object::UID`, `0x2::coin::Coin`, `0x2::balance::Balance`, or `0x1::option::Option`. +Those are package-defined datatypes, not language atoms. -```rust -use sui_move::{coin::Coin, sui::SUI, MoveType}; - -let _tag = as MoveType>::type_tag_static(); -``` +If application code needs framework types, generate them from package metadata or define them in +the consuming crate using the same `MoveType` / `MoveStruct` machinery used for user packages. This +keeps the core crate small enough to serve as the trusted type kernel for higher-level generated +bindings. -### Framework containers (`sui_move::containers`) +### What is deliberately excluded -`sui_move::containers` includes widely-used container structs such as `MoveOption`, -`Table`, and dynamic field shapes, represented in a way that preserves their tags. +- Framework mirrors such as `UID`, `ID`, `Coin`, `Balance`, `Table`, and `Clock` +- Package/module/function declaration IR +- Move expression/function-body IR +- Transaction building or execution ## 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..c5639b6 100644 --- a/sui-move/examples/derive_struct.rs +++ b/sui-move/examples/derive_struct.rs @@ -1,8 +1,31 @@ use sui_move::move_struct; +/// Local declaration for the framework-shaped `0x2::object::ID` type. +/// +/// `sui-move` intentionally does not export framework mirrors from its core. Generated package +/// bindings or local declarations provide these types when a package needs them. +#[move_struct(address = "0x2", module = "object", abilities = "copy, store")] +pub struct ID { + /// Raw object address bytes. + pub bytes: sui_move::prelude::Address, +} + +/// Local declaration for the framework-shaped `0x2::object::UID` type. +/// +/// The derive macro only needs a field whose type represents a Move `UID`; it does not require +/// the `UID` type to be exported by `sui-move`. +#[move_struct(address = "0x2", module = "object", abilities = "store")] +pub struct UID { + /// Inner object id. + pub id: ID, +} + +/// Example key object using a locally declared `UID`. #[move_struct(address = "0x1", module = "vault", abilities = "key, store")] pub struct Vault { - pub id: sui_move::types::UID, + /// Object identity field required for `key` structs. + pub id: UID, + /// Stored counter value. pub value: u64, } diff --git a/sui-move/examples/tag_checked_decode.rs b/sui-move/examples/tag_checked_decode.rs index 61583f7..f6f4160 100644 --- a/sui-move/examples/tag_checked_decode.rs +++ b/sui-move/examples/tag_checked_decode.rs @@ -1,29 +1,45 @@ -use std::marker::PhantomData; +use serde::{Deserialize, Serialize}; +use sui_move::{ + decode_keyed, parse_address, parse_identifier, HasKey, HasStore, MoveStruct, MoveType, +}; +use sui_sdk_types::{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; +/// Example key-bearing type used to demonstrate tag-checked decoding. +/// +/// In real package bindings, framework and user types should be generated from package metadata. +/// This example defines a small local type so the `sui-move` kernel remains self-contained. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +struct Counter { + value: u64, +} -fn main() { - let coin = Coin:: { - id: UID { - id: ID { - bytes: Address::new([7u8; 32]), - }, - }, - balance: Balance:: { - value: 10, - phantom: PhantomData, - }, - }; +impl MoveType for Counter { + fn type_tag_static() -> TypeTag { + TypeTag::Struct(Box::new(Self::struct_tag_static())) + } +} - let bytes = coin.to_bcs().unwrap(); +impl MoveStruct for Counter { + fn struct_tag_static() -> StructTag { + StructTag::new( + parse_address("0x123").expect("address literal"), + parse_identifier("counter").expect("module"), + parse_identifier("Counter").expect("name"), + vec![], + ) + } +} + +impl HasKey for Counter {} +impl HasStore for Counter {} + +fn main() { + let counter = Counter { value: 10 }; + let bytes = counter.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::(Counter::type_tag_static(), &bytes).unwrap(); + assert_eq!(inst.value.value, 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..3170f5c 100644 --- a/sui-move/examples/type_tags.rs +++ b/sui-move/examples/type_tags.rs @@ -1,13 +1,42 @@ +use serde::{Deserialize, Serialize}; use sui_move::prelude::*; -use sui_move::{coin::Coin, sui::SUI}; +use sui_move::{parse_address, parse_identifier}; + +/// Example package-defined type used to demonstrate the kernel type-tag API. +/// +/// Framework declarations such as `0x2::coin::Coin` are intentionally not exported from +/// `sui-move`; they should be generated from package metadata like user-defined types. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +struct Counter { + value: u64, +} + +impl MoveType for Counter { + fn type_tag_static() -> TypeTag { + TypeTag::Struct(Box::new(Self::struct_tag_static())) + } +} + +impl MoveStruct for Counter { + fn struct_tag_static() -> StructTag { + StructTag::new( + parse_address("0x123").expect("address literal"), + parse_identifier("counter").expect("module"), + parse_identifier("Counter").expect("name"), + vec![], + ) + } +} + +impl HasStore for Counter {} fn main() { assert_eq!(sui_move::type_tag_of::(), TypeTag::U64); - match as MoveType>::type_tag_static() { + match Counter::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(), "counter"); + assert_eq!(tag.name().to_string(), "Counter"); } other => panic!("expected struct type tag, got {other:?}"), } diff --git a/sui-move/src/containers.rs b/sui-move/src/containers.rs deleted file mode 100644 index 5f98fd4..0000000 --- a/sui-move/src/containers.rs +++ /dev/null @@ -1,159 +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::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..be0a408 100644 --- a/sui-move/src/decode.rs +++ b/sui-move/src/decode.rs @@ -21,25 +21,39 @@ pub fn decode_copyable(bytes: &[u8]) -> Result { -/// id: UID { -/// id: ID { -/// bytes: Address::new([0u8; 32]), -/// }, -/// }, -/// balance: Balance:: { -/// value: 10, -/// phantom: PhantomData, -/// }, -/// }; +/// #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +/// struct Counter { +/// value: u64, +/// } /// -/// let bytes = coin.to_bcs().unwrap(); -/// let inst = decode_keyed::>( as MoveType>::type_tag_static(), &bytes).unwrap(); -/// assert_eq!(inst.value.balance.value, 10); +/// impl MoveType for Counter { +/// fn type_tag_static() -> TypeTag { +/// TypeTag::Struct(Box::new(Self::struct_tag_static())) +/// } +/// } +/// +/// impl MoveStruct for Counter { +/// fn struct_tag_static() -> StructTag { +/// StructTag::new( +/// parse_address("0x1").unwrap(), +/// parse_identifier("counter").unwrap(), +/// parse_identifier("Counter").unwrap(), +/// vec![], +/// ) +/// } +/// } +/// +/// impl HasKey for Counter {} +/// impl HasStore for Counter {} +/// +/// let value = Counter { value: 10 }; +/// let bytes = value.to_bcs().unwrap(); +/// let inst = decode_keyed::(Counter::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..60423b8 100644 --- a/sui-move/src/lib.rs +++ b/sui-move/src/lib.rs @@ -12,14 +12,12 @@ pub mod prelude { //! Convenient imports for working with this crate. //! //! 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, - }; #[cfg(feature = "derive")] pub use crate::{move_module, move_struct}; + pub use crate::{ + Copyable, Droppable, HasCopy, HasDrop, HasKey, HasStore, MoveInstance, MoveStruct, + MoveType, Storable, + }; pub use sui_sdk_types::{Address, Identifier, StructTag, TypeTag}; } @@ -30,12 +28,8 @@ 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::*; /// A Rust type that corresponds to a Move type. /// @@ -82,14 +76,6 @@ pub trait MoveType: Serialize + for<'de> Deserialize<'de> + fmt::Debug + Partial /// Move structs have both a [`TypeTag`](sui_sdk_types::TypeTag) and a /// [`StructTag`](sui_sdk_types::StructTag). /// -/// # 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"); -/// ``` pub trait MoveStruct: MoveType { /// Construct the static struct tag (including type arguments). fn struct_tag_static() -> sui_sdk_types::StructTag; 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 386f19b..0000000 --- a/sui-move/src/primitives/mod.rs +++ /dev/null @@ -1,21 +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 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/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..a8cc9f9 100644 --- a/sui-move/tests/basic.rs +++ b/sui-move/tests/basic.rs @@ -1,242 +1,109 @@ use std::str::FromStr; -use sui_move::prelude::*; -use sui_move::{containers::MoveOption, decode_keyed, MoveInstance}; + +use serde::{Deserialize, Serialize}; +use sui_move::{ + decode_keyed, parse_address, parse_identifier, Copyable, HasKey, HasStore, MoveInstance, + MoveStruct, MoveType, Storable, +}; +use sui_sdk_types::{Address, StructTag, TypeTag}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +struct Counter { + value: u64, +} + +impl MoveType for Counter { + fn type_tag_static() -> TypeTag { + TypeTag::Struct(Box::new(Self::struct_tag_static())) + } +} + +impl MoveStruct for Counter { + fn struct_tag_static() -> StructTag { + StructTag::new( + parse_address("0x123").expect("address literal"), + parse_identifier("counter").expect("module"), + parse_identifier("Counter").expect("name"), + vec![], + ) + } +} + +impl HasKey for Counter {} +impl HasStore for Counter {} #[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(), 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(), - sui_sdk_types::TypeTag::U128 + TypeTag::U128 )); assert!(matches!( ::type_tag_static(), - sui_sdk_types::TypeTag::Bool - )); - assert!(matches!( - ::type_tag_static(), - sui_sdk_types::TypeTag::Address + TypeTag::Bool )); - - match as MoveType>::type_tag_static() { - sui_sdk_types::TypeTag::Vector(inner) => { - assert!(matches!(*inner, sui_sdk_types::TypeTag::U64)); - } - other => panic!("expected vector type tag, got {other:?}"), - } + assert_eq!(
::type_tag_static(), TypeTag::Address); } #[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:?}"), +fn vector_type_tags_are_recursive() { + match > as MoveType>::type_tag_static() { + TypeTag::Vector(outer) => match *outer { + TypeTag::Vector(inner) => assert!(matches!(*inner, TypeTag::U64)), + other => panic!("expected inner vector, got {other:?}"), + }, + other => panic!("expected outer vector, 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"); +#[test] +fn custom_struct_type_tag_matches_definition() { + match Counter::type_tag_static() { + TypeTag::Struct(tag) => { + assert_eq!(*tag.address(), Address::from_str("0x123").unwrap()); + assert_eq!(tag.module().to_string(), "counter"); + assert_eq!(tag.name().to_string(), "Counter"); + assert!(tag.type_params().is_empty()); } other => panic!("expected struct tag, got {other:?}"), } } #[test] -fn tag_verification_and_bcs_roundtrip() { - let value = MoveOption:: { vec: vec![1, 2, 3] }; +fn tag_verification_and_bcs_roundtrip_work_for_custom_types() { + let value = Counter { value: 7 }; 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 inst = MoveInstance::::from_raw_type(Counter::type_tag_static(), &bytes).unwrap(); + assert_eq!(inst.value, value); - let err = MoveInstance::>::from_raw_type(sui_sdk_types::TypeTag::U8, &bytes) - .unwrap_err(); + let err = MoveInstance::::from_raw_type(TypeTag::U64, &bytes).unwrap_err(); assert!(matches!(err, sui_move::DecodeError::TypeTagMismatch { .. })); } +#[test] +fn keyed_decode_uses_the_same_type_tag_boundary() { + let value = Counter { value: 9 }; + let bytes = value.to_bcs().unwrap(); + + let decoded = decode_keyed::(Counter::type_tag_static(), &bytes).unwrap(); + assert_eq!(decoded.value, value); +} + fn require_store(_: &T) {} fn require_copy(_: &T) {} #[test] -fn type_abilities_are_respected() { +fn primitive_ability_markers_are_available() { let value = 5u64; require_store(&value); require_copy(&value); - let uid = uid_with_byte(9); - require_store(&uid); -} - -#[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(); - 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 bytes = vec![1u8, 2, 3]; + require_store(&bytes); + require_copy(&bytes); } diff --git a/sui-move/tests/derive.rs b/sui-move/tests/derive.rs index 04cc759..af0d786 100644 --- a/sui-move/tests/derive.rs +++ b/sui-move/tests/derive.rs @@ -1,8 +1,21 @@ #![cfg(feature = "derive")] use std::str::FromStr; + use sui_move::prelude::*; -use sui_move::{containers::MoveOption, Copyable, MoveInstance, Storable}; +use sui_move::{Copyable, MoveInstance, Storable}; + +mod object { + #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, store")] + pub struct ID { + pub bytes: sui_move::prelude::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,19 +24,24 @@ mod vault { module = "vault", abilities = "key, store", phantoms = "T", - uid_type = "sui_move::types::UID" + uid_type = "crate::object::UID" )] pub struct Vault { - pub id: sui_move::types::UID, + pub id: crate::object::UID, pub balance: Vec, } } #[sui_move::move_module(address = "0x1", name = "wrapper")] mod wrapper { - #[sui_move::move_struct(address = "0x1", module = "wrapper", abilities = "key, store")] + #[sui_move::move_struct( + address = "0x1", + module = "wrapper", + abilities = "key, store", + uid_type = "crate::object::UID" + )] pub struct VaultWrapper { - pub id: sui_move::types::UID, + pub id: crate::object::UID, pub inner: crate::vault::Vault, } } @@ -45,16 +63,26 @@ mod bounded { fn type_tag_matches_move_definition() { let tag = vault::Vault::::type_tag_static(); match tag { - sui_sdk_types::TypeTag::Struct(inner) => { - let expected_addr = sui_sdk_types::Address::from_str("0x1").unwrap(); + TypeTag::Struct(inner) => { + let expected_addr = Address::from_str("0x1").unwrap(); assert_eq!(*inner.address(), expected_addr); assert_eq!(inner.module().to_string(), "vault"); assert_eq!(inner.name().to_string(), "Vault"); assert_eq!(inner.type_params().len(), 1); - assert!(matches!( - inner.type_params()[0], - sui_sdk_types::TypeTag::U64 - )); + assert!(matches!(inner.type_params()[0], TypeTag::U64)); + } + _ => panic!("expected struct type tag"), + } +} + +#[test] +fn local_uid_type_matches_framework_identity_without_core_exports() { + let tag = object::UID::type_tag_static(); + match tag { + TypeTag::Struct(inner) => { + assert_eq!(*inner.address(), Address::from_str("0x2").unwrap()); + assert_eq!(inner.module().to_string(), "object"); + assert_eq!(inner.name().to_string(), "UID"); } _ => panic!("expected struct type tag"), } @@ -63,17 +91,9 @@ 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 { - bytes: Address::new([0u8; 32]), - }, - }, + id: uid_with_byte(0), inner: vault::Vault:: { - id: sui_move::types::UID { - id: sui_move::types::ID { - bytes: Address::new([42u8; 32]), - }, - }, + id: uid_with_byte(42), balance: vec![9, 8], phantom_t: std::marker::PhantomData, }, @@ -85,7 +105,7 @@ fn nested_structs_are_supported() { assert_eq!(decoded.inner.balance, vec![9, 8]); match wrapper::VaultWrapper::type_tag_static() { - sui_sdk_types::TypeTag::Struct(tag) => { + TypeTag::Struct(tag) => { assert_eq!(tag.module().to_string(), "wrapper"); assert_eq!(tag.name().to_string(), "VaultWrapper"); } @@ -96,11 +116,7 @@ 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 { - bytes: Address::new([7u8; 32]), - }, - }, + id: uid_with_byte(7), balance: vec![1, 2, 3], phantom_t: std::marker::PhantomData, }; @@ -114,8 +130,7 @@ fn tag_verification_and_bcs_roundtrip() { assert_eq!(inst.value.id.id.bytes.as_bytes()[0], 7); assert_eq!(inst.value.balance, vec![1, 2, 3]); - let err = MoveInstance::>::from_raw_type(sui_sdk_types::TypeTag::U8, &bytes) - .unwrap_err(); + let err = MoveInstance::>::from_raw_type(TypeTag::U8, &bytes).unwrap_err(); assert!(matches!(err, sui_move::DecodeError::TypeTagMismatch { .. })); } @@ -130,29 +145,17 @@ fn type_abilities_are_respected() { require_copy(&boxed.value); let nested = vault::Vault { - id: sui_move::types::UID { - id: sui_move::types::ID { - bytes: Address::new([9u8; 32]), - }, - }, + id: uid_with_byte(9), balance: vec![bounded::Boxed:: { value: 7 }], phantom_t: std::marker::PhantomData, }; 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"), +fn uid_with_byte(byte: u8) -> object::UID { + object::UID { + id: object::ID { + bytes: Address::new([byte; 32]), + }, } } From 36168264d5e4c5fbb46472b99e7662ddcb44960e Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2026 19:07:29 +0200 Subject: [PATCH 2/6] feat: dependancy graph --- sui-move-codegen/README.md | 29 ++++ sui-move-codegen/src/render/calls.rs | 3 + sui-move-codegen/src/render/mod.rs | 191 +++++++++++++++++++++++++- sui-move-codegen/src/render/tx_ext.rs | 3 + sui-move-codegen/src/render/types.rs | 28 +++- 5 files changed, 252 insertions(+), 2 deletions(-) diff --git a/sui-move-codegen/README.md b/sui-move-codegen/README.md index d35ee4d..82e335e 100644 --- a/sui-move-codegen/README.md +++ b/sui-move-codegen/README.md @@ -349,6 +349,35 @@ To keep builds deterministic, fetch metadata once and commit it (JSON), then ren This avoids putting network access in CI/build scripts. +## Cross-package type resolution + +The renderer only treats primitive Move types and datatypes from the package being rendered as +intrinsic. Framework and dependency types are still ordinary package declarations, so register their +generated Rust bindings explicitly: + +```rust,no_run +use sui_move_codegen::fetch_package; +use sui_move_codegen::render::{render_package, RenderOptions}; +use sui_rpc::Client; +use sui_sdk_types::Address; + +# async fn demo() -> Result<(), Box> { +let mut client = Client::new(Client::MAINNET_FULLNODE)?; + +let framework = fetch_package(&mut client, "0x2".parse::
()?).await?; +let app = fetch_package(&mut client, "0x123".parse::
()?).await?; + +let opts = RenderOptions::default().with_external_package(&framework, "sui_framework"); +let code = render_package(&app, &opts); +# let _ = code; +# Ok(()) +# } +``` + +Without an external binding, an external datatype renders as a `compile_error!`. That keeps missing +dependency bindings explicit instead of silently reintroducing handwritten framework mirrors into +`sui-move`. + ## Using the generated code The rendered Rust is meant to live in its own crate or module. At minimum, the generated code diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index 5a17ab0..8cd4b81 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -167,6 +167,9 @@ fn is_object_type( if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) { return builtin.is_key; } + if let Some(external) = opts.external_types.get(type_name) { + return external.is_key; + } pkg.modules .get(&type_name.module) .and_then(|m| { diff --git a/sui-move-codegen/src/render/mod.rs b/sui-move-codegen/src/render/mod.rs index 0857803..020582b 100644 --- a/sui-move-codegen/src/render/mod.rs +++ b/sui-move-codegen/src/render/mod.rs @@ -10,10 +10,11 @@ //! - generated functions return `sui-move-call::CallSpec` //! - optionally, a `TxExt` trait is emitted to add calls directly to `sui-move-runtime::Tx` +use std::collections::BTreeMap; use std::fs; use std::path::Path; -use crate::ir::NormalizedPackage; +use crate::ir::{Ability, NormalizedPackage, TypeName}; mod builtins; mod calls; @@ -48,6 +49,25 @@ pub struct RenderOptions { /// - `use sui_move as sm;` /// - `use sui_move_call as sm_call;` pub use_aliases: bool, + /// Rust paths for Move datatypes defined outside the package currently being rendered. + /// + /// This is the explicit cross-package symbol table. The renderer still treats unknown external + /// datatypes as compile errors, but entries in this map are rendered as generated Rust paths + /// instead of being folded into the `sui-move` core. + pub external_types: BTreeMap, +} + +/// A generated Rust binding for a Move datatype outside the current package. +/// +/// `rust_path` is the path to the generated Rust type without generic arguments, for example +/// `sui_framework::object::UID` or `nexus_primitives::data::Data`. `is_key` carries the Move +/// `key` ability so call generation can decide whether a parameter should be an object argument. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExternalType { + /// Rust path to the generated type, without generic arguments. + pub rust_path: String, + /// Whether the external Move datatype has the `key` ability. + pub is_key: bool, } impl Default for RenderOptions { @@ -58,10 +78,102 @@ impl Default for RenderOptions { emit_tx_ext: false, flatten: false, use_aliases: true, + external_types: BTreeMap::new(), } } } +impl RenderOptions { + /// Return options with one externally generated Move datatype registered. + /// + /// This is useful when a dependency package is rendered in a custom layout and the caller wants + /// to provide the exact Rust path for one datatype. + #[must_use] + pub fn with_external_type( + mut self, + type_name: TypeName, + rust_path: impl Into, + is_key: bool, + ) -> Self { + self.add_external_type(type_name, rust_path, is_key); + self + } + + /// Register one externally generated Move datatype. + /// + /// The path should name the generated Rust type without generic arguments. Generic arguments + /// are rendered from the Move type reference at the use site. + pub fn add_external_type( + &mut self, + type_name: TypeName, + rust_path: impl Into, + is_key: bool, + ) { + self.external_types.insert( + type_name, + ExternalType { + rust_path: rust_path.into(), + is_key, + }, + ); + } + + /// Return options with every datatype from an external package registered. + /// + /// The `rust_root` should point at the generated package module or crate. Datatypes are mapped + /// using the default generated layout: `::::`. + #[must_use] + pub fn with_external_package( + mut self, + pkg: &NormalizedPackage, + rust_root: impl Into, + ) -> Self { + self.add_external_package(pkg, rust_root); + self + } + + /// Register every datatype from an external package using the default generated layout. + /// + /// Entries are added for the datatype address reported by metadata plus the package storage id + /// and original id, when present. That keeps generated paths stable across the address forms Sui + /// metadata can expose for upgraded packages. + pub fn add_external_package(&mut self, pkg: &NormalizedPackage, rust_root: impl Into) { + let rust_root = rust_root.into(); + + for module in pkg.modules.values() { + let module_ident = idents::ident(&module.name).to_string(); + for dt in &module.datatypes { + let type_ident = idents::ident(&dt.name).to_string(); + let rust_path = format!("{rust_root}::{module_ident}::{type_ident}"); + let is_key = dt.abilities.contains(&Ability::Key); + + for type_name in external_type_names(pkg, dt) { + self.add_external_type(type_name, rust_path.clone(), is_key); + } + } + } + } +} + +fn external_type_names(pkg: &NormalizedPackage, dt: &crate::ir::Datatype) -> Vec { + let mut names = vec![dt.type_name.clone()]; + names.push(TypeName { + address: pkg.storage_id.clone(), + module: dt.module.clone(), + name: dt.name.clone(), + }); + if let Some(original_id) = &pkg.original_id { + names.push(TypeName { + address: original_id.clone(), + module: dt.module.clone(), + name: dt.name.clone(), + }); + } + names.sort(); + names.dedup(); + names +} + /// Render a normalized package into a single Rust source string. /// /// The output is valid Rust source that you can write to a `.rs` file (or `include!` as a module). @@ -216,6 +328,83 @@ mod tests { assert!(code.contains("unknown external type `0x2::object::UID`")); } + #[test] + fn registered_external_package_types_render_as_generated_paths() { + let external = NormalizedPackage { + storage_id: "0x9".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([( + "dep".into(), + NormalizedModule { + name: "dep".into(), + datatypes: vec![Datatype { + type_name: TypeName::parse("0x9::dep::External").unwrap(), + module: "dep".into(), + name: "External".into(), + abilities: vec![Ability::Key, Ability::Store], + type_parameters: vec![], + kind: DatatypeKind::Struct { fields: vec![] }, + }], + functions: vec![], + }, + )]), + }; + + let pkg = NormalizedPackage { + storage_id: "0x1".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([( + "m".into(), + NormalizedModule { + name: "m".into(), + datatypes: vec![Datatype { + type_name: TypeName::parse("0x1::m::UsesExternal").unwrap(), + module: "m".into(), + name: "UsesExternal".into(), + abilities: vec![Ability::Store], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "external".into(), + position: 0, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0x9::dep::External").unwrap(), + type_arguments: vec![], + }, + }], + }, + }], + functions: vec![Function { + name: "touch".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("0x9::dep::External").unwrap(), + type_arguments: vec![], + }), + }, + }], + return_types: vec![], + }], + }, + )]), + }; + + let opts = RenderOptions::default().with_external_package(&external, "dep_bindings"); + let code = render_package(&pkg, &opts); + + assert!(!code.contains("unknown external type")); + assert!(code.contains("pub external: dep_bindings::dep::External")); + assert!(code.contains("&mut impl sm_call::ObjectArg")); + } + #[test] fn renders_tx_ext_trait_when_enabled() { let opts = RenderOptions { diff --git a/sui-move-codegen/src/render/tx_ext.rs b/sui-move-codegen/src/render/tx_ext.rs index 6e8ea9c..ce1a425 100644 --- a/sui-move-codegen/src/render/tx_ext.rs +++ b/sui-move-codegen/src/render/tx_ext.rs @@ -194,6 +194,9 @@ fn is_object_type( if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) { return builtin.is_key; } + if let Some(external) = opts.external_types.get(type_name) { + return external.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..3a74023 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, RenderOptions}; +use super::{builtins, idents, ExternalType, RenderOptions}; pub(crate) fn render_datatype( dt: &Datatype, @@ -399,6 +399,9 @@ fn render_type_ref( let is_local = is_local_type(type_name, pkg); if !is_local { + if let Some(external) = opts.external_types.get(type_name) { + return render_external_type(external, &args); + } // Keep generation deterministic: unknown external types must be supplied by the // consumer (e.g. another generated package crate). let msg = format!( @@ -475,6 +478,9 @@ fn render_type_ref_root( let is_local = is_local_type(type_name, pkg); if !is_local { + if let Some(external) = opts.external_types.get(type_name) { + return render_external_type(external, &args); + } let msg = format!( "sui-move-codegen: unknown external type `{}`; generate bindings for that package too", display_type_name(type_name) @@ -504,6 +510,26 @@ fn render_type_ref_root( } } +fn render_external_type(external: &ExternalType, args: &[TokenStream]) -> TokenStream { + let path = match syn::parse_str::(&external.rust_path) { + Ok(path) => quote! { #path }, + Err(_) => { + let msg = format!( + "sui-move-codegen: invalid external Rust type path `{}`", + external.rust_path + ); + let msg_lit = syn::LitStr::new(&msg, proc_macro2::Span::call_site()); + return quote! { compile_error!(#msg_lit) }; + } + }; + + if args.is_empty() { + path + } else { + quote! { #path<#(#args),*> } + } +} + fn prelude_type(use_aliases: bool, name: TokenStream) -> TokenStream { if use_aliases { quote! { sm::prelude::#name } From 8cf4c5a5926b5ab5aa21da9bf129dde3ba8ea08e Mon Sep 17 00:00:00 2001 From: David Date: Mon, 22 Jun 2026 08:52:02 +0200 Subject: [PATCH 3/6] feat: grpc only support --- MODEL.md | 2 +- sui-move-call/README.md | 2 +- sui-move-codegen/README.md | 9 ++-- sui-move-codegen/src/lib.rs | 9 ++-- sui-move-codegen/src/source.rs | 11 ++--- sui-move-runtime/README.md | 18 +++---- sui-move-runtime/src/lib.rs | 85 ++++++++++++++++++---------------- sui-move-runtime/src/tx.rs | 80 ++++++++++++++++---------------- 8 files changed, 111 insertions(+), 105 deletions(-) diff --git a/MODEL.md b/MODEL.md index 5320c97..db3274b 100644 --- a/MODEL.md +++ b/MODEL.md @@ -73,7 +73,7 @@ Execution and checkpoint inclusion are distinct: - a transaction can be **executed** (effects exist), - later it can be **checkpointed** (indexes are updated; “read-your-writes” on that node). -RPC calls may time out waiting for checkpoint inclusion even though execution succeeded. A correct +gRPC calls may time out waiting for checkpoint inclusion even though execution succeeded. A correct client abstraction must preserve recovery information (digest/effects when available) and represent finality explicitly rather than as a boolean. diff --git a/sui-move-call/README.md b/sui-move-call/README.md index e4e58ca..79cd92c 100644 --- a/sui-move-call/README.md +++ b/sui-move-call/README.md @@ -144,5 +144,5 @@ assert!(matches!(spec.arguments[1], CallArg::Receiving(_))); ## Non-goals - No transaction building: this crate does not produce `ProgrammableTransaction`. -- No execution/runtime: this crate does not talk to RPC or submit transactions. +- No execution/runtime: this crate does not talk to a network client or submit transactions. - No object fetching: object contents are not loaded or decoded here. diff --git a/sui-move-codegen/README.md b/sui-move-codegen/README.md index 82e335e..c77e8ff 100644 --- a/sui-move-codegen/README.md +++ b/sui-move-codegen/README.md @@ -325,12 +325,11 @@ assert!(code.contains("spec.push_type_arg::();")); ## Example: fetch over gRPC ```rust,no_run -use sui_move_codegen::fetch_package; -use sui_rpc::Client; +use sui_move_codegen::{fetch_package, GrpcClient}; use sui_sdk_types::Address; # async fn demo() -> Result<(), Box> { -let mut client = Client::new(Client::MAINNET_FULLNODE)?; +let mut client = GrpcClient::new(GrpcClient::MAINNET_FULLNODE)?; let package_id: Address = "0x2".parse()?; let pkg = fetch_package(&mut client, package_id).await?; @@ -358,11 +357,11 @@ generated Rust bindings explicitly: ```rust,no_run use sui_move_codegen::fetch_package; use sui_move_codegen::render::{render_package, RenderOptions}; -use sui_rpc::Client; +use sui_move_codegen::GrpcClient; use sui_sdk_types::Address; # async fn demo() -> Result<(), Box> { -let mut client = Client::new(Client::MAINNET_FULLNODE)?; +let mut client = GrpcClient::new(GrpcClient::MAINNET_FULLNODE)?; let framework = fetch_package(&mut client, "0x2".parse::
()?).await?; let app = fetch_package(&mut client, "0x123".parse::
()?).await?; diff --git a/sui-move-codegen/src/lib.rs b/sui-move-codegen/src/lib.rs index 654c336..c2720ab 100644 --- a/sui-move-codegen/src/lib.rs +++ b/sui-move-codegen/src/lib.rs @@ -13,12 +13,15 @@ pub mod render; pub use crate::source::fetch_package; +/// Sui gRPC client used by package metadata fetching. +pub type GrpcClient = sui_rpc::Client; + /// Errors from sourcing or normalizing package metadata. #[derive(thiserror::Error, Debug)] pub enum Error { - /// RPC request failed. - #[error("rpc: {0}")] - Rpc(String), + /// gRPC request failed. + #[error("grpc: {0}")] + Grpc(String), /// Package missing from response. #[error("package missing from response")] diff --git a/sui-move-codegen/src/source.rs b/sui-move-codegen/src/source.rs index bf48a55..ec9f23c 100644 --- a/sui-move-codegen/src/source.rs +++ b/sui-move-codegen/src/source.rs @@ -9,7 +9,7 @@ use crate::ir::{ Ability, Datatype, DatatypeKind, Field, Function, FunctionParam, NormalizedModule, NormalizedPackage, TypeName, TypeParameter, TypeRef, Variant, Visibility, }; -use crate::Error; +use crate::{Error, GrpcClient}; /// Fetch a Move package over gRPC and normalize it into [`NormalizedPackage`]. /// @@ -20,12 +20,11 @@ use crate::Error; /// /// # Example /// ```rust,no_run -/// use sui_move_codegen::fetch_package; -/// use sui_rpc::Client; +/// use sui_move_codegen::{fetch_package, GrpcClient}; /// use sui_sdk_types::Address; /// /// # async fn demo() -> Result<(), Box> { -/// let mut client = Client::new(Client::MAINNET_FULLNODE)?; +/// let mut client = GrpcClient::new(GrpcClient::MAINNET_FULLNODE)?; /// let package_id: Address = "0x2".parse()?; /// /// let pkg = fetch_package(&mut client, package_id).await?; @@ -35,14 +34,14 @@ use crate::Error; /// # } /// ``` pub async fn fetch_package( - client: &mut sui_rpc::Client, + client: &mut GrpcClient, package_id: Address, ) -> Result { let mut package_client = client.package_client(); let resp = package_client .get_package(proto::GetPackageRequest::new(&package_id)) .await - .map_err(|e| Error::Rpc(e.to_string()))? + .map_err(|e| Error::Grpc(e.to_string()))? .into_inner(); let package = resp.package.ok_or(Error::MissingPackage)?; diff --git a/sui-move-runtime/README.md b/sui-move-runtime/README.md index d9dabec..6bdaebd 100644 --- a/sui-move-runtime/README.md +++ b/sui-move-runtime/README.md @@ -53,7 +53,7 @@ fn touch_object(object: &impl ToCallArg, amount: u64) -> CallSpec { } async fn demo() -> Result<(), Error> { - let client = sui_rpc::Client::new(sui_rpc::Client::TESTNET_FULLNODE).unwrap(); + let client = GrpcClient::new(GrpcClient::TESTNET_FULLNODE).unwrap(); let signer = DummySigner; let sender: Address = "0x123".parse().unwrap(); let object_id: Address = "0x2".parse().unwrap(); @@ -82,7 +82,7 @@ async fn demo() -> Result<(), Error> { ## Core API - Runtime views: - - `Runtime`: owns RPC client, signer, and the handle cursor + - `Runtime`: owns gRPC client, signer, and the handle cursor - `Read`: read view (fetch typed handles) - `Tx`: transaction view (simulate/inspect/commit PTBs) - Ergonomic helper: @@ -208,7 +208,7 @@ All transaction actions take a sender address and are methods on a transaction b - `call` / `arg` / `input`: build the PTB in-place. - `simulate`: calls `simulate_transaction` with checks enabled. No signature is required and the chain is not mutated. Handles are not updated. -- `inspect`: calls `simulate_transaction` with checks disabled and asks RPC for +- `inspect`: calls `simulate_transaction` with checks disabled and asks gRPC for `command_outputs`. This is meant for debugging and observability, not for guaranteeing that a real on-chain commit will succeed. - `commit`: builds a full `Transaction`, signs it, and submits it. @@ -225,7 +225,7 @@ All transaction actions take a sender address and are methods on a transaction b `Tx::commit*` returns a [`Receipt`]. The receipt preserves recovery information: - `digest` is always present once submission succeeds. -- `effects`/`status` are present when returned by RPC (this crate requests `effects.bcs`). +- `effects`/`status` are present when returned by gRPC (this crate requests `effects.bcs`). - `requested_finality` is what the runtime asked for (defaults to checkpointed for commits). - `observed_finality` + `checkpoint_wait` describe what the runtime actually observed. @@ -263,7 +263,7 @@ use sui_sdk_types::Address; # } # # async fn demo() -> Result<(), Error> { - let client = sui_rpc::Client::new(sui_rpc::Client::TESTNET_FULLNODE).unwrap(); + let client = GrpcClient::new(GrpcClient::TESTNET_FULLNODE).unwrap(); let signer = DummySigner; let sender: Address = "0x123".parse().unwrap(); @@ -320,13 +320,13 @@ assert_eq!(ptb.commands.len(), 1); - `TxOptions::sponsor`: gas owner (defaults to sender) - `TxOptions::gas`: explicit gas object reference (otherwise the runtime selects one coin owned by the gas owner) -- `TxOptions::gas_price`: defaults to the reference gas price from RPC +- `TxOptions::gas_price`: defaults to the reference gas price from gRPC - `TxOptions::gas_budget`: defaults to `Runtime::default_gas_budget` - `TxOptions::expiration`: optional TTL - `TxOptions::finality`: `Checkpointed` (default) or `Executed` `simulate`/`inspect` do not sign or submit, and do not currently model explicit gas payment -configuration (they rely on the simulation RPC). +configuration (they rely on the simulation gRPC). ## Typed reads (tag-checked decoding) @@ -398,7 +398,7 @@ This crate makes handles *runtime-owned*: - `Read::object`/`Read::receiving_object` fetch the current `ObjectReference` and **intern** it in a cursor (your local frontier) keyed by `object_id`. - The returned `Object` / `ReceivingObject` is a small `Clone` handle backed by `Arc>`. -- `Tx::commit` requests `effects.bcs` from RPC, decodes `TransactionEffects`, extracts updated +- `Tx::commit` requests `effects.bcs` from gRPC, decodes `TransactionEffects`, extracts updated object information, derives an effects-based patch, and applies it to the cursor, updating any live handle cells that match those object ids. @@ -480,7 +480,7 @@ assert_eq!(decoded, 10); - `Runtime::with_cursor_snapshot` / `Runtime::cursor_snapshot` provide snapshot/restore for the cursor. - `Runtime::sync_transaction` fetches effects by digest and advances the cursor (recovery escape hatch). - `Read::refresh_id` / `Read::refresh_ids` refresh cursor state explicitly (external drift escape hatch). -- `Read::client_mut` gives direct access to the underlying `sui_rpc::Client` when needed. +- `Read::grpc_client_mut` gives direct access to the underlying `GrpcClient` when needed. ## Non-goals diff --git a/sui-move-runtime/src/lib.rs b/sui-move-runtime/src/lib.rs index 5eb6434..3843c49 100644 --- a/sui-move-runtime/src/lib.rs +++ b/sui-move-runtime/src/lib.rs @@ -26,7 +26,7 @@ use sui_sdk_types::{Address, Mutability, ProgrammableTransaction, TransactionEff /// This is a small umbrella enum that preserves the main failure boundary: /// - building a PTB (`Build`) /// - submitting/waiting (`Tx`) -/// - fetching objects for handles (`Rpc`) +/// - fetching objects for handles over Sui gRPC (`Grpc`) /// - simulation/dev-inspection (`Simulate`) #[derive(thiserror::Error, Debug)] pub enum Error { @@ -38,9 +38,9 @@ pub enum Error { #[error(transparent)] Tx(#[from] tx::TxError), - /// Fetching data from RPC failed. + /// Fetching data from Sui gRPC failed. #[error(transparent)] - Rpc(#[from] tx::RpcError), + Grpc(#[from] tx::GrpcError), /// Simulating or dev-inspecting failed. #[error(transparent)] @@ -67,12 +67,15 @@ pub enum Error { object_id: Address, /// Expected kind (for the API used). expected: &'static str, - /// Actual kind from RPC. + /// Actual kind from gRPC. actual: &'static str, }, } -/// Long-lived runtime owning RPC + signer + handle cursor. +/// Sui gRPC client used by the runtime. +pub type GrpcClient = sui_rpc::Client; + +/// Long-lived runtime owning a Sui gRPC client + signer + handle cursor. /// /// This is the entry point for the Read → Tx → Commit mental model: /// - [`Runtime::read`] for fetching/constructing typed handles @@ -84,7 +87,7 @@ pub enum Error { /// runtime’s cursor (your local frontier) by `object_id`. Clones of the same handle share the same /// internal cell, so they all see updates. /// -/// After [`Tx::commit`], the runtime decodes `TransactionEffects` from RPC, derives an +/// After [`Tx::commit`], the runtime decodes `TransactionEffects` from gRPC, derives an /// effects-based patch, and applies it to the cursor, updating any live handle cells whose object /// id appears in the effects. /// @@ -107,13 +110,13 @@ pub enum Error { /// # } /// # } /// -/// let client = sui_rpc::Client::new(sui_rpc::Client::TESTNET_FULLNODE).unwrap(); +/// let client = GrpcClient::new(GrpcClient::TESTNET_FULLNODE).unwrap(); /// let signer = DummySigner; /// let mut rt = Runtime::new(client, signer); /// # let _ = &mut rt; /// ``` pub struct Runtime { - client: sui_rpc::Client, + client: GrpcClient, signer: S, cursor: handles::Cursor, wait_timeout: Duration, @@ -121,8 +124,8 @@ pub struct Runtime { } impl Runtime { - /// Create a runtime from an RPC client and a signer. - pub fn new(client: sui_rpc::Client, signer: S) -> Self { + /// Create a runtime from a Sui gRPC client and a signer. + pub fn new(client: GrpcClient, signer: S) -> Self { Self { client, signer, @@ -150,9 +153,9 @@ impl Runtime { /// /// This is the recovery escape hatch for cases where you have a transaction digest but do not /// have effects locally (for example, a receipt that was persisted without `effects`, or - /// a receipt returned by an RPC node that did not include `effects.bcs`). + /// a receipt returned by a gRPC node that did not include `effects.bcs`). /// - /// If the transaction effects are returned by RPC, the runtime derives an effects patch and + /// If the transaction effects are returned by gRPC, the runtime derives an effects patch and /// applies it to its cursor, updating any matching runtime-owned handles. pub async fn sync_transaction( &mut self, @@ -203,15 +206,15 @@ impl Runtime { /// Read view: read/fetch helpers and handle construction. /// -/// This view is intentionally read-only with respect to the chain: it fetches data from RPC and +/// This view is intentionally read-only with respect to the chain: it fetches data from gRPC and /// constructs typed handles, but it does not submit transactions. pub struct Read<'a, S> { rt: &'a mut Runtime, } impl<'a, S: SuiSigner> Read<'a, S> { - /// Mutable access to the underlying RPC client (escape hatch). - pub fn client_mut(&mut self) -> &mut sui_rpc::Client { + /// Mutable access to the underlying Sui gRPC client (escape hatch). + pub fn grpc_client_mut(&mut self) -> &mut GrpcClient { &mut self.rt.client } @@ -230,13 +233,13 @@ impl<'a, S: SuiSigner> Read<'a, S> { .intern_untyped(fetched.reference, fetched.owner); Ok(()) } - Err(err @ tx::RpcError::Missing(_)) => { + Err(err @ tx::GrpcError::Missing(_)) => { self.rt .cursor .tombstone(object_id, TombstoneReason::NotExist); - Err(Error::Rpc(err)) + Err(Error::Grpc(err)) } - Err(err) => Err(Error::Rpc(err)), + Err(err) => Err(Error::Grpc(err)), } } @@ -281,13 +284,13 @@ impl<'a, S: SuiSigner> Read<'a, S> { let fetched = match tx::fetch_object_reference_and_owner(&mut self.rt.client, object_id).await { Ok(fetched) => fetched, - Err(err @ tx::RpcError::Missing(_)) => { + Err(err @ tx::GrpcError::Missing(_)) => { self.rt .cursor .tombstone(object_id, TombstoneReason::NotExist); - return Err(Error::Rpc(err)); + return Err(Error::Grpc(err)); } - Err(err) => return Err(Error::Rpc(err)), + Err(err) => return Err(Error::Grpc(err)), }; let kind = tx::classify_owner(&fetched.owner); @@ -322,13 +325,13 @@ impl<'a, S: SuiSigner> Read<'a, S> { let fetched = match tx::fetch_object_reference_and_owner(&mut self.rt.client, object_id).await { Ok(fetched) => fetched, - Err(err @ tx::RpcError::Missing(_)) => { + Err(err @ tx::GrpcError::Missing(_)) => { self.rt .cursor .tombstone(object_id, TombstoneReason::NotExist); - return Err(Error::Rpc(err)); + return Err(Error::Grpc(err)); } - Err(err) => return Err(Error::Rpc(err)), + Err(err) => return Err(Error::Grpc(err)), }; let obj = self @@ -352,13 +355,13 @@ impl<'a, S: SuiSigner> Read<'a, S> { let fetched = match tx::fetch_object_reference_and_owner(&mut self.rt.client, object_id).await { Ok(fetched) => fetched, - Err(err @ tx::RpcError::Missing(_)) => { + Err(err @ tx::GrpcError::Missing(_)) => { self.rt .cursor .tombstone(object_id, TombstoneReason::NotExist); - return Err(Error::Rpc(err)); + return Err(Error::Grpc(err)); } - Err(err) => return Err(Error::Rpc(err)), + Err(err) => return Err(Error::Grpc(err)), }; let obj = self @@ -385,13 +388,13 @@ impl<'a, S: SuiSigner> Read<'a, S> { let fetched = match tx::fetch_object_reference_and_owner(&mut self.rt.client, object_id).await { Ok(fetched) => fetched, - Err(err @ tx::RpcError::Missing(_)) => { + Err(err @ tx::GrpcError::Missing(_)) => { self.rt .cursor .tombstone(object_id, TombstoneReason::NotExist); - return Err(Error::Rpc(err)); + return Err(Error::Grpc(err)); } - Err(err) => return Err(Error::Rpc(err)), + Err(err) => return Err(Error::Grpc(err)), }; self.rt @@ -418,13 +421,13 @@ impl<'a, S: SuiSigner> Read<'a, S> { let fetched = match tx::fetch_object_reference_and_owner(&mut self.rt.client, object_id).await { Ok(fetched) => fetched, - Err(err @ tx::RpcError::Missing(_)) => { + Err(err @ tx::GrpcError::Missing(_)) => { self.rt .cursor .tombstone(object_id, TombstoneReason::NotExist); - return Err(Error::Rpc(err)); + return Err(Error::Grpc(err)); } - Err(err) => return Err(Error::Rpc(err)), + Err(err) => return Err(Error::Grpc(err)), }; self.rt @@ -452,13 +455,13 @@ impl<'a, S: SuiSigner> Read<'a, S> { let fetched = match tx::fetch_object_reference_and_owner(&mut self.rt.client, object_id).await { Ok(fetched) => fetched, - Err(err @ tx::RpcError::Missing(_)) => { + Err(err @ tx::GrpcError::Missing(_)) => { self.rt .cursor .tombstone(object_id, TombstoneReason::NotExist); - return Err(Error::Rpc(err)); + return Err(Error::Grpc(err)); } - Err(err) => return Err(Error::Rpc(err)), + Err(err) => return Err(Error::Grpc(err)), }; Ok(self .rt @@ -477,13 +480,13 @@ impl<'a, S: SuiSigner> Read<'a, S> { let fetched = match tx::fetch_object_reference_and_owner(&mut self.rt.client, object_id).await { Ok(fetched) => fetched, - Err(err @ tx::RpcError::Missing(_)) => { + Err(err @ tx::GrpcError::Missing(_)) => { self.rt .cursor .tombstone(object_id, TombstoneReason::NotExist); - return Err(Error::Rpc(err)); + return Err(Error::Grpc(err)); } - Err(err) => return Err(Error::Rpc(err)), + Err(err) => return Err(Error::Grpc(err)), }; let kind = tx::classify_owner(&fetched.owner); @@ -766,8 +769,8 @@ impl<'a, S: SuiSigner> Tx<'a, S> { pub mod prelude { pub use crate::{ BcsValue, CheckpointWaitOutcome, CommandOutputs, CursorSnapshot, EnsureSuccessError, Error, - Finality, InspectOptions, InspectReceipt, Object, ObservedFinality, Read, Receipt, - ReceivingObject, Runtime, SharedObject, SimulateOptions, SimulationReceipt, + Finality, GrpcClient, InspectOptions, InspectReceipt, Object, ObservedFinality, Read, + Receipt, ReceivingObject, Runtime, SharedObject, SimulateOptions, SimulationReceipt, TombstoneReason, Tx, TxOptions, }; pub use sui_move_call::prelude::*; diff --git a/sui-move-runtime/src/tx.rs b/sui-move-runtime/src/tx.rs index 0a2d2bd..fc8598f 100644 --- a/sui-move-runtime/src/tx.rs +++ b/sui-move-runtime/src/tx.rs @@ -17,6 +17,8 @@ use sui_sdk_types::{ TypeTag, UserSignature, }; +use crate::GrpcClient; + #[derive(Clone, Debug)] pub(crate) struct FetchedMoveObject { pub(crate) reference: ObjectReference, @@ -34,13 +36,13 @@ pub struct TxOptions { pub gas: Option, /// Optional explicit gas budget. Defaults to `Runtime::default_gas_budget`. pub gas_budget: Option, - /// Optional explicit gas price. Defaults to the reference gas price from RPC. + /// Optional explicit gas price. Defaults to the reference gas price from gRPC. pub gas_price: Option, /// Optional explicit expiration (TTL). Defaults to `None`. pub expiration: Option, /// Finality requested for the commit. /// - /// When `Checkpointed` (default), `commit*` uses an RPC method that also waits for checkpoint + /// When `Checkpointed` (default), `commit*` uses a gRPC method that also waits for checkpoint /// inclusion ("read-your-writes" consistency on that node). /// /// When `Executed`, `commit*` should only require execution (effects produced). @@ -52,7 +54,7 @@ pub struct TxOptions { pub enum Finality { /// Only require that the transaction was executed (effects produced). Executed, - /// Also wait for the transaction to be observed in a checkpoint on the connected RPC node. + /// Also wait for the transaction to be observed in a checkpoint on the connected gRPC node. Checkpointed, } @@ -99,7 +101,7 @@ pub enum CheckpointWaitOutcome { pub struct Receipt { /// Transaction digest. pub digest: Digest, - /// Transaction effects, if returned by RPC. + /// Transaction effects, if returned by gRPC. pub effects: Option, /// Execution status, if known. pub status: Option, @@ -147,9 +149,9 @@ pub struct SimulateOptions { /// /// Note: this option is ignored when checks are disabled (dev-inspect mode). /// - /// If you set this to `false`, the simulation RPC may require an explicit gas payment on the + /// If you set this to `false`, the simulation gRPC request may require an explicit gas payment on the /// provided transaction. The current `sui-move-runtime` simulation helper does not model - /// explicit gas payment configuration, so `false` is generally only useful if your RPC accepts + /// explicit gas payment configuration, so `false` is generally only useful if your gRPC server accepts /// omitted gas payment without auto-selection. pub do_gas_selection: bool, } @@ -175,9 +177,9 @@ pub struct InspectOptions {} /// runtime-owned handles. #[derive(Clone, Debug)] pub struct SimulationReceipt { - /// Transaction digest, if returned by RPC. + /// Transaction digest, if returned by gRPC. pub digest: Option, - /// Transaction effects, if returned by RPC. + /// Transaction effects, if returned by gRPC. pub effects: Option, } @@ -189,9 +191,9 @@ pub struct SimulationReceipt { /// `outputs[i]` corresponds to the `i`-th PTB command. #[derive(Clone, Debug)] pub struct InspectReceipt { - /// Transaction digest, if returned by RPC. + /// Transaction digest, if returned by gRPC. pub digest: Option, - /// Transaction effects, if returned by RPC. + /// Transaction effects, if returned by gRPC. pub effects: Option, /// Command outputs (return values + mutated-by-ref values), if requested. pub outputs: Vec, @@ -227,7 +229,7 @@ pub struct CommandOutputs { /// ``` #[derive(Clone, Debug, Default)] pub struct BcsValue { - /// Optional type name for this BCS blob, if provided by RPC. + /// Optional type name for this BCS blob, if provided by gRPC. pub name: Option, /// Raw BCS bytes. pub bytes: Vec, @@ -240,11 +242,11 @@ pub enum TxError { #[error("sign transaction: {0}")] Sign(String), - /// RPC execution failed. + /// gRPC execution failed. #[error("execute transaction: {0}")] Execute(String), - /// RPC returned a response missing the executed transaction. + /// gRPC returned a response missing the executed transaction. #[error("missing executed transaction in response")] MissingExecuted, @@ -252,7 +254,7 @@ pub enum TxError { #[error("proto conversion: {0}")] Proto(String), - /// Gas selection failed (no gas coin or RPC error). + /// Gas selection failed (no gas coin or gRPC error). #[error("select gas: {0}")] Gas(String), @@ -267,11 +269,11 @@ pub enum TxError { /// Errors for simulate/dev-inspect operations. #[derive(thiserror::Error, Debug)] pub enum SimulateError { - /// RPC error. - #[error("simulate transaction: {0}")] - Rpc(String), + /// gRPC error. + #[error("simulate transaction over grpc: {0}")] + Grpc(String), - /// RPC returned a response missing the executed transaction. + /// gRPC returned a response missing the executed transaction. #[error("missing executed transaction in response")] MissingExecuted, @@ -282,10 +284,10 @@ pub enum SimulateError { /// Errors for read/fetch helpers. #[derive(thiserror::Error, Debug)] -pub enum RpcError { - /// RPC error. - #[error("rpc: {0}")] - Rpc(String), +pub enum GrpcError { + /// gRPC error. + #[error("grpc: {0}")] + Grpc(String), /// Object not found. #[error("object {0} not found")] Missing(Address), @@ -298,9 +300,9 @@ pub enum RpcError { } pub(crate) async fn fetch_object_reference_and_owner( - client: &mut sui_rpc::Client, + client: &mut GrpcClient, id: Address, -) -> Result { +) -> Result { let mut req = GetObjectRequest::new(&id); let mut mask = FieldMaskTree::default(); for path in [ @@ -322,23 +324,23 @@ pub(crate) async fn fetch_object_reference_and_owner( .ledger_client() .get_object(req) .await - .map_err(|e| RpcError::Rpc(e.to_string()))? + .map_err(|e| GrpcError::Grpc(e.to_string()))? .into_inner(); - let obj_proto = resp.object.ok_or(RpcError::Missing(id))?; + let obj_proto = resp.object.ok_or(GrpcError::Missing(id))?; let proto_ref = obj_proto.object_reference(); let reference: ObjectReference = (&proto_ref) .try_into() - .map_err(|e: TryFromProtoError| RpcError::Proto(e.to_string()))?; + .map_err(|e: TryFromProtoError| GrpcError::Proto(e.to_string()))?; let sui_obj: sui_sdk_types::Object = (&obj_proto) .try_into() - .map_err(|e: TryFromProtoError| RpcError::Proto(e.to_string()))?; + .map_err(|e: TryFromProtoError| GrpcError::Proto(e.to_string()))?; let move_struct = sui_obj .as_struct() - .ok_or(RpcError::NotMoveStruct(*reference.object_id()))?; + .ok_or(GrpcError::NotMoveStruct(*reference.object_id()))?; Ok(FetchedMoveObject { reference, @@ -404,7 +406,7 @@ pub(crate) fn classify_owner(owner: &Owner) -> OwnerKind { } pub(crate) async fn submit_and_wait( - client: &mut sui_rpc::Client, + client: &mut GrpcClient, signer: &S, sender: Address, ptb: sui_sdk_types::ProgrammableTransaction, @@ -502,7 +504,7 @@ pub(crate) async fn submit_and_wait( .execution_client() .execute_transaction(req) .await - .map_err(|e| TxError::Execute(format!("execute_transaction rpc error: {e}")))?; + .map_err(|e| TxError::Execute(format!("execute_transaction grpc error: {e}")))?; let executed = response .into_inner() @@ -534,7 +536,7 @@ pub(crate) async fn submit_and_wait( } pub(crate) async fn fetch_transaction_effects( - client: &mut sui_rpc::Client, + client: &mut GrpcClient, digest: Digest, ) -> Result, TxError> { let mut req = GetTransactionRequest::new(&digest); @@ -548,7 +550,7 @@ pub(crate) async fn fetch_transaction_effects( .ledger_client() .get_transaction(req) .await - .map_err(|e| TxError::Execute(format!("get_transaction rpc error: {e}")))? + .map_err(|e| TxError::Execute(format!("get_transaction grpc error: {e}")))? .into_inner(); let executed = resp.transaction.ok_or(TxError::MissingExecuted)?; @@ -557,7 +559,7 @@ pub(crate) async fn fetch_transaction_effects( } async fn select_gas_coin( - client: &mut sui_rpc::Client, + client: &mut GrpcClient, owner: &Address, ) -> Result { let gas_tag = TypeTag::Struct(Box::new(StructTag::gas_coin())); @@ -579,7 +581,7 @@ async fn select_gas_coin( fn map_execute_wait_error(err: ExecuteAndWaitError, context_label: &'static str) -> TxError { match err { ExecuteAndWaitError::RpcError(e) => { - TxError::Execute(format!("{context_label} rpc error: {e}")) + TxError::Execute(format!("{context_label} grpc error: {e}")) } ExecuteAndWaitError::ProtoConversionError(e) => { TxError::Proto(format!("{context_label} proto conversion error: {e}")) @@ -646,7 +648,7 @@ fn decode_effects_and_status( } pub(crate) async fn simulate_ptb( - client: &mut sui_rpc::Client, + client: &mut GrpcClient, sender: Address, ptb: sui_sdk_types::ProgrammableTransaction, opts: SimulateOptions, @@ -667,14 +669,14 @@ pub(crate) async fn simulate_ptb( .execution_client() .simulate_transaction(req) .await - .map_err(|e| SimulateError::Rpc(e.to_string()))? + .map_err(|e| SimulateError::Grpc(e.to_string()))? .into_inner(); simulation_receipt_from_response(resp) } pub(crate) async fn inspect_ptb( - client: &mut sui_rpc::Client, + client: &mut GrpcClient, sender: Address, ptb: sui_sdk_types::ProgrammableTransaction, _opts: InspectOptions, @@ -699,7 +701,7 @@ pub(crate) async fn inspect_ptb( .execution_client() .simulate_transaction(req) .await - .map_err(|e| SimulateError::Rpc(e.to_string()))? + .map_err(|e| SimulateError::Grpc(e.to_string()))? .into_inner(); inspect_receipt_from_response(resp) From 1e843edefde461219c23ed2784a5ecf29e765715 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 22 Jun 2026 15:36:02 +0200 Subject: [PATCH 4/6] fix: align Sui dependency sources --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9bf71f3..e6084e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" bcs = "0.1.6" -sui-sdk-types = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["serde"] } -sui-rpc = { git = "https://github.com/mystenlabs/sui-rust-sdk" } -sui-crypto = { git = "https://github.com/mystenlabs/sui-rust-sdk" } +sui-sdk-types = { version = "0.3.1", features = ["serde"] } +sui-rpc = { version = "0.3.1" } +sui-crypto = { version = "0.3.0" } proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full"] } From ae41f1bed869e70ee0baabf8682c9cdcaae66e10 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 1 Jul 2026 13:05:12 +0200 Subject: [PATCH 5/6] feat: small fix Signed-off-by: David --- sui-move-codegen/README.md | 21 ++++++++++++- sui-move-codegen/src/render/calls.rs | 2 +- sui-move-codegen/src/render/mod.rs | 11 +++++++ sui-move-codegen/src/render/types.rs | 9 ++++-- sui-move-codegen/src/render/util.rs | 47 ++++++++++++++++++++++++++-- sui-move-derive/README.md | 2 ++ sui-move-derive/src/args.rs | 17 ++++++++++ sui-move-derive/src/expand.rs | 8 ++++- sui-move-derive/src/lib.rs | 9 ++++++ sui-move/tests/derive.rs | 33 +++++++++++++++++++ 10 files changed, 152 insertions(+), 7 deletions(-) diff --git a/sui-move-codegen/README.md b/sui-move-codegen/README.md index c77e8ff..fee827c 100644 --- a/sui-move-codegen/README.md +++ b/sui-move-codegen/README.md @@ -40,7 +40,8 @@ needing network access. Given a `NormalizedPackage` (either fetched from gRPC or loaded from JSON), this crate can render: -- A `pub const PACKAGE: Address` (the on-chain package id) +- A `pub const PACKAGE: Address` defaulting to the package id from metadata +- `package()` / `with_package(...)` helpers for scoped package-id overrides - 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) @@ -52,6 +53,24 @@ Those generated call builders are designed to be used directly in higher layers: - `sui-move-ptb` can consume `CallSpec` to build a `ProgrammableTransaction` - `sui-move-runtime` can consume `CallSpec` via its tx builder (or `sui_move_runtime::tx!`) +Use `with_package` when the same generated bindings should target a different deployment with the +same Move layout: + +```rust +# use sui_sdk_types::Address; +# mod bindings { +# pub const PACKAGE: sui_sdk_types::Address = sui_sdk_types::Address::ZERO; +# pub fn with_package(_package: sui_sdk_types::Address, f: impl FnOnce() -> R) -> R { f() } +# pub mod m { +# pub fn f(_arg0: u64) {} +# } +# } +# let localnet_package = Address::ZERO; +bindings::with_package(localnet_package, || { + bindings::m::f(10); +}); +``` + ## Example: render from an in-memory IR ```rust diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index 8cd4b81..e95e20c 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -76,7 +76,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(), #module_name, #function_name) .expect("valid Move identifiers"); #(#push_type_args)* #(#pushes)* diff --git a/sui-move-codegen/src/render/mod.rs b/sui-move-codegen/src/render/mod.rs index 020582b..efd48d5 100644 --- a/sui-move-codegen/src/render/mod.rs +++ b/sui-move-codegen/src/render/mod.rs @@ -311,6 +311,17 @@ mod tests { assert!(code.contains("push_arg_mut(arg0)")); } + #[test] + fn generated_bindings_use_scoped_package_for_calls_and_type_tags() { + let code = render_package(&demo_pkg(), &RenderOptions::default()); + + assert!(code.contains("thread_local!")); + assert!(code.contains("pub fn package() -> sui_move::prelude::Address")); + assert!(code.contains("pub fn with_package")); + assert!(code.contains("CallSpec::new(package(), \"m\", \"mutate\")")); + assert!(code.contains("address_fn = \"super::package\"")); + } + #[test] fn renders_structs_with_sui_move_move_struct_attribute() { let opts = RenderOptions { diff --git a/sui-move-codegen/src/render/types.rs b/sui-move-codegen/src/render/types.rs index 3a74023..3a8fd4d 100644 --- a/sui-move-codegen/src/render/types.rs +++ b/sui-move-codegen/src/render/types.rs @@ -82,6 +82,11 @@ 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("package", proc_macro2::Span::call_site()) + } else { + syn::LitStr::new("super::package", proc_macro2::Span::call_site()) + }; let fields_tokens = fields.iter().map(|f| { let ident = idents::ident(&f.name); @@ -107,6 +112,7 @@ fn render_struct( #doc #[#macro_path( address = #address_lit, + address_fn = #address_fn, module = #module_lit, #name_arg #abilities_arg @@ -264,7 +270,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()); @@ -275,7 +280,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),*], diff --git a/sui-move-codegen/src/render/util.rs b/sui-move-codegen/src/render/util.rs index d7ece25..8274e92 100644 --- a/sui-move-codegen/src/render/util.rs +++ b/sui-move-codegen/src/render/util.rs @@ -15,6 +15,7 @@ pub(crate) fn render_package_tokens(pkg: &NormalizedPackage, opts: &RenderOption }; let package_const = package_const_tokens(pkg, opts); + let package_scope = package_scope_tokens(opts); let mut modules = Vec::new(); for module in pkg.modules.values() { @@ -44,6 +45,7 @@ pub(crate) fn render_package_tokens(pkg: &NormalizedPackage, opts: &RenderOption quote! { #root_aliases #package_const + #package_scope #(#modules)* #tx_ext #reexports @@ -61,6 +63,7 @@ pub(crate) fn render_split_mod_rs_tokens( }; let package_const = package_const_tokens(pkg, opts); + let package_scope = package_scope_tokens(opts); let module_decls = pkg.modules.values().map(|module| { let module_ident = idents::ident(&module.name); @@ -90,6 +93,7 @@ pub(crate) fn render_split_mod_rs_tokens( quote! { #root_aliases #package_const + #package_scope #(#module_decls)* #tx_ext #reexports @@ -118,7 +122,7 @@ pub(crate) fn render_module_file( quote! { #aliases - use super::PACKAGE; + use super::package; #(#items)* } } @@ -150,7 +154,7 @@ pub(crate) fn render_module( quote! { pub mod #module_ident { #aliases - use super::PACKAGE; + use super::package; #(#items)* } } @@ -167,6 +171,45 @@ fn package_const_tokens(pkg: &NormalizedPackage, opts: &RenderOptions) -> TokenS } } +fn package_scope_tokens(opts: &RenderOptions) -> TokenStream { + let _ = opts; + let address_ty = quote! { sui_move::prelude::Address }; + quote! { + std::thread_local! { + static PACKAGE_OVERRIDE: std::cell::Cell> = + std::cell::Cell::new(None); + } + + /// Current package address for this generated binding. + /// + /// Returns the scoped override set by [`with_package`], or [`PACKAGE`] when no override is active. + pub fn package() -> #address_ty { + PACKAGE_OVERRIDE.with(|slot| slot.get().unwrap_or(PACKAGE)) + } + + /// Run a closure with this generated binding scoped to `package`. + /// + /// The previous package override is restored when the closure returns or unwinds. + pub fn with_package(package: #address_ty, f: impl FnOnce() -> R) -> R { + struct Reset(Option<#address_ty>); + + impl Drop for Reset { + fn drop(&mut self) { + PACKAGE_OVERRIDE.with(|slot| slot.set(self.0)); + } + } + + let previous = PACKAGE_OVERRIDE.with(|slot| { + let previous = slot.get(); + slot.set(Some(package)); + previous + }); + let _reset = Reset(previous); + f() + } + } +} + fn aliases(opts: &RenderOptions) -> TokenStream { if !opts.use_aliases { return quote! {}; diff --git a/sui-move-derive/README.md b/sui-move-derive/README.md index 00483a7..1e760fc 100644 --- a/sui-move-derive/README.md +++ b/sui-move-derive/README.md @@ -82,6 +82,8 @@ Required arguments: Optional arguments: +- `address_fn = "path::to::fn"`: Function returning the Move address to use for `StructTag`s. + `address` remains the default/documented address. - `name = "..."`: Override the Move struct name (defaults to the Rust struct name) - `abilities = "key, store, copy, drop"`: Move abilities (comma-separated) - `copy` implies `drop` diff --git a/sui-move-derive/src/args.rs b/sui-move-derive/src/args.rs index f620f20..a30e151 100644 --- a/sui-move-derive/src/args.rs +++ b/sui-move-derive/src/args.rs @@ -15,6 +15,7 @@ use syn::Lit; #[derive(Default)] pub(crate) struct MoveStructArgs { pub(crate) address: Option, + pub(crate) address_fn: Option, pub(crate) module: Option, pub(crate) name: Option, pub(crate) abilities: Vec, @@ -43,6 +44,22 @@ impl Parse for MoveStructArgs { return Err(syn::Error::new(lit.span(), "address must be a string")); } } + "address_fn" => { + if let Lit::Str(ref s) = lit { + let path: syn::Path = syn::parse_str(&s.value()).map_err(|_| { + syn::Error::new( + s.span(), + "address_fn must be a valid Rust function path", + ) + })?; + args.address_fn = Some(path); + } else { + return Err(syn::Error::new( + lit.span(), + "address_fn must be a string literal path", + )); + } + } "module" => { if let Lit::Str(s) = lit { args.module = Some(s.value()); diff --git a/sui-move-derive/src/expand.rs b/sui-move-derive/src/expand.rs index ebe9858..1fc494d 100644 --- a/sui-move-derive/src/expand.rs +++ b/sui-move-derive/src/expand.rs @@ -203,9 +203,15 @@ pub(crate) fn expand_move_struct( }) .collect::>(); + let address_expr = if let Some(address_fn) = &args.address_fn { + quote! { #address_fn() } + } else { + quote! { ::sui_move::parse_address(#address).expect("invalid address literal") } + }; + 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 1bee176..6967376 100644 --- a/sui-move-derive/src/lib.rs +++ b/sui-move-derive/src/lib.rs @@ -47,6 +47,8 @@ pub fn move_module(_args: TokenStream, input: TokenStream) -> TokenStream { /// /// # Arguments /// - `address = "0x..."` (required): Move address +/// - `address_fn = "path::to::fn"` (optional): Rust function returning the Move address to use +/// when building `StructTag`s; `address` remains the default/documented address /// - `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 @@ -123,12 +125,19 @@ mod tests { fn parse_args() { let args: MoveStructArgs = syn::parse_quote!( address = "0x1", + address_fn = "crate::package", module = "vault", abilities = "key, store", phantoms = "T", type_abilities = "T: store, copy" ); assert_eq!(args.address.as_deref(), Some("0x1")); + assert_eq!( + args.address_fn + .as_ref() + .map(|path| quote::ToTokens::to_token_stream(path).to_string()), + Some("crate :: package".to_string()) + ); 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/tests/derive.rs b/sui-move/tests/derive.rs index af0d786..308d8c0 100644 --- a/sui-move/tests/derive.rs +++ b/sui-move/tests/derive.rs @@ -59,6 +59,26 @@ mod bounded { } } +mod dynamic_address { + use std::str::FromStr; + + use sui_move::prelude::Address; + + pub fn package() -> Address { + Address::from_str("0x9").unwrap() + } + + #[sui_move::move_struct( + address = "0x1", + address_fn = "crate::dynamic_address::package", + module = "dynamic_address", + abilities = "copy, drop, store" + )] + pub struct Value { + pub value: u64, + } +} + #[test] fn type_tag_matches_move_definition() { let tag = vault::Vault::::type_tag_static(); @@ -75,6 +95,19 @@ fn type_tag_matches_move_definition() { } } +#[test] +fn address_fn_overrides_literal_address_for_type_tags() { + let tag = dynamic_address::Value::type_tag_static(); + match tag { + TypeTag::Struct(inner) => { + assert_eq!(*inner.address(), Address::from_str("0x9").unwrap()); + assert_eq!(inner.module().to_string(), "dynamic_address"); + assert_eq!(inner.name().to_string(), "Value"); + } + _ => panic!("expected struct type tag"), + } +} + #[test] fn local_uid_type_matches_framework_identity_without_core_exports() { let tag = object::UID::type_tag_static(); From f49a9fe09cafe22a87cbcf291e1414b3594c2de1 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 2 Jul 2026 10:42:58 +0200 Subject: [PATCH 6/6] chore: small fix --- sui-move-call/src/lib.rs | 88 +++- sui-move-codegen/README.md | 34 +- sui-move-codegen/src/ir.rs | 333 +++++++++++- sui-move-codegen/src/lib.rs | 5 + sui-move-codegen/src/render/calls.rs | 265 +++++++++- sui-move-codegen/src/render/mod.rs | 602 +++++++++++++++++++-- sui-move-codegen/src/render/tx_ext.rs | 40 +- sui-move-codegen/src/render/types.rs | 722 +++++++++++++++++++++++++- sui-move-codegen/src/render/util.rs | 143 +++-- sui-move-codegen/src/source.rs | 2 +- sui-move-codegen/src/source_names.rs | 414 +++++++++++++++ sui-move-derive/src/expand.rs | 109 ++-- sui-move-ptb/Cargo.toml | 1 + sui-move-ptb/src/lib.rs | 171 +++++- sui-move-ptb/tests/basic.rs | 166 +++++- sui-move-runtime/src/lib.rs | 6 +- sui-move/Cargo.toml | 1 - sui-move/src/builtins.rs | 15 + sui-move/src/lib.rs | 28 +- sui-move/tests/derive.rs | 77 +++ 20 files changed, 2970 insertions(+), 252 deletions(-) create mode 100644 sui-move-codegen/src/source_names.rs diff --git a/sui-move-call/src/lib.rs b/sui-move-call/src/lib.rs index 26568dd..488c38c 100644 --- a/sui-move-call/src/lib.rs +++ b/sui-move-call/src/lib.rs @@ -365,6 +365,55 @@ pub enum CallSpecError { /// The provided function string is not a valid Move identifier. #[error("invalid Move identifier for function: `{0}`")] Function(String), + /// A call argument could not be encoded or converted. + #[error(transparent)] + Arg(#[from] CallArgError), +} + +/// Move call target without encoded arguments. +/// +/// This is the lower-level boundary for PTB composition: generated code owns package, module, +/// function, and type arguments, while `sui-move-ptb` can wire arbitrary PTB `Argument`s including +/// previous command results. +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct CallTarget { + /// Package ID that contains the Move module. + pub package: Address, + /// Move module identifier. + pub module: Identifier, + /// Move function identifier. + pub function: Identifier, + /// Type arguments for the call (Move `TypeTag`s). + pub type_arguments: Vec, +} + +impl CallTarget { + /// Create an empty call target for a `(package, module, function)` triple. + pub fn new( + package: Address, + module: impl AsRef, + function: impl AsRef, + ) -> Result { + let module_str = module.as_ref(); + let function_str = function.as_ref(); + + let module = Identifier::from_str(module_str) + .map_err(|_| CallSpecError::Module(module_str.to_string()))?; + let function = Identifier::from_str(function_str) + .map_err(|_| CallSpecError::Function(function_str.to_string()))?; + + Ok(Self { + package, + module, + function, + type_arguments: Vec::new(), + }) + } + + /// Append a type argument derived from a `MoveType`. + pub fn push_type_arg(&mut self) { + self.type_arguments.push(T::type_tag_static()); + } } /// A description of a Move function call. @@ -419,21 +468,30 @@ impl CallSpec { module: impl AsRef, function: impl AsRef, ) -> Result { - let module_str = module.as_ref(); - let function_str = function.as_ref(); - - let module = Identifier::from_str(module_str) - .map_err(|_| CallSpecError::Module(module_str.to_string()))?; - let function = Identifier::from_str(function_str) - .map_err(|_| CallSpecError::Function(function_str.to_string()))?; + Ok(Self::from_target(CallTarget::new( + package, module, function, + )?)) + } - Ok(Self { - package, - module, - function, - type_arguments: Vec::new(), + /// Create an empty call spec from a generated call target. + pub fn from_target(target: CallTarget) -> Self { + Self { + package: target.package, + module: target.module, + function: target.function, + type_arguments: target.type_arguments, arguments: Vec::new(), - }) + } + } + + /// Return this call spec's target without its encoded arguments. + pub fn target(&self) -> CallTarget { + CallTarget { + package: self.package, + module: self.module.clone(), + function: self.function.clone(), + type_arguments: self.type_arguments.clone(), + } } /// Append a type argument derived from a `MoveType`. @@ -471,8 +529,8 @@ impl CallSpec { /// Convenience re-exports for downstream code. pub mod prelude { pub use crate::{ - CallArg, CallArgError, CallSpec, CallSpecError, MoveObject, ObjectArg, ReceivingMoveObject, - SharedMoveObject, ToCallArg, ToCallArgMut, + CallArg, CallArgError, CallSpec, CallSpecError, CallTarget, MoveObject, ObjectArg, + ReceivingMoveObject, SharedMoveObject, ToCallArg, ToCallArgMut, }; pub use sui_move::prelude::*; pub use sui_sdk_types::Mutability; diff --git a/sui-move-codegen/README.md b/sui-move-codegen/README.md index fee827c..996c904 100644 --- a/sui-move-codegen/README.md +++ b/sui-move-codegen/README.md @@ -33,19 +33,20 @@ The pipeline is intentionally split in two: (`NormalizedPackage`). 2. **Render (offline)**: render Rust source from that IR. -Because the IR is JSON-friendly, you can commit it and re-render deterministically in CI without +Because the IR is JSON-serializable, you can commit it and re-render deterministically in CI without needing network access. ## What gets generated Given a `NormalizedPackage` (either fetched from gRPC or loaded from JSON), this crate can render: -- A `pub const PACKAGE: Address` defaulting to the package id from metadata -- `package()` / `with_package(...)` helpers for scoped package-id overrides +- `CALL_PACKAGE` / `TYPE_PACKAGE` constants for generated calls and type identity +- `call_package()` / `type_package()` / `with_packages(...)` helpers for scoped package-id overrides - 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) -- Move functions as Rust functions that return `sui_move_call::CallSpec` +- Move functions as generated `*_target` functions +- Optional typed Rust functions that return `sui_move_call::CallSpec` - (optional) A `TxExt` trait implemented for `sui_move_runtime::Tx` (enable with `RenderOptions::emit_tx_ext`) @@ -53,20 +54,23 @@ Those generated call builders are designed to be used directly in higher layers: - `sui-move-ptb` can consume `CallSpec` to build a `ProgrammableTransaction` - `sui-move-runtime` can consume `CallSpec` via its tx builder (or `sui_move_runtime::tx!`) -Use `with_package` when the same generated bindings should target a different deployment with the -same Move layout: +Use `with_packages` when generated calls target one deployment package while type tags retain the +package that defines the Move types: ```rust # use sui_sdk_types::Address; # mod bindings { -# pub const PACKAGE: sui_sdk_types::Address = sui_sdk_types::Address::ZERO; -# pub fn with_package(_package: sui_sdk_types::Address, f: impl FnOnce() -> R) -> R { f() } +# pub fn with_packages( +# _call_package: sui_sdk_types::Address, +# _type_package: sui_sdk_types::Address, +# f: impl FnOnce() -> R, +# ) -> R { f() } # pub mod m { # pub fn f(_arg0: u64) {} # } # } # let localnet_package = Address::ZERO; -bindings::with_package(localnet_package, || { +bindings::with_packages(localnet_package, localnet_package, || { bindings::m::f(10); }); ``` @@ -120,7 +124,7 @@ let pkg = NormalizedPackage { }; let code = render_package(&pkg, &RenderOptions::default()); -assert!(code.contains("pub const PACKAGE")); +assert!(code.contains("pub const CALL_PACKAGE")); assert!(code.contains("pub struct S")); assert!(code.contains("pub fn f")); ``` @@ -204,7 +208,7 @@ let pkg = NormalizedPackage { }; let code = render_package(&pkg, &RenderOptions::default()); -let start = code.find("pub fn mutate").unwrap(); +let start = code.find("pub fn mutate(").unwrap(); let sig_end = start + code[start..].find('{').unwrap(); let sig = &code[start..sig_end]; @@ -331,14 +335,14 @@ let pkg = NormalizedPackage { }; let code = render_package(&pkg, &RenderOptions::default()); -let start = code.find("pub fn id").unwrap(); +let start = code.find("pub fn id(").unwrap(); let sig_end = start + code[start..].find('{').unwrap(); let sig = &code[start..sig_end]; assert!(sig.contains("pub fn id(arg0: Vec)")); assert!(sig.contains("where")); assert!(sig.contains("T0: sm::MoveType + sm::HasStore")); -assert!(code.contains("spec.push_type_arg::();")); +assert!(code.contains("target.push_type_arg::();")); ``` ## Example: fetch over gRPC @@ -353,14 +357,14 @@ let package_id: Address = "0x2".parse()?; let pkg = fetch_package(&mut client, package_id).await?; let json = pkg.to_json_string()?; -println!("{json}"); +println!("{} bytes", json.len()); # Ok(()) # } ``` ## Recommended workflow -To keep builds deterministic, fetch metadata once and commit it (JSON), then render from JSON: +To keep builds deterministic, fetch metadata once and commit it as JSON, then render from JSON: 1. Fetch and save `NormalizedPackage` JSON (out-of-band; not in `build.rs`) 2. Render Rust bindings from that JSON during builds or as a pre-generation step diff --git a/sui-move-codegen/src/ir.rs b/sui-move-codegen/src/ir.rs index b38a19b..e67c3be 100644 --- a/sui-move-codegen/src/ir.rs +++ b/sui-move-codegen/src/ir.rs @@ -66,6 +66,22 @@ fn normalize_address(input: &str) -> String { format!("0x{addr}") } +fn replace_address(address: &mut String, replacements: &[(A, B)]) +where + A: AsRef, + B: AsRef, +{ + let normalized = normalize_address(address); + if let Some((_, replacement)) = replacements + .iter() + .find(|(actual, _)| normalize_address(actual.as_ref()) == normalized) + { + *address = normalize_address(replacement.as_ref()); + } else { + *address = normalized; + } +} + /// A Move type reference from package metadata. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TypeRef { @@ -107,6 +123,64 @@ pub enum TypeRef { TypeParameter(u32), } +impl TypeRef { + fn replace_addresses(&mut self, replacements: &[(A, B)]) + where + A: AsRef, + B: AsRef, + { + match self { + TypeRef::Vector(inner) | TypeRef::Ref { inner, .. } => { + inner.replace_addresses(replacements); + } + TypeRef::Datatype { + type_name, + type_arguments, + } => { + replace_address(&mut type_name.address, replacements); + for ty in type_arguments { + ty.replace_addresses(replacements); + } + } + TypeRef::Address + | TypeRef::Bool + | TypeRef::U8 + | TypeRef::U16 + | TypeRef::U32 + | TypeRef::U64 + | TypeRef::U128 + | TypeRef::U256 + | TypeRef::TypeParameter(_) => {} + } + } + + fn collect_addresses<'a>(&'a self, out: &mut Vec<&'a str>) { + match self { + TypeRef::Vector(inner) | TypeRef::Ref { inner, .. } => { + inner.collect_addresses(out); + } + TypeRef::Datatype { + type_name, + type_arguments, + } => { + out.push(type_name.address.as_str()); + for ty in type_arguments { + ty.collect_addresses(out); + } + } + TypeRef::Address + | TypeRef::Bool + | TypeRef::U8 + | TypeRef::U16 + | TypeRef::U32 + | TypeRef::U64 + | TypeRef::U128 + | TypeRef::U256 + | TypeRef::TypeParameter(_) => {} + } + } +} + /// A function parameter (name is synthesized; Sui metadata does not carry parameter names). #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct FunctionParam { @@ -237,16 +311,157 @@ pub struct NormalizedPackage { pub modules: BTreeMap, } +/// Function parameter-name overlay failed because source and IR arities differ. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FunctionParameterNameMismatch { + /// Move module name. + pub module: String, + /// Move function name. + pub function: String, + /// Number of parameters in the normalized IR. + pub ir_count: usize, + /// Number of parameters recovered from source. + pub source_count: usize, +} + +impl std::fmt::Display for FunctionParameterNameMismatch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "`{}::{}` function parameter count mismatch for source names: IR has {}, source has {}", + self.module, self.function, self.ir_count, self.source_count + ) + } +} + +impl std::error::Error for FunctionParameterNameMismatch {} + impl NormalizedPackage { /// Serialize the normalized package to pretty JSON. - pub fn to_json_string(&self) -> serde_json::Result { + pub fn to_json_string(&self) -> Result { serde_json::to_string_pretty(self) } /// Parse a normalized package from JSON. - pub fn from_json_str(input: &str) -> serde_json::Result { + pub fn from_json_str(input: &str) -> Result { serde_json::from_str(input) } + + /// Rewrite every package/type address in this IR using normalized `actual -> replacement` + /// pairs. + pub fn replace_addresses(&mut self, replacements: &[(A, B)]) + where + A: AsRef, + B: AsRef, + { + replace_address(&mut self.storage_id, replacements); + if let Some(original_id) = &mut self.original_id { + replace_address(original_id, replacements); + } + + for module in self.modules.values_mut() { + for datatype in &mut module.datatypes { + replace_address(&mut datatype.type_name.address, replacements); + match &mut datatype.kind { + DatatypeKind::Struct { fields } => { + for field in fields { + field.ty.replace_addresses(replacements); + } + } + DatatypeKind::Enum { variants } => { + for variant in variants { + for field in &mut variant.fields { + field.ty.replace_addresses(replacements); + } + } + } + } + } + + for function in &mut module.functions { + for parameter in &mut function.parameters { + parameter.ty.replace_addresses(replacements); + } + for return_type in &mut function.return_types { + return_type.replace_addresses(replacements); + } + } + } + } + + /// Return every package/type address referenced by this IR. + pub fn referenced_addresses(&self) -> Vec<&str> { + let mut addresses = vec![self.storage_id.as_str()]; + if let Some(original_id) = &self.original_id { + addresses.push(original_id.as_str()); + } + + for module in self.modules.values() { + for datatype in &module.datatypes { + addresses.push(datatype.type_name.address.as_str()); + match &datatype.kind { + DatatypeKind::Struct { fields } => { + for field in fields { + field.ty.collect_addresses(&mut addresses); + } + } + DatatypeKind::Enum { variants } => { + for variant in variants { + for field in &variant.fields { + field.ty.collect_addresses(&mut addresses); + } + } + } + } + } + + for function in &module.functions { + for parameter in &function.parameters { + parameter.ty.collect_addresses(&mut addresses); + } + for return_type in &function.return_types { + return_type.collect_addresses(&mut addresses); + } + } + } + + addresses + } + + /// Replace synthesized `argN` function parameter names with names recovered from Move source. + pub fn apply_function_parameter_names( + &mut self, + names: &BTreeMap<(String, String), Vec>, + ) -> Result<(), FunctionParameterNameMismatch> { + for (module_name, module) in &mut self.modules { + for function in &mut module.functions { + let Some(source_names) = + names.get(&(module_name.to_owned(), function.name.to_owned())) + else { + continue; + }; + + if function.parameters.len() != source_names.len() { + return Err(FunctionParameterNameMismatch { + module: module_name.to_owned(), + function: function.name.to_owned(), + ir_count: function.parameters.len(), + source_count: source_names.len(), + }); + } + + for (index, (parameter, source_name)) in + function.parameters.iter_mut().zip(source_names).enumerate() + { + if parameter.name == format!("arg{index}") { + parameter.name = source_name.to_owned(); + } + } + } + } + + Ok(()) + } } #[cfg(test)] @@ -320,10 +535,116 @@ mod tests { )]), }; - let json = pkg.to_json_string().expect("serialize"); - let decoded = NormalizedPackage::from_json_str(&json).expect("deserialize"); + let encoded = pkg.to_json_string().expect("serialize"); + let decoded = NormalizedPackage::from_json_str(&encoded).expect("deserialize"); assert_eq!(pkg, decoded); - assert!(json.contains("\"storage_id\"")); - assert!(json.contains("\"modules\"")); + assert!(!encoded.is_empty()); + } + + #[test] + fn replace_addresses_rewrites_package_and_nested_type_refs() { + let mut pkg = NormalizedPackage { + storage_id: "0x0009".into(), + original_id: Some("0x9".into()), + version: 1, + modules: BTreeMap::from([( + "m".into(), + NormalizedModule { + name: "m".into(), + datatypes: vec![Datatype { + type_name: TypeName::parse("0x09::m::Obj").unwrap(), + module: "m".into(), + name: "Obj".into(), + abilities: vec![], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "inner".into(), + position: 0, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0x0009::m::Inner").unwrap(), + type_arguments: vec![TypeRef::Datatype { + type_name: TypeName::parse("0x9::m::Leaf").unwrap(), + type_arguments: vec![], + }], + }, + }], + }, + }], + functions: vec![Function { + name: "use_obj".into(), + visibility: Visibility::Public, + is_entry: false, + type_parameters: vec![], + parameters: vec![FunctionParam { + name: "arg0".into(), + ty: TypeRef::Ref { + mutable: true, + inner: Box::new(TypeRef::Datatype { + type_name: TypeName::parse("0x9::m::Obj").unwrap(), + type_arguments: vec![], + }), + }, + }], + return_types: vec![TypeRef::Datatype { + type_name: TypeName::parse("0x9::m::Obj").unwrap(), + type_arguments: vec![], + }], + }], + }, + )]), + }; + + pkg.replace_addresses(&[("0x9", "0xa1")]); + + assert_eq!(pkg.storage_id, "0xa1"); + assert_eq!(pkg.original_id.as_deref(), Some("0xa1")); + assert!(pkg + .referenced_addresses() + .into_iter() + .all(|address| address == "0xa1")); + } + + #[test] + fn applies_source_names_only_to_synthesized_parameters() { + let mut pkg = NormalizedPackage { + storage_id: "0x1".into(), + original_id: None, + version: 1, + modules: BTreeMap::from([( + "m".into(), + NormalizedModule { + name: "m".into(), + datatypes: vec![], + functions: vec![Function { + name: "f".into(), + visibility: Visibility::Public, + is_entry: true, + type_parameters: vec![], + parameters: vec![ + FunctionParam { + name: "arg0".into(), + ty: TypeRef::U64, + }, + FunctionParam { + name: "explicit".into(), + ty: TypeRef::Bool, + }, + ], + return_types: vec![], + }], + }, + )]), + }; + let names = BTreeMap::from([( + ("m".to_string(), "f".to_string()), + vec!["amount".to_string(), "flag".to_string()], + )]); + + pkg.apply_function_parameter_names(&names).unwrap(); + let parameters = &pkg.modules["m"].functions[0].parameters; + + assert_eq!(parameters[0].name, "amount"); + assert_eq!(parameters[1].name, "explicit"); } } diff --git a/sui-move-codegen/src/lib.rs b/sui-move-codegen/src/lib.rs index c2720ab..4479751 100644 --- a/sui-move-codegen/src/lib.rs +++ b/sui-move-codegen/src/lib.rs @@ -4,6 +4,7 @@ //! See `README.md` for the crate-level overview. mod source; +mod source_names; /// Normalized, serde-friendly package IR. pub mod ir; @@ -12,6 +13,10 @@ pub mod ir; pub mod render; pub use crate::source::fetch_package; +pub use crate::source_names::{ + apply_function_parameter_names_from_sources, function_parameter_names_from_sources, + SourceNameError, +}; /// Sui gRPC client used by package metadata fetching. pub type GrpcClient = sui_rpc::Client; diff --git a/sui-move-codegen/src/render/calls.rs b/sui-move-codegen/src/render/calls.rs index e95e20c..70953a0 100644 --- a/sui-move-codegen/src/render/calls.rs +++ b/sui-move-codegen/src/render/calls.rs @@ -1,14 +1,19 @@ -//! Call-stub rendering (Move functions → `CallSpec` builders). +//! Call-stub rendering (Move functions → call targets and optional `CallSpec` builders). //! //! Design goals: //! - keep the generated API “honest”: it mirrors the Move signature shape (generic params, `&mut`) -//! - keep generated calls composable: every function returns a `CallSpec` +//! - keep generated calls composable: every function exposes a `CallTarget` +//! - optionally mirror the Move signature as a typed `CallSpec` builder //! - keep `TxContext` out of user code: higher layers supply it during PTB building use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use std::collections::{BTreeMap, BTreeSet}; -use crate::ir::{Ability, Function, NormalizedModule, NormalizedPackage, TypeRef, Visibility}; +use crate::ir::{ + Ability, Datatype, DatatypeKind, Field, Function, NormalizedModule, NormalizedPackage, + TypeName, TypeRef, Visibility, +}; use super::{builtins, idents, types, RenderOptions}; @@ -20,7 +25,7 @@ pub(crate) fn render_functions( module .functions .iter() - .filter(|f| matches!(f.visibility, Visibility::Public)) + .filter(|f| matches!(f.visibility, Visibility::Public) || f.is_entry) .map(|f| render_function(module, f, pkg, opts)) .collect() } @@ -38,23 +43,29 @@ fn render_function( }; let fn_ident = idents::ident(&f.name); + let target_fn_ident = idents::ident(&format!("{}_target", f.name)); let type_params = (0..f.type_parameters.len()) .map(|i| format_ident!("T{i}")) .collect::>(); let fn_generics = type_generics(&type_params); - let bounds = type_param_bounds(f, opts.use_aliases); + let bounds = type_param_bounds(f, pkg, opts.use_aliases); let where_clause = where_clause(&bounds); 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 target_init = if type_params.is_empty() { + quote! { let target = #sm_call::CallTarget::new(call_package(), #module_name, #function_name)?; } + } else { + quote! { let mut target = #sm_call::CallTarget::new(call_package(), #module_name, #function_name)?; } + }; + let push_type_args = type_params .iter() - .map(|ty| quote! { spec.push_type_arg::<#ty>(); }); - - let (params, pushes, skipped_tx_context) = render_params_and_pushes(module, f, pkg, opts); + .map(|ty| quote! { target.push_type_arg::<#ty>(); }); + let skipped_tx_context = has_tx_context(f); let signature = move_signature_string(module, f); let doc = doc_lines(&[ format!("Move: `{signature}`"), @@ -69,19 +80,44 @@ fn render_function( "Note: this function is not marked `entry`.".to_string() }, ]); + let target_call = if type_params.is_empty() { + quote! { #target_fn_ident() } + } else { + quote! { #target_fn_ident::<#(#type_params),*>() } + }; + let spec_builder = if opts.emit_call_specs { + let (params, pushes) = render_params_and_pushes(module, f, pkg, opts); + let spec_init = if pushes.is_empty() { + quote! { let spec = #sm_call::CallSpec::from_target(#target_call?); } + } else { + quote! { let mut spec = #sm_call::CallSpec::from_target(#target_call?); } + }; + + quote! { + #doc + pub fn #fn_ident #fn_generics ( #(#params),* ) -> Result<#sm_call::CallSpec, #sm_call::CallSpecError> + #where_clause + { + #spec_init + #(#pushes)* + Ok(spec) + } + } + } else { + quote! {} + }; quote! { #doc - #[must_use] - pub fn #fn_ident #fn_generics ( #(#params),* ) -> #sm_call::CallSpec + pub fn #target_fn_ident #fn_generics () -> Result<#sm_call::CallTarget, #sm_call::CallSpecError> #where_clause { - let mut spec = #sm_call::CallSpec::new(package(), #module_name, #function_name) - .expect("valid Move identifiers"); + #target_init #(#push_type_args)* - #(#pushes)* - spec + Ok(target) } + + #spec_builder } } @@ -90,7 +126,7 @@ fn render_params_and_pushes( f: &Function, pkg: &NormalizedPackage, opts: &RenderOptions, -) -> (Vec, Vec, bool) { +) -> (Vec, Vec) { let sm_call = if opts.use_aliases { quote! { sm_call } } else { @@ -99,12 +135,10 @@ fn render_params_and_pushes( let mut params = Vec::new(); let mut pushes = Vec::new(); - let mut skipped_tx_context = false; let mut arg_idx: usize = 0; for p in &f.parameters { if is_tx_context(&p.ty) { - skipped_tx_context = true; continue; } @@ -123,18 +157,22 @@ fn render_params_and_pushes( }; params.push(quote! { #arg_ident: #param_ty }); if ref_mutable { - pushes.push(quote! { spec.push_arg_mut(#arg_ident).expect("encode arg"); }); + pushes.push(quote! { spec.push_arg_mut(#arg_ident)?; }); } else { - pushes.push(quote! { spec.push_arg(#arg_ident).expect("encode arg"); }); + pushes.push(quote! { spec.push_arg(#arg_ident)?; }); } } else { let value_ty = types::render_type_ref_in_module(inner, &module.name, pkg, opts); params.push(quote! { #arg_ident: #value_ty }); - pushes.push(quote! { spec.push_arg(&#arg_ident).expect("encode arg"); }); + pushes.push(quote! { spec.push_arg(&#arg_ident)?; }); } } - (params, pushes, skipped_tx_context) + (params, pushes) +} + +fn has_tx_context(f: &Function) -> bool { + f.parameters.iter().any(|p| is_tx_context(&p.ty)) } fn is_tx_context(ty: &TypeRef) -> bool { @@ -189,27 +227,35 @@ fn is_object_type( } } -fn type_param_bounds(f: &Function, use_aliases: bool) -> Vec { +pub(super) fn type_param_bounds( + f: &Function, + pkg: &NormalizedPackage, + use_aliases: bool, +) -> Vec { let sm = if use_aliases { quote! { sm } } else { quote! { sui_move } }; + let required = required_type_param_abilities(f, pkg); + f.type_parameters .iter() .enumerate() - .map(|(idx, p)| { + .map(|(idx, _)| { let ty = format_ident!("T{idx}"); - let base = if p.constraints.contains(&Ability::Key) { + let abilities = required.get(&idx).cloned().unwrap_or_default(); + + let base = if abilities.contains(&Ability::Key) { quote! { #sm::MoveStruct } } else { quote! { #sm::MoveType } }; let mut bounds: Vec = vec![base]; - for a in &p.constraints { + for a in abilities { bounds.push(match a { Ability::Copy => quote! { #sm::HasCopy }, Ability::Drop => quote! { #sm::HasDrop }, @@ -222,6 +268,175 @@ fn type_param_bounds(f: &Function, use_aliases: bool) -> Vec { .collect() } +fn required_type_param_abilities( + f: &Function, + pkg: &NormalizedPackage, +) -> BTreeMap> { + let mut required: BTreeMap> = f + .type_parameters + .iter() + .enumerate() + .map(|(idx, p)| (idx, p.constraints.iter().cloned().collect())) + .collect(); + + for p in &f.parameters { + if is_tx_context(&p.ty) { + continue; + } + + let (_, inner) = split_ref(&p.ty); + collect_move_type_bounds(inner, pkg, &mut required); + } + + required +} + +fn collect_move_type_bounds( + ty: &TypeRef, + pkg: &NormalizedPackage, + required: &mut BTreeMap>, +) { + match ty { + TypeRef::Vector(inner) => collect_move_type_bounds(inner, pkg, required), + TypeRef::Ref { inner, .. } => collect_move_type_bounds(inner, pkg, required), + TypeRef::Datatype { + type_name, + type_arguments, + } => { + if let Some(dt) = find_local_datatype(pkg, type_name) { + collect_datatype_impl_bounds(dt, type_arguments, pkg, required); + } + } + _ => {} + } +} + +fn collect_ability_bounds( + ty: &TypeRef, + ability: Ability, + pkg: &NormalizedPackage, + required: &mut BTreeMap>, +) { + match ty { + TypeRef::TypeParameter(idx) => { + required.entry(*idx as usize).or_default().insert(ability); + } + TypeRef::Vector(inner) => collect_ability_bounds(inner, ability, pkg, required), + TypeRef::Ref { inner, .. } => collect_ability_bounds(inner, ability, pkg, required), + TypeRef::Datatype { + type_name, + type_arguments, + } => { + if let Some(dt) = find_local_datatype(pkg, type_name) { + collect_datatype_impl_bounds(dt, type_arguments, pkg, required); + } + } + _ => {} + } +} + +fn collect_datatype_impl_bounds( + dt: &Datatype, + type_arguments: &[TypeRef], + pkg: &NormalizedPackage, + required: &mut BTreeMap>, +) { + for (idx, param) in dt.type_parameters.iter().enumerate() { + let Some(arg) = type_arguments.get(idx) else { + continue; + }; + + collect_move_type_bounds(arg, pkg, required); + for ability in ¶m.constraints { + collect_ability_bounds(arg, ability.clone(), pkg, required); + } + } + + match &dt.kind { + DatatypeKind::Struct { fields } => { + collect_field_ability_bounds(fields, &dt.abilities, type_arguments, pkg, required); + } + DatatypeKind::Enum { variants } => { + for variant in variants { + collect_field_ability_bounds( + &variant.fields, + &dt.abilities, + type_arguments, + pkg, + required, + ); + } + } + } +} + +fn collect_field_ability_bounds( + fields: &[Field], + abilities: &[Ability], + type_arguments: &[TypeRef], + pkg: &NormalizedPackage, + required: &mut BTreeMap>, +) { + for field in fields { + let ty = substitute_type_params(&field.ty, type_arguments); + for ability in abilities { + match ability { + Ability::Copy | Ability::Drop | Ability::Store => { + collect_ability_bounds(&ty, ability.clone(), pkg, required); + } + Ability::Key => {} + } + } + } +} + +fn substitute_type_params(ty: &TypeRef, type_arguments: &[TypeRef]) -> TypeRef { + match ty { + TypeRef::Vector(inner) => { + TypeRef::Vector(Box::new(substitute_type_params(inner, type_arguments))) + } + TypeRef::Ref { mutable, inner } => TypeRef::Ref { + mutable: *mutable, + inner: Box::new(substitute_type_params(inner, type_arguments)), + }, + TypeRef::Datatype { + type_name, + type_arguments: args, + } => TypeRef::Datatype { + type_name: type_name.clone(), + type_arguments: args + .iter() + .map(|arg| substitute_type_params(arg, type_arguments)) + .collect(), + }, + TypeRef::TypeParameter(idx) => type_arguments + .get(*idx as usize) + .cloned() + .unwrap_or(TypeRef::TypeParameter(*idx)), + other => other.clone(), + } +} + +fn find_local_datatype<'a>( + pkg: &'a NormalizedPackage, + type_name: &TypeName, +) -> Option<&'a Datatype> { + if type_name.address != pkg.storage_id + && match &pkg.original_id { + Some(original_id) => type_name.address != *original_id, + None => true, + } + { + return None; + } + + pkg.modules + .get(&type_name.module)? + .datatypes + .iter() + .find(|dt| dt.type_name.name == type_name.name) +} + fn where_clause(bounds: &[TokenStream]) -> TokenStream { if bounds.is_empty() { quote! {} diff --git a/sui-move-codegen/src/render/mod.rs b/sui-move-codegen/src/render/mod.rs index efd48d5..8c2ae50 100644 --- a/sui-move-codegen/src/render/mod.rs +++ b/sui-move-codegen/src/render/mod.rs @@ -7,7 +7,8 @@ //! //! The generated code is designed to plug into the rest of the workspace: //! - generated types implement `sui-move` traits (`MoveType` / `MoveStruct`) and ability markers -//! - generated functions return `sui-move-call::CallSpec` +//! - generated call targets identify Move functions for PTB builders +//! - generated call-spec builders can return `sui-move-call::CallSpec` //! - optionally, a `TxExt` trait is emitted to add calls directly to `sui-move-runtime::Tx` use std::collections::BTreeMap; @@ -32,6 +33,11 @@ pub struct RenderOptions { pub emit_types: bool, /// Emit call builder functions. pub emit_calls: bool, + /// Emit typed `CallSpec` builders in addition to generated `*_target` functions. + /// + /// Set this to `false` for consumers that compose PTBs from generated targets and explicit + /// `sui_sdk_types::Argument`s. + pub emit_call_specs: bool, /// Emit runtime helpers for `sui-move-runtime` (`TxExt`). /// /// When enabled, generated code includes a `TxExt` trait implemented for @@ -49,6 +55,8 @@ pub struct RenderOptions { /// - `use sui_move as sm;` /// - `use sui_move_call as sm_call;` pub use_aliases: bool, + /// If `true`, re-export generated datatypes from the package root. + pub emit_reexports: bool, /// Rust paths for Move datatypes defined outside the package currently being rendered. /// /// This is the explicit cross-package symbol table. The renderer still treats unknown external @@ -75,9 +83,11 @@ impl Default for RenderOptions { Self { emit_types: true, emit_calls: true, + emit_call_specs: true, emit_tx_ext: false, flatten: false, use_aliases: true, + emit_reexports: true, external_types: BTreeMap::new(), } } @@ -174,6 +184,19 @@ fn external_type_names(pkg: &NormalizedPackage, dt: &crate::ir::Datatype) -> Vec names } +/// Rendered package split into package-root code and one generated module block per Move module. +/// +/// This is useful for build scripts that need to choose their own `include!` layout without +/// parsing rendered Rust source text. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RenderedPackageParts { + /// Package-root code: package constants, package scoping helpers, optional `TxExt`, and + /// optional root re-exports. + pub root: String, + /// `pub mod ... { ... }` blocks keyed by Move module name. + pub modules: BTreeMap, +} + /// Render a normalized package into a single Rust source string. /// /// The output is valid Rust source that you can write to a `.rs` file (or `include!` as a module). @@ -199,18 +222,39 @@ fn external_type_names(pkg: &NormalizedPackage, dt: &crate::ir::Datatype) -> Vec /// }; /// /// let code = render_package(&pkg, &RenderOptions::default()); -/// assert!(code.contains("pub const PACKAGE")); +/// assert!(code.contains("pub const CALL_PACKAGE")); /// ``` pub fn render_package(pkg: &NormalizedPackage, opts: &RenderOptions) -> String { let tokens = util::render_package_tokens(pkg, opts); util::prettify(tokens) } +/// Render a normalized package into package-root source and per-module `pub mod ...` blocks. +/// +/// Unlike [`render_package_split`], this does not write files or assume `mod.rs` plus sibling +/// module files. Callers decide how to include or store the returned strings. +pub fn render_package_parts(pkg: &NormalizedPackage, opts: &RenderOptions) -> RenderedPackageParts { + let mut parts_opts = opts.clone(); + parts_opts.flatten = false; + + let root = util::prettify(util::render_package_root_tokens(pkg, &parts_opts, false)); + let modules = pkg + .modules + .values() + .map(|module| { + let tokens = util::render_module(module, pkg, &parts_opts); + (module.name.clone(), util::prettify(tokens)) + }) + .collect(); + + RenderedPackageParts { root, modules } +} + /// Render a normalized package into multiple files (`mod.rs` + one file per Move module). /// /// This is convenient if you want the generated code to mirror the Move module structure on disk. /// The output directory will contain: -/// - `mod.rs` (with `PACKAGE`, `pub mod ...;`, and `pub use ...;` re-exports) +/// - `mod.rs` (with package scope helpers, `pub mod ...;`, and `pub use ...;` re-exports) /// - one `*.rs` file per Move module pub fn render_package_split( pkg: &NormalizedPackage, @@ -245,6 +289,102 @@ mod tests { use crate::ir::*; fn demo_pkg() -> NormalizedPackage { + NormalizedPackage { + storage_id: "0x1".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([( + "m".into(), + NormalizedModule { + name: "m".into(), + datatypes: vec![ + Datatype { + type_name: TypeName::parse("0x1::m::Obj").unwrap(), + module: "m".into(), + name: "Obj".into(), + abilities: vec![Ability::Key, Ability::Store], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "id".into(), + position: 0, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0x2::object::UID").unwrap(), + type_arguments: vec![], + }, + }], + }, + }, + Datatype { + type_name: TypeName::parse("0x1::m::Pair").unwrap(), + module: "m".into(), + name: "Pair".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![ + Field { + name: "left".into(), + position: 0, + ty: TypeRef::U8, + }, + Field { + name: "right".into(), + position: 1, + ty: TypeRef::Bool, + }, + ], + }, + }, + ], + functions: vec![ + Function { + name: "mutate".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("0x1::m::Obj").unwrap(), + type_arguments: vec![], + }), + }, + }, + FunctionParam { + name: "arg1".into(), + ty: TypeRef::Ref { + mutable: true, + inner: Box::new(TypeRef::Datatype { + type_name: TypeName::parse( + "0x2::tx_context::TxContext", + ) + .unwrap(), + type_arguments: vec![], + }), + }, + }, + ], + return_types: vec![], + }, + Function { + name: "private_entry".into(), + visibility: Visibility::Private, + is_entry: true, + type_parameters: vec![], + parameters: vec![], + return_types: vec![], + }, + ], + }, + )]), + } + } + + fn generic_value_arg_pkg() -> NormalizedPackage { NormalizedPackage { storage_id: "0x1".into(), original_id: None, @@ -254,57 +394,299 @@ mod tests { NormalizedModule { name: "m".into(), datatypes: vec![Datatype { - type_name: TypeName::parse("0x1::m::Obj").unwrap(), + type_name: TypeName::parse("0x1::m::Cloneable").unwrap(), module: "m".into(), - name: "Obj".into(), - abilities: vec![Ability::Key, Ability::Store], - type_parameters: vec![], + name: "Cloneable".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![TypeParameter { + constraints: vec![Ability::Store, Ability::Copy], + is_phantom: false, + }], kind: DatatypeKind::Struct { fields: vec![Field { - name: "id".into(), + name: "value".into(), position: 0, - ty: TypeRef::Datatype { - type_name: TypeName::parse("0x2::object::UID").unwrap(), - type_arguments: vec![], - }, + ty: TypeRef::TypeParameter(0), }], }, }], functions: vec![Function { - name: "mutate".into(), + name: "use_cloneable".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("0x1::m::Obj").unwrap(), - type_arguments: vec![], - }), + type_parameters: vec![TypeParameter { + constraints: vec![Ability::Store, Ability::Copy], + is_phantom: false, + }], + parameters: vec![FunctionParam { + name: "arg0".into(), + ty: TypeRef::Datatype { + type_name: TypeName::parse("0x1::m::Cloneable").unwrap(), + type_arguments: vec![TypeRef::TypeParameter(0)], + }, + }], + return_types: vec![], + }], + }, + )]), + } + } + + fn option_pkg() -> NormalizedPackage { + NormalizedPackage { + storage_id: "0x1".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([( + "option".into(), + NormalizedModule { + name: "option".into(), + datatypes: vec![Datatype { + type_name: TypeName::parse("0x1::option::Option").unwrap(), + module: "option".into(), + name: "Option".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![TypeParameter { + constraints: vec![], + is_phantom: false, + }], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "vec".into(), + position: 0, + ty: TypeRef::Vector(Box::new(TypeRef::TypeParameter(0))), + }], + }, + }], + functions: vec![], + }, + )]), + } + } + + fn layout_helper_pkg() -> NormalizedPackage { + NormalizedPackage { + storage_id: "0x2".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([ + ( + "table_vec".into(), + NormalizedModule { + name: "table_vec".into(), + datatypes: vec![Datatype { + type_name: TypeName::parse("0x2::table_vec::TableVec").unwrap(), + module: "table_vec".into(), + name: "TableVec".into(), + abilities: vec![Ability::Store], + type_parameters: vec![TypeParameter { + constraints: vec![Ability::Store], + is_phantom: true, + }], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "contents".into(), + position: 0, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0x2::table::Table").unwrap(), + type_arguments: vec![ + TypeRef::U64, + TypeRef::TypeParameter(0), + ], + }, + }], + }, + }], + functions: vec![], + }, + ), + ( + "vec_map".into(), + NormalizedModule { + name: "vec_map".into(), + datatypes: vec![ + Datatype { + type_name: TypeName::parse("0x2::vec_map::Entry").unwrap(), + module: "vec_map".into(), + name: "Entry".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![ + TypeParameter { + constraints: vec![Ability::Copy], + is_phantom: false, + }, + TypeParameter { + constraints: vec![], + is_phantom: false, + }, + ], + kind: DatatypeKind::Struct { + fields: vec![ + Field { + name: "key".into(), + position: 0, + ty: TypeRef::TypeParameter(0), + }, + Field { + name: "value".into(), + position: 1, + ty: TypeRef::TypeParameter(1), + }, + ], }, }, - FunctionParam { - name: "arg1".into(), - ty: TypeRef::Ref { - mutable: true, - inner: Box::new(TypeRef::Datatype { - type_name: TypeName::parse("0x2::tx_context::TxContext") - .unwrap(), - type_arguments: vec![], - }), + Datatype { + type_name: TypeName::parse("0x2::vec_map::VecMap").unwrap(), + module: "vec_map".into(), + name: "VecMap".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![ + TypeParameter { + constraints: vec![Ability::Copy], + is_phantom: false, + }, + TypeParameter { + constraints: vec![], + is_phantom: false, + }, + ], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "contents".into(), + position: 0, + ty: TypeRef::Vector(Box::new(TypeRef::Datatype { + type_name: TypeName::parse("0x2::vec_map::Entry") + .unwrap(), + type_arguments: vec![ + TypeRef::TypeParameter(0), + TypeRef::TypeParameter(1), + ], + })), + }], }, }, ], - return_types: vec![], - }], + functions: vec![], + }, + ), + ]), + } + } + + fn rust_copy_shape_pkg() -> NormalizedPackage { + NormalizedPackage { + storage_id: "0x1".into(), + original_id: None, + version: 0, + modules: BTreeMap::from([( + "m".into(), + NormalizedModule { + name: "m".into(), + datatypes: vec![ + Datatype { + type_name: TypeName::parse("0x1::m::Scalar").unwrap(), + module: "m".into(), + name: "Scalar".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "value".into(), + position: 0, + ty: TypeRef::U64, + }], + }, + }, + Datatype { + type_name: TypeName::parse("0x1::m::Nested").unwrap(), + module: "m".into(), + name: "Nested".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "scalar".into(), + position: 0, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0x1::m::Scalar").unwrap(), + type_arguments: vec![], + }, + }], + }, + }, + Datatype { + type_name: TypeName::parse("0x1::m::Bytes").unwrap(), + module: "m".into(), + name: "Bytes".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "value".into(), + position: 0, + ty: TypeRef::Vector(Box::new(TypeRef::U8)), + }], + }, + }, + Datatype { + type_name: TypeName::parse("0x1::m::Generic").unwrap(), + module: "m".into(), + name: "Generic".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![TypeParameter { + constraints: vec![Ability::Store, Ability::Copy], + is_phantom: false, + }], + kind: DatatypeKind::Struct { + fields: vec![Field { + name: "value".into(), + position: 0, + ty: TypeRef::TypeParameter(0), + }], + }, + }, + Datatype { + type_name: TypeName::parse("0x1::m::ScalarChoice").unwrap(), + module: "m".into(), + name: "ScalarChoice".into(), + abilities: vec![Ability::Store, Ability::Copy, Ability::Drop], + type_parameters: vec![], + kind: DatatypeKind::Enum { + variants: vec![ + Variant { + name: "None".into(), + position: 0, + fields: vec![], + }, + Variant { + name: "Some".into(), + position: 1, + fields: vec![Field { + name: "value".into(), + position: 0, + ty: TypeRef::Datatype { + type_name: TypeName::parse("0x1::m::Nested") + .unwrap(), + type_arguments: vec![], + }, + }], + }, + ], + }, + }, + ], + functions: vec![], }, )]), } } + fn item_prefix<'a>(code: &'a str, item: &str) -> &'a str { + let end = code.find(item).expect("rendered item"); + let start = code[..end].rfind("///Move type:").unwrap_or(0); + &code[start..end] + } + #[test] fn renders_mutable_object_params_with_push_arg_mut() { let code = render_package(&demo_pkg(), &RenderOptions::default()); @@ -316,10 +698,105 @@ mod tests { let code = render_package(&demo_pkg(), &RenderOptions::default()); assert!(code.contains("thread_local!")); - assert!(code.contains("pub fn package() -> sui_move::prelude::Address")); - assert!(code.contains("pub fn with_package")); - assert!(code.contains("CallSpec::new(package(), \"m\", \"mutate\")")); - assert!(code.contains("address_fn = \"super::package\"")); + assert!(code.contains("pub fn call_package() -> sui_move::prelude::Address")); + assert!(code.contains("pub fn type_package() -> sui_move::prelude::Address")); + assert!(code.contains("pub fn with_packages")); + 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\"")); + } + + #[test] + fn generated_bindings_split_call_and_type_package_scopes() { + let code = render_package(&demo_pkg(), &RenderOptions::default()); + + assert!(code.contains("pub const CALL_PACKAGE")); + assert!(code.contains("pub const TYPE_PACKAGE")); + assert!(code.contains("pub fn call_package() -> sui_move::prelude::Address")); + 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\"")); + } + + #[test] + fn generated_calls_include_entry_functions_even_when_not_public() { + let code = render_package(&demo_pkg(), &RenderOptions::default()); + assert!(code.contains("pub fn private_entry")); + assert!(code.contains("pub fn private_entry_target")); + } + + #[test] + fn generated_calls_return_result_instead_of_panicking() { + let code = render_package(&demo_pkg(), &RenderOptions::default()); + assert!(code.contains("-> Result Result")); + assert!(code.contains("T0: sm::MoveType + sm::HasCopy + sm::HasDrop + sm::HasStore")); + } + + #[test] + fn generated_structs_include_named_field_constructors() { + let code = render_package(&demo_pkg(), &RenderOptions::default()); + + assert!(code.contains("pub fn new(left: u8, right: bool) -> Self")); + assert!(code.contains("left: left.into()")); + assert!(code.contains("right: right.into()")); + } + + #[test] + fn generated_option_layout_helpers_do_not_require_move_type_bounds() { + let code = render_package(&option_pkg(), &RenderOptions::default()); + + assert!(code.contains("pub fn from_option(value: std::option::Option) -> Self")); + assert!(code.contains("impl Default for Option")); + assert!(code.contains("impl From> for Option")); + assert!(!code.contains("T0: sm::MoveType")); + } + + #[test] + fn generated_collection_layout_helpers_use_minimal_rust_bounds() { + let code = render_package(&layout_helper_pkg(), &RenderOptions::default()); + + assert!(code.contains("pub fn size_u64(&self) -> u64")); + assert!(code.contains("pub fn into_hash_map(self) -> std::collections::HashMap")); + assert!(!code.contains("T0: sm::MoveType")); + assert!(!code.contains("T1: sm::MoveType")); + assert!(!code.contains("T0: sm::MoveType + sm::HasCopy")); + } + + #[test] + fn generated_rust_copy_tracks_rust_carrier_shape() { + let code = render_package(&rust_copy_shape_pkg(), &RenderOptions::default()); + + assert!(item_prefix(&code, "pub struct Scalar").contains("#[derive(::core::marker::Copy)]")); + assert!(item_prefix(&code, "pub struct Nested").contains("#[derive(::core::marker::Copy)]")); + assert!(!item_prefix(&code, "pub struct Bytes").contains("#[derive(::core::marker::Copy)]")); + assert!( + !item_prefix(&code, "pub struct Generic").contains("#[derive(::core::marker::Copy)]") + ); + assert!(item_prefix(&code, "pub enum ScalarChoice").contains("::core::marker::Copy")); } #[test] @@ -416,6 +893,52 @@ mod tests { assert!(code.contains("&mut impl sm_call::ObjectArg")); } + #[test] + fn render_package_parts_returns_root_and_modules_without_string_parsing() { + let opts = RenderOptions { + emit_reexports: false, + ..RenderOptions::default() + }; + let parts = render_package_parts(&demo_pkg(), &opts); + + assert!(parts.root.contains("pub const CALL_PACKAGE")); + assert!(parts.root.contains("pub fn with_packages")); + assert!(!parts.root.contains("pub mod m")); + assert!(!parts.root.contains("pub use m::Obj")); + + 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("pub struct Obj")); + assert!(module.contains("pub fn mutate")); + } + + #[test] + fn render_package_parts_can_emit_targets_without_call_specs() { + let opts = RenderOptions { + emit_call_specs: false, + ..RenderOptions::default() + }; + let parts = render_package_parts(&demo_pkg(), &opts); + let module = parts.modules.get("m").expect("module body"); + + assert!(module.contains("pub fn mutate_target() -> Result TxExt for sui_move_runtime::Tx<'a, S>")); - assert!(code.contains("self.call(m::mutate")); + assert!(code.contains("let spec = m::mutate")); + assert!(code.contains("self.call(spec)")); } #[test] diff --git a/sui-move-codegen/src/render/tx_ext.rs b/sui-move-codegen/src/render/tx_ext.rs index ce1a425..8b20aa7 100644 --- a/sui-move-codegen/src/render/tx_ext.rs +++ b/sui-move-codegen/src/render/tx_ext.rs @@ -24,7 +24,7 @@ pub(crate) fn render_tx_ext(pkg: &NormalizedPackage, opts: &RenderOptions) -> To for f in module .functions .iter() - .filter(|f| matches!(f.visibility, Visibility::Public)) + .filter(|f| matches!(f.visibility, Visibility::Public) || f.is_entry) { let (trait_method, impl_method) = render_method(module, f, pkg, opts); trait_methods.push(trait_method); @@ -77,7 +77,7 @@ fn render_method( .map(|i| format_ident!("T{i}")) .collect::>(); let fn_generics = type_generics(&type_params); - let bounds = type_param_bounds(f, opts.use_aliases); + let bounds = super::calls::type_param_bounds(f, pkg, opts.use_aliases); let where_clause = where_clause(&bounds); let (params, args, skipped_tx_context) = render_params_and_args(f, pkg, opts); @@ -111,7 +111,8 @@ fn render_method( -> Result #where_clause { - self.call(#call_expr) + let spec = #call_expr?; + self.call(spec) } }; @@ -216,39 +217,6 @@ fn is_object_type( } } -fn type_param_bounds(f: &Function, use_aliases: bool) -> Vec { - let sm = if use_aliases { - quote! { sm } - } else { - quote! { sui_move } - }; - - f.type_parameters - .iter() - .enumerate() - .map(|(idx, p)| { - let ty = format_ident!("T{idx}"); - - let base = if p.constraints.contains(&Ability::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 }, - }); - } - quote! { #ty: #(#bounds)+* } - }) - .collect() -} - fn where_clause(bounds: &[TokenStream]) -> TokenStream { if bounds.is_empty() { quote! {} diff --git a/sui-move-codegen/src/render/types.rs b/sui-move-codegen/src/render/types.rs index 3a8fd4d..b25836b 100644 --- a/sui-move-codegen/src/render/types.rs +++ b/sui-move-codegen/src/render/types.rs @@ -6,6 +6,8 @@ //! Enums are emitted as Rust `enum`s with manual `MoveType` / `MoveStruct` impls. (Move enum //! support is still evolving, so this layer keeps the implementation small and explicit.) +use std::collections::BTreeSet; + use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -18,9 +20,14 @@ pub(crate) fn render_datatype( pkg: &NormalizedPackage, opts: &RenderOptions, ) -> TokenStream { - match &dt.kind { + 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! { + #datatype + #helpers } } @@ -83,9 +90,9 @@ fn render_struct( 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("package", proc_macro2::Span::call_site()) + syn::LitStr::new("type_package", proc_macro2::Span::call_site()) } else { - syn::LitStr::new("super::package", proc_macro2::Span::call_site()) + syn::LitStr::new("super::type_package", proc_macro2::Span::call_site()) }; let fields_tokens = fields.iter().map(|f| { @@ -107,9 +114,15 @@ fn render_struct( } else { quote! { sui_move::move_struct } }; + let rust_copy_derive = is_rust_copy_datatype(dt, pkg).then(|| { + quote! { + #[derive(::core::marker::Copy)] + } + }); quote! { #doc + #rust_copy_derive #[#macro_path( address = #address_lit, address_fn = #address_fn, @@ -141,6 +154,7 @@ fn render_enum( let abilities = abilities_string(&dt.abilities); let bounds = type_param_bounds(dt, opts.use_aliases); + let rust_copy = is_rust_copy_datatype(dt, pkg); let sm = if opts.use_aliases { quote! { sm } @@ -150,7 +164,7 @@ fn render_enum( let struct_tag_builder = struct_tag_builder_tokens(dt, opts.use_aliases); let where_clause = where_clause(&bounds); - let derives = enum_derives(&dt.abilities, opts.use_aliases); + let derives = enum_derives(&dt.abilities, rust_copy, opts.use_aliases); let serde_crate_attr = serde_crate_attr(); let variants_tokens = variants.iter().map(|v| { @@ -263,6 +277,693 @@ fn ability_impls_for_datatype( quote! { #(#out)* } } +fn render_canonical_helpers( + dt: &Datatype, + pkg: &NormalizedPackage, + opts: &RenderOptions, +) -> TokenStream { + if !matches!(dt.kind, DatatypeKind::Struct { .. }) { + return quote! {}; + } + + let mut helpers = Vec::new(); + if let Some(helper) = render_struct_constructor_helpers(dt, pkg, opts) { + helpers.push(helper); + } + if is_type(&dt.type_name, "0x1", "ascii", "String") { + helpers.push(render_ascii_string_helpers(dt)); + } + if is_type(&dt.type_name, "0x1", "type_name", "TypeName") { + helpers.push(render_type_name_helpers(dt, pkg, opts)); + } else if let Some(helper) = render_ascii_name_wrapper_helpers(dt, pkg, opts) { + helpers.push(helper); + } + if is_type(&dt.type_name, "0x1", "option", "Option") { + helpers.push(render_option_helpers(dt, opts)); + } + if is_type(&dt.type_name, "0x2", "object", "ID") { + helpers.push(render_object_id_helpers(dt)); + } + if is_type(&dt.type_name, "0x2", "object", "UID") { + helpers.push(render_object_uid_helpers(dt)); + } + if is_type(&dt.type_name, "0x2", "table", "Table") { + helpers.push(render_table_like_helpers( + dt, + opts, + quote! { + Self { + id: super::object::UID::new(id), + size, + phantom_t0: std::marker::PhantomData, + phantom_t1: std::marker::PhantomData, + } + }, + )); + } + if is_type(&dt.type_name, "0x2", "linked_table", "LinkedTable") { + helpers.push(render_table_like_helpers( + dt, + opts, + quote! { + Self { + id: super::object::UID::new(id), + size, + head: Default::default(), + tail: Default::default(), + phantom_t1: std::marker::PhantomData, + } + }, + )); + } + if is_type(&dt.type_name, "0x2", "object_table", "ObjectTable") { + helpers.push(render_id_size_helpers(dt, opts)); + } + if is_type(&dt.type_name, "0x2", "bag", "Bag") + || is_type(&dt.type_name, "0x2", "object_bag", "ObjectBag") + { + helpers.push(render_table_like_helpers( + dt, + opts, + quote! { + Self { + id: super::object::UID::new(id), + size, + } + }, + )); + } + if is_type(&dt.type_name, "0x2", "table_vec", "TableVec") { + helpers.push(render_table_vec_helpers(dt, opts)); + } + if is_type(&dt.type_name, "0x2", "vec_map", "VecMap") { + helpers.push(render_vec_map_helpers(dt, opts)); + } + + quote! { #(#helpers)* } +} + +fn is_rust_copy_datatype(dt: &Datatype, pkg: &NormalizedPackage) -> bool { + rust_copy_type_keys(pkg).contains(&local_type_key(&dt.type_name, pkg)) +} + +fn rust_copy_type_keys(pkg: &NormalizedPackage) -> BTreeSet { + let datatypes = pkg + .modules + .values() + .flat_map(|module| module.datatypes.iter()) + .collect::>(); + let mut copy_types = BTreeSet::new(); + + loop { + let mut changed = false; + for dt in &datatypes { + let key = local_type_key(&dt.type_name, pkg); + if copy_types.contains(&key) { + continue; + } + if datatype_has_rust_copy_shape(dt, pkg, ©_types) { + copy_types.insert(key); + changed = true; + } + } + + if !changed { + return copy_types; + } + } +} + +fn datatype_has_rust_copy_shape( + dt: &Datatype, + pkg: &NormalizedPackage, + copy_types: &BTreeSet, +) -> bool { + if !dt.abilities.contains(&Ability::Copy) || !dt.type_parameters.is_empty() { + return false; + } + + match &dt.kind { + DatatypeKind::Struct { fields } => fields + .iter() + .all(|field| type_has_rust_copy_shape(&field.ty, pkg, copy_types)), + DatatypeKind::Enum { variants } => variants.iter().all(|variant| { + variant + .fields + .iter() + .all(|field| type_has_rust_copy_shape(&field.ty, pkg, copy_types)) + }), + } +} + +fn type_has_rust_copy_shape( + ty: &TypeRef, + pkg: &NormalizedPackage, + copy_types: &BTreeSet, +) -> bool { + match ty { + TypeRef::Address + | TypeRef::Bool + | TypeRef::U8 + | TypeRef::U16 + | TypeRef::U32 + | TypeRef::U64 + | TypeRef::U128 + | TypeRef::U256 => true, + TypeRef::Ref { mutable, .. } => !mutable, + TypeRef::Datatype { + type_name, + type_arguments, + } => { + type_arguments.is_empty() + && is_local_type(type_name, pkg) + && copy_types.contains(&local_type_key(type_name, pkg)) + } + TypeRef::Vector(_) | TypeRef::TypeParameter(_) => false, + } +} + +fn local_type_key(type_name: &TypeName, pkg: &NormalizedPackage) -> String { + let address = if is_local_type(type_name, pkg) { + normalize_address(&pkg.storage_id) + } else { + normalize_address(&type_name.address) + }; + format!("{}::{}::{}", address, type_name.module, type_name.name) +} + +fn render_struct_constructor_helpers( + dt: &Datatype, + pkg: &NormalizedPackage, + opts: &RenderOptions, +) -> Option { + if has_specialized_new_helper(dt, pkg, opts) { + return None; + } + + let DatatypeKind::Struct { fields } = &dt.kind else { + return None; + }; + + let type_ident = idents::ident(&dt.name); + let type_params = type_params_idents(dt.type_parameters.len()); + let (impl_generics, type_generics) = impl_and_type_generics(&type_params); + + let mut params = Vec::new(); + let mut initializers = Vec::new(); + let field_names = fields + .iter() + .map(|field| field.name.as_str()) + .collect::>(); + for field in fields { + let field_ident = idents::ident(&field.name); + if field.name.starts_with("phantom_") { + initializers.push(quote! { #field_ident: std::marker::PhantomData }); + continue; + } + + let field_ty = render_type_ref(&field.ty, &dt.type_name, pkg, opts); + let param_ty = constructor_param_type(&field.ty, field_ty); + params.push(quote! { #field_ident: #param_ty }); + initializers.push(quote! { #field_ident: #field_ident.into() }); + } + for (idx, type_param) in dt.type_parameters.iter().enumerate() { + let field_name = format!("phantom_t{idx}"); + if type_param.is_phantom && !field_names.contains(&field_name.as_str()) { + let field_ident = format_ident!("phantom_t{idx}"); + initializers.push(quote! { #field_ident: std::marker::PhantomData }); + } + } + + Some(quote! { + impl #impl_generics #type_ident #type_generics + { + pub fn new(#(#params),*) -> Self { + Self { + #(#initializers),* + } + } + } + }) +} + +fn constructor_param_type(ty: &TypeRef, rendered: TokenStream) -> TokenStream { + match ty { + TypeRef::Datatype { .. } | TypeRef::TypeParameter(_) => quote! { impl Into<#rendered> }, + TypeRef::Address + | TypeRef::Bool + | TypeRef::U8 + | TypeRef::U16 + | TypeRef::U32 + | TypeRef::U64 + | TypeRef::U128 + | TypeRef::U256 + | TypeRef::Vector(_) + | TypeRef::Ref { .. } => rendered, + } +} + +fn has_specialized_new_helper( + dt: &Datatype, + pkg: &NormalizedPackage, + opts: &RenderOptions, +) -> bool { + is_type(&dt.type_name, "0x1", "type_name", "TypeName") + || ascii_name_field(dt, pkg, opts).is_some() + || is_type(&dt.type_name, "0x2", "object", "ID") + || is_type(&dt.type_name, "0x2", "object", "UID") + || is_type(&dt.type_name, "0x2", "table", "Table") + || is_type(&dt.type_name, "0x2", "linked_table", "LinkedTable") + || is_type(&dt.type_name, "0x2", "bag", "Bag") + || is_type(&dt.type_name, "0x2", "object_bag", "ObjectBag") + || is_type(&dt.type_name, "0x2", "table_vec", "TableVec") +} + +fn is_type(type_name: &TypeName, address: &str, module: &str, name: &str) -> bool { + normalize_address(&type_name.address) == normalize_address(address) + && type_name.module == module + && type_name.name == name +} + +fn normalize_address(address: &str) -> String { + let trimmed = address.trim_start_matches("0x"); + let trimmed = trimmed.trim_start_matches('0'); + if trimmed.is_empty() { + "0".to_string() + } else { + trimmed.to_ascii_lowercase() + } +} + +fn render_ascii_string_helpers(dt: &Datatype) -> TokenStream { + let type_ident = idents::ident(&dt.name); + quote! { + impl #type_ident { + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.bytes).expect("generated Move ASCII string must be UTF-8") + } + + pub fn into_string(self) -> std::string::String { + std::string::String::from_utf8(self.bytes) + .expect("generated Move ASCII string must be UTF-8") + } + } + + impl From<&str> for #type_ident { + fn from(value: &str) -> Self { + Self { + bytes: value.as_bytes().to_vec(), + } + } + } + + impl From for #type_ident { + fn from(value: std::string::String) -> Self { + Self { + bytes: value.into_bytes(), + } + } + } + + impl From<#type_ident> for std::string::String { + fn from(value: #type_ident) -> Self { + value.into_string() + } + } + + impl AsRef for #type_ident { + fn as_ref(&self) -> &str { + self.as_str() + } + } + + impl std::fmt::Display for #type_ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } + } + } +} + +fn render_type_name_helpers( + dt: &Datatype, + pkg: &NormalizedPackage, + opts: &RenderOptions, +) -> TokenStream { + let type_ident = idents::ident(&dt.name); + let Some((field_ident, field_ty)) = ascii_name_field(dt, pkg, opts) else { + return quote! {}; + }; + + quote! { + impl #type_ident { + pub fn new(name: &str) -> Self { + Self { + #field_ident: #field_ty::from(name), + } + } + + pub fn as_str(&self) -> &str { + self.#field_ident.as_str() + } + + fn normalize(name: &str) -> std::borrow::Cow<'_, str> { + let trimmed = name.trim_start_matches("0x"); + if trimmed.len() == name.len() { + std::borrow::Cow::Borrowed(name) + } else { + std::borrow::Cow::Owned(trimmed.to_string()) + } + } + + pub fn matches_qualified_name(&self, expected: &str) -> bool { + Self::normalize(self.as_str()).eq_ignore_ascii_case(&Self::normalize(expected)) + } + } + + impl From<&str> for #type_ident { + fn from(name: &str) -> Self { + #type_ident::new(name) + } + } + + impl From for #type_ident { + fn from(name: std::string::String) -> Self { + #type_ident { + #field_ident: #field_ty::from(name), + } + } + } + + impl std::fmt::Display for #type_ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.#field_ident.fmt(f) + } + } + } +} + +fn render_ascii_name_wrapper_helpers( + dt: &Datatype, + pkg: &NormalizedPackage, + opts: &RenderOptions, +) -> Option { + let type_ident = idents::ident(&dt.name); + let (field_ident, field_ty) = ascii_name_field(dt, pkg, opts)?; + + Some(quote! { + impl #type_ident { + pub fn new(name: impl Into<#field_ty>) -> Self { + Self { #field_ident: name.into() } + } + + pub fn as_str(&self) -> &str { + self.#field_ident.as_str() + } + } + + impl From<&str> for #type_ident { + fn from(value: &str) -> Self { + Self::new(value) + } + } + + impl From for #type_ident { + fn from(value: std::string::String) -> Self { + Self::new(value) + } + } + }) +} + +fn ascii_name_field( + dt: &Datatype, + pkg: &NormalizedPackage, + opts: &RenderOptions, +) -> Option<(syn::Ident, TokenStream)> { + let DatatypeKind::Struct { fields } = &dt.kind else { + return None; + }; + let [field] = fields.as_slice() else { + return None; + }; + if field.name != "name" || !is_ascii_string_ref(&field.ty) { + return None; + } + let field_ident = idents::ident(&field.name); + let field_ty = render_type_ref(&field.ty, &dt.type_name, pkg, opts); + Some((field_ident, field_ty)) +} + +fn is_ascii_string_ref(ty: &TypeRef) -> bool { + matches!( + ty, + TypeRef::Datatype { + type_name, + type_arguments + } if type_arguments.is_empty() && is_type(type_name, "0x1", "ascii", "String") + ) +} + +fn render_option_helpers(dt: &Datatype, _opts: &RenderOptions) -> TokenStream { + let type_ident = idents::ident(&dt.name); + let type_params = type_params_idents(dt.type_parameters.len()); + if type_params.len() != 1 { + return quote! {}; + } + let t0 = &type_params[0]; + let (impl_generics, type_generics) = impl_and_type_generics(&type_params); + + quote! { + impl #impl_generics #type_ident #type_generics + { + pub fn from_option(value: std::option::Option<#t0>) -> Self { + Self { + vec: value.into_iter().collect(), + } + } + + pub fn into_option(self) -> std::option::Option<#t0> { + self.vec.into_iter().next() + } + + pub fn as_option(&self) -> std::option::Option<&#t0> { + self.vec.first() + } + + pub fn copied_option(&self) -> std::option::Option<#t0> + where + #t0: Copy, + { + self.as_option().copied() + } + + pub fn cloned_option(&self) -> std::option::Option<#t0> + where + #t0: Clone, + { + self.as_option().cloned() + } + } + + impl #impl_generics Default for #type_ident #type_generics + { + fn default() -> Self { + Self::from_option(None) + } + } + + impl #impl_generics From> for #type_ident #type_generics + { + fn from(value: std::option::Option<#t0>) -> Self { + Self::from_option(value) + } + } + } +} + +fn render_object_id_helpers(dt: &Datatype) -> TokenStream { + let type_ident = idents::ident(&dt.name); + quote! { + impl #type_ident { + pub fn new(bytes: sui_move::prelude::Address) -> Self { + Self { bytes } + } + + pub fn address(&self) -> sui_move::prelude::Address { + self.bytes + } + } + + impl From for #type_ident { + fn from(value: sui_move::prelude::Address) -> Self { + Self::new(value) + } + } + + impl From<#type_ident> for sui_move::prelude::Address { + fn from(value: #type_ident) -> Self { + value.bytes + } + } + + impl std::fmt::Display for #type_ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.bytes.fmt(f) + } + } + } +} + +fn render_object_uid_helpers(dt: &Datatype) -> TokenStream { + let type_ident = idents::ident(&dt.name); + quote! { + impl #type_ident { + pub fn new(bytes: sui_move::prelude::Address) -> Self { + Self { + id: ID::new(bytes), + } + } + + pub fn address(&self) -> sui_move::prelude::Address { + self.id.bytes + } + } + + impl From for #type_ident { + fn from(value: sui_move::prelude::Address) -> Self { + Self::new(value) + } + } + + impl From<#type_ident> for sui_move::prelude::Address { + fn from(value: #type_ident) -> Self { + value.id.bytes + } + } + } +} + +fn render_table_like_helpers( + dt: &Datatype, + _opts: &RenderOptions, + constructor: TokenStream, +) -> TokenStream { + let type_ident = idents::ident(&dt.name); + let type_params = type_params_idents(dt.type_parameters.len()); + let (impl_generics, type_generics) = impl_and_type_generics(&type_params); + + quote! { + impl #impl_generics #type_ident #type_generics + { + pub fn new(id: sui_move::prelude::Address, size: u64) -> Self { + #constructor + } + + pub fn id(&self) -> sui_move::prelude::Address { + self.id.id.bytes + } + + pub fn size(&self) -> usize { + usize::try_from(self.size).unwrap_or(usize::MAX) + } + + pub fn size_u64(&self) -> u64 { + self.size + } + } + } +} + +fn render_id_size_helpers(dt: &Datatype, _opts: &RenderOptions) -> TokenStream { + let type_ident = idents::ident(&dt.name); + let type_params = type_params_idents(dt.type_parameters.len()); + let (impl_generics, type_generics) = impl_and_type_generics(&type_params); + + quote! { + impl #impl_generics #type_ident #type_generics + { + pub fn id(&self) -> sui_move::prelude::Address { + self.id.id.bytes + } + + pub fn size(&self) -> usize { + usize::try_from(self.size).unwrap_or(usize::MAX) + } + + pub fn size_u64(&self) -> u64 { + self.size + } + } + } +} + +fn render_table_vec_helpers(dt: &Datatype, _opts: &RenderOptions) -> TokenStream { + let type_ident = idents::ident(&dt.name); + let type_params = type_params_idents(dt.type_parameters.len()); + let (impl_generics, type_generics) = impl_and_type_generics(&type_params); + + quote! { + impl #impl_generics #type_ident #type_generics + { + pub fn new(id: sui_move::prelude::Address, size: u64) -> Self { + Self { + contents: super::table::Table::new(id, size), + phantom_t0: std::marker::PhantomData, + } + } + + pub fn id(&self) -> sui_move::prelude::Address { + self.contents.id() + } + + pub fn size(&self) -> usize { + self.contents.size() + } + + pub fn size_u64(&self) -> u64 { + self.contents.size_u64() + } + } + } +} + +fn render_vec_map_helpers(dt: &Datatype, _opts: &RenderOptions) -> TokenStream { + let type_ident = idents::ident(&dt.name); + let type_params = type_params_idents(dt.type_parameters.len()); + if type_params.len() != 2 { + return quote! {}; + } + let k = &type_params[0]; + let v = &type_params[1]; + let (impl_generics, type_generics) = impl_and_type_generics(&type_params); + + quote! { + impl #impl_generics #type_ident #type_generics + { + pub fn get(&self, key: &#k) -> std::option::Option<&#v> + where + #k: Eq, + { + self.contents + .iter() + .find(|entry| &entry.key == key) + .map(|entry| &entry.value) + } + + pub fn into_hash_map(self) -> std::collections::HashMap<#k, #v> + where + #k: Eq + std::hash::Hash, + { + self.contents + .into_iter() + .map(|entry| (entry.key, entry.value)) + .collect() + } + } + } +} + fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { let sm = if use_aliases { quote! { sm } @@ -280,7 +981,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { quote! { #sm::__private::sui_sdk_types::StructTag::new( - package(), + type_package(), #sm::parse_identifier(#module).expect("invalid module"), #sm::parse_identifier(#name).expect("invalid struct name"), vec![#(#ty_params_for_tag),*], @@ -288,7 +989,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream { } } -fn enum_derives(abilities: &[Ability], use_aliases: bool) -> TokenStream { +fn enum_derives(abilities: &[Ability], rust_copy: bool, use_aliases: bool) -> TokenStream { let sm = if use_aliases { quote! { sm } } else { @@ -296,13 +997,16 @@ fn enum_derives(abilities: &[Ability], use_aliases: bool) -> TokenStream { }; let has_copy = abilities.contains(&Ability::Copy); + let rust_copy_derive = rust_copy.then(|| quote! { ::core::marker::Copy, }); if has_copy { quote! { + #rust_copy_derive ::core::clone::Clone, ::core::fmt::Debug, ::core::cmp::PartialEq, ::core::cmp::Eq, + ::core::hash::Hash, #sm::__private::serde::Serialize, #sm::__private::serde::Deserialize, } @@ -311,6 +1015,7 @@ fn enum_derives(abilities: &[Ability], use_aliases: bool) -> TokenStream { ::core::fmt::Debug, ::core::cmp::PartialEq, ::core::cmp::Eq, + ::core::hash::Hash, #sm::__private::serde::Serialize, #sm::__private::serde::Deserialize, } @@ -544,11 +1249,12 @@ fn prelude_type(use_aliases: bool, name: TokenStream) -> TokenStream { } fn is_local_type(type_name: &TypeName, pkg: &NormalizedPackage) -> bool { - if type_name.address == pkg.storage_id { + let address = normalize_address(&type_name.address); + if address == normalize_address(&pkg.storage_id) { return true; } match &pkg.original_id { - Some(orig) => type_name.address == *orig, + Some(orig) => address == normalize_address(orig), None => false, } } diff --git a/sui-move-codegen/src/render/util.rs b/sui-move-codegen/src/render/util.rs index 8274e92..e98a2ea 100644 --- a/sui-move-codegen/src/render/util.rs +++ b/sui-move-codegen/src/render/util.rs @@ -8,47 +8,16 @@ use crate::ir::{NormalizedModule, NormalizedPackage}; use super::{calls, idents, tx_ext, types, RenderOptions}; pub(crate) fn render_package_tokens(pkg: &NormalizedPackage, opts: &RenderOptions) -> TokenStream { - let root_aliases = if opts.emit_tx_ext && !opts.flatten { - aliases(opts) - } else { - quote! {} - }; - - let package_const = package_const_tokens(pkg, opts); - let package_scope = package_scope_tokens(opts); - let mut modules = Vec::new(); for module in pkg.modules.values() { modules.push(render_module(module, pkg, opts)); } - let tx_ext = if opts.emit_tx_ext { - tx_ext::render_tx_ext(pkg, opts) - } else { - quote! {} - }; - - let reexports = if opts.flatten || !opts.emit_types { - quote! {} - } else { - 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); - reexp.push(quote! { pub use #module_ident::#ty_ident; }); - } - } - quote! { #(#reexp)* } - }; + let root = render_package_root_tokens(pkg, opts, false); quote! { - #root_aliases - #package_const - #package_scope + #root #(#modules)* - #tx_ext - #reexports } } @@ -56,7 +25,15 @@ pub(crate) fn render_split_mod_rs_tokens( pkg: &NormalizedPackage, opts: &RenderOptions, ) -> TokenStream { - let root_aliases = if opts.emit_tx_ext { + render_package_root_tokens(pkg, opts, true) +} + +pub(crate) fn render_package_root_tokens( + pkg: &NormalizedPackage, + opts: &RenderOptions, + emit_module_decls: bool, +) -> TokenStream { + let root_aliases = if opts.emit_tx_ext && (emit_module_decls || !opts.flatten) { aliases(opts) } else { quote! {} @@ -65,12 +42,17 @@ pub(crate) fn render_split_mod_rs_tokens( let package_const = package_const_tokens(pkg, opts); let package_scope = package_scope_tokens(opts); - let module_decls = pkg.modules.values().map(|module| { - let module_ident = idents::ident(&module.name); - quote! { pub mod #module_ident; } - }); + let module_decls = if emit_module_decls { + let module_decls = pkg.modules.values().map(|module| { + let module_ident = idents::ident(&module.name); + quote! { pub mod #module_ident; } + }); + quote! { #(#module_decls)* } + } else { + quote! {} + }; - let reexports = if !opts.emit_types { + let reexports = if !opts.emit_types || !opts.emit_reexports { quote! {} } else { let mut out = Vec::new(); @@ -94,7 +76,7 @@ pub(crate) fn render_split_mod_rs_tokens( #root_aliases #package_const #package_scope - #(#module_decls)* + #module_decls #tx_ext #reexports } @@ -122,7 +104,7 @@ pub(crate) fn render_module_file( quote! { #aliases - use super::package; + use super::{call_package, type_package}; #(#items)* } } @@ -154,7 +136,7 @@ pub(crate) fn render_module( quote! { pub mod #module_ident { #aliases - use super::package; + use super::{call_package, type_package}; #(#items)* } } @@ -162,12 +144,19 @@ pub(crate) fn render_module( } fn package_const_tokens(pkg: &NormalizedPackage, opts: &RenderOptions) -> TokenStream { - let addr = pkg.storage_id.as_str(); + let call_addr = pkg.storage_id.as_str(); + let type_addr = pkg + .original_id + .as_deref() + .unwrap_or(pkg.storage_id.as_str()); let _ = opts; let address_ty = quote! { sui_move::prelude::Address }; quote! { - /// Package address (the on-chain package object id). - pub const PACKAGE: #address_ty = #address_ty::from_static(#addr); + /// Package address used as the target for generated Move calls. + pub const CALL_PACKAGE: #address_ty = #address_ty::from_static(#call_addr); + + /// Package address used for generated Move type identity. + pub const TYPE_PACKAGE: #address_ty = #address_ty::from_static(#type_addr); } } @@ -176,30 +165,41 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream { let address_ty = quote! { sui_move::prelude::Address }; quote! { std::thread_local! { - static PACKAGE_OVERRIDE: std::cell::Cell> = + static CALL_PACKAGE_OVERRIDE: std::cell::Cell> = std::cell::Cell::new(None); + static TYPE_PACKAGE_OVERRIDE: std::cell::Cell> = + std::cell::Cell::new(None); + } + + /// Current call package address for this generated binding. + /// + /// Returns the scoped override set by [`with_call_package`] or [`with_packages`], or + /// [`CALL_PACKAGE`] when no override is active. + pub fn call_package() -> #address_ty { + CALL_PACKAGE_OVERRIDE.with(|slot| slot.get().unwrap_or(CALL_PACKAGE)) } - /// Current package address for this generated binding. + /// Current type package address for this generated binding. /// - /// Returns the scoped override set by [`with_package`], or [`PACKAGE`] when no override is active. - pub fn package() -> #address_ty { - PACKAGE_OVERRIDE.with(|slot| slot.get().unwrap_or(PACKAGE)) + /// 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)) } - /// Run a closure with this generated binding scoped to `package`. + /// Run a closure with this generated binding scoped to `package` for Move calls. /// - /// The previous package override is restored when the closure returns or unwinds. - pub fn with_package(package: #address_ty, f: impl FnOnce() -> R) -> R { + /// The previous call package override is restored when the closure returns or unwinds. + pub fn with_call_package(package: #address_ty, f: impl FnOnce() -> R) -> R { struct Reset(Option<#address_ty>); impl Drop for Reset { fn drop(&mut self) { - PACKAGE_OVERRIDE.with(|slot| slot.set(self.0)); + CALL_PACKAGE_OVERRIDE.with(|slot| slot.set(self.0)); } } - let previous = PACKAGE_OVERRIDE.with(|slot| { + let previous = CALL_PACKAGE_OVERRIDE.with(|slot| { let previous = slot.get(); slot.set(Some(package)); previous @@ -207,6 +207,39 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream { let _reset = Reset(previous); f() } + + /// Run a closure with this generated binding scoped to `package` for Move type identity. + /// + /// 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>); + + impl Drop for Reset { + fn drop(&mut self) { + TYPE_PACKAGE_OVERRIDE.with(|slot| slot.set(self.0)); + } + } + + let previous = TYPE_PACKAGE_OVERRIDE.with(|slot| { + let previous = slot.get(); + slot.set(Some(package)); + previous + }); + let _reset = Reset(previous); + f() + } + + /// Run a closure with explicit call and type package scopes. + /// + /// Use this for upgraded packages where calls target the current package object but type + /// tags must retain the original defining package address. + pub fn with_packages( + call_package: #address_ty, + type_package: #address_ty, + f: impl FnOnce() -> R, + ) -> R { + with_call_package(call_package, || with_type_package(type_package, f)) + } } } diff --git a/sui-move-codegen/src/source.rs b/sui-move-codegen/src/source.rs index ec9f23c..a58ade5 100644 --- a/sui-move-codegen/src/source.rs +++ b/sui-move-codegen/src/source.rs @@ -29,7 +29,7 @@ use crate::{Error, GrpcClient}; /// /// let pkg = fetch_package(&mut client, package_id).await?; /// let json = pkg.to_json_string()?; -/// println!("{json}"); +/// println!("{} bytes", json.len()); /// # Ok(()) /// # } /// ``` diff --git a/sui-move-codegen/src/source_names.rs b/sui-move-codegen/src/source_names.rs new file mode 100644 index 0000000..cd0b39a --- /dev/null +++ b/sui-move-codegen/src/source_names.rs @@ -0,0 +1,414 @@ +//! Recover Move function parameter names from source files. + +use { + crate::ir::{FunctionParameterNameMismatch, NormalizedPackage}, + std::{ + collections::BTreeMap, + io, + path::{Path, PathBuf}, + }, +}; + +/// Errors from recovering function parameter names from Move sources. +#[derive(Debug, thiserror::Error)] +pub enum SourceNameError { + /// Reading the source directory failed. + #[error("read Move source directory {}: {source}", path.display())] + ReadDir { + /// Directory that was read. + path: PathBuf, + /// Underlying filesystem error. + #[source] + source: io::Error, + }, + + /// Reading an entry from the source directory failed. + #[error("read entry under {}: {source}", path.display())] + ReadDirEntry { + /// Directory being iterated. + path: PathBuf, + /// Underlying filesystem error. + #[source] + source: io::Error, + }, + + /// Reading a Move source file failed. + #[error("read Move source {}: {source}", path.display())] + ReadSource { + /// Source file that was read. + path: PathBuf, + /// Underlying filesystem error. + #[source] + source: io::Error, + }, + + /// A source file did not contain a supported `module package::name;` header. + #[error("Move source {} does not declare a `module package::name;` header", path.display())] + MissingModuleHeader { + /// Source file that was parsed. + path: PathBuf, + }, + + /// Source names could not be applied because source and IR arities differ. + #[error("{0}")] + FunctionParameterNameMismatch(#[from] FunctionParameterNameMismatch), +} + +/// Apply recovered source parameter names to synthesized `argN` names in a normalized package. +/// +/// `source_dir` is the package `sources/` directory. Missing directories are treated as empty so +/// callers can invoke this uniformly for packages that may not have local sources. +pub fn apply_function_parameter_names_from_sources( + package: &mut NormalizedPackage, + source_dir: impl AsRef, +) -> Result<(), SourceNameError> { + let names = function_parameter_names_from_sources(source_dir)?; + if names.is_empty() { + return Ok(()); + } + + package.apply_function_parameter_names(&names)?; + Ok(()) +} + +/// Recover function parameter names from all `.move` files under a package `sources/` directory. +/// +/// The returned map is keyed by `(module_name, function_name)`. +pub fn function_parameter_names_from_sources( + source_dir: impl AsRef, +) -> Result>, SourceNameError> { + let source_dir = source_dir.as_ref(); + if !source_dir.is_dir() { + return Ok(BTreeMap::new()); + } + + let mut names = BTreeMap::new(); + let entries = std::fs::read_dir(source_dir).map_err(|source| SourceNameError::ReadDir { + path: source_dir.to_path_buf(), + source, + })?; + + for entry in entries { + let entry = entry.map_err(|source| SourceNameError::ReadDirEntry { + path: source_dir.to_path_buf(), + source, + })?; + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) != Some("move") { + continue; + } + + let source = + std::fs::read_to_string(&path).map_err(|source| SourceNameError::ReadSource { + path: path.clone(), + source, + })?; + let source = strip_move_comments(&source); + let module_name = parse_move_module_name(&source) + .ok_or_else(|| SourceNameError::MissingModuleHeader { path: path.clone() })?; + + for (function_name, parameter_names) in parse_move_function_parameter_names(&source) { + names.insert((module_name.clone(), function_name), parameter_names); + } + } + + Ok(names) +} + +fn strip_move_comments(source: &str) -> String { + let mut stripped = String::with_capacity(source.len()); + let mut chars = source.chars().peekable(); + let mut in_block_comment = false; + + while let Some(ch) = chars.next() { + if in_block_comment { + if ch == '*' && chars.peek() == Some(&'/') { + chars.next(); + in_block_comment = false; + } + continue; + } + + if ch == '/' && chars.peek() == Some(&'/') { + for next in chars.by_ref() { + if next == '\n' { + stripped.push('\n'); + break; + } + } + continue; + } + if ch == '/' && chars.peek() == Some(&'*') { + chars.next(); + in_block_comment = true; + continue; + } + + stripped.push(ch); + } + + stripped +} + +fn parse_move_module_name(source: &str) -> Option { + let marker = "module "; + let start = source.find(marker)? + marker.len(); + let rest = &source[start..]; + let module_start = rest.find("::")? + 2; + let module = rest[module_start..] + .chars() + .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_') + .collect::(); + if module.is_empty() { + None + } else { + Some(module) + } +} + +fn parse_move_function_parameter_names(source: &str) -> Vec<(String, Vec)> { + let mut functions = Vec::new(); + let bytes = source.as_bytes(); + let mut offset = 0; + + while let Some(relative_fun) = source[offset..].find("fun") { + let fun_start = offset + relative_fun; + if !is_keyword_at(bytes, fun_start, b"fun") { + offset = fun_start + 3; + continue; + } + + let mut cursor = fun_start + 3; + cursor = skip_ascii_whitespace(source, cursor); + let name_start = cursor; + while cursor < source.len() && is_identifier_byte(source.as_bytes()[cursor]) { + cursor += 1; + } + if cursor == name_start { + offset = fun_start + 3; + continue; + } + let function_name = source[name_start..cursor].to_string(); + cursor = skip_ascii_whitespace(source, cursor); + if source[cursor..].starts_with('<') { + let Some(after_type_parameters) = skip_balanced(source, cursor, '<', '>') else { + offset = cursor; + continue; + }; + cursor = skip_ascii_whitespace(source, after_type_parameters); + } + if !source[cursor..].starts_with('(') { + offset = cursor; + continue; + } + let Some(params_end) = skip_balanced(source, cursor, '(', ')') else { + offset = cursor + 1; + continue; + }; + let params = &source[cursor + 1..params_end - 1]; + functions.push((function_name, parse_move_parameter_names(params))); + offset = params_end; + } + + functions +} + +fn parse_move_parameter_names(params: &str) -> Vec { + split_top_level_commas(params) + .into_iter() + .filter_map(|parameter| { + let parameter = parameter.trim(); + if parameter.is_empty() { + return None; + } + let colon = parameter.find(':')?; + let name = parameter[..colon].trim(); + let name = name.strip_prefix("mut ").unwrap_or(name).trim(); + if name.is_empty() { + None + } else { + Some(name.to_string()) + } + }) + .collect() +} + +fn split_top_level_commas(input: &str) -> Vec<&str> { + let mut segments = Vec::new(); + let mut start = 0; + let mut angle_depth = 0u64; + let mut paren_depth = 0u64; + + for (index, ch) in input.char_indices() { + match ch { + '<' => angle_depth += 1, + '>' if angle_depth > 0 => angle_depth -= 1, + '(' => paren_depth += 1, + ')' if paren_depth > 0 => paren_depth -= 1, + ',' if angle_depth == 0 && paren_depth == 0 => { + segments.push(&input[start..index]); + start = index + ch.len_utf8(); + } + _ => {} + } + } + segments.push(&input[start..]); + segments +} + +fn skip_ascii_whitespace(source: &str, mut cursor: usize) -> usize { + while cursor < source.len() && source.as_bytes()[cursor].is_ascii_whitespace() { + cursor += 1; + } + cursor +} + +fn skip_balanced(source: &str, start: usize, open: char, close: char) -> Option { + let mut depth = 0u64; + for (relative, ch) in source[start..].char_indices() { + if ch == open { + depth += 1; + } else if ch == close { + depth = depth.checked_sub(1)?; + if depth == 0 { + return Some(start + relative + ch.len_utf8()); + } + } + } + None +} + +fn is_keyword_at(bytes: &[u8], index: usize, keyword: &[u8]) -> bool { + bytes[index..].starts_with(keyword) + && index + .checked_sub(1) + .map(|prev| !is_identifier_byte(bytes[prev])) + .unwrap_or(true) + && bytes + .get(index + keyword.len()) + .map(|next| !is_identifier_byte(*next)) + .unwrap_or(true) +} + +fn is_identifier_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::ir::{Function, FunctionParam, NormalizedModule, TypeRef, Visibility}, + std::time::{SystemTime, UNIX_EPOCH}, + }; + + #[test] + fn parses_move_source_parameter_names() { + let source = strip_move_comments( + r#" +module nexus_workflow::execution_settlement; + +// fun ignored(arg0: u64) {} + public fun record_committed_tool_result_gas_charge_by_leader( + execution: &mut DAGExecution, + leader_cap: &CloneableOwnerCap, + mut walk_index: u64, + commit_tx_digest: vector, + commit_gas_charge: u64, + settlement_gas_charge: u64, +) {} +"#, + ); + + assert_eq!( + parse_move_module_name(&source).as_deref(), + Some("execution_settlement") + ); + let functions = parse_move_function_parameter_names(&source); + assert_eq!(functions.len(), 1); + assert_eq!( + functions[0].0, + "record_committed_tool_result_gas_charge_by_leader" + ); + assert_eq!( + functions[0].1, + vec![ + "execution", + "leader_cap", + "walk_index", + "commit_tx_digest", + "commit_gas_charge", + "settlement_gas_charge" + ] + ); + } + + #[test] + fn applies_source_parameter_names_to_package() { + let temp = std::env::temp_dir().join(format!( + "sui-move-codegen-source-names-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after epoch") + .as_nanos() + )); + let source_dir = temp.join("sources"); + std::fs::create_dir_all(&source_dir).expect("create source dir"); + std::fs::write( + source_dir.join("execution_settlement.move"), + r#" +module nexus_workflow::execution_settlement; + +public fun record_committed_tool_result_gas_charge_by_leader( + execution: &mut DAGExecution, + leader_cap: &CloneableOwnerCap, + walk_index: u64, +) {} +"#, + ) + .expect("write source"); + + let mut package = NormalizedPackage { + storage_id: "0xa4".to_string(), + original_id: None, + version: 1, + modules: BTreeMap::from([( + "execution_settlement".to_string(), + NormalizedModule { + name: "execution_settlement".to_string(), + datatypes: vec![], + functions: vec![Function { + name: "record_committed_tool_result_gas_charge_by_leader".to_string(), + visibility: Visibility::Public, + is_entry: false, + type_parameters: vec![], + parameters: vec![ + FunctionParam { + name: "arg0".to_string(), + ty: TypeRef::U64, + }, + FunctionParam { + name: "arg1".to_string(), + ty: TypeRef::U64, + }, + FunctionParam { + name: "arg2".to_string(), + ty: TypeRef::U64, + }, + ], + return_types: vec![], + }], + }, + )]), + }; + + apply_function_parameter_names_from_sources(&mut package, &source_dir) + .expect("apply names"); + + let parameters = &package.modules["execution_settlement"].functions[0].parameters; + assert_eq!(parameters[0].name, "execution"); + assert_eq!(parameters[1].name, "leader_cap"); + assert_eq!(parameters[2].name, "walk_index"); + std::fs::remove_dir_all(temp).expect("remove temp dir"); + } +} diff --git a/sui-move-derive/src/expand.rs b/sui-move-derive/src/expand.rs index 1fc494d..797305e 100644 --- a/sui-move-derive/src/expand.rs +++ b/sui-move-derive/src/expand.rs @@ -16,7 +16,10 @@ use quote::{format_ident, quote}; use syn::parse::Parser; use syn::parse_quote; use syn::spanned::Spanned; -use syn::{Data, DeriveInput, Field, FieldMutability, Fields, GenericParam, TypeParam}; +use syn::{ + Data, DeriveInput, Field, FieldMutability, Fields, GenericParam, TypeParam, WhereClause, + WherePredicate, +}; use crate::abilities::{parse_inline_abilities, AbilityFlags}; use crate::args::MoveStructArgs; @@ -137,7 +140,7 @@ pub(crate) fn expand_move_struct( } } - let mut where_bounds: Vec = Vec::new(); + let mut type_param_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_bounds.push(parse_quote!(#ident: #(#bounds)+*)); } if has_key @@ -171,19 +174,24 @@ pub(crate) fn expand_move_struct( )); } + let mut store_bounds = type_param_bounds.clone(); + let mut copy_bounds = type_param_bounds.clone(); + let mut drop_bounds = type_param_bounds.clone(); + let key_bounds = type_param_bounds.clone(); + for field in &original_fields { if is_phantom_field_type(&field.ty) { continue; } let ty = &field.ty; if has_copy { - where_bounds.push(parse_quote!(#ty: ::sui_move::HasCopy)); + copy_bounds.push(parse_quote!(#ty: ::sui_move::HasCopy)); } if has_drop { - where_bounds.push(parse_quote!(#ty: ::sui_move::HasDrop)); + drop_bounds.push(parse_quote!(#ty: ::sui_move::HasDrop)); } if has_store { - where_bounds.push(parse_quote!(#ty: ::sui_move::HasStore)); + store_bounds.push(parse_quote!(#ty: ::sui_move::HasStore)); } } @@ -218,17 +226,14 @@ pub(crate) fn expand_move_struct( ) }; - let mut derives: Vec = Vec::new(); - if has_copy { - derives.push(parse_quote!(::core::clone::Clone)); - } - derives.extend([ + let derives: Vec = vec![ parse_quote!(::core::fmt::Debug), parse_quote!(::core::cmp::PartialEq), parse_quote!(::core::cmp::Eq), + parse_quote!(::core::hash::Hash), parse_quote!(::sui_move::__private::serde::Serialize), parse_quote!(::sui_move::__private::serde::Deserialize), - ]); + ]; let mut output_struct = input; output_struct.ident = struct_ident.clone(); @@ -291,52 +296,71 @@ pub(crate) fn expand_move_struct( } 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 mut clone_bounds = type_param_bounds.clone(); + clone_bounds.extend( + original_fields + .iter() + .filter(|field| !is_phantom_field_type(&field.ty)) + .map(|field| { + let ty = &field.ty; + parse_quote!(#ty: ::core::clone::Clone) + }) + .collect::>(), + ); + let clone_where_clause = where_clause_with(&generics, &clone_bounds); + + let (impl_generics, ty_generics, _) = generics.split_for_impl(); + let move_type_where_clause = where_clause_with(&generics, &type_param_bounds); + let key_where_clause = where_clause_with(&generics, &key_bounds); + let store_where_clause = where_clause_with(&generics, &store_bounds); + let copy_where_clause = where_clause_with(&generics, ©_bounds); + let drop_where_clause = where_clause_with(&generics, &drop_bounds); let ability_impls = { let mut impls = Vec::new(); if has_key { impls.push(quote! { - impl #impl_generics ::sui_move::HasKey for #struct_ident #ty_generics #where_clause {} + impl #impl_generics ::sui_move::HasKey for #struct_ident #ty_generics #key_where_clause {} }); } if has_store { impls.push(quote! { - impl #impl_generics ::sui_move::HasStore for #struct_ident #ty_generics #where_clause {} + impl #impl_generics ::sui_move::HasStore for #struct_ident #ty_generics #store_where_clause {} }); } if has_copy { impls.push(quote! { - impl #impl_generics ::sui_move::HasCopy for #struct_ident #ty_generics #where_clause {} + impl #impl_generics ::sui_move::HasCopy for #struct_ident #ty_generics #copy_where_clause {} }); } if has_drop { impls.push(quote! { - impl #impl_generics ::sui_move::HasDrop for #struct_ident #ty_generics #where_clause {} + impl #impl_generics ::sui_move::HasDrop for #struct_ident #ty_generics #drop_where_clause {} }); } quote! { #(#impls)* } }; + let cloned_fields = fields.iter().map(|field| { + let ident = field + .ident + .as_ref() + .expect("move_struct supports only named fields"); + quote! { #ident: ::core::clone::Clone::clone(&self.#ident), } + }); + Ok(quote! { #output_struct - impl #impl_generics ::sui_move::MoveType for #struct_ident #ty_generics #where_clause { + impl #impl_generics ::core::clone::Clone for #struct_ident #ty_generics #clone_where_clause { + fn clone(&self) -> Self { + Self { + #(#cloned_fields)* + } + } + } + + impl #impl_generics ::sui_move::MoveType for #struct_ident #ty_generics #move_type_where_clause { fn type_tag_static() -> ::sui_move::__private::sui_sdk_types::TypeTag { ::sui_move::__private::sui_sdk_types::TypeTag::Struct(Box::new( ::struct_tag_static(), @@ -344,7 +368,7 @@ pub(crate) fn expand_move_struct( } } - impl #impl_generics ::sui_move::MoveStruct for #struct_ident #ty_generics #where_clause { + impl #impl_generics ::sui_move::MoveStruct for #struct_ident #ty_generics #move_type_where_clause { fn struct_tag_static() -> ::sui_move::__private::sui_sdk_types::StructTag { #struct_tag_builder } @@ -353,3 +377,20 @@ pub(crate) fn expand_move_struct( #ability_impls }) } + +fn where_clause_with(generics: &syn::Generics, bounds: &[WherePredicate]) -> Option { + let mut where_clause = generics.where_clause.clone(); + if bounds.is_empty() { + return where_clause; + } + + if let Some(ref mut existing) = where_clause { + existing.predicates.extend(bounds.iter().cloned()); + return where_clause; + } + + Some(WhereClause { + where_token: Default::default(), + predicates: bounds.iter().cloned().collect(), + }) +} diff --git a/sui-move-ptb/Cargo.toml b/sui-move-ptb/Cargo.toml index fc994c4..aaebdb6 100644 --- a/sui-move-ptb/Cargo.toml +++ b/sui-move-ptb/Cargo.toml @@ -6,6 +6,7 @@ description = "Programmable-transaction builder for typed Move calls on Sui." readme = "README.md" [dependencies] +sui-move = { path = "../sui-move" } sui-move-call = { path = "../sui-move-call" } sui-sdk-types = { workspace = true } thiserror = { workspace = true } diff --git a/sui-move-ptb/src/lib.rs b/sui-move-ptb/src/lib.rs index 8a1ccc6..3f18631 100644 --- a/sui-move-ptb/src/lib.rs +++ b/sui-move-ptb/src/lib.rs @@ -1,12 +1,18 @@ #![doc = include_str!("../README.md")] #![deny(missing_docs)] -use sui_move_call::{CallArg, CallArgError, CallSpec, ToCallArg}; +use std::borrow::Borrow; + +use sui_move_call::{CallArg, CallArgError, CallSpec, CallSpecError, CallTarget, ToCallArg}; use sui_sdk_types::{ - Address, Argument, Command, MakeMoveVector, MergeCoins, MoveCall, Mutability, - ProgrammableTransaction, Publish, SharedInput, SplitCoins, TransferObjects, TypeTag, + Address, Argument, Command, FundsWithdrawal, MakeMoveVector, MergeCoins, MoveCall, Mutability, + ObjectReference, Owner, ProgrammableTransaction, Publish, SharedInput, SplitCoins, + TransferObjects, TypeTag, WithdrawFrom, }; +/// Fixed shared object ID for `0x2::clock::Clock`. +pub const CLOCK_OBJECT_ID: Address = Address::from_static("0x6"); + /// Errors that can occur while building a `ProgrammableTransaction`. #[derive(thiserror::Error, Debug)] pub enum BuildError { @@ -14,6 +20,10 @@ pub enum BuildError { #[error(transparent)] CallArg(#[from] CallArgError), + /// A generated call spec could not be constructed. + #[error(transparent)] + CallSpec(#[from] CallSpecError), + /// Too many inputs were added (PTB uses `u16` indices). #[error("too many PTB inputs (u16 overflow)")] TooManyInputs, @@ -30,6 +40,22 @@ pub enum BuildError { /// Duplicated object id. object_id: Address, }, + + /// Ownership metadata cannot be represented as a normal transaction input. + #[error("object {object_id} has unsupported owner for transaction input: {owner:?}")] + UnsupportedOwner { + /// Object id whose owner could not be represented. + object_id: Address, + /// Unsupported owner metadata returned by Sui. + owner: Owner, + }, + + /// A nested result was requested from an argument that is not a command result. + #[error("expected command result argument, got {argument:?}")] + ExpectedCommandResult { + /// Argument that was expected to be `Argument::Result`. + argument: Argument, + }, } /// Mutable builder for `ProgrammableTransaction`. @@ -183,6 +209,93 @@ impl PtbBuilder { self.input(value.to_call_arg()?) } + /// Add pre-encoded BCS bytes as a pure PTB input. + /// + /// Prefer [`Self::arg`] when the Rust type is available. Use this for dynamic ABI edges where + /// the exact Move type is discovered at runtime and the value has already been BCS encoded. + pub fn pure_bcs(&mut self, bytes: Vec) -> Result { + self.input(CallArg::Pure(bytes)) + } + + /// Add an owned or immutable object reference as a PTB input. + pub fn owned_object(&mut self, object: &ObjectReference) -> Result { + self.input(CallArg::ImmutableOrOwned(object.clone())) + } + + /// Add a shared object reference as a PTB input. + /// + /// The object's version is used as the initial shared version. If the same shared object is + /// added later with stronger mutability, the existing input is upgraded in place. + pub fn shared_object>( + &mut self, + object: &ObjectReference, + mutability: M, + ) -> Result { + self.shared_object_by_id(*object.object_id(), object.version(), mutability) + } + + /// Add a shared object input from its ID and initial shared version. + pub fn shared_object_by_id>( + &mut self, + object_id: Address, + initial_shared_version: u64, + mutability: M, + ) -> Result { + self.input(CallArg::Shared(SharedInput::new( + object_id, + initial_shared_version, + mutability, + ))) + } + + /// Add an object input using ownership metadata returned by Sui RPC. + /// + /// Shared-like objects become `Input::Shared`; immutable/address-owned objects can be used + /// only when immutable access was requested. + pub fn object_from_owner( + &mut self, + object: &ObjectReference, + owner: O, + mutability: M, + ) -> Result + where + O: Borrow, + M: Into, + { + let owner = *owner.borrow(); + let mutability = mutability.into(); + let object_id = *object.object_id(); + match owner { + Owner::Shared(initial_shared_version) => { + self.shared_object_by_id(object_id, initial_shared_version, mutability) + } + Owner::ConsensusAddress { start_version, .. } => { + self.shared_object_by_id(object_id, start_version, mutability) + } + Owner::Immutable if mutability == Mutability::Immutable => self.owned_object(object), + Owner::Address(_) if mutability == Mutability::Immutable => self.owned_object(object), + owner => Err(BuildError::UnsupportedOwner { object_id, owner }), + } + } + + /// Add a coin withdrawn from the transaction sender's SIP-58 address balance. + pub fn funds_withdrawal_coin( + &mut self, + coin_type: TypeTag, + amount: u64, + ) -> Result { + self.input(CallArg::FundsWithdrawal(FundsWithdrawal::new( + amount, + coin_type, + WithdrawFrom::Sender, + ))) + } + + /// Add the Sui clock object as an immutable shared PTB input. + pub fn clock(&mut self) -> Result { + self.shared_object_by_id(CLOCK_OBJECT_ID, 1, Mutability::Immutable) + } + /// Add a Move call command from a typed `CallSpec`. /// /// This consumes the spec, allocates all of its inputs into the PTB input table (reusing @@ -209,6 +322,27 @@ impl PtbBuilder { Ok(Argument::Result(cmd_idx)) } + /// Add a Move call command from a generated call target and explicit PTB arguments. + /// + /// Use this for composed PTBs where some arguments are previous command results instead of + /// fresh transaction inputs. Generated bindings should still provide the [`CallTarget`], so + /// package/module/function/type-argument identity does not fall back to hand-written strings. + pub fn call_target( + &mut self, + target: CallTarget, + arguments: Vec, + ) -> Result { + let cmd_idx = self.push_command(Command::MoveCall(MoveCall { + package: target.package, + module: target.module, + function: target.function, + type_arguments: target.type_arguments, + arguments, + }))?; + + Ok(Argument::Result(cmd_idx)) + } + /// Add a `TransferObjects` command. /// /// This is a thin wrapper around `sui_sdk_types::Command::TransferObjects`. @@ -264,6 +398,14 @@ impl PtbBuilder { Ok(Argument::Result(cmd_idx)) } + /// Build a typed Move `vector` from already-built PTB arguments. + pub fn move_vector( + &mut self, + elements: Vec, + ) -> Result { + self.make_move_vector(Some(T::type_tag_static()), elements) + } + /// Add a `Publish` command. /// /// This is a thin wrapper around `sui_sdk_types::Command::Publish`. @@ -271,12 +413,17 @@ impl PtbBuilder { &mut self, modules: Vec>, dependencies: Vec
, - ) -> Result<(), BuildError> { - self.push_command(Command::Publish(Publish { + ) -> Result { + let cmd_idx = self.push_command(Command::Publish(Publish { modules, dependencies, }))?; - Ok(()) + Ok(Argument::Result(cmd_idx)) + } + + /// Return a nested result argument for a multi-return command. + pub fn nested_result(&self, argument: Argument, ix: u16) -> Result { + nested_result(argument, ix) } /// Finish and return the underlying `ProgrammableTransaction`. @@ -317,6 +464,16 @@ pub fn ptb( Ok(builder.finish()) } +/// Return a nested result argument for a multi-return command. +/// +/// This is a checked wrapper around [`Argument::nested`] so transaction builders can propagate a +/// normal [`BuildError`] instead of panicking or unwrapping. +pub fn nested_result(argument: Argument, ix: u16) -> Result { + argument + .nested(ix) + .ok_or(BuildError::ExpectedCommandResult { argument }) +} + /// Build a `ProgrammableTransaction` using a scoped `PtbBuilder`. /// /// This is a thin macro wrapper around [`ptb`]. It exists purely for call-site ergonomics. @@ -365,7 +522,7 @@ macro_rules! ptb { /// Convenience re-exports for downstream code. pub mod prelude { - pub use crate::{ptb, BuildError, PtbBuilder}; + pub use crate::{ptb, BuildError, PtbBuilder, CLOCK_OBJECT_ID}; pub use sui_move_call::prelude::*; pub use sui_sdk_types::{Argument, Command, ProgrammableTransaction}; } diff --git a/sui-move-ptb/tests/basic.rs b/sui-move-ptb/tests/basic.rs index e6b4e6f..073a1e4 100644 --- a/sui-move-ptb/tests/basic.rs +++ b/sui-move-ptb/tests/basic.rs @@ -1,10 +1,12 @@ use std::str::FromStr; -use sui_move_call::{CallArg, CallSpec, MoveObject, ReceivingMoveObject, SharedMoveObject}; +use sui_move_call::{ + CallArg, CallSpec, CallTarget, MoveObject, ReceivingMoveObject, SharedMoveObject, +}; use sui_move_ptb::{ptb, BuildError, PtbBuilder}; use sui_sdk_types::{ - Address, Argument, Command, Digest, FundsWithdrawal, Mutability, ObjectReference, TypeTag, - WithdrawFrom, + Address, Argument, Command, Digest, FundsWithdrawal, Mutability, ObjectReference, Owner, + TypeTag, WithdrawFrom, }; #[sui_move::move_struct(address = "0x2", module = "object", abilities = "copy, store")] @@ -26,6 +28,10 @@ fn mk_obj(id: &str, version: u64) -> ObjectReference { ObjectReference::new(Address::from_str(id).unwrap(), version, Digest::default()) } +fn addr(byte: u8) -> Address { + Address::new([byte; 32]) +} + #[test] fn build_move_call_from_callspec() { let package = Address::from_str("0x1").unwrap(); @@ -50,6 +56,80 @@ fn build_move_call_from_callspec() { assert_eq!(call.arguments, vec![Argument::Input(0), Argument::Input(1)]); } +#[test] +fn generic_object_helpers_build_canonical_inputs() { + let object = mk_obj("0x2", 3); + let shared_id = Address::from_str("0x3").unwrap(); + + let mut tx = PtbBuilder::new(); + let owned = tx.owned_object(&object).unwrap(); + let shared = tx.shared_object_by_id(shared_id, 7, true).unwrap(); + let clock = tx.clock().unwrap(); + + assert_eq!(owned, Argument::Input(0)); + assert_eq!(shared, Argument::Input(1)); + assert_eq!(clock, Argument::Input(2)); + + assert!(matches!(tx.inputs()[0], CallArg::ImmutableOrOwned(_))); + + let CallArg::Shared(shared) = &tx.inputs()[1] else { + panic!("expected shared input") + }; + assert_eq!(shared.object_id(), shared_id); + assert_eq!(shared.version(), 7); + assert_eq!(shared.mutability(), Mutability::Mutable); + + let CallArg::Shared(clock) = &tx.inputs()[2] else { + panic!("expected shared clock input") + }; + assert_eq!(clock.object_id(), sui_move_ptb::CLOCK_OBJECT_ID); + assert_eq!(clock.version(), 1); + assert_eq!(clock.mutability(), Mutability::Immutable); +} + +#[test] +fn object_from_owner_uses_owner_shape() { + let mut tx = PtbBuilder::new(); + let shared_object = ObjectReference::new(addr(0x02), 9, Digest::default()); + let owned_object = ObjectReference::new(addr(0x03), 9, Digest::default()); + let mutable_owned_object = ObjectReference::new(addr(0x04), 9, Digest::default()); + + let shared = tx + .object_from_owner(&shared_object, Owner::Shared(5), true) + .unwrap(); + let owned = tx + .object_from_owner(&owned_object, Owner::Address(addr(0xAA)), false) + .unwrap(); + + assert_eq!(shared, Argument::Input(0)); + assert_eq!(owned, Argument::Input(1)); + assert!(matches!(tx.inputs()[0], CallArg::Shared(_))); + assert!(matches!(tx.inputs()[1], CallArg::ImmutableOrOwned(_))); + + let err = tx + .object_from_owner(&mutable_owned_object, Owner::Address(addr(0xAA)), true) + .unwrap_err(); + assert!(matches!(err, BuildError::UnsupportedOwner { .. })); +} + +#[test] +fn builds_raw_bcs_inputs_funds_withdrawals_and_typed_vectors() { + let mut tx = PtbBuilder::new(); + let raw = tx.pure_bcs(vec![1, 2, 3]).unwrap(); + let amount = tx.arg(&10u64).unwrap(); + let coin = tx.funds_withdrawal_coin(TypeTag::U64, 11).unwrap(); + let vector = tx.move_vector::(vec![amount]).unwrap(); + + assert_eq!(raw, Argument::Input(0)); + assert_eq!(coin, Argument::Input(2)); + assert_eq!(vector, Argument::Result(0)); + + let pt = tx.finish(); + assert!(matches!(pt.inputs[0], CallArg::Pure(_))); + assert!(matches!(pt.inputs[2], CallArg::FundsWithdrawal(_))); + assert!(matches!(pt.commands[0], Command::MakeMoveVector(_))); +} + #[test] fn supports_shared_and_receiving_inputs() { let package = Address::from_str("0x1").unwrap(); @@ -71,6 +151,69 @@ fn supports_shared_and_receiving_inputs() { assert!(matches!(pt.inputs[1], CallArg::Receiving(_))); } +#[test] +fn build_move_call_from_target_and_existing_arguments() { + let package = Address::from_str("0x1").unwrap(); + let mut target = CallTarget::new(package, "m", "composed").unwrap(); + target.push_type_arg::(); + + let mut builder = PtbBuilder::new(); + let value = builder.arg(&7u64).unwrap(); + let result = builder + .call_target(target, vec![Argument::Gas, value]) + .unwrap(); + + assert_eq!(result, Argument::Result(0)); + let pt = builder.finish(); + assert_eq!(pt.commands.len(), 1); + + let Command::MoveCall(call) = &pt.commands[0] else { + panic!("expected move call"); + }; + assert_eq!(call.package, package); + assert_eq!(call.module.as_str(), "m"); + assert_eq!(call.function.as_str(), "composed"); + assert_eq!(call.arguments, vec![Argument::Gas, Argument::Input(0)]); +} + +#[test] +fn build_call_target_from_existing_arguments() { + let package = Address::from_str("0x1").unwrap(); + let mut target = CallTarget::new(package, "m", "composed").unwrap(); + target.push_type_arg::(); + + let mut builder = PtbBuilder::new(); + let value = builder.arg(&7u64).unwrap(); + let result = builder + .call_target(target, vec![Argument::Gas, value]) + .unwrap(); + + assert_eq!(result, Argument::Result(0)); + let pt = builder.finish(); + assert_eq!(pt.commands.len(), 1); + + let Command::MoveCall(call) = &pt.commands[0] else { + panic!("expected move call"); + }; + assert_eq!(call.package, package); + assert_eq!(call.module.as_str(), "m"); + assert_eq!(call.function.as_str(), "composed"); + assert_eq!(call.arguments, vec![Argument::Gas, Argument::Input(0)]); +} + +#[test] +fn publish_returns_upgrade_cap_result() { + let mut builder = PtbBuilder::new(); + let result = builder + .publish(vec![vec![1, 2, 3]], vec![Address::from_str("0x1").unwrap()]) + .unwrap(); + + assert_eq!(result, Argument::Result(0)); + let pt = builder.finish(); + assert_eq!(pt.commands.len(), 1); + assert!(matches!(pt.commands[0], Command::Publish(_))); +} + #[test] fn dedups_identical_inputs_except_funds_withdrawal() { let package = Address::from_str("0x1").unwrap(); @@ -145,3 +288,20 @@ fn rejects_duplicate_object_ids_between_receiving_and_input_objects() { let err = tx.arg(&receiving).unwrap_err(); assert!(matches!(err, BuildError::DuplicateObjectRefInput { .. })); } + +#[test] +fn nested_result_helper_requires_command_result() { + let tx = PtbBuilder::new(); + let result = Argument::Result(7); + assert_eq!( + tx.nested_result(result, 2).unwrap(), + Argument::NestedResult(7, 2) + ); + assert_eq!( + sui_move_ptb::nested_result(result, 2).unwrap(), + Argument::NestedResult(7, 2) + ); + + let err = sui_move_ptb::nested_result(Argument::Input(0), 0).unwrap_err(); + assert!(matches!(err, BuildError::ExpectedCommandResult { .. })); +} diff --git a/sui-move-runtime/src/lib.rs b/sui-move-runtime/src/lib.rs index 3843c49..63f8119 100644 --- a/sui-move-runtime/src/lib.rs +++ b/sui-move-runtime/src/lib.rs @@ -34,6 +34,10 @@ pub enum Error { #[error(transparent)] Build(#[from] sui_move_ptb::BuildError), + /// Constructing a generated Move call failed. + #[error(transparent)] + CallSpec(#[from] sui_move_call::CallSpecError), + /// Signing or submitting failed. #[error(transparent)] Tx(#[from] tx::TxError), @@ -615,7 +619,7 @@ impl<'a, S: SuiSigner> Tx<'a, S> { &mut self, modules: Vec>, dependencies: Vec
, - ) -> Result<(), Error> { + ) -> Result { Ok(self.ptb.publish(modules, dependencies)?) } diff --git a/sui-move/Cargo.toml b/sui-move/Cargo.toml index 2b1af6e..f842e94 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/src/builtins.rs b/sui-move/src/builtins.rs index 0287130..9488902 100644 --- a/sui-move/src/builtins.rs +++ b/sui-move/src/builtins.rs @@ -22,6 +22,7 @@ impl_primitive!(u16, U16); impl_primitive!(u32, U32); impl_primitive!(u64, U64); impl_primitive!(u128, U128); +impl_primitive!(crate::U256, U256); impl_primitive!(bool, Bool); impl MoveType for sui_sdk_types::Address { @@ -49,9 +50,23 @@ impl_ability_markers_primitive!(u16); impl_ability_markers_primitive!(u32); impl_ability_markers_primitive!(u64); impl_ability_markers_primitive!(u128); +impl_ability_markers_primitive!(crate::U256); impl_ability_markers_primitive!(bool); impl_ability_markers_primitive!(sui_sdk_types::Address); impl HasCopy for Vec {} impl HasDrop for Vec {} impl HasStore for Vec {} + +#[cfg(test)] +mod tests { + use crate::MoveType; + + #[test] + fn u256_has_move_type_tag_and_fixed_width_bcs() { + let value = crate::U256::from_le_bytes([7u8; 32]); + + assert_eq!(crate::U256::type_tag_static(), sui_sdk_types::TypeTag::U256); + assert_eq!(bcs::to_bytes(&value).unwrap().len(), 32); + } +} diff --git a/sui-move/src/lib.rs b/sui-move/src/lib.rs index 60423b8..51cc955 100644 --- a/sui-move/src/lib.rs +++ b/sui-move/src/lib.rs @@ -16,7 +16,7 @@ pub mod prelude { pub use crate::{move_module, move_struct}; pub use crate::{ Copyable, Droppable, HasCopy, HasDrop, HasKey, HasStore, MoveInstance, MoveStruct, - MoveType, Storable, + MoveType, Storable, U256, }; pub use sui_sdk_types::{Address, Identifier, StructTag, TypeTag}; } @@ -31,6 +31,27 @@ mod builtins; pub mod decode; pub use decode::{decode_copyable, decode_keyed, decode_storable}; +/// Move `u256` value represented as 32 little-endian bytes. +/// +/// This intentionally keeps arithmetic out of `sui-move`; the type exists so generated bindings +/// can carry, serialize, and type-tag Move `u256` values without inventing package-specific +/// representations. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct U256([u8; 32]); + +impl U256 { + /// Construct a `u256` from its little-endian byte representation. + pub const fn from_le_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Return the little-endian byte representation. + pub const fn to_le_bytes(self) -> [u8; 32] { + self.0 + } +} + /// A Rust type that corresponds to a Move type. /// /// Implementors provide a static [`TypeTag`](sui_sdk_types::TypeTag) (including any type @@ -64,11 +85,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). diff --git a/sui-move/tests/derive.rs b/sui-move/tests/derive.rs index 308d8c0..54ba1df 100644 --- a/sui-move/tests/derive.rs +++ b/sui-move/tests/derive.rs @@ -59,6 +59,34 @@ mod bounded { } } +mod phantom_copy { + #[sui_move::move_struct( + address = "0x1", + module = "phantom_copy", + abilities = "copy, drop, store", + phantoms = "T" + )] + pub struct Marker { + pub value: u64, + } +} + +mod conditional_abilities { + #[sui_move::move_struct(address = "0x1", module = "conditional_abilities", abilities = "store")] + pub struct StoreOnly { + pub value: u64, + } + + #[sui_move::move_struct( + address = "0x1", + module = "conditional_abilities", + abilities = "copy, drop, store" + )] + pub struct OptionLike { + pub vec: Vec, + } +} + mod dynamic_address { use std::str::FromStr; @@ -170,6 +198,21 @@ fn tag_verification_and_bcs_roundtrip() { fn require_store(_: &T) {} fn require_copy(_: &T) {} +#[derive( + Debug, + PartialEq, + Eq, + sui_move::__private::serde::Serialize, + sui_move::__private::serde::Deserialize, +)] +struct NonCloneType; + +impl MoveType for NonCloneType { + fn type_tag_static() -> TypeTag { + TypeTag::Bool + } +} + #[test] fn type_abilities_are_respected() { let boxed = bounded::Boxed:: { value: 5 }; @@ -185,6 +228,40 @@ fn type_abilities_are_respected() { assert_eq!(nested.balance[0].value, 7); } +#[test] +fn phantom_copy_type_parameter_does_not_require_clone() { + let marker = phantom_copy::Marker:: { + value: 11, + phantom_t: std::marker::PhantomData, + }; + + let cloned = marker.clone(); + require_copy(&marker); + assert_eq!(cloned.value, 11); +} + +#[test] +fn ability_marker_impls_use_per_ability_field_bounds() { + let wrapped = conditional_abilities::OptionLike { + vec: vec![conditional_abilities::StoreOnly { value: 9 }], + }; + + require_store(&wrapped); + assert_eq!(wrapped.vec[0].value, 9); +} + +#[test] +fn generated_structs_clone_from_move_copy_abilities() { + let store_only = conditional_abilities::StoreOnly { value: 3 }; + let cloned = store_only.clone(); + assert_eq!(cloned.value, 3); + + let copy_value = dynamic_address::Value { value: 4 }; + let cloned_copy_value = copy_value.clone(); + require_copy(©_value); + assert_eq!(cloned_copy_value.value, 4); +} + fn uid_with_byte(byte: u8) -> object::UID { object::UID { id: object::ID {