diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4880b2a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: Rust CI + +on: + push: + branches: [main] + pull_request: + +jobs: + cargo-check: + name: Cargo check + runs-on: namespace-profile-default + env: + CARGO_TERM_COLOR: always + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install Rust (latest nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + + - name: Cache build + uses: Swatinem/rust-cache@v2 + with: + workspaces: . + + - name: cargo check + run: cargo check --all --all-targets + + tests: + name: Tests + doctests + runs-on: namespace-profile-default + env: + CARGO_TERM_COLOR: always + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install Rust (latest nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + + - name: Cache build + uses: Swatinem/rust-cache@v2 + with: + workspaces: . + + - name: cargo test + run: cargo test --all --all-targets + + rustfmt: + name: Rustfmt + runs-on: namespace-profile-default + env: + CARGO_TERM_COLOR: always + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install Rust (latest nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + components: rustfmt + + - name: Cache build + uses: Swatinem/rust-cache@v2 + with: + workspaces: . + + - name: cargo fmt --check (nightly) + run: cargo +nightly fmt --all -- --check + + clippy: + name: Clippy + runs-on: namespace-profile-default + env: + CARGO_TERM_COLOR: always + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install Rust (latest nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + components: clippy + + - name: Cache build + uses: Swatinem/rust-cache@v2 + with: + workspaces: . + + - name: cargo clippy + run: cargo +nightly clippy --all --all-targets -- -D warnings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4cb965 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +.DS_Store +**/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..808ade5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,358 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "bcs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b6598a2f5d564fb7855dc6b06fd1c38cff5a72bd8b863a4d021938497b440a" +dependencies = [ + "serde", + "thiserror", +] + +[[package]] +name = "bnum" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119771309b95163ec7aaf79810da82f7cd0599c19722d48b9c03894dca833966" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "roaring" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f08d6a905edb32d74a5d5737a0c9d7e950c312f3c46cb0ca0a2ca09ea11878a0" +dependencies = [ + "bytemuck", + "byteorder", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64", + "chrono", + "hex", + "serde_core", + "serde_json", + "time", +] + +[[package]] +name = "sui-move" +version = "0.1.0" +dependencies = [ + "bcs", + "serde", + "serde_json", + "sui-move-derive", + "sui-sdk-types", + "thiserror", +] + +[[package]] +name = "sui-move-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sui-sdk-types" +version = "0.1.0" +source = "git+https://github.com/mystenlabs/sui-rust-sdk#fca0ea3a409a71f9be5235711d706507d8d390f3" +dependencies = [ + "base64ct", + "bcs", + "bnum", + "bs58", + "bytes", + "bytestring", + "itertools", + "roaring", + "serde", + "serde_derive", + "serde_json", + "serde_with", + "winnow", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index b81d195..dfb015a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,8 @@ [workspace] resolver = "2" members = [ - "move-binding-core", - "move-binding-env", - "move-binding-bindgen", + "sui-move", + "sui-move-derive", ] [workspace.dependencies] @@ -13,3 +12,7 @@ thiserror = "1.0" sui-sdk-types = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["serde"] } sui-rpc = { git = "https://github.com/mystenlabs/sui-rust-sdk" } sui-transaction-builder = { git = "https://github.com/mystenlabs/sui-rust-sdk" } +bcs = "0.1" +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } diff --git a/move-binding-bindgen/Cargo.toml b/move-binding-bindgen/Cargo.toml deleted file mode 100644 index b7bf8bd..0000000 --- a/move-binding-bindgen/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "move-binding-bindgen" -version = "0.1.0" -edition = "2021" - -[dependencies] -anyhow = { workspace = true } -move-binding-core = { path = "../move-binding-core" } -serde = { workspace = true, features = ["derive"] } -thiserror = { workspace = true } diff --git a/move-binding-bindgen/src/lib.rs b/move-binding-bindgen/src/lib.rs deleted file mode 100644 index a2ed000..0000000 --- a/move-binding-bindgen/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Bindgen: generate Rust bindings from local Move ABI. - -/// Placeholder entry for future generator. -pub fn generate() { - // TODO: implement ABI parsing and code generation. -} diff --git a/move-binding-core/Cargo.toml b/move-binding-core/Cargo.toml deleted file mode 100644 index f8420ab..0000000 --- a/move-binding-core/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "move-binding-core" -version = "0.1.0" -edition = "2021" - -[dependencies] -serde = { workspace = true, features = ["derive"] } -anyhow = { workspace = true } -thiserror = { workspace = true } -sui-sdk-types = { workspace = true, features = ["serde"] } diff --git a/move-binding-core/src/lib.rs b/move-binding-core/src/lib.rs deleted file mode 100644 index e81526f..0000000 --- a/move-binding-core/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Core traits and helpers for typed Move bindings. - -pub use sui_sdk_types as sui; - -/// Placeholder trait for Move type identity. -pub trait MoveType { - fn type_tag() -> sui::TypeTag; -} - -/// Placeholder trait for Move structs with layout information. -pub trait MoveStruct: MoveType {} diff --git a/move-binding-env/Cargo.toml b/move-binding-env/Cargo.toml deleted file mode 100644 index 69aa0d7..0000000 --- a/move-binding-env/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "move-binding-env" -version = "0.1.0" -edition = "2021" - -[dependencies] -anyhow = { workspace = true } -move-binding-core = { path = "../move-binding-core" } -sui-rpc = { workspace = true } -sui-transaction-builder = { workspace = true } -serde = { workspace = true, features = ["derive"] } diff --git a/move-binding-env/src/lib.rs b/move-binding-env/src/lib.rs deleted file mode 100644 index e7cad4e..0000000 --- a/move-binding-env/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Session helper wrapping sui-rpc and sui-transaction-builder. - -pub struct SuiEnv; - -impl SuiEnv { - /// Placeholder connect; to be implemented. - pub fn new() -> Self { - Self - } -} diff --git a/sui-move-derive/Cargo.toml b/sui-move-derive/Cargo.toml new file mode 100644 index 0000000..a32f4c6 --- /dev/null +++ b/sui-move-derive/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "sui-move-derive" +version = "0.1.0" +edition = "2021" +description = "Procedural macros for sui-move typed structs." + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true, features = ["full"] } diff --git a/sui-move-derive/src/lib.rs b/sui-move-derive/src/lib.rs new file mode 100644 index 0000000..1376320 --- /dev/null +++ b/sui-move-derive/src/lib.rs @@ -0,0 +1,633 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use std::collections::{BTreeMap, BTreeSet}; +use syn::parse::{Parse, ParseStream, Parser}; +use syn::parse_quote; +use syn::spanned::Spanned; +use syn::{ + parse_macro_input, Attribute, Data, DeriveInput, Field, FieldMutability, Fields, GenericParam, + Lit, Meta, TypeParam, +}; + +fn parse_serde_attr(kind: &str) -> syn::Result { + let ident = syn::Ident::new(kind, proc_macro2::Span::call_site()); + let meta: Meta = syn::parse_quote!(serde(#ident)); + Ok(Attribute { + pound_token: Default::default(), + style: syn::AttrStyle::Outer, + bracket_token: Default::default(), + meta, + }) +} + +fn has_phantom_attr(attrs: &[Attribute]) -> bool { + attrs.iter().any(|a| { + let path = a.path(); + path.is_ident("phantom") + || path + .segments + .last() + .map(|seg| seg.ident == "phantom") + .unwrap_or(false) + }) +} + +#[derive(Clone, Copy)] +enum Ability { + Key, + Store, + Copy, + Drop, +} + +#[derive(Default, Clone, Copy)] +struct AbilityFlags { + key: bool, + store: bool, + copy: bool, + drop: bool, +} + +impl AbilityFlags { + fn from_list(list: &[String], span: proc_macro2::Span) -> syn::Result { + let mut flags = AbilityFlags::default(); + let mut seen = BTreeSet::new(); + for item in list { + let ident = item.to_lowercase(); + if !seen.insert(ident.clone()) { + continue; + } + let ability = match ident.as_str() { + "key" => Ability::Key, + "store" => Ability::Store, + "copy" => Ability::Copy, + "drop" => Ability::Drop, + _ => { + return Err(syn::Error::new( + span, + format!( + "unknown ability `{ident}`; expected one of key, store, copy, drop" + ), + )) + } + }; + match ability { + Ability::Key => flags.key = true, + Ability::Store => flags.store = true, + Ability::Copy => flags.copy = true, + Ability::Drop => flags.drop = true, + } + } + + if flags.copy { + flags.drop = true; + } + if flags.key && !flags.store { + return Err(syn::Error::new(span, "ability `key` requires `store`")); + } + if flags.key && flags.copy { + return Err(syn::Error::new( + span, + "a struct cannot have both `key` and `copy` abilities", + )); + } + Ok(flags) + } +} + +#[proc_macro_attribute] +pub fn move_module(_args: TokenStream, input: TokenStream) -> TokenStream { + input +} + +/// Attribute macro that turns a Rust struct into a Move-shaped type. +/// Usage: +/// ```ignore +/// #[move_struct(address = "0x1", module = "vault", abilities = "key, store", phantoms = "T")] +/// pub struct Vault { ... } +/// ``` +#[proc_macro_attribute] +pub fn move_struct(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as MoveStructArgs); + let input = parse_macro_input!(item as DeriveInput); + match expand_move_struct(args, input) { + Ok(ts) => ts, + Err(err) => err.to_compile_error().into(), + } +} + +#[derive(Default)] +struct MoveStructArgs { + address: Option, + module: Option, + name: Option, + abilities: Vec, + phantoms: Vec, + type_abilities: BTreeMap>, + uid_type: Option, +} + +impl Parse for MoveStructArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut args = MoveStructArgs::default(); + let parser = syn::meta::parser(|meta| { + let ident = meta + .path + .get_ident() + .ok_or_else(|| syn::Error::new(meta.path.span(), "expected identifier key"))? + .to_string(); + let lit: Lit = meta.value()?.parse()?; + match ident.as_str() { + "address" => { + if let Lit::Str(s) = lit { + args.address = Some(s.value()); + } else { + return Err(syn::Error::new(lit.span(), "address must be a string")); + } + } + "module" => { + if let Lit::Str(s) = lit { + args.module = Some(s.value()); + } else { + return Err(syn::Error::new(lit.span(), "module must be a string")); + } + } + "name" => { + if let Lit::Str(s) = lit { + args.name = Some(s.value()); + } else { + return Err(syn::Error::new(lit.span(), "name must be a string")); + } + } + "abilities" => { + if let Lit::Str(s) = lit { + args.abilities = s + .value() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } else { + return Err(syn::Error::new( + lit.span(), + "abilities must be a string literal, e.g., \"key, store\"", + )); + } + } + "phantoms" => { + if let Lit::Str(s) = lit { + args.phantoms = s + .value() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } else { + return Err(syn::Error::new( + lit.span(), + "phantoms must be a string literal, e.g., \"T, U\"", + )); + } + } + "type_abilities" => { + if let Lit::Str(ref s) = lit { + args.type_abilities = parse_type_abilities(&s.value(), lit.span())?; + } else { + return Err(syn::Error::new( + lit.span(), + "type_abilities must be a string literal, e.g., \"T: store, copy; U: drop\"", + )); + } + } + "uid_type" => { + if let Lit::Str(ref s) = lit { + let ty: syn::Type = syn::parse_str(&s.value()).map_err(|_| { + syn::Error::new( + s.span(), + "uid_type must be a valid Rust type path, e.g., \"sui_move::types::UID\"", + ) + })?; + args.uid_type = Some(ty); + } else { + return Err(syn::Error::new( + lit.span(), + "uid_type must be a string literal path", + )); + } + } + other => { + return Err(syn::Error::new( + meta.path.span(), + format!("unknown attribute key `{other}`"), + )); + } + } + Ok(()) + }); + let tokens: proc_macro2::TokenStream = input.parse()?; + parser.parse2(tokens)?; + Ok(args) + } +} + +fn parse_type_abilities( + raw: &str, + span: proc_macro2::Span, +) -> syn::Result>> { + let mut map = BTreeMap::new(); + for entry in raw.split(';') { + let entry = entry.trim(); + if entry.is_empty() { + continue; + } + let mut parts = entry.splitn(2, ':'); + let name = parts + .next() + .ok_or_else(|| syn::Error::new(span, "expected type parameter name"))? + .trim(); + let abilities = parts + .next() + .ok_or_else(|| syn::Error::new(span, "expected `:` followed by abilities"))? + .trim(); + if name.is_empty() { + return Err(syn::Error::new(span, "missing type parameter name")); + } + if abilities.is_empty() { + return Err(syn::Error::new( + span, + "missing abilities after `:` in type_abilities", + )); + } + let ability_list = abilities + .split(',') + .map(|a| a.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect::>(); + map.insert(name.to_string(), ability_list); + } + Ok(map) +} + +fn parse_inline_abilities(param: &TypeParam) -> syn::Result> { + let mut flags = AbilityFlags::default(); + for bound in ¶m.bounds { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + if let Some(seg) = trait_bound.path.segments.last() { + let name = seg.ident.to_string().to_lowercase(); + match name.as_str() { + "hascopy" | "copyable" => flags.copy = true, + "hasdrop" | "droppable" => flags.drop = true, + "hasstore" | "storable" => flags.store = true, + "haskey" => flags.key = true, + _ => {} + } + } + } + } + if flags.copy { + flags.drop = true; + } + Ok((flags.key || flags.store || flags.copy || flags.drop).then_some(flags)) +} + +fn expand_move_struct(args: MoveStructArgs, input: DeriveInput) -> syn::Result { + let span = input.span(); + let struct_ident = input.ident.clone(); + let generics = input.generics.clone(); + let struct_vis = input.vis.clone(); + + let data = match input.data.clone() { + Data::Struct(s) => s, + _ => { + return Err(syn::Error::new( + span, + "#[move_struct] only supports structs", + )) + } + }; + + let original_fields = match &data.fields { + Fields::Named(named) => named.named.clone().into_iter().collect::>(), + _ => unreachable!(), + }; + + let mut fields = match data.fields { + Fields::Named(named) => named.named.into_iter().collect::>(), + _ => { + return Err(syn::Error::new( + span, + "#[move_struct] currently supports only structs with named fields", + )) + } + }; + + // Inject PhantomData fields for phantom params + let phantom_param_names: Vec = args.phantoms.iter().map(|s| s.to_string()).collect(); + + let phantom_params: Vec<&TypeParam> = generics + .params + .iter() + .filter_map(|param| match param { + GenericParam::Type(ty) => Some(ty), + _ => None, + }) + .filter(|ty| { + let name = ty.ident.to_string(); + has_phantom_attr(&ty.attrs) || phantom_param_names.iter().any(|p| p == &name) + }) + .collect(); + + let mut type_ability_flags: BTreeMap = args + .type_abilities + .iter() + .map(|(name, abilities)| { + AbilityFlags::from_list(abilities, span).map(|flags| (name.clone(), flags)) + }) + .collect::>()?; + + // Merge inline generic ability annotations (#[abilities(copy, store)] T). + for param in generics.params.iter().filter_map(|p| match p { + GenericParam::Type(t) => Some(t), + _ => None, + }) { + if let Some(flags) = parse_inline_abilities(param)? { + type_ability_flags + .entry(param.ident.to_string()) + .and_modify(|f| { + f.key |= flags.key; + f.store |= flags.store; + f.copy |= flags.copy; + f.drop |= flags.drop; + }) + .or_insert(flags); + } + } + + for ty in &phantom_params { + let ident = &ty.ident; + let field_ident = format_ident!("phantom_{}", ident.to_string().to_lowercase()); + fields.push(Field { + attrs: vec![parse_serde_attr("skip")?, parse_serde_attr("default")?], + vis: struct_vis.clone(), + mutability: FieldMutability::None, + ident: Some(field_ident), + colon_token: Some(Default::default()), + ty: syn::parse_quote!(std::marker::PhantomData<#ident>), + }); + } + + let abilities = AbilityFlags::from_list(&args.abilities, span)?; + let has_key = abilities.key; + let has_store = abilities.store; + let has_copy = abilities.copy; + let has_drop = abilities.drop; + + let type_param_idents: Vec<&TypeParam> = generics + .params + .iter() + .filter_map(|p| match p { + GenericParam::Type(t) => Some(t), + _ => None, + }) + .collect(); + + for name in type_ability_flags.keys() { + let target_ident = syn::Ident::new(name, span); + let exists = type_param_idents + .iter() + .any(|param| param.ident == target_ident); + if !exists { + return Err(syn::Error::new( + span, + format!("type_abilities refers to unknown type parameter `{name}`"), + )); + } + } + + let mut where_bounds: Vec = Vec::new(); + for param in &type_param_idents { + let ident = ¶m.ident; + let mut bounds: Vec = vec![syn::parse_quote!(::sui_move::MoveType)]; + if let Some(abilities) = type_ability_flags.get(&ident.to_string()) { + if abilities.copy { + bounds.push(syn::parse_quote!(::sui_move::HasCopy)); + } + if abilities.drop { + bounds.push(syn::parse_quote!(::sui_move::HasDrop)); + } + if abilities.store { + bounds.push(syn::parse_quote!(::sui_move::HasStore)); + } + if abilities.key { + bounds.push(syn::parse_quote!(::sui_move::HasKey)); + } + } + where_bounds.push(syn::parse_quote!(#ident: #(#bounds)+*)); + } + + if has_key + && !original_fields + .iter() + .any(|f| is_uid_field(f, args.uid_type.as_ref())) + { + return Err(syn::Error::new( + span, + "ability `key` requires a field `id: UID` (or uid_type override)", + )); + } + + // Ability constraints derived from struct-level abilities for each field (skip injected phantoms). + for field in &original_fields { + if is_phantom_field_type(&field.ty) { + continue; + } + let ty = &field.ty; + if has_copy { + where_bounds.push(syn::parse_quote!(#ty: ::sui_move::HasCopy)); + } + if has_drop { + where_bounds.push(syn::parse_quote!(#ty: ::sui_move::HasDrop)); + } + if has_store { + where_bounds.push(syn::parse_quote!(#ty: ::sui_move::HasStore)); + } + } + + let address = args + .address + .ok_or_else(|| syn::Error::new(span, "address is required in #[move_struct]"))?; + let module_name = args + .module + .ok_or_else(|| syn::Error::new(span, "module is required in #[move_struct]"))?; + let struct_name = args.name.unwrap_or_else(|| struct_ident.to_string()); + + let ty_params_for_tag = type_param_idents + .iter() + .map(|p| { + let ident = &p.ident; + quote! { <#ident as ::sui_move::MoveType>::type_tag_static() } + }) + .collect::>(); + + let struct_tag_builder = quote! { + ::sui_sdk_types::StructTag::new( + ::sui_move::parse_address(#address).expect("invalid address literal"), + ::sui_move::parse_identifier(#module_name).expect("invalid module"), + ::sui_move::parse_identifier(#struct_name).expect("invalid struct name"), + vec![#(#ty_params_for_tag),*], + ) + }; + + let derives: Vec = vec![ + parse_quote!(::core::clone::Clone), + parse_quote!(::core::fmt::Debug), + parse_quote!(::core::cmp::PartialEq), + parse_quote!(::core::cmp::Eq), + parse_quote!(::serde::Serialize), + parse_quote!(::serde::Deserialize), + ]; + + let mut output_struct = input; + output_struct.ident = struct_ident.clone(); + output_struct.generics = generics.clone(); + output_struct.data = Data::Struct(syn::DataStruct { + struct_token: Default::default(), + fields: Fields::Named(syn::FieldsNamed { + brace_token: Default::default(), + named: fields.clone().into_iter().collect(), + }), + semi_token: None, + }); + output_struct + .attrs + .retain(|a| !a.path().is_ident("move_struct")); + output_struct + .attrs + .push(parse_quote!(#[derive(#(#derives),*)])); + + 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 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 {} + }); + } + if has_store { + impls.push(quote! { + impl #impl_generics ::sui_move::HasStore for #struct_ident #ty_generics #where_clause {} + }); + } + if has_copy { + impls.push(quote! { + impl #impl_generics ::sui_move::HasCopy for #struct_ident #ty_generics #where_clause {} + }); + } + if has_drop { + impls.push(quote! { + impl #impl_generics ::sui_move::HasDrop for #struct_ident #ty_generics #where_clause {} + }); + } + quote! { #(#impls)* } + }; + + let expanded = quote! { + #output_struct + + impl #impl_generics ::sui_move::MoveType for #struct_ident #ty_generics #where_clause { + fn type_tag_static() -> ::sui_sdk_types::TypeTag { + ::sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) + } + } + + impl #impl_generics ::sui_move::MoveStruct for #struct_ident #ty_generics #where_clause { + fn struct_tag_static() -> ::sui_sdk_types::StructTag { + #struct_tag_builder + } + } + + #ability_impls + }; + + Ok(expanded.into()) +} + +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 { + return false; + } + if let Some(ty) = uid_override { + return types_equal(&field.ty, ty); + } + match &field.ty { + syn::Type::Path(p) => p + .path + .segments + .last() + .map(|seg| seg.ident == "UID") + .unwrap_or(false), + _ => false, + } +} + +fn is_phantom_field_type(ty: &syn::Type) -> bool { + match ty { + syn::Type::Path(p) => p + .path + .segments + .last() + .map(|seg| seg.ident == "PhantomData") + .unwrap_or(false), + _ => false, + } +} + +fn types_equal(a: &syn::Type, b: &syn::Type) -> bool { + quote::ToTokens::to_token_stream(a).to_string() + == quote::ToTokens::to_token_stream(b).to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + #[test] + fn parse_args() { + let args: MoveStructArgs = syn::parse_quote!( + address = "0x1", + module = "vault", + abilities = "key, store", + phantoms = "T", + type_abilities = "T: store, copy" + ); + assert_eq!(args.address.as_deref(), Some("0x1")); + assert_eq!(args.module.as_deref(), Some("vault")); + assert_eq!(args.name, None); + assert_eq!(args.abilities, vec!["key".to_string(), "store".to_string()]); + assert_eq!(args.phantoms, vec!["T".to_string()]); + let mut expected = BTreeMap::new(); + expected.insert( + "T".to_string(), + vec!["store".to_string(), "copy".to_string()], + ); + assert_eq!(args.type_abilities, expected); + } +} diff --git a/sui-move/Cargo.toml b/sui-move/Cargo.toml new file mode 100644 index 0000000..28d0aa3 --- /dev/null +++ b/sui-move/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sui-move" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +description = "Move-shaped typed layer for Rust, built on top of sui-sdk-types." + +[dependencies] +serde = { workspace = true, features = ["derive"] } +bcs = { workspace = true } +sui-sdk-types = { workspace = true } +thiserror = { workspace = true } +sui-move-derive = { path = "../sui-move-derive" } +serde_json = "1.0" diff --git a/sui-move/src/lib.rs b/sui-move/src/lib.rs new file mode 100644 index 0000000..375cb40 --- /dev/null +++ b/sui-move/src/lib.rs @@ -0,0 +1,359 @@ +//! Move-shaped typed layer for Rust, built on top of `sui-sdk-types`. +//! +//! This crate exposes a small set of traits that mirror Move's type system +//! (type tags, struct tags, abilities) plus helpers for safely decoding data +//! that carries a Move type tag. Attribute macros are provided by the companion +//! `sui-move-derive` crate and re-exported here. + +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +pub use sui_move_derive::{move_module, move_struct}; + +pub mod prelude { + //! Convenient import of the core traits, macros, and common Sui types. + pub use crate::{ + containers::DynamicField, containers::DynamicObjectField, containers::MoveOption, + containers::Table, move_module, move_struct, types::ID, types::UID, Copyable, Droppable, + HasCopy, HasDrop, HasKey, HasStore, MoveInstance, MoveStruct, MoveType, Storable, + }; + pub use sui_sdk_types::{Address, Identifier, StructTag, TypeTag}; +} + +/// A Move type. Implementors know how to produce their `TypeTag` and +/// serialize/deserialize themselves with BCS/serde. +pub trait MoveType: + Clone + Serialize + for<'de> Deserialize<'de> + fmt::Debug + PartialEq + Eq +{ + /// Construct the static type tag for this type (including type arguments). + fn type_tag_static() -> sui_sdk_types::TypeTag; + + /// Convenience to get the type tag for this value. + fn type_tag(&self) -> sui_sdk_types::TypeTag { + Self::type_tag_static() + } + + fn to_bcs(&self) -> Result, bcs::Error> { + bcs::to_bytes(self) + } + + fn from_bcs(bytes: &[u8]) -> Result + where + Self: Sized, + { + bcs::from_bytes(bytes) + } + + fn to_json(&self) -> serde_json::Value { + serde_json::to_value(self).expect("serialization should not fail") + } +} + +/// A Move struct (including any type parameters). +pub trait MoveStruct: MoveType { + /// Construct the static struct tag (including type arguments). + fn struct_tag_static() -> sui_sdk_types::StructTag; + + fn struct_tag(&self) -> sui_sdk_types::StructTag { + Self::struct_tag_static() + } +} + +/// Ability markers matching Move abilities. +pub trait HasKey {} +pub trait HasStore {} +pub trait HasCopy {} +pub trait HasDrop {} + +/// Combinators that encode Move ability surfaces into Rust type bounds. +pub trait Storable: MoveType + HasStore {} +impl Storable for T {} + +pub trait Copyable: MoveType + HasCopy + HasDrop {} +impl Copyable for T {} + +pub trait Droppable: MoveType + HasDrop {} +impl Droppable for T {} + +/// Errors that can occur when verifying or decoding Move data. +#[derive(thiserror::Error, Debug)] +pub enum DecodeError { + #[error("type tag mismatch. expected {expected:?}, got {got:?}")] + TypeTagMismatch { + expected: sui_sdk_types::TypeTag, + got: sui_sdk_types::TypeTag, + }, + #[error(transparent)] + Bcs(#[from] bcs::Error), + #[error("failed to parse identifier: {0}")] + Identifier(String), + #[error("failed to parse address: {0}")] + Address(String), +} + +/// A typed value accompanied by its Move type tag. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MoveInstance { + pub type_tag: sui_sdk_types::TypeTag, + pub value: T, +} + +impl MoveInstance { + /// Decode from raw type tag + BCS bytes, verifying that the tag matches the type. + pub fn from_raw_type( + type_tag: sui_sdk_types::TypeTag, + bytes: &[u8], + ) -> Result { + let expected = T::type_tag_static(); + if type_tag != expected { + return Err(DecodeError::TypeTagMismatch { + expected, + got: type_tag, + }); + } + let value = T::from_bcs(bytes)?; + Ok(Self { type_tag, value }) + } +} + +/// Utility used by the derive macros to construct identifiers and addresses +/// from string literals at runtime. +pub fn parse_identifier(value: &str) -> Result { + sui_sdk_types::Identifier::from_str(value) + .map_err(|_| DecodeError::Identifier(value.to_string())) +} + +pub fn parse_address(value: &str) -> Result { + sui_sdk_types::Address::from_str(value).map_err(|_| DecodeError::Address(value.to_string())) +} + +// Primitive MoveType implementations + +macro_rules! impl_primitive { + ($ty:ty, $variant:ident) => { + impl MoveType for $ty { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::$variant + } + } + }; +} + +impl_primitive!(u8, U8); +impl_primitive!(u16, U16); +impl_primitive!(u32, U32); +impl_primitive!(u64, U64); +impl_primitive!(u128, U128); +impl_primitive!(bool, Bool); + +impl MoveType for sui_sdk_types::Address { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::Address + } +} + +impl MoveType for Vec { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::Vector(Box::new(T::type_tag_static())) + } +} + +macro_rules! impl_ability_markers_primitive { + ($ty:ty) => { + impl HasCopy for $ty {} + impl HasDrop for $ty {} + impl HasStore for $ty {} + }; +} + +impl_ability_markers_primitive!(u8); +impl_ability_markers_primitive!(u16); +impl_ability_markers_primitive!(u32); +impl_ability_markers_primitive!(u64); +impl_ability_markers_primitive!(u128); +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 {} + +pub mod types { + use super::*; + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct ID { + pub bytes: Vec, + } + + 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 {} + + #[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 {} +} + +pub mod containers { + use super::*; + + #[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 {} + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Table { + pub id: crate::types::UID, + #[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 HasStore for Table {} + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct DynamicField { + pub id: crate::types::UID, + pub name: K, + pub value: V, + } + + 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![K::type_tag_static(), V::type_tag_static()], + ) + } + } + + impl HasStore for DynamicField {} + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct DynamicObjectField { + pub id: crate::types::UID, + pub name: K, + pub value: V, + } + + impl MoveType for DynamicObjectField { + fn type_tag_static() -> sui_sdk_types::TypeTag { + sui_sdk_types::TypeTag::Struct(Box::new(Self::struct_tag_static())) + } + } + + impl MoveStruct for DynamicObjectField { + 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("DynamicField").expect("name"), + vec![K::type_tag_static(), V::type_tag_static()], + ) + } + } + + impl HasStore for DynamicObjectField {} +} + +/// Ability-aware decoding helpers. +pub fn decode_storable(bytes: &[u8]) -> Result { + Ok(T::from_bcs(bytes)?) +} + +pub fn decode_copyable(bytes: &[u8]) -> Result { + Ok(T::from_bcs(bytes)?) +} + +pub fn decode_keyed( + type_tag: sui_sdk_types::TypeTag, + bytes: &[u8], +) -> Result, DecodeError> { + MoveInstance::from_raw_type(type_tag, bytes) +} diff --git a/sui-move/tests/basic.rs b/sui-move/tests/basic.rs new file mode 100644 index 0000000..11e167e --- /dev/null +++ b/sui-move/tests/basic.rs @@ -0,0 +1,193 @@ +use std::str::FromStr; +use sui_move::prelude::*; +use sui_move::{containers::MoveOption, Copyable, MoveInstance, Storable}; + +#[sui_move_derive::move_module(address = "0x1", name = "vault")] +mod vault { + use super::*; + + #[sui_move_derive::move_struct( + address = "0x1", + module = "vault", + abilities = "key, store", + phantoms = "T", + uid_type = "sui_move::types::UID" + )] + pub struct Vault { + pub id: sui_move::types::UID, + pub balance: Vec, + } +} + +#[sui_move_derive::move_module(address = "0x1", name = "wrapper")] +mod wrapper { + use super::*; + + #[sui_move_derive::move_struct(address = "0x1", module = "wrapper", abilities = "key, store")] + pub struct VaultWrapper { + pub id: sui_move::types::UID, + pub inner: crate::vault::Vault, + } +} + +#[sui_move_derive::move_module(address = "0x1", name = "bounded")] +mod bounded { + use super::*; + + #[sui_move_derive::move_struct( + address = "0x1", + module = "bounded", + abilities = "copy, store, drop", + type_abilities = "T: store, copy" + )] + pub struct Boxed { + pub value: T, + } +} + +#[test] +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(); + 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 + )); + } + _ => panic!("expected struct type tag"), + } +} + +#[test] +fn nested_structs_are_supported() { + let wrapper = wrapper::VaultWrapper { + id: sui_move::types::UID { + id: sui_move::types::ID { + bytes: vec![0u8; 32], + }, + }, + inner: vault::Vault:: { + id: sui_move::types::UID { + id: sui_move::types::ID { + bytes: vec![42u8; 32], + }, + }, + balance: vec![9, 8], + phantom_t: std::marker::PhantomData, + }, + }; + let bytes = wrapper.to_bcs().unwrap(); + let decoded = wrapper::VaultWrapper::from_bcs(&bytes).unwrap(); + assert_eq!(decoded.inner.id.id.bytes[0], 42); + assert_eq!(decoded.inner.balance, vec![9, 8]); + match wrapper::VaultWrapper::type_tag_static() { + sui_sdk_types::TypeTag::Struct(tag) => { + assert_eq!(tag.module().to_string(), "wrapper"); + assert_eq!(tag.name().to_string(), "VaultWrapper"); + } + _ => panic!("expected struct tag"), + } +} + +#[test] +fn tag_verification_and_bcs_roundtrip() { + let value = vault::Vault:: { + id: sui_move::types::UID { + id: sui_move::types::ID { + bytes: vec![7u8; 32], + }, + }, + balance: vec![1, 2, 3], + phantom_t: std::marker::PhantomData, + }; + let bytes = value.to_bcs().unwrap(); + + // Correct tag decodes + let inst = MoveInstance::>::from_raw_type( + vault::Vault::::type_tag_static(), + &bytes, + ) + .unwrap(); + assert_eq!(inst.value.id.id.bytes[0], 7); + assert_eq!(inst.value.balance, vec![1, 2, 3]); + + // Wrong tag is rejected + let err = MoveInstance::>::from_raw_type(sui_sdk_types::TypeTag::U8, &bytes) + .unwrap_err(); + matches!(err, sui_move::DecodeError::TypeTagMismatch { .. }); +} + +fn require_store(_: &T) {} +fn require_copy(_: &T) {} + +#[test] +fn type_abilities_are_respected() { + let boxed = bounded::Boxed:: { value: 5 }; + require_store(&boxed); + require_store(&boxed.value); + require_copy(&boxed.value); + + let nested = vault::Vault { + id: sui_move::types::UID { + id: sui_move::types::ID { + bytes: vec![9u8; 32], + }, + }, + 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"), + } + + let uid = sui_move::types::UID { + id: sui_move::types::ID { + bytes: vec![0u8; 32], + }, + }; + let table = sui_move::containers::Table:: { + id: uid.clone(), + 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, uid); + + let df = sui_move::containers::DynamicField:: { + id: uid.clone(), + 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.clone(), + name: 1, + value: 2, + }; + let dof_bytes = dof.to_bcs().unwrap(); + let decoded_dof = + sui_move::containers::DynamicObjectField::::from_bcs(&dof_bytes).unwrap(); + assert_eq!(decoded_dof.name, 1); +}