From 92942eedf96f71a10882adda68d6067f38e160ab Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Tue, 30 Dec 2025 05:06:51 +0200 Subject: [PATCH 01/11] add the #[contract] macro Signed-off-by: Valentyn Faychuk --- contract_samples/rust/Cargo.lock | 45 ++++++++++++++ contract_samples/rust/Cargo.toml | 41 +------------ contract_samples/rust/build_and_validate.sh | 31 +++++----- .../rust/examples/counter_macro.rs | 39 ++++++++++++ contract_samples/rust/sdk-macros/Cargo.toml | 12 ++++ contract_samples/rust/sdk-macros/src/lib.rs | 61 +++++++++++++++++++ contract_samples/rust/sdk/Cargo.toml | 38 ++++++++++++ .../rust/{ => sdk}/src/context.rs | 0 .../rust/{ => sdk}/src/encoding.rs | 0 contract_samples/rust/{ => sdk}/src/lib.rs | 1 + .../rust/{ => sdk}/src/storage.rs | 0 11 files changed, 215 insertions(+), 53 deletions(-) create mode 100644 contract_samples/rust/examples/counter_macro.rs create mode 100644 contract_samples/rust/sdk-macros/Cargo.toml create mode 100644 contract_samples/rust/sdk-macros/src/lib.rs create mode 100644 contract_samples/rust/sdk/Cargo.toml rename contract_samples/rust/{ => sdk}/src/context.rs (100%) rename contract_samples/rust/{ => sdk}/src/encoding.rs (100%) rename contract_samples/rust/{ => sdk}/src/lib.rs (98%) rename contract_samples/rust/{ => sdk}/src/storage.rs (100%) diff --git a/contract_samples/rust/Cargo.lock b/contract_samples/rust/Cargo.lock index 44631dd..1aad6c8 100644 --- a/contract_samples/rust/Cargo.lock +++ b/contract_samples/rust/Cargo.lock @@ -6,9 +6,19 @@ version = 4 name = "amadeus-sdk" version = "0.1.0" dependencies = [ + "amadeus-sdk-macros", "dlmalloc", ] +[[package]] +name = "amadeus-sdk-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -32,6 +42,41 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +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 = "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 = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/contract_samples/rust/Cargo.toml b/contract_samples/rust/Cargo.toml index 87e94fe..351d8ca 100644 --- a/contract_samples/rust/Cargo.toml +++ b/contract_samples/rust/Cargo.toml @@ -1,17 +1,6 @@ -[package] -name = "amadeus-sdk" -version = "0.1.0" -edition = "2021" -authors = ["Amadeus Team"] -description = "Rust SDK for writing Amadeus smart contracts" -license = "MIT" -repository = "https://github.com/amadeusprotocol/node" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -dlmalloc = { version = "0.2", features = ["global"] } +[workspace] +resolver = "3" +members = ["sdk", "sdk-macros"] [profile.release] opt-level = "z" @@ -25,27 +14,3 @@ opt-level = "z" [profile.dev] panic = "abort" - -# WASM-specific build config -[package.metadata.wasm-pack.profile.release] -wasm-opt = false - -[[example]] -name = "counter" -path = "examples/counter.rs" -crate-type = ["cdylib"] - -[[example]] -name = "deposit" -path = "examples/deposit.rs" -crate-type = ["cdylib"] - -[[example]] -name = "coin" -path = "examples/coin.rs" -crate-type = ["cdylib"] - -[[example]] -name = "nft" -path = "examples/nft.rs" -crate-type = ["cdylib"] diff --git a/contract_samples/rust/build_and_validate.sh b/contract_samples/rust/build_and_validate.sh index 0654bc1..952363f 100755 --- a/contract_samples/rust/build_and_validate.sh +++ b/contract_samples/rust/build_and_validate.sh @@ -1,19 +1,20 @@ #!/bin/bash - set -e -# Build each example -cargo build --example counter --target wasm32-unknown-unknown --release -cargo build --example deposit --target wasm32-unknown-unknown --release -cargo build --example coin --target wasm32-unknown-unknown --release -cargo build --example nft --target wasm32-unknown-unknown --release +cargo build -p amadeus-sdk --example counter --target wasm32-unknown-unknown --release +cargo build -p amadeus-sdk --example counter_macro --target wasm32-unknown-unknown --release +cargo build -p amadeus-sdk --example deposit --target wasm32-unknown-unknown --release +cargo build -p amadeus-sdk --example coin --target wasm32-unknown-unknown --release +cargo build -p amadeus-sdk --example nft --target wasm32-unknown-unknown --release + +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/counter.wasm -o target/wasm32-unknown-unknown/release/examples/counter_opt.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/counter_macro.wasm -o target/wasm32-unknown-unknown/release/examples/counter_macro_opt.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/deposit.wasm -o target/wasm32-unknown-unknown/release/examples/deposit_opt.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/coin.wasm -o target/wasm32-unknown-unknown/release/examples/coin_opt.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/nft.wasm -o target/wasm32-unknown-unknown/release/examples/nft_opt.wasm -# Validate each contract -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/counter.wasm https://mainnet-rpc.ama.one/api/contract/validate -echo "" -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/deposit.wasm https://mainnet-rpc.ama.one/api/contract/validate -echo "" -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/coin.wasm https://mainnet-rpc.ama.one/api/contract/validate -echo "" -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/nft.wasm https://mainnet-rpc.ama.one/api/contract/validate -echo "" +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/counter_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/counter_macro_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/deposit_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/coin_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/nft_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate diff --git a/contract_samples/rust/examples/counter_macro.rs b/contract_samples/rust/examples/counter_macro.rs new file mode 100644 index 0000000..32120d5 --- /dev/null +++ b/contract_samples/rust/examples/counter_macro.rs @@ -0,0 +1,39 @@ +#![no_std] +#![no_main] +extern crate alloc; +use amadeus_sdk::*; +use alloc::string::String; +use alloc::vec::Vec; + +#[no_mangle] +pub extern "C" fn init() { + log("Init called during deployment of contract"); + kv_put("inited", "true"); +} + +#[contract] +fn get() -> i128 { + kv_get("the_counter").unwrap_or(0) +} + +#[contract] +fn increment(amount: Vec) -> String { + kv_increment("the_counter", amount) +} + +#[contract] +fn increment_another_counter(contract: Vec) -> Vec { + let incr_by = 3i64; + log("increment_another_counter"); + call!(contract.as_slice(), "increment", [incr_by]) +} + +#[contract] +fn set_message(msg: String) { + kv_put("message", msg); +} + +#[contract] +fn add_value(key: Vec, value: Vec) { + kv_put(key, value); +} diff --git a/contract_samples/rust/sdk-macros/Cargo.toml b/contract_samples/rust/sdk-macros/Cargo.toml new file mode 100644 index 0000000..3de694e --- /dev/null +++ b/contract_samples/rust/sdk-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "amadeus-sdk-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" diff --git a/contract_samples/rust/sdk-macros/src/lib.rs b/contract_samples/rust/sdk-macros/src/lib.rs new file mode 100644 index 0000000..0fdb661 --- /dev/null +++ b/contract_samples/rust/sdk-macros/src/lib.rs @@ -0,0 +1,61 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, FnArg, ItemFn, ReturnType, Type}; + +#[proc_macro_attribute] +pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + let vis = &input.vis; + let fn_name = &input.sig.ident; + let impl_fn_name = syn::Ident::new(&format!("{}_impl", fn_name), fn_name.span()); + let inputs = &input.sig.inputs; + let output = &input.sig.output; + let block = &input.block; + let attrs = &input.attrs; + let has_return = !matches!(output, ReturnType::Default); + + let mut param_count = 0; + let mut wrapper_params = quote!{}; + let mut deserializations = quote!{}; + let mut call_args = quote!{}; + + for arg in inputs.iter() { + if let FnArg::Typed(pat_type) = arg { + let param_name = &pat_type.pat; + let ptr_name = syn::Ident::new(&format!("arg{}_ptr", param_count), fn_name.span()); + + let deserialize_fn = match &*pat_type.ty { + Type::Path(tp) if quote!(#tp).to_string().contains("String") => quote!(read_string), + _ => quote!(read_bytes), + }; + + if param_count > 0 { + wrapper_params.extend(quote!(, #ptr_name: i32)); + call_args.extend(quote!(, #param_name)); + } else { + wrapper_params.extend(quote!(#ptr_name: i32)); + call_args.extend(quote!(#param_name)); + } + + deserializations.extend(quote! { let #param_name = #deserialize_fn(#ptr_name); }); + param_count += 1; + } + } + + let wrapper_sig = if param_count == 0 { + quote!(#[no_mangle] pub extern "C" fn #fn_name()) + } else { + quote!(#[no_mangle] pub extern "C" fn #fn_name(#wrapper_params)) + }; + + let wrapper_call = if has_return { + quote!(ret(#impl_fn_name(#call_args));) + } else { + quote!(#impl_fn_name(#call_args);) + }; + + TokenStream::from(quote! { + #wrapper_sig { #deserializations #wrapper_call } + #(#attrs)* #vis fn #impl_fn_name(#inputs) #output #block + }) +} diff --git a/contract_samples/rust/sdk/Cargo.toml b/contract_samples/rust/sdk/Cargo.toml new file mode 100644 index 0000000..fc6e917 --- /dev/null +++ b/contract_samples/rust/sdk/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "amadeus-sdk" +version = "0.1.0" +edition = "2021" +authors = ["Amadeus Team"] +description = "Rust SDK for writing Amadeus smart contracts" +license = "MIT" +repository = "https://github.com/amadeusprotocol/node" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +dlmalloc = { version = "0.2", features = ["global"] } +amadeus-sdk-macros = { path = "../sdk-macros" } + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[[example]] +name = "counter" +path = "../examples/counter.rs" + +[[example]] +name = "deposit" +path = "../examples/deposit.rs" + +[[example]] +name = "coin" +path = "../examples/coin.rs" + +[[example]] +name = "nft" +path = "../examples/nft.rs" + +[[example]] +name = "counter_macro" +path = "../examples/counter_macro.rs" diff --git a/contract_samples/rust/src/context.rs b/contract_samples/rust/sdk/src/context.rs similarity index 100% rename from contract_samples/rust/src/context.rs rename to contract_samples/rust/sdk/src/context.rs diff --git a/contract_samples/rust/src/encoding.rs b/contract_samples/rust/sdk/src/encoding.rs similarity index 100% rename from contract_samples/rust/src/encoding.rs rename to contract_samples/rust/sdk/src/encoding.rs diff --git a/contract_samples/rust/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs similarity index 98% rename from contract_samples/rust/src/lib.rs rename to contract_samples/rust/sdk/src/lib.rs index c6b6f69..16af869 100644 --- a/contract_samples/rust/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -8,6 +8,7 @@ pub mod encoding; pub use context::*; pub use storage::*; pub use encoding::*; +pub use amadeus_sdk_macros::contract; use core::panic::PanicInfo; diff --git a/contract_samples/rust/src/storage.rs b/contract_samples/rust/sdk/src/storage.rs similarity index 100% rename from contract_samples/rust/src/storage.rs rename to contract_samples/rust/sdk/src/storage.rs From fbcf6aa38a1d6df8e3ce1e680796760827df4ed8 Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Tue, 30 Dec 2025 05:36:28 +0200 Subject: [PATCH 02/11] cleanup examples Signed-off-by: Valentyn Faychuk --- contract_samples/rust/README.md | 8 ---- contract_samples/rust/build_and_validate.sh | 3 -- contract_samples/rust/examples/coin.rs | 32 ++++--------- contract_samples/rust/examples/counter.rs | 22 ++++----- .../rust/examples/counter_macro.rs | 39 ---------------- contract_samples/rust/examples/deposit.rs | 46 +++++++------------ contract_samples/rust/examples/nft.rs | 26 ++++------- contract_samples/rust/sdk/Cargo.toml | 4 -- 8 files changed, 44 insertions(+), 136 deletions(-) delete mode 100644 contract_samples/rust/examples/counter_macro.rs diff --git a/contract_samples/rust/README.md b/contract_samples/rust/README.md index 5030eb6..d7b00ae 100644 --- a/contract_samples/rust/README.md +++ b/contract_samples/rust/README.md @@ -26,14 +26,6 @@ cargo install amadeus-cli To build the wasm smart contracts, simply run the `./build_and_validate.sh`. The artifacts will be placed in `target/wasm32-unknown-unknown/release/examples`. -Optionally you can optimize the resulting wasm contracts. - -```bash -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/counter.wasm -o counter.wasm -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/deposit.wasm -o deposit.wasm -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/coin.wasm -o coin.wasm -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/nft.wasm -o nft.wasm -``` ### Testing diff --git a/contract_samples/rust/build_and_validate.sh b/contract_samples/rust/build_and_validate.sh index 952363f..1bb0fdb 100755 --- a/contract_samples/rust/build_and_validate.sh +++ b/contract_samples/rust/build_and_validate.sh @@ -2,19 +2,16 @@ set -e cargo build -p amadeus-sdk --example counter --target wasm32-unknown-unknown --release -cargo build -p amadeus-sdk --example counter_macro --target wasm32-unknown-unknown --release cargo build -p amadeus-sdk --example deposit --target wasm32-unknown-unknown --release cargo build -p amadeus-sdk --example coin --target wasm32-unknown-unknown --release cargo build -p amadeus-sdk --example nft --target wasm32-unknown-unknown --release wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/counter.wasm -o target/wasm32-unknown-unknown/release/examples/counter_opt.wasm -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/counter_macro.wasm -o target/wasm32-unknown-unknown/release/examples/counter_macro_opt.wasm wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/deposit.wasm -o target/wasm32-unknown-unknown/release/examples/deposit_opt.wasm wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/coin.wasm -o target/wasm32-unknown-unknown/release/examples/coin_opt.wasm wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/nft.wasm -o target/wasm32-unknown-unknown/release/examples/nft_opt.wasm curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/counter_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/counter_macro_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/deposit_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/coin_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/nft_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate diff --git a/contract_samples/rust/examples/coin.rs b/contract_samples/rust/examples/coin.rs index 3926b94..730862b 100644 --- a/contract_samples/rust/examples/coin.rs +++ b/contract_samples/rust/examples/coin.rs @@ -2,7 +2,7 @@ #![no_main] extern crate alloc; use amadeus_sdk::*; -use alloc::{vec::Vec}; +use alloc::{vec::Vec, string::String}; fn vault_key(symbol: &Vec) -> Vec { b!("vault:", account_caller(), ":", symbol) @@ -11,43 +11,31 @@ fn vault_key(symbol: &Vec) -> Vec { #[no_mangle] pub extern "C" fn init() { log("init called"); - let mint_a_billion = encoding::coin_raw(1_000_000_000, 9); call!("Coin", "create_and_mint", [ "USDFAKE", mint_a_billion, 9, "false", "false", "false" ]); } -#[no_mangle] -pub extern "C" fn deposit() { +#[contract] +fn deposit() -> String { log("deposit called"); - let (has_attachment, (symbol, amount)) = get_attachment(); amadeus_sdk::assert!(has_attachment, "deposit has no attachment"); - let amount_i128 = i128::from_bytes(amount); amadeus_sdk::assert!(amount_i128 > 100, "deposit amount less than 100"); - - let total_vault_deposited = kv_increment(vault_key(&symbol), amount_i128); - ret(total_vault_deposited); + kv_increment(vault_key(&symbol), amount_i128) } -#[no_mangle] -pub extern "C" fn withdraw(symbol_ptr: i32, amount_ptr: i32) { +#[contract] +fn withdraw(symbol: Vec, amount: Vec) -> i128 { log("withdraw called"); - - let withdraw_symbol = read_bytes(symbol_ptr); - let withdraw_amount = read_bytes(amount_ptr); - let withdraw_amount_int = encoding::bytes_to_i128(&withdraw_amount); + let withdraw_amount_int = encoding::bytes_to_i128(&amount); amadeus_sdk::assert!(withdraw_amount_int > 0, "amount lte 0"); - - let key = vault_key(&withdraw_symbol); + let key = vault_key(&symbol); let vault_balance: i128 = kv_get(&key).unwrap_or(0); amadeus_sdk::assert!(vault_balance >= withdraw_amount_int, "insufficient funds"); - kv_increment(key, -withdraw_amount_int); - - call!("Coin", "transfer", [account_caller(), withdraw_amount, withdraw_symbol]); - - ret(vault_balance - withdraw_amount_int); + call!("Coin", "transfer", [account_caller(), amount, symbol]); + vault_balance - withdraw_amount_int } diff --git a/contract_samples/rust/examples/counter.rs b/contract_samples/rust/examples/counter.rs index 27e6a84..0eadea1 100644 --- a/contract_samples/rust/examples/counter.rs +++ b/contract_samples/rust/examples/counter.rs @@ -2,6 +2,7 @@ #![no_main] extern crate alloc; use amadeus_sdk::*; +use alloc::{vec::Vec, string::String}; #[no_mangle] pub extern "C" fn init() { @@ -9,22 +10,19 @@ pub extern "C" fn init() { kv_put("inited", "true"); } -#[no_mangle] -pub extern "C" fn get() { - ret(kv_get("the_counter").unwrap_or(0)); +#[contract] +fn get() -> i128 { + kv_get("the_counter").unwrap_or(0) } -#[no_mangle] -pub extern "C" fn increment(amount_ptr: i32) { - let amount = read_bytes(amount_ptr); - let new_counter = kv_increment("the_counter", amount); - ret(new_counter); +#[contract] +fn increment(amount: Vec) -> String { + kv_increment("the_counter", amount) } -#[no_mangle] -pub extern "C" fn increment_another_counter(contract_ptr: i32) { - let contract = read_bytes(contract_ptr); +#[contract] +fn increment_another_counter(contract: Vec) -> Vec { let incr_by = 3i64; log("increment_another_counter"); - ret(call!(contract.as_slice(), "increment", [incr_by])); + call!(contract.as_slice(), "increment", [incr_by]) } diff --git a/contract_samples/rust/examples/counter_macro.rs b/contract_samples/rust/examples/counter_macro.rs deleted file mode 100644 index 32120d5..0000000 --- a/contract_samples/rust/examples/counter_macro.rs +++ /dev/null @@ -1,39 +0,0 @@ -#![no_std] -#![no_main] -extern crate alloc; -use amadeus_sdk::*; -use alloc::string::String; -use alloc::vec::Vec; - -#[no_mangle] -pub extern "C" fn init() { - log("Init called during deployment of contract"); - kv_put("inited", "true"); -} - -#[contract] -fn get() -> i128 { - kv_get("the_counter").unwrap_or(0) -} - -#[contract] -fn increment(amount: Vec) -> String { - kv_increment("the_counter", amount) -} - -#[contract] -fn increment_another_counter(contract: Vec) -> Vec { - let incr_by = 3i64; - log("increment_another_counter"); - call!(contract.as_slice(), "increment", [incr_by]) -} - -#[contract] -fn set_message(msg: String) { - kv_put("message", msg); -} - -#[contract] -fn add_value(key: Vec, value: Vec) { - kv_put(key, value); -} diff --git a/contract_samples/rust/examples/deposit.rs b/contract_samples/rust/examples/deposit.rs index ac4dde1..35876be 100644 --- a/contract_samples/rust/examples/deposit.rs +++ b/contract_samples/rust/examples/deposit.rs @@ -2,56 +2,42 @@ #![no_main] extern crate alloc; use amadeus_sdk::*; -use alloc::{vec::Vec}; +use alloc::{vec::Vec, string::String}; fn vault_key(symbol: &Vec) -> Vec { b!("vault:", account_caller(), ":", symbol) } -#[no_mangle] -pub extern "C" fn balance(symbol_ptr: i32) { - let key = vault_key(&read_bytes(symbol_ptr)); - ret(kv_get(key).unwrap_or(0)); +#[contract] +fn balance(symbol: Vec) -> i128 { + kv_get(vault_key(&symbol)).unwrap_or(0) } -#[no_mangle] -pub extern "C" fn deposit() { +#[contract] +fn deposit() -> String { log("deposit called"); - let (has_attachment, (symbol, amount)) = get_attachment(); amadeus_sdk::assert!(has_attachment, "deposit has no attachment"); - let amount_i128 = i128::from_bytes(amount); amadeus_sdk::assert!(amount_i128 > 100, "deposit amount less than 100"); - - let total_vault_deposited = kv_increment(vault_key(&symbol), amount_i128); - ret(total_vault_deposited); + kv_increment(vault_key(&symbol), amount_i128) } -#[no_mangle] -pub extern "C" fn withdraw(symbol_ptr: i32, amount_ptr: i32) { +#[contract] +fn withdraw(symbol: Vec, amount: Vec) -> i128 { log("withdraw called"); - - let withdraw_symbol = read_bytes(symbol_ptr); - let withdraw_amount = read_bytes(amount_ptr); - let withdraw_amount_int = encoding::bytes_to_i128(&withdraw_amount); + let withdraw_amount_int = encoding::bytes_to_i128(&amount); amadeus_sdk::assert!(withdraw_amount_int > 0, "amount lte 0"); - - let key = vault_key(&withdraw_symbol); + let key = vault_key(&symbol); let vault_balance: i128 = kv_get(&key).unwrap_or(0); amadeus_sdk::assert!(vault_balance >= withdraw_amount_int, "insufficient funds"); - kv_increment(key, -withdraw_amount_int); - - call!("Coin", "transfer", [account_caller(), withdraw_amount, withdraw_symbol]); - - ret(vault_balance - withdraw_amount_int); + call!("Coin", "transfer", [account_caller(), amount, symbol]); + vault_balance - withdraw_amount_int } -#[no_mangle] -pub extern "C" fn burn(symbol_ptr: i32, amount_ptr: i32) { - let symbol = read_string(symbol_ptr); - let amount = read_bytes(amount_ptr); +#[contract] +fn burn(symbol: String, amount: Vec) -> Vec { log("burn"); - ret(call!("Coin", "transfer", [BURN_ADDRESS, amount, symbol])); + call!("Coin", "transfer", [BURN_ADDRESS, amount, symbol]) } diff --git a/contract_samples/rust/examples/nft.rs b/contract_samples/rust/examples/nft.rs index ae35a85..7fd2cb9 100644 --- a/contract_samples/rust/examples/nft.rs +++ b/contract_samples/rust/examples/nft.rs @@ -2,17 +2,16 @@ #![no_main] extern crate alloc; use amadeus_sdk::*; +use alloc::{vec::Vec, string::String}; #[no_mangle] pub extern "C" fn init() { call!("Nft", "create_collection", ["AGENTIC", "false"]); } -#[no_mangle] -pub extern "C" fn view_nft(collection_ptr: i32, token_ptr: i32) { - let collection = read_string(collection_ptr); - let token = read_bytes(token_ptr); - let url = match (collection.as_str(), token.as_slice()) { +#[contract] +fn view_nft(collection: String, token: Vec) -> &'static str { + match (collection.as_str(), token.as_slice()) { ("AGENTIC", b"1") => "https://ipfs.io/ipfs/bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4/welcome-to-IPFS.jpg", ("AGENTIC", b"2") => "https://ipfs.io/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/readme", ("AGENTIC", b"3") => "https://ipfs.io/ipfs/QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB", @@ -20,18 +19,17 @@ pub extern "C" fn view_nft(collection_ptr: i32, token_ptr: i32) { ("AGENTIC", b"5") => "https://ipfs.io/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN", ("AGENTIC", b"6") => "https://ipfs.io/ipfs/QmTn4KLRkKPDkB3KpJWGXZHPPh5dFnKqNcPjX4ZcbPvKwv", _ => "https://ipfs.io/ipfs/bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4/welcome-to-IPFS.jpg", - }; - ret(url); + } } -#[no_mangle] -pub extern "C" fn claim() { +#[contract] +fn claim() -> i64 { log("claiming"); call!("Nft", "mint", [account_caller(), 1, "AGENTIC", "2"]); call!("Nft", "mint", [account_caller(), 1, "AGENTIC", "2"]); let random_token = roll_dice(); call!("Nft", "mint", [account_caller(), 1, "AGENTIC", random_token]); - ret(random_token); + random_token } static mut PRNG_STATE: u64 = 0; @@ -41,25 +39,17 @@ fn roll_dice() -> i64 { unsafe { if !PRNG_INIT { let s = seed(); - - // (Using FNV-1a hash algorithm for decent distribution) let mut h: u64 = 0xcbf29ce484222325; for &byte in s.iter() { h = h ^ (byte as u64); h = h.wrapping_mul(0x100000001b3); } - PRNG_STATE = h; PRNG_INIT = true; } - - // 2. "Increment" the seed (Step the LCG) - // Constants from Musl Libc / Knuth - // state = state * 6364136223846793005 + 1442695040888963407 PRNG_STATE = PRNG_STATE .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); - let result = (PRNG_STATE >> 32) as i64; (result.abs() % 6) + 1 } diff --git a/contract_samples/rust/sdk/Cargo.toml b/contract_samples/rust/sdk/Cargo.toml index fc6e917..3e547f8 100644 --- a/contract_samples/rust/sdk/Cargo.toml +++ b/contract_samples/rust/sdk/Cargo.toml @@ -32,7 +32,3 @@ path = "../examples/coin.rs" [[example]] name = "nft" path = "../examples/nft.rs" - -[[example]] -name = "counter_macro" -path = "../examples/counter_macro.rs" From 59e9fa585c04ab167d8a77be8580973a2f9d2db8 Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Tue, 30 Dec 2025 06:52:15 +0200 Subject: [PATCH 03/11] update docs and groom api Signed-off-by: Valentyn Faychuk --- contract_samples/rust/README.md | 5 ++--- contract_samples/rust/build_and_validate.sh | 19 +++++++++++-------- ex/lib/api/api_tx.ex | 6 +++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/contract_samples/rust/README.md b/contract_samples/rust/README.md index d7b00ae..4fef5bb 100644 --- a/contract_samples/rust/README.md +++ b/contract_samples/rust/README.md @@ -41,8 +41,7 @@ ama get-pk --sk wallet.sk ama gen-sk counter.sk export COUNTER_PK=$(ama get-pk --sk counter.sk) ama tx --sk wallet.sk --url https://testnet-rpc.ama.one Coin transfer '[{"b58": "'$COUNTER_PK'"}, "2000000000", "AMA"]' -ama deploy-tx --sk counter.sk counter.wasm --url https://testnet-rpc.ama.one -ama tx --sk counter.sk --url https://testnet-rpc.ama.one $COUNTER_PK init '[]' +ama deploy-tx --sk counter.sk counter.wasm init '[]' --url https://testnet-rpc.ama.one curl "https://testnet-rpc.ama.one/api/contract/view/$COUNTER_PK/get" ama tx --sk wallet.sk --url https://testnet-rpc.ama.one $COUNTER_PK increment '["5"]' curl "https://testnet-rpc.ama.one/api/contract/view/$COUNTER_PK/get" @@ -58,7 +57,7 @@ ama tx --sk wallet.sk $DEPOSIT_PK balance '["AMA"]' --url https://testnet-rpc.am ama gen-sk coin.sk export COIN_PK=$(ama get-pk --sk coin.sk) ama tx --sk wallet.sk Coin transfer '[{"b58": "'$COIN_PK'"}, "2000000000", "AMA"]' --url https://testnet-rpc.ama.one -ama deploy-tx --sk coin.sk coin.wasm --url https://testnet-rpc.ama.one +ama deploy-tx --sk coin.sk coin.wasm init --url https://testnet-rpc.ama.one ama tx --sk wallet.sk $COIN_PK deposit '[]' AMA 1500000000 --url https://testnet-rpc.ama.one ama tx --sk wallet.sk $COIN_PK withdraw '["AMA", "500000000"]' --url https://testnet-rpc.ama.one ama tx --sk wallet.sk $COIN_PK withdraw '["AMA", "1000000000"]' --url https://testnet-rpc.ama.one diff --git a/contract_samples/rust/build_and_validate.sh b/contract_samples/rust/build_and_validate.sh index 1bb0fdb..efa51fe 100755 --- a/contract_samples/rust/build_and_validate.sh +++ b/contract_samples/rust/build_and_validate.sh @@ -1,17 +1,20 @@ #!/bin/bash set -e +script_dir=$(dirname "$0") +cd "$script_dir" + cargo build -p amadeus-sdk --example counter --target wasm32-unknown-unknown --release cargo build -p amadeus-sdk --example deposit --target wasm32-unknown-unknown --release cargo build -p amadeus-sdk --example coin --target wasm32-unknown-unknown --release cargo build -p amadeus-sdk --example nft --target wasm32-unknown-unknown --release -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/counter.wasm -o target/wasm32-unknown-unknown/release/examples/counter_opt.wasm -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/deposit.wasm -o target/wasm32-unknown-unknown/release/examples/deposit_opt.wasm -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/coin.wasm -o target/wasm32-unknown-unknown/release/examples/coin_opt.wasm -wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/nft.wasm -o target/wasm32-unknown-unknown/release/examples/nft_opt.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/counter.wasm -o counter.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/deposit.wasm -o deposit.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/coin.wasm -o coin.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/nft.wasm -o nft.wasm -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/counter_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/deposit_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/coin_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate -curl -X POST -H "Content-Type: application/octet-stream" --data-binary @target/wasm32-unknown-unknown/release/examples/nft_opt.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @counter.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @deposit.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @coin.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @nft.wasm https://mainnet-rpc.ama.one/api/contract/validate diff --git a/ex/lib/api/api_tx.ex b/ex/lib/api/api_tx.ex index f659c89..5d2c459 100644 --- a/ex/lib/api/api_tx.ex +++ b/ex/lib/api/api_tx.ex @@ -173,8 +173,10 @@ defmodule API.TX do tx = put_in(tx, [:tx, :signer], Base58.encode(tx.tx.signer)) action = TX.action(tx) - action = if !BlsEx.validate_public_key(action.contract) do action else + action = if is_binary(action.contract) and byte_size(action.contract) == 48 do Map.put(action, :contract, Base58.encode(action.contract)) + else + action end args = Enum.map(action.args, fn(arg)-> cond do @@ -195,8 +197,6 @@ defmodule API.TX do logs = Enum.map(logs, fn(line)-> RocksDB.ascii_dump(line) end) receipt = %{success: success, result: result, logs: logs, exec_used: exec_used} - #TODO: remove result later - tx = Map.put(tx, :result, %{error: result}) tx = Map.put(tx, :receipt, receipt) if !Map.has_key?(tx, :metadata) do tx else From c1c3617e934d85ef6182590d76a785d3001f10cd Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Mon, 5 Jan 2026 02:48:39 +0200 Subject: [PATCH 04/11] add auto state management Signed-off-by: Valentyn Faychuk --- contract_samples/rust/.gitignore | 1 + .../rust/examples/counter_macros.rs | 39 +++++ contract_samples/rust/sdk-macros/Cargo.toml | 2 +- contract_samples/rust/sdk-macros/src/lib.rs | 162 +++++++++++++++--- contract_samples/rust/sdk/Cargo.toml | 4 + contract_samples/rust/sdk/src/lib.rs | 45 ++++- 6 files changed, 226 insertions(+), 27 deletions(-) create mode 100644 contract_samples/rust/examples/counter_macros.rs diff --git a/contract_samples/rust/.gitignore b/contract_samples/rust/.gitignore index 350b825..500c7da 100644 --- a/contract_samples/rust/.gitignore +++ b/contract_samples/rust/.gitignore @@ -1,2 +1,3 @@ /target/ *.wasm +*.sk diff --git a/contract_samples/rust/examples/counter_macros.rs b/contract_samples/rust/examples/counter_macros.rs new file mode 100644 index 0000000..c243dd4 --- /dev/null +++ b/contract_samples/rust/examples/counter_macros.rs @@ -0,0 +1,39 @@ +#![no_std] +#![no_main] + +extern crate alloc; +use alloc::vec::Vec; +use alloc::string::ToString; +use amadeus_sdk::*; + +#[derive(Contract, Default)] +struct SimpleCounter { + count: LazyCell, + owner: LazyCell>, +} + +#[contract] +impl SimpleCounter { + pub fn init(&mut self) { + self.owner.set(account_current()); + self.count.set(0); + log("Counter initialized"); + } + + pub fn increment(&mut self, amount: Vec) { + let amount_val = i128::from_bytes(amount); + let current = self.count.get(); + self.count.set(current + amount_val); + } + + pub fn get(&self) -> Vec { + self.count.get().to_string().into_bytes() + } + + pub fn reset(&mut self) { + let caller = account_caller(); + let owner = self.owner.get(); + amadeus_sdk::assert!(caller == owner, "unauthorized"); + self.count.set(0); + } +} diff --git a/contract_samples/rust/sdk-macros/Cargo.toml b/contract_samples/rust/sdk-macros/Cargo.toml index 3de694e..c79fd14 100644 --- a/contract_samples/rust/sdk-macros/Cargo.toml +++ b/contract_samples/rust/sdk-macros/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" proc-macro = true [dependencies] -syn = { version = "2.0", features = ["full"] } +syn = { version = "2.0", features = ["full", "extra-traits"] } quote = "1.0" proc-macro2 = "1.0" diff --git a/contract_samples/rust/sdk-macros/src/lib.rs b/contract_samples/rust/sdk-macros/src/lib.rs index 0fdb661..c7e162d 100644 --- a/contract_samples/rust/sdk-macros/src/lib.rs +++ b/contract_samples/rust/sdk-macros/src/lib.rs @@ -1,61 +1,173 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, FnArg, ItemFn, ReturnType, Type}; +use syn::{parse_macro_input, DeriveInput, Data, Fields, ItemImpl, ItemFn, ImplItem, FnArg, ReturnType, Type}; + +#[proc_macro_derive(Contract)] +pub fn derive_contract(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let Data::Struct(data) = &input.data else { return TokenStream::new() }; + let Fields::Named(fields) = &data.fields else { return TokenStream::new() }; + + let init_calls = fields.named.iter().map(|f| { + let field = f.ident.as_ref().unwrap(); + let key = field.to_string(); + quote! { self.#field = LazyCell::new(b!("__state__::", #key)); } + }); + + let flush_calls = fields.named.iter().map(|f| { + let field = f.ident.as_ref().unwrap(); + quote! { self.#field.flush(); } + }); + + TokenStream::from(quote! { + impl #name { + pub fn __init_lazy_fields(&mut self) { #(#init_calls)* } + pub fn __flush_lazy_fields(&self) { #(#flush_calls)* } + } + }) +} #[proc_macro_attribute] pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as ItemFn); + if let Ok(impl_block) = syn::parse::(item.clone()) { + return handle_impl_block(impl_block); + } + if let Ok(function) = syn::parse::(item.clone()) { + return handle_function(function); + } + item +} + +fn handle_impl_block(impl_block: ItemImpl) -> TokenStream { + let self_ty = &impl_block.self_ty; + let mut methods = Vec::new(); + let mut wrappers = Vec::new(); + + for item in impl_block.items.iter() { + let ImplItem::Fn(method) = item else { continue }; + if !matches!(method.vis, syn::Visibility::Public(_)) { continue }; + + let name = &method.sig.ident; + let has_return = !matches!(method.sig.output, ReturnType::Default); + let has_self = method.sig.inputs.iter().any(|arg| matches!(arg, FnArg::Receiver(_))); + + if !has_self { + methods.push(method); + continue; + } + + let args: Vec<_> = method.sig.inputs.iter() + .filter_map(|arg| match arg { + FnArg::Typed(pat_type) => { + let param = &pat_type.pat; + let ptr = syn::Ident::new(&format!("{}_ptr", quote!(#param)), name.span()); + let deser = match &*pat_type.ty { + Type::Path(tp) if quote!(#tp).to_string().contains("String") => quote!(read_string), + _ => quote!(read_bytes), + }; + Some((quote!(#ptr: i32), quote!(let #param = #deser(#ptr);), quote!(#param))) + } + _ => None, + }) + .collect(); + + let params: Vec<_> = args.iter().map(|(p, _, _)| p).collect(); + let deserializations: Vec<_> = args.iter().map(|(_, d, _)| d).collect(); + let call_args: Vec<_> = args.iter().map(|(_, _, c)| c).collect(); + + let sig = if params.is_empty() { + quote!(#[no_mangle] pub extern "C" fn #name()) + } else { + quote!(#[no_mangle] pub extern "C" fn #name(#(#params),*)) + }; + + let call = if call_args.is_empty() { + quote!(#name()) + } else { + quote!(#name(#(#call_args),*)) + }; + + let body = if has_return { + quote! { + #(#deserializations)* + let mut state = #self_ty::default(); + state.__init_lazy_fields(); + let result = state.#call; + state.__flush_lazy_fields(); + ret(result); + } + } else { + quote! { + #(#deserializations)* + let mut state = #self_ty::default(); + state.__init_lazy_fields(); + state.#call; + state.__flush_lazy_fields(); + } + }; + + wrappers.push(quote! { #sig { #body } }); + methods.push(method); + } + + TokenStream::from(quote! { + impl #self_ty { #(#methods)* } + #(#wrappers)* + }) +} + +fn handle_function(input: ItemFn) -> TokenStream { let vis = &input.vis; - let fn_name = &input.sig.ident; - let impl_fn_name = syn::Ident::new(&format!("{}_impl", fn_name), fn_name.span()); + let name = &input.sig.ident; + let impl_name = syn::Ident::new(&format!("{}_impl", name), name.span()); let inputs = &input.sig.inputs; let output = &input.sig.output; let block = &input.block; let attrs = &input.attrs; let has_return = !matches!(output, ReturnType::Default); - let mut param_count = 0; - let mut wrapper_params = quote!{}; + let mut idx = 0; + let mut params = quote!{}; let mut deserializations = quote!{}; let mut call_args = quote!{}; for arg in inputs.iter() { if let FnArg::Typed(pat_type) = arg { - let param_name = &pat_type.pat; - let ptr_name = syn::Ident::new(&format!("arg{}_ptr", param_count), fn_name.span()); - - let deserialize_fn = match &*pat_type.ty { + let param = &pat_type.pat; + let ptr = syn::Ident::new(&format!("arg{}_ptr", idx), name.span()); + let deser = match &*pat_type.ty { Type::Path(tp) if quote!(#tp).to_string().contains("String") => quote!(read_string), _ => quote!(read_bytes), }; - if param_count > 0 { - wrapper_params.extend(quote!(, #ptr_name: i32)); - call_args.extend(quote!(, #param_name)); + if idx > 0 { + params.extend(quote!(, #ptr: i32)); + call_args.extend(quote!(, #param)); } else { - wrapper_params.extend(quote!(#ptr_name: i32)); - call_args.extend(quote!(#param_name)); + params.extend(quote!(#ptr: i32)); + call_args.extend(quote!(#param)); } - deserializations.extend(quote! { let #param_name = #deserialize_fn(#ptr_name); }); - param_count += 1; + deserializations.extend(quote! { let #param = #deser(#ptr); }); + idx += 1; } } - let wrapper_sig = if param_count == 0 { - quote!(#[no_mangle] pub extern "C" fn #fn_name()) + let sig = if idx == 0 { + quote!(#[no_mangle] pub extern "C" fn #name()) } else { - quote!(#[no_mangle] pub extern "C" fn #fn_name(#wrapper_params)) + quote!(#[no_mangle] pub extern "C" fn #name(#params)) }; - let wrapper_call = if has_return { - quote!(ret(#impl_fn_name(#call_args));) + let call = if has_return { + quote!(ret(#impl_name(#call_args));) } else { - quote!(#impl_fn_name(#call_args);) + quote!(#impl_name(#call_args);) }; TokenStream::from(quote! { - #wrapper_sig { #deserializations #wrapper_call } - #(#attrs)* #vis fn #impl_fn_name(#inputs) #output #block + #sig { #deserializations #call } + #(#attrs)* #vis fn #impl_name(#inputs) #output #block }) } diff --git a/contract_samples/rust/sdk/Cargo.toml b/contract_samples/rust/sdk/Cargo.toml index 3e547f8..2ac693a 100644 --- a/contract_samples/rust/sdk/Cargo.toml +++ b/contract_samples/rust/sdk/Cargo.toml @@ -32,3 +32,7 @@ path = "../examples/coin.rs" [[example]] name = "nft" path = "../examples/nft.rs" + +[[example]] +name = "counter_macros" +path = "../examples/counter_macros.rs" diff --git a/contract_samples/rust/sdk/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs index 16af869..5f7be42 100644 --- a/contract_samples/rust/sdk/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -8,7 +8,7 @@ pub mod encoding; pub use context::*; pub use storage::*; pub use encoding::*; -pub use amadeus_sdk_macros::contract; +pub use amadeus_sdk_macros::{contract, Contract}; use core::panic::PanicInfo; @@ -101,3 +101,46 @@ impl Payload for &alloc::vec::Vec { impl_payload_for_ints!(u8, u16, u32, u64, u128, usize); impl_payload_for_ints!(i8, i16, i32, i64, i128, isize); + +pub struct LazyCell { + key: Vec, + value: core::cell::RefCell>, + dirty: core::cell::Cell, +} + +impl Default for LazyCell { + fn default() -> Self { + Self::new(Vec::new()) + } +} + +impl LazyCell { + pub fn new(key: Vec) -> Self { + Self { + key, + value: core::cell::RefCell::new(None), + dirty: core::cell::Cell::new(false), + } + } + + pub fn flush(&self) where T: Payload + Clone { + if self.dirty.get() { + if let Some(val) = self.value.borrow().as_ref() { + kv_put(&self.key, val.clone()); + } + } + } + + pub fn get(&self) -> T where T: FromKvBytes + Default + Clone { + if self.value.borrow().is_none() { + let loaded = kv_get::(&self.key).unwrap_or_default(); + *self.value.borrow_mut() = Some(loaded); + } + self.value.borrow().as_ref().unwrap().clone() + } + + pub fn set(&self, val: T) { + *self.value.borrow_mut() = Some(val); + self.dirty.set(true); + } +} From 1c7708c16424ac7e9047defcc882fdbd264c3c6c Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Mon, 5 Jan 2026 03:27:03 +0200 Subject: [PATCH 05/11] make LazyCell implicit Signed-off-by: Valentyn Faychuk --- .../rust/examples/counter_macros.rs | 21 ++++----- contract_samples/rust/sdk-macros/src/lib.rs | 45 +++++++++++++++--- contract_samples/rust/sdk/src/lib.rs | 46 ++++++++++++++++++- 3 files changed, 92 insertions(+), 20 deletions(-) diff --git a/contract_samples/rust/examples/counter_macros.rs b/contract_samples/rust/examples/counter_macros.rs index c243dd4..97d055b 100644 --- a/contract_samples/rust/examples/counter_macros.rs +++ b/contract_samples/rust/examples/counter_macros.rs @@ -6,34 +6,31 @@ use alloc::vec::Vec; use alloc::string::ToString; use amadeus_sdk::*; -#[derive(Contract, Default)] +#[contract_state] struct SimpleCounter { - count: LazyCell, - owner: LazyCell>, + count: i128, + owner: Vec, } #[contract] impl SimpleCounter { pub fn init(&mut self) { - self.owner.set(account_current()); - self.count.set(0); + *self.owner = account_current(); + *self.count = 0; log("Counter initialized"); } pub fn increment(&mut self, amount: Vec) { - let amount_val = i128::from_bytes(amount); - let current = self.count.get(); - self.count.set(current + amount_val); + *self.count += i128::from_bytes(amount); } pub fn get(&self) -> Vec { - self.count.get().to_string().into_bytes() + (*self.count).to_string().into_bytes() } pub fn reset(&mut self) { let caller = account_caller(); - let owner = self.owner.get(); - amadeus_sdk::assert!(caller == owner, "unauthorized"); - self.count.set(0); + amadeus_sdk::assert!(*self.owner == caller, "unauthorized"); + *self.count = 0; } } diff --git a/contract_samples/rust/sdk-macros/src/lib.rs b/contract_samples/rust/sdk-macros/src/lib.rs index c7e162d..aa032a3 100644 --- a/contract_samples/rust/sdk-macros/src/lib.rs +++ b/contract_samples/rust/sdk-macros/src/lib.rs @@ -1,18 +1,33 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput, Data, Fields, ItemImpl, ItemFn, ImplItem, FnArg, ReturnType, Type}; +use syn::{parse_macro_input, ItemImpl, ItemFn, ImplItem, FnArg, ReturnType, Type, ItemStruct, Fields}; -#[proc_macro_derive(Contract)] -pub fn derive_contract(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); +#[proc_macro_attribute] +pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemStruct); let name = &input.ident; - let Data::Struct(data) = &input.data else { return TokenStream::new() }; - let Fields::Named(fields) = &data.fields else { return TokenStream::new() }; + let vis = &input.vis; + let attrs = &input.attrs; + + let Fields::Named(ref fields) = input.fields else { + return TokenStream::from(quote! { #input }); + }; + + let transformed_fields = fields.named.iter().map(|f| { + let field_name = &f.ident; + let field_vis = &f.vis; + let field_attrs = &f.attrs; + let field_ty = &f.ty; + quote! { + #(#field_attrs)* + #field_vis #field_name: LazyCell<#field_ty> + } + }); let init_calls = fields.named.iter().map(|f| { let field = f.ident.as_ref().unwrap(); let key = field.to_string(); - quote! { self.#field = LazyCell::new(b!("__state__::", #key)); } + quote! { self.#field = LazyCell::new(#key.as_bytes().to_vec()); } }); let flush_calls = fields.named.iter().map(|f| { @@ -20,7 +35,23 @@ pub fn derive_contract(input: TokenStream) -> TokenStream { quote! { self.#field.flush(); } }); + let default_fields = fields.named.iter().map(|f| { + let field_name = &f.ident; + quote! { #field_name: LazyCell::default() } + }); + TokenStream::from(quote! { + #(#attrs)* + #vis struct #name { + #(#transformed_fields),* + } + + impl Default for #name { + fn default() -> Self { + Self { #(#default_fields),* } + } + } + impl #name { pub fn __init_lazy_fields(&mut self) { #(#init_calls)* } pub fn __flush_lazy_fields(&self) { #(#flush_calls)* } diff --git a/contract_samples/rust/sdk/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs index 5f7be42..d029d94 100644 --- a/contract_samples/rust/sdk/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -8,7 +8,7 @@ pub mod encoding; pub use context::*; pub use storage::*; pub use encoding::*; -pub use amadeus_sdk_macros::{contract, Contract}; +pub use amadeus_sdk_macros::{contract, contract_state}; use core::panic::PanicInfo; @@ -108,6 +108,39 @@ pub struct LazyCell { dirty: core::cell::Cell, } +impl core::ops::Deref for LazyCell +where + T: FromKvBytes + Default + Clone +{ + type Target = T; + + fn deref(&self) -> &T { + if self.value.borrow().is_none() { + let loaded = kv_get::(&self.key).unwrap_or_default(); + *self.value.borrow_mut() = Some(loaded); + } + unsafe { + (*self.value.as_ptr()).as_ref().unwrap() + } + } +} + +impl core::ops::DerefMut for LazyCell +where + T: FromKvBytes + Default + Clone +{ + fn deref_mut(&mut self) -> &mut T { + if self.value.borrow().is_none() { + let loaded = kv_get::(&self.key).unwrap_or_default(); + *self.value.borrow_mut() = Some(loaded); + } + self.dirty.set(true); + unsafe { + (*self.value.as_ptr()).as_mut().unwrap() + } + } +} + impl Default for LazyCell { fn default() -> Self { Self::new(Vec::new()) @@ -143,4 +176,15 @@ impl LazyCell { *self.value.borrow_mut() = Some(val); self.dirty.set(true); } + + pub fn update(&self, f: F) where T: FromKvBytes + Default + Clone, F: FnOnce(T) -> T { + let current = self.get(); + let new_value = f(current); + self.set(new_value); + } + + pub fn add(&self, amount: T) where T: FromKvBytes + Default + Clone + core::ops::Add { + let current = self.get(); + self.set(current + amount); + } } From 3b6a19514097de098fe6d7d6611f229f9b767628 Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Mon, 5 Jan 2026 04:17:09 +0200 Subject: [PATCH 06/11] add contract state nesting Signed-off-by: Valentyn Faychuk --- .../rust/examples/counter_macros.rs | 19 ++++-- contract_samples/rust/sdk-macros/src/lib.rs | 62 +++++++++++++++---- contract_samples/rust/sdk/src/lib.rs | 58 ++++++++++++++--- 3 files changed, 113 insertions(+), 26 deletions(-) diff --git a/contract_samples/rust/examples/counter_macros.rs b/contract_samples/rust/examples/counter_macros.rs index 97d055b..894e2d6 100644 --- a/contract_samples/rust/examples/counter_macros.rs +++ b/contract_samples/rust/examples/counter_macros.rs @@ -7,17 +7,24 @@ use alloc::string::ToString; use amadeus_sdk::*; #[contract_state] -struct SimpleCounter { - count: i128, +struct Metadata { owner: Vec, + created_at: i128, +} + +#[contract_state] +struct Counter { + count: i128, + #[nested] + metadata: Metadata, } #[contract] -impl SimpleCounter { +impl Counter { pub fn init(&mut self) { - *self.owner = account_current(); + *self.metadata.owner = account_current(); + *self.metadata.created_at = entry_slot() as i128; *self.count = 0; - log("Counter initialized"); } pub fn increment(&mut self, amount: Vec) { @@ -30,7 +37,7 @@ impl SimpleCounter { pub fn reset(&mut self) { let caller = account_caller(); - amadeus_sdk::assert!(*self.owner == caller, "unauthorized"); + amadeus_sdk::assert!(*self.metadata.owner == caller, "unauthorized"); *self.count = 0; } } diff --git a/contract_samples/rust/sdk-macros/src/lib.rs b/contract_samples/rust/sdk-macros/src/lib.rs index aa032a3..06d6f72 100644 --- a/contract_samples/rust/sdk-macros/src/lib.rs +++ b/contract_samples/rust/sdk-macros/src/lib.rs @@ -13,31 +13,64 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { return TokenStream::from(quote! { #input }); }; + let is_nested = |f: &syn::Field| -> bool { + f.attrs.iter().any(|attr| attr.path().is_ident("nested")) + }; + let transformed_fields = fields.named.iter().map(|f| { let field_name = &f.ident; let field_vis = &f.vis; - let field_attrs = &f.attrs; let field_ty = &f.ty; - quote! { - #(#field_attrs)* - #field_vis #field_name: LazyCell<#field_ty> + let filtered_attrs: Vec<_> = f.attrs.iter() + .filter(|attr| !attr.path().is_ident("nested")) + .collect(); + + if is_nested(f) { + quote! { + #(#filtered_attrs)* + #field_vis #field_name: #field_ty + } + } else { + quote! { + #(#filtered_attrs)* + #field_vis #field_name: LazyCell<#field_ty> + } } }); let init_calls = fields.named.iter().map(|f| { let field = f.ident.as_ref().unwrap(); let key = field.to_string(); - quote! { self.#field = LazyCell::new(#key.as_bytes().to_vec()); } + + if is_nested(f) { + quote! { + let mut key = prefix.clone(); + key.extend_from_slice(#key.as_bytes()); + key.push(b':'); + self.#field.__init_lazy_fields(key); + } + } else { + quote! { + let mut key = prefix.clone(); + key.extend_from_slice(#key.as_bytes()); + self.#field = LazyCell::new(key); + } + } }); let flush_calls = fields.named.iter().map(|f| { let field = f.ident.as_ref().unwrap(); - quote! { self.#field.flush(); } + + if is_nested(f) { + quote! { self.#field.__flush_lazy_fields(); } + } else { + quote! { self.#field.flush(); } + } }); let default_fields = fields.named.iter().map(|f| { let field_name = &f.ident; - quote! { #field_name: LazyCell::default() } + quote! { #field_name: Default::default() } }); TokenStream::from(quote! { @@ -52,9 +85,14 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { } } - impl #name { - pub fn __init_lazy_fields(&mut self) { #(#init_calls)* } - pub fn __flush_lazy_fields(&self) { #(#flush_calls)* } + impl ContractState for #name { + fn __init_lazy_fields(&mut self, prefix: alloc::vec::Vec) { + #(#init_calls)* + } + + fn __flush_lazy_fields(&self) { + #(#flush_calls)* + } } }) } @@ -123,7 +161,7 @@ fn handle_impl_block(impl_block: ItemImpl) -> TokenStream { quote! { #(#deserializations)* let mut state = #self_ty::default(); - state.__init_lazy_fields(); + state.__init_lazy_fields(alloc::vec::Vec::new()); let result = state.#call; state.__flush_lazy_fields(); ret(result); @@ -132,7 +170,7 @@ fn handle_impl_block(impl_block: ItemImpl) -> TokenStream { quote! { #(#deserializations)* let mut state = #self_ty::default(); - state.__init_lazy_fields(); + state.__init_lazy_fields(alloc::vec::Vec::new()); state.#call; state.__flush_lazy_fields(); } diff --git a/contract_samples/rust/sdk/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs index d029d94..49d56ac 100644 --- a/contract_samples/rust/sdk/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -10,6 +10,11 @@ pub use storage::*; pub use encoding::*; pub use amadeus_sdk_macros::{contract, contract_state}; +pub trait ContractState { + fn __init_lazy_fields(&mut self, prefix: Vec); + fn __flush_lazy_fields(&self); +} + use core::panic::PanicInfo; use alloc::{borrow::Cow, vec::Vec, string::String, string::ToString}; @@ -141,6 +146,7 @@ where } } + impl Default for LazyCell { fn default() -> Self { Self::new(Vec::new()) @@ -156,14 +162,6 @@ impl LazyCell { } } - pub fn flush(&self) where T: Payload + Clone { - if self.dirty.get() { - if let Some(val) = self.value.borrow().as_ref() { - kv_put(&self.key, val.clone()); - } - } - } - pub fn get(&self) -> T where T: FromKvBytes + Default + Clone { if self.value.borrow().is_none() { let loaded = kv_get::(&self.key).unwrap_or_default(); @@ -188,3 +186,47 @@ impl LazyCell { self.set(current + amount); } } + +impl LazyCell { + pub fn flush(&self) { + if self.dirty.get() { + if let Some(val) = self.value.borrow().as_ref() { + kv_put(&self.key, val.clone()); + } + } + } +} + +impl LazyCell { + + pub fn with_mut(&mut self, f: F) -> R + where + F: FnOnce(&mut T) -> R + { + if self.value.borrow().is_none() { + let mut loaded = T::default(); + loaded.__init_lazy_fields(self.key.clone()); + *self.value.borrow_mut() = Some(loaded); + } + self.dirty.set(true); + unsafe { + let ptr = self.value.as_ptr(); + f((*ptr).as_mut().unwrap()) + } + } + + pub fn with(&self, f: F) -> R + where + F: FnOnce(&T) -> R + { + if self.value.borrow().is_none() { + let mut loaded = T::default(); + loaded.__init_lazy_fields(self.key.clone()); + *self.value.borrow_mut() = Some(loaded); + } + unsafe { + let ptr = self.value.as_ptr(); + f((*ptr).as_ref().unwrap()) + } + } +} From 70565661f62b4e73b6daee0d9276245d0f1aa21e Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Mon, 5 Jan 2026 05:37:19 +0200 Subject: [PATCH 07/11] add maps and flat modifier Signed-off-by: Valentyn Faychuk --- .../rust/examples/counter_macros.rs | 43 ----- contract_samples/rust/examples/showcase.rs | 100 +++++++++++ contract_samples/rust/sdk-macros/src/lib.rs | 48 +++-- contract_samples/rust/sdk/Cargo.toml | 4 +- contract_samples/rust/sdk/src/lib.rs | 164 ++++++++++++++++++ contract_samples/rust/sdk/src/storage.rs | 6 + 6 files changed, 308 insertions(+), 57 deletions(-) delete mode 100644 contract_samples/rust/examples/counter_macros.rs create mode 100644 contract_samples/rust/examples/showcase.rs diff --git a/contract_samples/rust/examples/counter_macros.rs b/contract_samples/rust/examples/counter_macros.rs deleted file mode 100644 index 894e2d6..0000000 --- a/contract_samples/rust/examples/counter_macros.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![no_std] -#![no_main] - -extern crate alloc; -use alloc::vec::Vec; -use alloc::string::ToString; -use amadeus_sdk::*; - -#[contract_state] -struct Metadata { - owner: Vec, - created_at: i128, -} - -#[contract_state] -struct Counter { - count: i128, - #[nested] - metadata: Metadata, -} - -#[contract] -impl Counter { - pub fn init(&mut self) { - *self.metadata.owner = account_current(); - *self.metadata.created_at = entry_slot() as i128; - *self.count = 0; - } - - pub fn increment(&mut self, amount: Vec) { - *self.count += i128::from_bytes(amount); - } - - pub fn get(&self) -> Vec { - (*self.count).to_string().into_bytes() - } - - pub fn reset(&mut self) { - let caller = account_caller(); - amadeus_sdk::assert!(*self.metadata.owner == caller, "unauthorized"); - *self.count = 0; - } -} diff --git a/contract_samples/rust/examples/showcase.rs b/contract_samples/rust/examples/showcase.rs new file mode 100644 index 0000000..35e6b66 --- /dev/null +++ b/contract_samples/rust/examples/showcase.rs @@ -0,0 +1,100 @@ +#![no_std] +#![no_main] + +extern crate alloc; +use alloc::vec::Vec; +use alloc::string::{String, ToString}; +use amadeus_sdk::*; + +#[contract_state] +struct Match { + #[flat] score: u16, + #[flat] opponent: String, +} + +#[contract_state] +struct TournamentInfo { + #[flat] name: String, + #[flat] prize_pool: u64, +} + +#[contract_state] +struct Leaderboard { + #[flat] total_matches: i32, + player_wins: Map, + players: MapNested>, + tournament: TournamentInfo, +} + +#[contract] +impl Leaderboard { + pub fn increment_total_matches(&mut self) { + *self.total_matches += 1; + } + + pub fn get_total_matches(&mut self) -> Vec { + (*self.total_matches).to_string().into_bytes() + } + + pub fn record_match(&mut self, player: String, match_id: Vec, score: Vec, opponent: String) { + let match_id_u64 = u64::from_bytes(match_id); + self.players.with_mut(player, |matches| { + matches.with_mut(match_id_u64, |m| { + *m.score = u16::from_bytes(score); + *m.opponent = opponent; + }); + }); + *self.total_matches += 1; + } + + pub fn get_match_score(&mut self, player: String, match_id: Vec) -> Vec { + let match_id_u64 = u64::from_bytes(match_id); + self.players.with_mut(player, |matches| { + matches.with_mut(match_id_u64, |m| { + (*m.score).to_string().into_bytes() + }) + }) + } + + pub fn get_match_opponent(&mut self, player: String, match_id: Vec) -> String { + let match_id_u64 = u64::from_bytes(match_id); + self.players.with_mut(player, |matches| { + matches.with_mut(match_id_u64, |m| { + (*m.opponent).clone() + }) + }) + } + + pub fn set_tournament_info(&mut self, name: String, prize_pool: Vec) { + *self.tournament.name = name; + *self.tournament.prize_pool = u64::from_bytes(prize_pool); + } + + pub fn get_tournament_name(&mut self) -> String { + (*self.tournament.name).clone() + } + + pub fn get_tournament_prize(&mut self) -> Vec { + (*self.tournament.prize_pool).to_string().into_bytes() + } + + pub fn record_win(&mut self, player: String) { + if let Some(wins) = self.player_wins.get_mut(&player) { + **wins += 1; + } else { + self.player_wins.insert(player, 1); + } + } + + pub fn get_player_wins(&mut self, player: String) -> Vec { + if let Some(wins) = self.player_wins.get(&player) { + (*wins).to_string().into_bytes() + } else { + "0".to_string().into_bytes() + } + } + + pub fn set_player_wins(&mut self, player: String, wins: Vec) { + self.player_wins.insert(player, u32::from_bytes(wins)); + } +} diff --git a/contract_samples/rust/sdk-macros/src/lib.rs b/contract_samples/rust/sdk-macros/src/lib.rs index 06d6f72..ed890db 100644 --- a/contract_samples/rust/sdk-macros/src/lib.rs +++ b/contract_samples/rust/sdk-macros/src/lib.rs @@ -13,8 +13,26 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { return TokenStream::from(quote! { #input }); }; - let is_nested = |f: &syn::Field| -> bool { - f.attrs.iter().any(|attr| attr.path().is_ident("nested")) + let is_flat = |f: &syn::Field| -> bool { + f.attrs.iter().any(|attr| attr.path().is_ident("flat")) + }; + + let is_map = |f: &syn::Field| -> bool { + if let Type::Path(type_path) = &f.ty { + if let Some(segment) = type_path.path.segments.first() { + return segment.ident == "Map"; + } + } + false + }; + + let is_map_nested = |f: &syn::Field| -> bool { + if let Type::Path(type_path) = &f.ty { + if let Some(segment) = type_path.path.segments.first() { + return segment.ident == "MapNested"; + } + } + false }; let transformed_fields = fields.named.iter().map(|f| { @@ -22,18 +40,18 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { let field_vis = &f.vis; let field_ty = &f.ty; let filtered_attrs: Vec<_> = f.attrs.iter() - .filter(|attr| !attr.path().is_ident("nested")) + .filter(|attr| !attr.path().is_ident("flat")) .collect(); - if is_nested(f) { + if is_flat(f) { quote! { #(#filtered_attrs)* - #field_vis #field_name: #field_ty + #field_vis #field_name: LazyCell<#field_ty> } } else { quote! { #(#filtered_attrs)* - #field_vis #field_name: LazyCell<#field_ty> + #field_vis #field_name: #field_ty } } }); @@ -42,18 +60,24 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { let field = f.ident.as_ref().unwrap(); let key = field.to_string(); - if is_nested(f) { + if is_flat(f) { + quote! { + let mut key = prefix.clone(); + key.extend_from_slice(#key.as_bytes()); + self.#field = LazyCell::new(key); + } + } else if is_map(f) || is_map_nested(f) { quote! { let mut key = prefix.clone(); key.extend_from_slice(#key.as_bytes()); - key.push(b':'); self.#field.__init_lazy_fields(key); } } else { quote! { let mut key = prefix.clone(); key.extend_from_slice(#key.as_bytes()); - self.#field = LazyCell::new(key); + key.push(b':'); + self.#field.__init_lazy_fields(key); } } }); @@ -61,10 +85,10 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { let flush_calls = fields.named.iter().map(|f| { let field = f.ident.as_ref().unwrap(); - if is_nested(f) { - quote! { self.#field.__flush_lazy_fields(); } - } else { + if is_flat(f) { quote! { self.#field.flush(); } + } else { + quote! { self.#field.__flush_lazy_fields(); } } }); diff --git a/contract_samples/rust/sdk/Cargo.toml b/contract_samples/rust/sdk/Cargo.toml index 2ac693a..541db42 100644 --- a/contract_samples/rust/sdk/Cargo.toml +++ b/contract_samples/rust/sdk/Cargo.toml @@ -34,5 +34,5 @@ name = "nft" path = "../examples/nft.rs" [[example]] -name = "counter_macros" -path = "../examples/counter_macros.rs" +name = "showcase" +path = "../examples/showcase.rs" diff --git a/contract_samples/rust/sdk/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs index 49d56ac..b0cae28 100644 --- a/contract_samples/rust/sdk/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -230,3 +230,167 @@ impl LazyCell { } } } + +use alloc::collections::BTreeMap; + +pub struct Map { + prefix: Vec, + cache: BTreeMap, LazyCell>, + _phantom: core::marker::PhantomData, +} + +impl Default for Map { + fn default() -> Self { + Self { + prefix: Vec::new(), + cache: BTreeMap::new(), + _phantom: core::marker::PhantomData, + } + } +} + +impl Map +where + K: Payload, + V: FromKvBytes + Payload + Default + Clone +{ + fn build_key(&self, key: &K) -> Vec { + let key_bytes = key.to_payload(); + b!(self.prefix.as_slice(), b":", key_bytes.as_ref()) + } + + pub fn get(&mut self, key: &K) -> Option<&LazyCell> { + let storage_key = self.build_key(key); + + if !self.cache.contains_key(&storage_key) { + if kv_exists(&storage_key) { + self.cache.insert(storage_key.clone(), LazyCell::new(storage_key.clone())); + } else { + return None; + } + } + + self.cache.get(&storage_key) + } + + pub fn get_mut(&mut self, key: &K) -> Option<&mut LazyCell> { + let storage_key = self.build_key(key); + + if !self.cache.contains_key(&storage_key) { + if kv_exists(&storage_key) { + self.cache.insert(storage_key.clone(), LazyCell::new(storage_key.clone())); + } else { + return None; + } + } + + self.cache.get_mut(&storage_key) + } + + pub fn insert(&mut self, key: K, value: V) { + let storage_key = self.build_key(&key); + let cell: LazyCell = LazyCell::new(storage_key.clone()); + cell.set(value); + self.cache.insert(storage_key, cell); + } + + pub fn remove(&mut self, key: &K) { + let storage_key = self.build_key(key); + self.cache.remove(&storage_key); + kv_delete(&storage_key); + } +} + +impl ContractState for Map +where + K: Payload, + V: FromKvBytes + Payload + Default + Clone +{ + fn __init_lazy_fields(&mut self, prefix: Vec) { + self.prefix = prefix; + } + + fn __flush_lazy_fields(&self) { + for cell in self.cache.values() { + cell.flush(); + } + } +} + +pub struct MapNested { + prefix: Vec, + cache: BTreeMap, V>, + _phantom: core::marker::PhantomData, +} + +impl Default for MapNested { + fn default() -> Self { + Self { + prefix: Vec::new(), + cache: BTreeMap::new(), + _phantom: core::marker::PhantomData, + } + } +} + +impl MapNested +where + K: Payload, + V: ContractState + Default +{ + fn build_key(&self, key: &K) -> Vec { + let key_bytes = key.to_payload(); + b!(self.prefix.as_slice(), b":", key_bytes.as_ref()) + } + + pub fn with(&mut self, key: K, f: F) -> R + where + F: FnOnce(&V) -> R + { + let storage_key = self.build_key(&key); + + if !self.cache.contains_key(&storage_key) { + let mut value = V::default(); + value.__init_lazy_fields(storage_key.clone()); + self.cache.insert(storage_key.clone(), value); + } + + f(self.cache.get(&storage_key).unwrap()) + } + + pub fn with_mut(&mut self, key: K, f: F) -> R + where + F: FnOnce(&mut V) -> R + { + let storage_key = self.build_key(&key); + + if !self.cache.contains_key(&storage_key) { + let mut value = V::default(); + value.__init_lazy_fields(storage_key.clone()); + self.cache.insert(storage_key.clone(), value); + } + + f(self.cache.get_mut(&storage_key).unwrap()) + } + + pub fn remove(&mut self, key: &K) { + let storage_key = self.build_key(key); + self.cache.remove(&storage_key); + } +} + +impl ContractState for MapNested +where + K: Payload, + V: ContractState + Default +{ + fn __init_lazy_fields(&mut self, prefix: Vec) { + self.prefix = prefix; + } + + fn __flush_lazy_fields(&self) { + for value in self.cache.values() { + value.__flush_lazy_fields(); + } + } +} diff --git a/contract_samples/rust/sdk/src/storage.rs b/contract_samples/rust/sdk/src/storage.rs index e789a4a..d1afa3b 100644 --- a/contract_samples/rust/sdk/src/storage.rs +++ b/contract_samples/rust/sdk/src/storage.rs @@ -10,6 +10,12 @@ impl FromKvBytes for Vec { fn from_bytes(data: Vec) -> Self { data } } +impl FromKvBytes for String { + fn from_bytes(data: Vec) -> Self { + String::from_utf8(data).unwrap_or_default() + } +} + macro_rules! impl_from_kv_bytes_for_int { ($type:ty, $converter:path) => { impl FromKvBytes for $type { From e8d3e9768ddc863538f4a78e3715628581a0a172 Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Mon, 5 Jan 2026 06:30:44 +0200 Subject: [PATCH 08/11] cleanup and refactor the code Signed-off-by: Valentyn Faychuk --- contract_samples/rust/build_and_validate.sh | 3 + contract_samples/rust/examples/showcase.rs | 33 ++--- contract_samples/rust/sdk-macros/src/lib.rs | 131 ++++++++++---------- contract_samples/rust/sdk/src/lib.rs | 116 +++++++---------- 4 files changed, 132 insertions(+), 151 deletions(-) diff --git a/contract_samples/rust/build_and_validate.sh b/contract_samples/rust/build_and_validate.sh index efa51fe..6ebf954 100755 --- a/contract_samples/rust/build_and_validate.sh +++ b/contract_samples/rust/build_and_validate.sh @@ -8,13 +8,16 @@ cargo build -p amadeus-sdk --example counter --target wasm32-unknown-unknown --r cargo build -p amadeus-sdk --example deposit --target wasm32-unknown-unknown --release cargo build -p amadeus-sdk --example coin --target wasm32-unknown-unknown --release cargo build -p amadeus-sdk --example nft --target wasm32-unknown-unknown --release +cargo build -p amadeus-sdk --example showcase --target wasm32-unknown-unknown --release wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/counter.wasm -o counter.wasm wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/deposit.wasm -o deposit.wasm wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/coin.wasm -o coin.wasm wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/nft.wasm -o nft.wasm +wasm-opt -Oz --enable-bulk-memory target/wasm32-unknown-unknown/release/examples/showcase.wasm -o showcase.wasm curl -X POST -H "Content-Type: application/octet-stream" --data-binary @counter.wasm https://mainnet-rpc.ama.one/api/contract/validate curl -X POST -H "Content-Type: application/octet-stream" --data-binary @deposit.wasm https://mainnet-rpc.ama.one/api/contract/validate curl -X POST -H "Content-Type: application/octet-stream" --data-binary @coin.wasm https://mainnet-rpc.ama.one/api/contract/validate curl -X POST -H "Content-Type: application/octet-stream" --data-binary @nft.wasm https://mainnet-rpc.ama.one/api/contract/validate +curl -X POST -H "Content-Type: application/octet-stream" --data-binary @showcase.wasm https://mainnet-rpc.ama.one/api/contract/validate diff --git a/contract_samples/rust/examples/showcase.rs b/contract_samples/rust/examples/showcase.rs index 35e6b66..4fb37e0 100644 --- a/contract_samples/rust/examples/showcase.rs +++ b/contract_samples/rust/examples/showcase.rs @@ -18,11 +18,15 @@ struct TournamentInfo { #[flat] prize_pool: u64, } +type PlayerWinsMap = MapFlat; +type MatchesMap = Map; +type PlayersMap = Map; + #[contract_state] struct Leaderboard { #[flat] total_matches: i32, - player_wins: Map, - players: MapNested>, + player_wins: PlayerWinsMap, + players: PlayersMap, tournament: TournamentInfo, } @@ -36,38 +40,35 @@ impl Leaderboard { (*self.total_matches).to_string().into_bytes() } - pub fn record_match(&mut self, player: String, match_id: Vec, score: Vec, opponent: String) { - let match_id_u64 = u64::from_bytes(match_id); + pub fn record_match(&mut self, player: String, match_id: u64, score: u16, opponent: String) { self.players.with_mut(player, |matches| { - matches.with_mut(match_id_u64, |m| { - *m.score = u16::from_bytes(score); + matches.with_mut(match_id, |m| { + *m.score = score; *m.opponent = opponent; }); }); *self.total_matches += 1; } - pub fn get_match_score(&mut self, player: String, match_id: Vec) -> Vec { - let match_id_u64 = u64::from_bytes(match_id); + pub fn get_match_score(&mut self, player: String, match_id: u64) -> Vec { self.players.with_mut(player, |matches| { - matches.with_mut(match_id_u64, |m| { + matches.with_mut(match_id, |m| { (*m.score).to_string().into_bytes() }) }) } - pub fn get_match_opponent(&mut self, player: String, match_id: Vec) -> String { - let match_id_u64 = u64::from_bytes(match_id); + pub fn get_match_opponent(&mut self, player: String, match_id: u64) -> String { self.players.with_mut(player, |matches| { - matches.with_mut(match_id_u64, |m| { + matches.with_mut(match_id, |m| { (*m.opponent).clone() }) }) } - pub fn set_tournament_info(&mut self, name: String, prize_pool: Vec) { + pub fn set_tournament_info(&mut self, name: String, prize_pool: u64) { *self.tournament.name = name; - *self.tournament.prize_pool = u64::from_bytes(prize_pool); + *self.tournament.prize_pool = prize_pool; } pub fn get_tournament_name(&mut self) -> String { @@ -94,7 +95,7 @@ impl Leaderboard { } } - pub fn set_player_wins(&mut self, player: String, wins: Vec) { - self.player_wins.insert(player, u32::from_bytes(wins)); + pub fn set_player_wins(&mut self, player: String, wins: u32) { + self.player_wins.insert(player, wins); } } diff --git a/contract_samples/rust/sdk-macros/src/lib.rs b/contract_samples/rust/sdk-macros/src/lib.rs index ed890db..e11a724 100644 --- a/contract_samples/rust/sdk-macros/src/lib.rs +++ b/contract_samples/rust/sdk-macros/src/lib.rs @@ -17,24 +17,6 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { f.attrs.iter().any(|attr| attr.path().is_ident("flat")) }; - let is_map = |f: &syn::Field| -> bool { - if let Type::Path(type_path) = &f.ty { - if let Some(segment) = type_path.path.segments.first() { - return segment.ident == "Map"; - } - } - false - }; - - let is_map_nested = |f: &syn::Field| -> bool { - if let Type::Path(type_path) = &f.ty { - if let Some(segment) = type_path.path.segments.first() { - return segment.ident == "MapNested"; - } - } - false - }; - let transformed_fields = fields.named.iter().map(|f| { let field_name = &f.ident; let field_vis = &f.vis; @@ -56,45 +38,34 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { } }); - let init_calls = fields.named.iter().map(|f| { + let init_fields = fields.named.iter().map(|f| { let field = f.ident.as_ref().unwrap(); + let field_ty = &f.ty; let key = field.to_string(); if is_flat(f) { quote! { - let mut key = prefix.clone(); - key.extend_from_slice(#key.as_bytes()); - self.#field = LazyCell::new(key); - } - } else if is_map(f) || is_map_nested(f) { - quote! { - let mut key = prefix.clone(); - key.extend_from_slice(#key.as_bytes()); - self.#field.__init_lazy_fields(key); + #field: { + let mut key = prefix.clone(); + key.extend_from_slice(#key.as_bytes()); + LazyCell::with_prefix(key) + } } } else { quote! { - let mut key = prefix.clone(); - key.extend_from_slice(#key.as_bytes()); - key.push(b':'); - self.#field.__init_lazy_fields(key); + #field: { + let mut key = prefix.clone(); + key.extend_from_slice(#key.as_bytes()); + key.push(b':'); + #field_ty::with_prefix(key) + } } } }); let flush_calls = fields.named.iter().map(|f| { let field = f.ident.as_ref().unwrap(); - - if is_flat(f) { - quote! { self.#field.flush(); } - } else { - quote! { self.#field.__flush_lazy_fields(); } - } - }); - - let default_fields = fields.named.iter().map(|f| { - let field_name = &f.ident; - quote! { #field_name: Default::default() } + quote! { self.#field.flush(); } }); TokenStream::from(quote! { @@ -103,18 +74,14 @@ pub fn contract_state(_attr: TokenStream, item: TokenStream) -> TokenStream { #(#transformed_fields),* } - impl Default for #name { - fn default() -> Self { - Self { #(#default_fields),* } - } - } - impl ContractState for #name { - fn __init_lazy_fields(&mut self, prefix: alloc::vec::Vec) { - #(#init_calls)* + fn with_prefix(prefix: alloc::vec::Vec) -> Self { + Self { + #(#init_fields),* + } } - fn __flush_lazy_fields(&self) { + fn flush(&self) { #(#flush_calls)* } } @@ -132,6 +99,21 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { item } +fn is_integer_type(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.first() { + let ident = &segment.ident; + return matches!( + ident.to_string().as_str(), + "i8" | "i16" | "i32" | "i64" | "i128" | + "u8" | "u16" | "u32" | "u64" | "u128" | + "isize" | "usize" + ); + } + } + false +} + fn handle_impl_block(impl_block: ItemImpl) -> TokenStream { let self_ty = &impl_block.self_ty; let mut methods = Vec::new(); @@ -154,12 +136,22 @@ fn handle_impl_block(impl_block: ItemImpl) -> TokenStream { .filter_map(|arg| match arg { FnArg::Typed(pat_type) => { let param = &pat_type.pat; + let ty = &pat_type.ty; let ptr = syn::Ident::new(&format!("{}_ptr", quote!(#param)), name.span()); - let deser = match &*pat_type.ty { - Type::Path(tp) if quote!(#tp).to_string().contains("String") => quote!(read_string), - _ => quote!(read_bytes), + + let deser = if let Type::Path(tp) = &**ty { + if quote!(#tp).to_string().contains("String") { + quote!(read_string(#ptr)) + } else if is_integer_type(ty) { + quote!(#ty::from_bytes(read_bytes(#ptr))) + } else { + quote!(read_bytes(#ptr)) + } + } else { + quote!(read_bytes(#ptr)) }; - Some((quote!(#ptr: i32), quote!(let #param = #deser(#ptr);), quote!(#param))) + + Some((quote!(#ptr: i32), quote!(let #param = #deser;), quote!(#param))) } _ => None, }) @@ -184,19 +176,17 @@ fn handle_impl_block(impl_block: ItemImpl) -> TokenStream { let body = if has_return { quote! { #(#deserializations)* - let mut state = #self_ty::default(); - state.__init_lazy_fields(alloc::vec::Vec::new()); + let mut state = <#self_ty as ContractState>::with_prefix(alloc::vec::Vec::new()); let result = state.#call; - state.__flush_lazy_fields(); + state.flush(); ret(result); } } else { quote! { #(#deserializations)* - let mut state = #self_ty::default(); - state.__init_lazy_fields(alloc::vec::Vec::new()); + let mut state = <#self_ty as ContractState>::with_prefix(alloc::vec::Vec::new()); state.#call; - state.__flush_lazy_fields(); + state.flush(); } }; @@ -228,10 +218,19 @@ fn handle_function(input: ItemFn) -> TokenStream { for arg in inputs.iter() { if let FnArg::Typed(pat_type) = arg { let param = &pat_type.pat; + let ty = &pat_type.ty; let ptr = syn::Ident::new(&format!("arg{}_ptr", idx), name.span()); - let deser = match &*pat_type.ty { - Type::Path(tp) if quote!(#tp).to_string().contains("String") => quote!(read_string), - _ => quote!(read_bytes), + + let deser = if let Type::Path(tp) = &**ty { + if quote!(#tp).to_string().contains("String") { + quote!(read_string(#ptr)) + } else if is_integer_type(ty) { + quote!(#ty::from_bytes(read_bytes(#ptr))) + } else { + quote!(read_bytes(#ptr)) + } + } else { + quote!(read_bytes(#ptr)) }; if idx > 0 { @@ -242,7 +241,7 @@ fn handle_function(input: ItemFn) -> TokenStream { call_args.extend(quote!(#param)); } - deserializations.extend(quote! { let #param = #deser(#ptr); }); + deserializations.extend(quote! { let #param = #deser; }); idx += 1; } } diff --git a/contract_samples/rust/sdk/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs index b0cae28..d846468 100644 --- a/contract_samples/rust/sdk/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -11,8 +11,8 @@ pub use encoding::*; pub use amadeus_sdk_macros::{contract, contract_state}; pub trait ContractState { - fn __init_lazy_fields(&mut self, prefix: Vec); - fn __flush_lazy_fields(&self); + fn with_prefix(prefix: Vec) -> Self; + fn flush(&self); } use core::panic::PanicInfo; @@ -147,21 +147,25 @@ where } -impl Default for LazyCell { - fn default() -> Self { - Self::new(Vec::new()) - } -} - -impl LazyCell { - pub fn new(key: Vec) -> Self { +impl ContractState for LazyCell { + fn with_prefix(prefix: Vec) -> Self { Self { - key, + key: prefix, value: core::cell::RefCell::new(None), dirty: core::cell::Cell::new(false), } } + fn flush(&self) { + if self.dirty.get() { + if let Some(val) = self.value.borrow().as_ref() { + kv_put(&self.key, val.clone()); + } + } + } +} + +impl LazyCell { pub fn get(&self) -> T where T: FromKvBytes + Default + Clone { if self.value.borrow().is_none() { let loaded = kv_get::(&self.key).unwrap_or_default(); @@ -187,16 +191,6 @@ impl LazyCell { } } -impl LazyCell { - pub fn flush(&self) { - if self.dirty.get() { - if let Some(val) = self.value.borrow().as_ref() { - kv_put(&self.key, val.clone()); - } - } - } -} - impl LazyCell { pub fn with_mut(&mut self, f: F) -> R @@ -204,8 +198,7 @@ impl LazyCell { F: FnOnce(&mut T) -> R { if self.value.borrow().is_none() { - let mut loaded = T::default(); - loaded.__init_lazy_fields(self.key.clone()); + let loaded = T::with_prefix(self.key.clone()); *self.value.borrow_mut() = Some(loaded); } self.dirty.set(true); @@ -220,8 +213,7 @@ impl LazyCell { F: FnOnce(&T) -> R { if self.value.borrow().is_none() { - let mut loaded = T::default(); - loaded.__init_lazy_fields(self.key.clone()); + let loaded = T::with_prefix(self.key.clone()); *self.value.borrow_mut() = Some(loaded); } unsafe { @@ -233,30 +225,20 @@ impl LazyCell { use alloc::collections::BTreeMap; -pub struct Map { +pub struct MapFlat { prefix: Vec, cache: BTreeMap, LazyCell>, _phantom: core::marker::PhantomData, } -impl Default for Map { - fn default() -> Self { - Self { - prefix: Vec::new(), - cache: BTreeMap::new(), - _phantom: core::marker::PhantomData, - } - } -} - -impl Map +impl MapFlat where K: Payload, V: FromKvBytes + Payload + Default + Clone { fn build_key(&self, key: &K) -> Vec { let key_bytes = key.to_payload(); - b!(self.prefix.as_slice(), b":", key_bytes.as_ref()) + b!(self.prefix.as_slice(), key_bytes.as_ref()) } pub fn get(&mut self, key: &K) -> Option<&LazyCell> { @@ -264,7 +246,7 @@ where if !self.cache.contains_key(&storage_key) { if kv_exists(&storage_key) { - self.cache.insert(storage_key.clone(), LazyCell::new(storage_key.clone())); + self.cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); } else { return None; } @@ -278,7 +260,7 @@ where if !self.cache.contains_key(&storage_key) { if kv_exists(&storage_key) { - self.cache.insert(storage_key.clone(), LazyCell::new(storage_key.clone())); + self.cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); } else { return None; } @@ -289,7 +271,7 @@ where pub fn insert(&mut self, key: K, value: V) { let storage_key = self.build_key(&key); - let cell: LazyCell = LazyCell::new(storage_key.clone()); + let cell: LazyCell = LazyCell::with_prefix(storage_key.clone()); cell.set(value); self.cache.insert(storage_key, cell); } @@ -301,46 +283,40 @@ where } } -impl ContractState for Map +impl ContractState for MapFlat where K: Payload, V: FromKvBytes + Payload + Default + Clone { - fn __init_lazy_fields(&mut self, prefix: Vec) { - self.prefix = prefix; + fn with_prefix(prefix: Vec) -> Self { + Self { + prefix, + cache: BTreeMap::new(), + _phantom: core::marker::PhantomData, + } } - fn __flush_lazy_fields(&self) { + fn flush(&self) { for cell in self.cache.values() { cell.flush(); } } } -pub struct MapNested { +pub struct Map { prefix: Vec, cache: BTreeMap, V>, _phantom: core::marker::PhantomData, } -impl Default for MapNested { - fn default() -> Self { - Self { - prefix: Vec::new(), - cache: BTreeMap::new(), - _phantom: core::marker::PhantomData, - } - } -} - -impl MapNested +impl Map where K: Payload, - V: ContractState + Default + V: ContractState { fn build_key(&self, key: &K) -> Vec { let key_bytes = key.to_payload(); - b!(self.prefix.as_slice(), b":", key_bytes.as_ref()) + b!(self.prefix.as_slice(), key_bytes.as_ref()) } pub fn with(&mut self, key: K, f: F) -> R @@ -350,8 +326,7 @@ where let storage_key = self.build_key(&key); if !self.cache.contains_key(&storage_key) { - let mut value = V::default(); - value.__init_lazy_fields(storage_key.clone()); + let value = V::with_prefix(storage_key.clone()); self.cache.insert(storage_key.clone(), value); } @@ -365,8 +340,7 @@ where let storage_key = self.build_key(&key); if !self.cache.contains_key(&storage_key) { - let mut value = V::default(); - value.__init_lazy_fields(storage_key.clone()); + let value = V::with_prefix(storage_key.clone()); self.cache.insert(storage_key.clone(), value); } @@ -379,18 +353,22 @@ where } } -impl ContractState for MapNested +impl ContractState for Map where K: Payload, - V: ContractState + Default + V: ContractState { - fn __init_lazy_fields(&mut self, prefix: Vec) { - self.prefix = prefix; + fn with_prefix(prefix: Vec) -> Self { + Self { + prefix, + cache: BTreeMap::new(), + _phantom: core::marker::PhantomData, + } } - fn __flush_lazy_fields(&self) { + fn flush(&self) { for value in self.cache.values() { - value.__flush_lazy_fields(); + value.flush(); } } } From 450a4519d42c501e28b43e5999ca6f0129f9d68b Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Mon, 5 Jan 2026 07:09:43 +0200 Subject: [PATCH 09/11] fix the with and with_mut in maps Signed-off-by: Valentyn Faychuk --- contract_samples/rust/examples/coin.rs | 4 +- contract_samples/rust/examples/counter.rs | 5 +- contract_samples/rust/examples/deposit.rs | 4 +- contract_samples/rust/examples/nft.rs | 4 +- contract_samples/rust/examples/showcase.rs | 42 ++-- contract_samples/rust/sdk/src/lib.rs | 241 +++++++++++++++++---- 6 files changed, 232 insertions(+), 68 deletions(-) diff --git a/contract_samples/rust/examples/coin.rs b/contract_samples/rust/examples/coin.rs index 730862b..f4f4ea6 100644 --- a/contract_samples/rust/examples/coin.rs +++ b/contract_samples/rust/examples/coin.rs @@ -1,8 +1,10 @@ #![no_std] #![no_main] + extern crate alloc; +use alloc::vec::Vec; +use alloc::string::String; use amadeus_sdk::*; -use alloc::{vec::Vec, string::String}; fn vault_key(symbol: &Vec) -> Vec { b!("vault:", account_caller(), ":", symbol) diff --git a/contract_samples/rust/examples/counter.rs b/contract_samples/rust/examples/counter.rs index 0eadea1..61c05dc 100644 --- a/contract_samples/rust/examples/counter.rs +++ b/contract_samples/rust/examples/counter.rs @@ -1,12 +1,13 @@ #![no_std] #![no_main] + extern crate alloc; +use alloc::vec::Vec; +use alloc::string::String; use amadeus_sdk::*; -use alloc::{vec::Vec, string::String}; #[no_mangle] pub extern "C" fn init() { - log("Init called during deployment of contract"); kv_put("inited", "true"); } diff --git a/contract_samples/rust/examples/deposit.rs b/contract_samples/rust/examples/deposit.rs index 35876be..d48fcb0 100644 --- a/contract_samples/rust/examples/deposit.rs +++ b/contract_samples/rust/examples/deposit.rs @@ -1,8 +1,10 @@ #![no_std] #![no_main] + extern crate alloc; +use alloc::vec::Vec; +use alloc::string::String; use amadeus_sdk::*; -use alloc::{vec::Vec, string::String}; fn vault_key(symbol: &Vec) -> Vec { b!("vault:", account_caller(), ":", symbol) diff --git a/contract_samples/rust/examples/nft.rs b/contract_samples/rust/examples/nft.rs index 7fd2cb9..cc91d0e 100644 --- a/contract_samples/rust/examples/nft.rs +++ b/contract_samples/rust/examples/nft.rs @@ -1,8 +1,10 @@ #![no_std] #![no_main] + extern crate alloc; +use alloc::vec::Vec; +use alloc::string::String; use amadeus_sdk::*; -use alloc::{vec::Vec, string::String}; #[no_mangle] pub extern "C" fn init() { diff --git a/contract_samples/rust/examples/showcase.rs b/contract_samples/rust/examples/showcase.rs index 4fb37e0..377835c 100644 --- a/contract_samples/rust/examples/showcase.rs +++ b/contract_samples/rust/examples/showcase.rs @@ -36,34 +36,36 @@ impl Leaderboard { *self.total_matches += 1; } - pub fn get_total_matches(&mut self) -> Vec { + pub fn get_total_matches(&self) -> Vec { (*self.total_matches).to_string().into_bytes() } pub fn record_match(&mut self, player: String, match_id: u64, score: u16, opponent: String) { - self.players.with_mut(player, |matches| { - matches.with_mut(match_id, |m| { + if let Some(matches) = self.players.get_mut(player.clone()) { + if let Some(m) = matches.get_mut(match_id) { *m.score = score; *m.opponent = opponent; - }); - }); + } + } *self.total_matches += 1; } - pub fn get_match_score(&mut self, player: String, match_id: u64) -> Vec { - self.players.with_mut(player, |matches| { - matches.with_mut(match_id, |m| { - (*m.score).to_string().into_bytes() - }) - }) + pub fn get_match_score(&self, player: String, match_id: u64) -> Vec { + if let Some(matches) = self.players.get(player) { + if let Some(m) = matches.get(match_id) { + return (*m.score).to_string().into_bytes(); + } + } + "0".to_string().into_bytes() } - pub fn get_match_opponent(&mut self, player: String, match_id: u64) -> String { - self.players.with_mut(player, |matches| { - matches.with_mut(match_id, |m| { - (*m.opponent).clone() - }) - }) + pub fn get_match_opponent(&self, player: String, match_id: u64) -> String { + if let Some(matches) = self.players.get(player) { + if let Some(m) = matches.get(match_id) { + return (*m.opponent).clone(); + } + } + String::new() } pub fn set_tournament_info(&mut self, name: String, prize_pool: u64) { @@ -71,11 +73,11 @@ impl Leaderboard { *self.tournament.prize_pool = prize_pool; } - pub fn get_tournament_name(&mut self) -> String { + pub fn get_tournament_name(&self) -> String { (*self.tournament.name).clone() } - pub fn get_tournament_prize(&mut self) -> Vec { + pub fn get_tournament_prize(&self) -> Vec { (*self.tournament.prize_pool).to_string().into_bytes() } @@ -87,7 +89,7 @@ impl Leaderboard { } } - pub fn get_player_wins(&mut self, player: String) -> Vec { + pub fn get_player_wins(&self, player: String) -> Vec { if let Some(wins) = self.player_wins.get(&player) { (*wins).to_string().into_bytes() } else { diff --git a/contract_samples/rust/sdk/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs index d846468..dcb1030 100644 --- a/contract_samples/rust/sdk/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -10,6 +10,8 @@ pub use storage::*; pub use encoding::*; pub use amadeus_sdk_macros::{contract, contract_state}; +use alloc::vec; + pub trait ContractState { fn with_prefix(prefix: Vec) -> Self; fn flush(&self); @@ -227,7 +229,7 @@ use alloc::collections::BTreeMap; pub struct MapFlat { prefix: Vec, - cache: BTreeMap, LazyCell>, + cache: core::cell::UnsafeCell, LazyCell>>, _phantom: core::marker::PhantomData, } @@ -241,44 +243,54 @@ where b!(self.prefix.as_slice(), key_bytes.as_ref()) } - pub fn get(&mut self, key: &K) -> Option<&LazyCell> { + pub fn get(&self, key: &K) -> Option<&LazyCell> { let storage_key = self.build_key(key); - if !self.cache.contains_key(&storage_key) { - if kv_exists(&storage_key) { - self.cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); - } else { - return None; + unsafe { + let cache = &mut *self.cache.get(); + if !cache.contains_key(&storage_key) { + if kv_exists(&storage_key) { + cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); + } else { + return None; + } } - } - self.cache.get(&storage_key) + cache.get(&storage_key) + } } pub fn get_mut(&mut self, key: &K) -> Option<&mut LazyCell> { let storage_key = self.build_key(key); - if !self.cache.contains_key(&storage_key) { - if kv_exists(&storage_key) { - self.cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); - } else { - return None; + unsafe { + let cache = &mut *self.cache.get(); + if !cache.contains_key(&storage_key) { + if kv_exists(&storage_key) { + cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); + } else { + return None; + } } - } - self.cache.get_mut(&storage_key) + cache.get_mut(&storage_key) + } } pub fn insert(&mut self, key: K, value: V) { let storage_key = self.build_key(&key); let cell: LazyCell = LazyCell::with_prefix(storage_key.clone()); cell.set(value); - self.cache.insert(storage_key, cell); + unsafe { + (*self.cache.get()).insert(storage_key, cell); + } } pub fn remove(&mut self, key: &K) { let storage_key = self.build_key(key); - self.cache.remove(&storage_key); + unsafe { + (*self.cache.get()).remove(&storage_key); + } kv_delete(&storage_key); } } @@ -291,24 +303,80 @@ where fn with_prefix(prefix: Vec) -> Self { Self { prefix, - cache: BTreeMap::new(), + cache: core::cell::UnsafeCell::new(BTreeMap::new()), _phantom: core::marker::PhantomData, } } fn flush(&self) { - for cell in self.cache.values() { - cell.flush(); + unsafe { + for cell in (*self.cache.get()).values() { + cell.flush(); + } } } } pub struct Map { prefix: Vec, - cache: BTreeMap, V>, + cache: core::cell::UnsafeCell, V>>, _phantom: core::marker::PhantomData, } +pub struct MapIter<'a, K, V> { + map: &'a Map, + keys: Vec>, + index: usize, +} + +impl<'a, K, V> Iterator for MapIter<'a, K, V> +where + V: ContractState +{ + type Item = &'a V; + + fn next(&mut self) -> Option { + if self.index >= self.keys.len() { + return None; + } + + let key = &self.keys[self.index]; + self.index += 1; + + unsafe { + let cache = &*self.map.cache.get(); + cache.get(key).map(|v| &*(v as *const V)) + } + } +} + +pub struct MapIterMut<'a, K, V> { + map: &'a Map, + keys: Vec>, + index: usize, +} + +impl<'a, K, V> Iterator for MapIterMut<'a, K, V> +where + V: ContractState +{ + type Item = &'a mut V; + + fn next(&mut self) -> Option { + if self.index >= self.keys.len() { + return None; + } + + let key = &self.keys[self.index]; + self.index += 1; + + unsafe { + let cache = &mut *self.map.cache.get(); + cache.get_mut(key).map(|v| &mut *(v as *mut V)) + } + } +} + impl Map where K: Payload, @@ -319,37 +387,122 @@ where b!(self.prefix.as_slice(), key_bytes.as_ref()) } - pub fn with(&mut self, key: K, f: F) -> R - where - F: FnOnce(&V) -> R - { + pub fn get(&self, key: K) -> Option<&V> { let storage_key = self.build_key(&key); - if !self.cache.contains_key(&storage_key) { - let value = V::with_prefix(storage_key.clone()); - self.cache.insert(storage_key.clone(), value); - } + unsafe { + let cache = &mut *self.cache.get(); + if !cache.contains_key(&storage_key) { + // Cannot use kv_exists() because V is a ContractState (not a single value). + // ContractState uses the key as a prefix for nested fields, so we check if + // any keys exist with this prefix using kv_get_next(). + let (first_key, _) = kv_get_next(&storage_key, &vec![]); + if first_key.is_some() { + let value = V::with_prefix(storage_key.clone()); + cache.insert(storage_key.clone(), value); + } else { + return None; + } + } - f(self.cache.get(&storage_key).unwrap()) + cache.get(&storage_key).map(|v| &*(v as *const V)) + } } - pub fn with_mut(&mut self, key: K, f: F) -> R - where - F: FnOnce(&mut V) -> R - { + pub fn get_mut(&mut self, key: K) -> Option<&mut V> { let storage_key = self.build_key(&key); - if !self.cache.contains_key(&storage_key) { - let value = V::with_prefix(storage_key.clone()); - self.cache.insert(storage_key.clone(), value); + unsafe { + let cache = &mut *self.cache.get(); + if !cache.contains_key(&storage_key) { + // Cannot use kv_exists() because V is a ContractState (not a single value). + // ContractState uses the key as a prefix for nested fields, so we check if + // any keys exist with this prefix using kv_get_next(). + let (first_key, _) = kv_get_next(&storage_key, &vec![]); + if first_key.is_some() { + let value = V::with_prefix(storage_key.clone()); + cache.insert(storage_key.clone(), value); + } else { + return None; + } + } + + cache.get_mut(&storage_key) + } + } + + pub fn with(&self) -> MapIter<'_, K, V> { + let mut keys = Vec::new(); + let mut current_key = vec![]; + + unsafe { + let cache = &mut *self.cache.get(); + + loop { + let (key, _) = kv_get_next(&self.prefix, ¤t_key); + match key { + Some(k) => { + if !cache.contains_key(&k) { + let value = V::with_prefix(k.clone()); + cache.insert(k.clone(), value); + } + keys.push(k.clone()); + current_key = k; + } + None => break, + } + } } - f(self.cache.get_mut(&storage_key).unwrap()) + MapIter { + map: self, + keys, + index: 0, + } + } + + pub fn with_mut(&mut self) -> MapIterMut<'_, K, V> { + let mut keys = Vec::new(); + let mut current_key = vec![]; + + unsafe { + let cache = &mut *self.cache.get(); + + loop { + let (key, _) = kv_get_next(&self.prefix, ¤t_key); + match key { + Some(k) => { + if !cache.contains_key(&k) { + let value = V::with_prefix(k.clone()); + cache.insert(k.clone(), value); + } + keys.push(k.clone()); + current_key = k; + } + None => break, + } + } + } + + MapIterMut { + map: self, + keys, + index: 0, + } + } + + pub fn insert(&mut self, key: K, value: V) { + let storage_key = self.build_key(&key); + unsafe { + (*self.cache.get()).insert(storage_key, value); + } } pub fn remove(&mut self, key: &K) { let storage_key = self.build_key(key); - self.cache.remove(&storage_key); + unsafe { + (*self.cache.get()).remove(&storage_key); + } } } @@ -361,14 +514,16 @@ where fn with_prefix(prefix: Vec) -> Self { Self { prefix, - cache: BTreeMap::new(), + cache: core::cell::UnsafeCell::new(BTreeMap::new()), _phantom: core::marker::PhantomData, } } fn flush(&self) { - for value in self.cache.values() { - value.flush(); + unsafe { + for value in (*self.cache.get()).values() { + value.flush(); + } } } } From 88d898765e339282647c6b52d51c582d89aa34d7 Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Tue, 6 Jan 2026 04:50:59 +0200 Subject: [PATCH 10/11] fix kv_exists usage and add testing Signed-off-by: Valentyn Faychuk --- contract_samples/rust/README.md | 20 +- contract_samples/rust/examples/showcase.rs | 113 +++++++++-- contract_samples/rust/sdk-macros/src/lib.rs | 16 +- contract_samples/rust/sdk/Cargo.toml | 7 +- contract_samples/rust/sdk/src/context.rs | 132 +++++++++--- contract_samples/rust/sdk/src/encoding.rs | 5 + contract_samples/rust/sdk/src/lib.rs | 28 ++- contract_samples/rust/sdk/src/storage.rs | 36 ++++ contract_samples/rust/sdk/src/testing.rs | 213 ++++++++++++++++++++ 9 files changed, 519 insertions(+), 51 deletions(-) create mode 100644 contract_samples/rust/sdk/src/testing.rs diff --git a/contract_samples/rust/README.md b/contract_samples/rust/README.md index 4fef5bb..487c6e8 100644 --- a/contract_samples/rust/README.md +++ b/contract_samples/rust/README.md @@ -27,7 +27,14 @@ cargo install amadeus-cli To build the wasm smart contracts, simply run the `./build_and_validate.sh`. The artifacts will be placed in `target/wasm32-unknown-unknown/release/examples`. -### Testing +## Unit Testing + +```bash +cargo +nightly test --example showcase --features testing --no-default-features -- --nocapture --test-threads=1 +cargo expand -p amadeus-sdk --example showcase --target wasm32-unknown-unknown +``` + +### Testnet Deployment Make sure you have `amadeus-cli` installed. Follow the code snippet below to run each example on the testnet. @@ -72,4 +79,15 @@ ama tx --sk wallet.sk $NFT_PK init '[]' --url https://testnet-rpc.ama.one ama tx --sk wallet.sk $NFT_PK claim '[]' --url https://testnet-rpc.ama.one ama tx --sk wallet.sk $NFT_PK view_nft '["AGENTIC", "1"]' --url https://testnet-rpc.ama.one ama tx --sk wallet.sk $NFT_PK claim '[]' --url https://testnet-rpc.ama.one + +ama gen-sk showcase.sk +export SHOWCASE_PK=$(ama get-pk --sk showcase.sk) +ama tx --sk wallet.sk Coin transfer '[{"b58": "'$SHOWCASE_PK'"}, "2000000000", "AMA"]' --url https://testnet-rpc.ama.one +ama deploy-tx --sk showcase.sk showcase.wasm --url https://testnet-rpc.ama.one +ama tx --sk wallet.sk $SHOWCASE_PK increment_total_matches '[]' --url https://testnet-rpc.ama.one +ama tx --sk wallet.sk $SHOWCASE_PK set_tournament_info '["World Cup", "1000000"]' --url https://testnet-rpc.ama.one +ama tx --sk wallet.sk $SHOWCASE_PK record_win '["alice"]' --url https://testnet-rpc.ama.one +ama tx --sk wallet.sk $SHOWCASE_PK record_win '["alice"]' --url https://testnet-rpc.ama.one +ama tx --sk wallet.sk $SHOWCASE_PK get_player_wins '["alice"]' --url https://testnet-rpc.ama.one +ama tx --sk wallet.sk $SHOWCASE_PK get_tournament_name '[]' --url https://testnet-rpc.ama.one ``` diff --git a/contract_samples/rust/examples/showcase.rs b/contract_samples/rust/examples/showcase.rs index 377835c..efa9a89 100644 --- a/contract_samples/rust/examples/showcase.rs +++ b/contract_samples/rust/examples/showcase.rs @@ -1,8 +1,12 @@ -#![no_std] -#![no_main] +#![cfg_attr(not(any(test, feature = "testing")), no_std)] +#![cfg_attr(not(any(test, feature = "testing")), no_main)] +#![cfg_attr(any(test, feature = "testing"), feature(thread_local))] extern crate alloc; -use alloc::vec::Vec; + +#[cfg(any(test, feature = "testing"))] +extern crate std; + use alloc::string::{String, ToString}; use amadeus_sdk::*; @@ -36,8 +40,8 @@ impl Leaderboard { *self.total_matches += 1; } - pub fn get_total_matches(&self) -> Vec { - (*self.total_matches).to_string().into_bytes() + pub fn get_total_matches(&self) -> i32 { + *self.total_matches } pub fn record_match(&mut self, player: String, match_id: u64, score: u16, opponent: String) { @@ -50,13 +54,13 @@ impl Leaderboard { *self.total_matches += 1; } - pub fn get_match_score(&self, player: String, match_id: u64) -> Vec { + pub fn get_match_score(&self, player: String, match_id: u64) -> u16 { if let Some(matches) = self.players.get(player) { if let Some(m) = matches.get(match_id) { - return (*m.score).to_string().into_bytes(); + return *m.score; } } - "0".to_string().into_bytes() + 0 } pub fn get_match_opponent(&self, player: String, match_id: u64) -> String { @@ -77,8 +81,8 @@ impl Leaderboard { (*self.tournament.name).clone() } - pub fn get_tournament_prize(&self) -> Vec { - (*self.tournament.prize_pool).to_string().into_bytes() + pub fn get_tournament_prize(&self) -> u64 { + *self.tournament.prize_pool } pub fn record_win(&mut self, player: String) { @@ -89,11 +93,11 @@ impl Leaderboard { } } - pub fn get_player_wins(&self, player: String) -> Vec { + pub fn get_player_wins(&self, player: String) -> u32 { if let Some(wins) = self.player_wins.get(&player) { - (*wins).to_string().into_bytes() + **wins } else { - "0".to_string().into_bytes() + 0 } } @@ -101,3 +105,86 @@ impl Leaderboard { self.player_wins.insert(player, wins); } } + +#[cfg(test)] +mod tests { + use super::*; + use amadeus_sdk::testing::*; + + #[test] + fn test_increment_total_matches() { + reset(); + let mut state = Leaderboard::with_prefix(Vec::new()); + state.increment_total_matches(); + state.flush(); + println!("\n{}\n", dump()); + let state2 = Leaderboard::with_prefix(Vec::new()); + assert_eq!(state2.get_total_matches(), 1); + } + + #[test] + fn test_set_tournament_info() { + reset(); + let mut state = Leaderboard::with_prefix(Vec::new()); + state.set_tournament_info("World Cup".to_string(), 1000000); + state.flush(); + println!("\n{}\n", dump()); + let state2 = Leaderboard::with_prefix(Vec::new()); + assert_eq!(state2.get_tournament_name(), "World Cup"); + assert_eq!(state2.get_tournament_prize(), 1000000); + } + + #[test] + fn test_record_win() { + reset(); + let mut state = Leaderboard::with_prefix(Vec::new()); + state.record_win("alice".to_string()); + state.flush(); + println!("\n{}\n", dump()); + let state2 = Leaderboard::with_prefix(Vec::new()); + assert_eq!(state2.get_player_wins("alice".to_string()), 1); + } + + #[test] + fn test_multiple_operations() { + reset(); + + let mut state = Leaderboard::with_prefix(Vec::new()); + state.increment_total_matches(); + state.flush(); + println!("After increment_total_matches():\n{}\n", dump()); + + let mut state = Leaderboard::with_prefix(Vec::new()); + state.set_tournament_info("World Cup".to_string(), 1000000); + state.flush(); + println!("After set_tournament_info():\n{}\n", dump()); + + let mut state = Leaderboard::with_prefix(Vec::new()); + state.record_win("alice".to_string()); + state.flush(); + println!("After record_win(alice):\n{}\n", dump()); + + let mut state = Leaderboard::with_prefix(Vec::new()); + state.record_win("alice".to_string()); + state.flush(); + println!("After record_win(alice) 2nd:\n{}\n", dump()); + + let mut state = Leaderboard::with_prefix(Vec::new()); + state.record_win("bob".to_string()); + state.flush(); + println!("After record_win(bob):\n{}\n", dump()); + + let mut state = Leaderboard::with_prefix(Vec::new()); + state.set_player_wins("charlie".to_string(), 5); + state.flush(); + println!("After set_player_wins(charlie, 5):\n{}\n", dump()); + + let state = Leaderboard::with_prefix(Vec::new()); + assert_eq!(state.get_total_matches(), 1); + assert_eq!(state.get_player_wins("alice".to_string()), 2); + assert_eq!(state.get_player_wins("bob".to_string()), 1); + assert_eq!(state.get_player_wins("charlie".to_string()), 5); + assert_eq!(state.get_tournament_name(), "World Cup"); + assert_eq!(state.get_tournament_prize(), 1000000); + } +} diff --git a/contract_samples/rust/sdk-macros/src/lib.rs b/contract_samples/rust/sdk-macros/src/lib.rs index e11a724..a0319ca 100644 --- a/contract_samples/rust/sdk-macros/src/lib.rs +++ b/contract_samples/rust/sdk-macros/src/lib.rs @@ -173,13 +173,27 @@ fn handle_impl_block(impl_block: ItemImpl) -> TokenStream { quote!(#name(#(#call_args),*)) }; + let return_wrapper = if has_return { + if let syn::ReturnType::Type(_, ret_type) = &method.sig.output { + if is_integer_type(ret_type) { + quote! { ret(result.to_string().into_bytes()); } + } else { + quote! { ret(result); } + } + } else { + quote! { ret(result); } + } + } else { + quote! {} + }; + let body = if has_return { quote! { #(#deserializations)* let mut state = <#self_ty as ContractState>::with_prefix(alloc::vec::Vec::new()); let result = state.#call; state.flush(); - ret(result); + #return_wrapper } } else { quote! { diff --git a/contract_samples/rust/sdk/Cargo.toml b/contract_samples/rust/sdk/Cargo.toml index 541db42..bedc55c 100644 --- a/contract_samples/rust/sdk/Cargo.toml +++ b/contract_samples/rust/sdk/Cargo.toml @@ -11,8 +11,13 @@ repository = "https://github.com/amadeusprotocol/node" crate-type = ["cdylib", "rlib"] [dependencies] -dlmalloc = { version = "0.2", features = ["global"] } amadeus-sdk-macros = { path = "../sdk-macros" } +dlmalloc = { version = "0.2", features = ["global"], optional = true } + +[features] +default = ["use-dlmalloc"] +use-dlmalloc = ["dlmalloc"] +testing = [] [package.metadata.wasm-pack.profile.release] wasm-opt = false diff --git a/contract_samples/rust/sdk/src/context.rs b/contract_samples/rust/sdk/src/context.rs index c49c8f4..a80fda2 100644 --- a/contract_samples/rust/sdk/src/context.rs +++ b/contract_samples/rust/sdk/src/context.rs @@ -4,22 +4,88 @@ use alloc::{vec, borrow::Cow, vec::Vec}; pub const BURN_ADDRESS: &[u8] = &[0u8; 48]; +#[cfg(not(test))] pub fn seed() -> Vec { read_bytes(1100) } +#[cfg(test)] +pub fn seed() -> Vec { crate::testing::mock_imports::import_seed() } + +#[cfg(not(test))] pub fn entry_slot() -> u64 { read_u64(2000) } +#[cfg(test)] +pub fn entry_slot() -> u64 { crate::testing::mock_imports::import_entry_slot() } + +#[cfg(not(test))] pub fn entry_height() -> u64 { read_u64(2010) } +#[cfg(test)] +pub fn entry_height() -> u64 { crate::testing::mock_imports::import_entry_height() } + +#[cfg(not(test))] pub fn entry_epoch() -> u64 { read_u64(2020) } +#[cfg(test)] +pub fn entry_epoch() -> u64 { crate::testing::mock_imports::import_entry_epoch() } + +#[cfg(not(test))] pub fn entry_signer() -> Vec { read_bytes(2100) } +#[cfg(test)] +pub fn entry_signer() -> Vec { crate::testing::mock_imports::import_entry_signer() } + +#[cfg(not(test))] pub fn entry_prev_hash() -> Vec { read_bytes(2200) } +#[cfg(test)] +pub fn entry_prev_hash() -> Vec { Vec::new() } // Not mocked yet + +#[cfg(not(test))] pub fn entry_vr() -> Vec { read_bytes(2300) } +#[cfg(test)] +pub fn entry_vr() -> Vec { Vec::new() } // Not mocked yet + +#[cfg(not(test))] pub fn entry_dr() -> Vec { read_bytes(2400) } +#[cfg(test)] +pub fn entry_dr() -> Vec { Vec::new() } // Not mocked yet + +#[cfg(not(test))] pub fn tx_nonce() -> u64 { read_u64(3000) } +#[cfg(test)] +pub fn tx_nonce() -> u64 { crate::testing::mock_imports::import_tx_nonce() } + +#[cfg(not(test))] pub fn tx_signer() -> Vec { read_bytes(3100) } +#[cfg(test)] +pub fn tx_signer() -> Vec { crate::testing::mock_imports::import_tx_signer() } + +#[cfg(not(test))] pub fn account_current() -> Vec { read_bytes(4000) } +#[cfg(test)] +pub fn account_current() -> Vec { crate::testing::mock_imports::import_account_current() } + +#[cfg(not(test))] pub fn account_caller() -> Vec { read_bytes(4100) } +#[cfg(test)] +pub fn account_caller() -> Vec { crate::testing::mock_imports::import_account_caller() } + +#[cfg(not(test))] pub fn account_origin() -> Vec { read_bytes(4200) } +#[cfg(test)] +pub fn account_origin() -> Vec { crate::testing::mock_imports::import_account_origin() } + +#[cfg(not(test))] pub fn attached_symbol() -> Vec { read_bytes(5000) } +#[cfg(test)] +pub fn attached_symbol() -> Vec { + let (has, (symbol, _)) = crate::testing::mock_imports::import_get_attachment(); + if has { symbol } else { Vec::new() } +} + +#[cfg(not(test))] pub fn attached_amount() -> Vec { read_bytes(5100) } +#[cfg(test)] +pub fn attached_amount() -> Vec { + let (has, (_, amount)) = crate::testing::mock_imports::import_get_attachment(); + if has { amount } else { Vec::new() } +} +#[cfg(not(test))] pub fn get_attachment() -> (bool, (Vec, Vec)) { unsafe { let header = core::ptr::read_unaligned(5000 as *const u32); @@ -30,59 +96,67 @@ pub fn get_attachment() -> (bool, (Vec, Vec)) { (true, (attached_symbol(), attached_amount())) } +#[cfg(test)] +pub fn get_attachment() -> (bool, (Vec, Vec)) { + crate::testing::mock_imports::import_get_attachment() +} + +#[cfg(not(any(test, feature = "testing")))] extern "C" { fn import_log(p: *const u8, l: usize); fn import_return(p: *const u8, l: usize); fn import_call(args_ptr: *const u8, extra_args_ptr: *const u8) -> i32; } -pub fn log(line: impl Payload) { - let line_cow = line.to_payload(); - let line_bytes = line_cow.as_ref(); - unsafe { import_log(line_bytes.as_ptr(), line_bytes.len()); } -} - -pub fn ret(val: impl Payload) { - let val_cow = val.to_payload(); - let val_bytes = val_cow.as_ref(); - unsafe { import_return(val_bytes.as_ptr(), val_bytes.len()); } -} - -// [Count (u32)] [Ptr1 (u32)] [Len1 (u32)] [Ptr2 (u32)] [Len2 (u32)] ... -fn build_table(items: &[Cow<[u8]>]) -> Vec { +#[allow(dead_code)] +fn build_table(items: &[alloc::borrow::Cow<[u8]>]) -> Vec { let count = items.len(); let table_size = 4 + (count * 8); let mut table = vec![0u8; table_size]; - let count_bytes = (count as u32).to_le_bytes(); table[0..4].copy_from_slice(&count_bytes); - for (i, item) in items.iter().enumerate() { let bytes = item.as_ref(); - let ptr_val = bytes.as_ptr() as u32; let len_val = bytes.len() as u32; - let offset = 4 + (i * 8); - table[offset..offset+4].copy_from_slice(&ptr_val.to_le_bytes()); table[offset+4..offset+8].copy_from_slice(&len_val.to_le_bytes()); } - table } +pub fn log(line: impl Payload) { + let line_cow = line.to_payload(); + let line_bytes = line_cow.as_ref(); + #[cfg(any(test, feature = "testing"))] + { + let s = alloc::string::String::from_utf8_lossy(line_bytes); + crate::testing::mock_imports::import_log(&s); + } + #[cfg(not(any(test, feature = "testing")))] + unsafe { import_log(line_bytes.as_ptr(), line_bytes.len()); } +} + +#[allow(unused_variables)] +pub fn ret(val: impl Payload) { + #[cfg(not(any(test, feature = "testing")))] + { + let val_cow = val.to_payload(); + let val_bytes = val_cow.as_ref(); + unsafe { import_return(val_bytes.as_ptr(), val_bytes.len()); } + } +} + +#[cfg(not(any(test, feature = "testing")))] pub fn call(contract: impl Payload, func: impl Payload, args: &[&dyn Payload], extra_args: &[&dyn Payload]) -> Vec { let mut main_owners = Vec::with_capacity(2 + args.len()); - main_owners.push(contract.to_payload()); main_owners.push(func.to_payload()); for arg in args { main_owners.push(arg.to_payload()); } - let main_table = build_table(&main_owners); - let (_extra_owners, extra_ptr) = if extra_args.is_empty() { (Vec::new(), core::ptr::null()) } else { @@ -91,16 +165,18 @@ pub fn call(contract: impl Payload, func: impl Payload, args: &[&dyn Payload], e owners.push(arg.to_payload()); } let t = build_table(&owners); - let p = t.as_ptr(); - (owners, p) + (owners, t.as_ptr()) }; - unsafe { - let error_ptr = import_call(main_table.as_ptr(), extra_ptr); - read_bytes(error_ptr) + read_bytes(import_call(main_table.as_ptr(), extra_ptr)) } } +#[cfg(any(test, feature = "testing"))] +pub fn call(_contract: impl Payload, _func: impl Payload, _args: &[&dyn Payload], _extra_args: &[&dyn Payload]) -> Vec { + Vec::new() +} + #[macro_export] macro_rules! call { ($contract:expr, $func:expr, [ $( $arg:expr ),* ], [ $( $earg:expr ),* ]) => { diff --git a/contract_samples/rust/sdk/src/encoding.rs b/contract_samples/rust/sdk/src/encoding.rs index 1c607c6..9c39bc6 100644 --- a/contract_samples/rust/sdk/src/encoding.rs +++ b/contract_samples/rust/sdk/src/encoding.rs @@ -97,3 +97,8 @@ impl_bytes_to_int!(bytes_to_u16, u16); impl_bytes_to_int!(bytes_to_u32, u32); impl_bytes_to_int!(bytes_to_u64, u64); impl_bytes_to_int!(bytes_to_u128, u128); + +pub fn i128_to_bytes(val: i128) -> Vec { + use alloc::string::ToString; + val.to_string().into_bytes() +} diff --git a/contract_samples/rust/sdk/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs index dcb1030..2a7ee62 100644 --- a/contract_samples/rust/sdk/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -1,10 +1,19 @@ -#![no_std] +#![cfg_attr(not(any(test, feature = "testing")), no_std)] +#![cfg_attr(any(test, feature = "testing"), feature(thread_local))] +#![allow(unused_imports)] + extern crate alloc; +#[cfg(any(test, feature = "testing"))] +extern crate std; + pub mod context; pub mod storage; pub mod encoding; +#[cfg(any(test, feature = "testing"))] +pub mod testing; + pub use context::*; pub use storage::*; pub use encoding::*; @@ -22,12 +31,13 @@ use core::panic::PanicInfo; use alloc::{borrow::Cow, vec::Vec, string::String, string::ToString}; -#[cfg(not(test))] +#[cfg(not(any(test, feature = "testing")))] #[panic_handler] fn panic(_: &PanicInfo) -> ! { loop {} } +#[cfg(all(feature = "use-dlmalloc", not(any(test, feature = "testing"))))] #[global_allocator] static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; @@ -49,8 +59,14 @@ macro_rules! abort { { $crate::context::log($msg); - #[cfg(target_arch = "wasm32")] + #[cfg(all(target_arch = "wasm32", not(test)))] core::arch::wasm32::unreachable(); + + #[cfg(test)] + panic!($msg); + + #[allow(unreachable_code)] + loop {} } }; } @@ -245,17 +261,15 @@ where pub fn get(&self, key: &K) -> Option<&LazyCell> { let storage_key = self.build_key(key); - unsafe { let cache = &mut *self.cache.get(); if !cache.contains_key(&storage_key) { - if kv_exists(&storage_key) { + if kv_get::(&storage_key).is_some() { cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); } else { return None; } } - cache.get(&storage_key) } } @@ -266,7 +280,7 @@ where unsafe { let cache = &mut *self.cache.get(); if !cache.contains_key(&storage_key) { - if kv_exists(&storage_key) { + if kv_get::(&storage_key).is_some() { cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); } else { return None; diff --git a/contract_samples/rust/sdk/src/storage.rs b/contract_samples/rust/sdk/src/storage.rs index d1afa3b..75a0b74 100644 --- a/contract_samples/rust/sdk/src/storage.rs +++ b/contract_samples/rust/sdk/src/storage.rs @@ -38,6 +38,7 @@ impl_from_kv_bytes_for_int!(u32, crate::encoding::bytes_to_u32); impl_from_kv_bytes_for_int!(u64, crate::encoding::bytes_to_u64); impl_from_kv_bytes_for_int!(u128, crate::encoding::bytes_to_u128); +#[cfg(not(any(test, feature = "testing")))] extern "C" { fn import_kv_get(p: *const u8, l: usize) -> i32; fn import_kv_exists(p: *const u8, l: usize) -> i32; @@ -54,6 +55,11 @@ pub fn kv_put(key: impl Payload, value: impl Payload) { let key_bytes = key_cow.as_ref(); let value_cow = value.to_payload(); let value_bytes = value_cow.as_ref(); + #[cfg(any(test, feature = "testing"))] + { + crate::testing::mock_imports::import_kv_put(key_bytes, value_bytes); + } + #[cfg(not(any(test, feature = "testing")))] unsafe { import_kv_put( key_bytes.as_ptr(), @@ -67,6 +73,11 @@ pub fn kv_put(key: impl Payload, value: impl Payload) { pub fn kv_get(key: impl Payload) -> Option { let key_cow = key.to_payload(); let key_bytes = key_cow.as_ref(); + #[cfg(any(test, feature = "testing"))] + { + crate::testing::mock_imports::import_kv_get(key_bytes).map(|data| T::from_bytes(data)) + } + #[cfg(not(any(test, feature = "testing")))] unsafe { let ptr = import_kv_get(key_bytes.as_ptr(), key_bytes.len()); if *(ptr as *const i32) == -1 { @@ -83,6 +94,11 @@ pub fn kv_increment(key: impl Payload, amount: impl Payload) -> String { let key_bytes = key_cow.as_ref(); let amount_cow = amount.to_payload(); let amount_bytes = amount_cow.as_ref(); + #[cfg(any(test, feature = "testing"))] + { + crate::testing::mock_imports::import_kv_increment(key_bytes, amount_bytes) + } + #[cfg(not(any(test, feature = "testing")))] unsafe { read_string(import_kv_increment( key_bytes.as_ptr(), @@ -96,6 +112,11 @@ pub fn kv_increment(key: impl Payload, amount: impl Payload) -> String { pub fn kv_delete(key: impl Payload) { let key_cow = key.to_payload(); let key_bytes = key_cow.as_ref(); + #[cfg(any(test, feature = "testing"))] + { + crate::testing::mock_imports::import_kv_delete(key_bytes); + } + #[cfg(not(any(test, feature = "testing")))] unsafe { import_kv_delete( key_bytes.as_ptr(), @@ -107,6 +128,11 @@ pub fn kv_delete(key: impl Payload) { pub fn kv_exists(key: impl Payload) -> bool { let key_cow = key.to_payload(); let key_bytes = key_cow.as_ref(); + #[cfg(any(test, feature = "testing"))] + { + crate::testing::mock_imports::import_kv_exists(key_bytes) + } + #[cfg(not(any(test, feature = "testing")))] unsafe { let ptr = import_kv_exists(key_bytes.as_ptr(), key_bytes.len()); *(ptr as *const i32) == 1 @@ -118,6 +144,11 @@ pub fn kv_get_prev(prefix: impl Payload, key: impl Payload) -> (Option>, let prefix_bytes = prefix_cow.as_ref(); let key_cow = key.to_payload(); let key_bytes = key_cow.as_ref(); + #[cfg(any(test, feature = "testing"))] + { + crate::testing::mock_imports::import_kv_get_prev(prefix_bytes, key_bytes) + } + #[cfg(not(any(test, feature = "testing")))] unsafe { let ptr = import_kv_get_prev(prefix_bytes.as_ptr(), prefix_bytes.len(), key_bytes.as_ptr(), key_bytes.len()); let len = *(ptr as *const i32); @@ -134,6 +165,11 @@ pub fn kv_get_next(prefix: impl Payload, key: impl Payload) -> (Option>, let prefix_bytes = prefix_cow.as_ref(); let key_cow = key.to_payload(); let key_bytes = key_cow.as_ref(); + #[cfg(any(test, feature = "testing"))] + { + crate::testing::mock_imports::import_kv_get_next(prefix_bytes, key_bytes) + } + #[cfg(not(any(test, feature = "testing")))] unsafe { let ptr = import_kv_get_next(prefix_bytes.as_ptr(), prefix_bytes.len(), key_bytes.as_ptr(), key_bytes.len()); let len = *(ptr as *const i32); diff --git a/contract_samples/rust/sdk/src/testing.rs b/contract_samples/rust/sdk/src/testing.rs new file mode 100644 index 0000000..e2b4822 --- /dev/null +++ b/contract_samples/rust/sdk/src/testing.rs @@ -0,0 +1,213 @@ +use std::collections::BTreeMap; +use std::vec::Vec; +use std::string::String; +use std::cell::RefCell; + +thread_local! { + static MOCK_STORAGE: RefCell, Vec>> = RefCell::new(BTreeMap::new()); + static MOCK_CONTEXT: RefCell = RefCell::new(MockContext::default()); +} + +#[derive(Clone, Debug)] +pub struct MockContext { + pub entry_slot: u64, + pub entry_height: u64, + pub entry_epoch: u64, + pub entry_signer: Vec, + pub tx_nonce: u64, + pub tx_signer: Vec, + pub account_current: Vec, + pub account_caller: Vec, + pub account_origin: Vec, + pub attachment: Option<(Vec, Vec)>, // (symbol, amount) + pub seed: Vec, +} + +impl Default for MockContext { + fn default() -> Self { + Self { + entry_slot: 0, + entry_height: 0, + entry_epoch: 0, + entry_signer: Vec::new(), + tx_nonce: 0, + tx_signer: Vec::new(), + account_current: Vec::new(), + account_caller: Vec::new(), + account_origin: Vec::new(), + attachment: None, + seed: vec![0u8; 32], + } + } +} + +pub fn reset() { + MOCK_STORAGE.with(|s| s.borrow_mut().clear()); + MOCK_CONTEXT.with(|c| *c.borrow_mut() = MockContext::default()); +} + +pub fn set_context(ctx: MockContext) { + MOCK_CONTEXT.with(|c| *c.borrow_mut() = ctx); +} + +pub fn get_storage() -> BTreeMap, Vec> { + MOCK_STORAGE.with(|s| s.borrow().clone()) +} + +pub fn dump() -> String { + MOCK_STORAGE.with(|s| { + s.borrow() + .iter() + .map(|(k, v)| format!("{}={}", String::from_utf8_lossy(k), String::from_utf8_lossy(v))) + .collect::>() + .join("\n") + }) +} + +#[allow(dead_code)] +pub(crate) mod mock_imports { + use super::*; + use crate::encoding::*; + + pub fn import_kv_get(key: &[u8]) -> Option> { + MOCK_STORAGE.with(|s| s.borrow().get(key).cloned()) + } + + pub fn import_kv_exists(key: &[u8]) -> bool { + MOCK_STORAGE.with(|s| s.borrow().contains_key(key)) + } + + pub fn import_kv_put(key: &[u8], value: &[u8]) { + MOCK_STORAGE.with(|s| { + s.borrow_mut().insert(key.to_vec(), value.to_vec()); + }); + } + + pub fn import_kv_increment(key: &[u8], amount: &[u8]) -> String { + let amt = bytes_to_i128(amount); + MOCK_STORAGE.with(|s| { + let mut storage = s.borrow_mut(); + let current = storage.get(key) + .map(|v| bytes_to_i128(v)) + .unwrap_or(0); + let new_val = current + amt; + storage.insert(key.to_vec(), i128_to_bytes(new_val)); + new_val.to_string() + }) + } + + pub fn import_kv_delete(key: &[u8]) { + MOCK_STORAGE.with(|s| { + s.borrow_mut().remove(key); + }); + } + + pub fn import_kv_get_prev(prefix: &[u8], key: &[u8]) -> (Option>, Option>) { + MOCK_STORAGE.with(|s| { + let storage = s.borrow(); + let full_key = if key.is_empty() { + prefix.to_vec() + } else { + [prefix, key].concat() + }; + + let mut found = None; + for (k, v) in storage.iter().rev() { + if k.starts_with(prefix) && k < &full_key { + found = Some((k.clone(), v.clone())); + break; + } + } + + match found { + Some((k, v)) => (Some(k), Some(v)), + None => (None, None), + } + }) + } + + pub fn import_kv_get_next(prefix: &[u8], key: &[u8]) -> (Option>, Option>) { + MOCK_STORAGE.with(|s| { + let storage = s.borrow(); + let full_key = if key.is_empty() { + prefix.to_vec() + } else { + [prefix, key].concat() + }; + + let mut found = None; + for (k, v) in storage.iter() { + if k.starts_with(prefix) && k > &full_key { + found = Some((k.clone(), v.clone())); + break; + } + } + + match found { + Some((k, v)) => (Some(k), Some(v)), + None => (None, None), + } + }) + } + + pub fn import_entry_slot() -> u64 { + MOCK_CONTEXT.with(|c| c.borrow().entry_slot) + } + + pub fn import_entry_height() -> u64 { + MOCK_CONTEXT.with(|c| c.borrow().entry_height) + } + + pub fn import_entry_epoch() -> u64 { + MOCK_CONTEXT.with(|c| c.borrow().entry_epoch) + } + + pub fn import_entry_signer() -> Vec { + MOCK_CONTEXT.with(|c| c.borrow().entry_signer.clone()) + } + + pub fn import_tx_nonce() -> u64 { + MOCK_CONTEXT.with(|c| c.borrow().tx_nonce) + } + + pub fn import_tx_signer() -> Vec { + MOCK_CONTEXT.with(|c| c.borrow().tx_signer.clone()) + } + + pub fn import_account_current() -> Vec { + MOCK_CONTEXT.with(|c| c.borrow().account_current.clone()) + } + + pub fn import_account_caller() -> Vec { + MOCK_CONTEXT.with(|c| c.borrow().account_caller.clone()) + } + + pub fn import_account_origin() -> Vec { + MOCK_CONTEXT.with(|c| c.borrow().account_origin.clone()) + } + + pub fn import_get_attachment() -> (bool, (Vec, Vec)) { + MOCK_CONTEXT.with(|c| { + match &c.borrow().attachment { + Some((symbol, amount)) => (true, (symbol.clone(), amount.clone())), + None => (false, (Vec::new(), Vec::new())), + } + }) + } + + pub fn import_seed() -> Vec { + MOCK_CONTEXT.with(|c| c.borrow().seed.clone()) + } + + pub fn import_log(msg: &str) { + #[cfg(test)] + println!("{}", msg); + } +} + +#[macro_export] +macro_rules! testing_env { + ($ctx:expr) => { + $crate::testing::set_context($ctx); + }; +} From ee78bad3e62b74fae3a9c8dde9c0d1cfebb0cb63 Mon Sep 17 00:00:00 2001 From: Valentyn Faychuk Date: Tue, 6 Jan 2026 07:44:41 +0200 Subject: [PATCH 11/11] fix the kv_exists bug Signed-off-by: Valentyn Faychuk --- contract_samples/rust/sdk/src/lib.rs | 4 ++-- contract_samples/rust/sdk/src/storage.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contract_samples/rust/sdk/src/lib.rs b/contract_samples/rust/sdk/src/lib.rs index 2a7ee62..43b90cc 100644 --- a/contract_samples/rust/sdk/src/lib.rs +++ b/contract_samples/rust/sdk/src/lib.rs @@ -264,7 +264,7 @@ where unsafe { let cache = &mut *self.cache.get(); if !cache.contains_key(&storage_key) { - if kv_get::(&storage_key).is_some() { + if kv_exists(&storage_key) { cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); } else { return None; @@ -280,7 +280,7 @@ where unsafe { let cache = &mut *self.cache.get(); if !cache.contains_key(&storage_key) { - if kv_get::(&storage_key).is_some() { + if kv_exists(&storage_key) { cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); } else { return None; diff --git a/contract_samples/rust/sdk/src/storage.rs b/contract_samples/rust/sdk/src/storage.rs index 75a0b74..50698b2 100644 --- a/contract_samples/rust/sdk/src/storage.rs +++ b/contract_samples/rust/sdk/src/storage.rs @@ -134,8 +134,8 @@ pub fn kv_exists(key: impl Payload) -> bool { } #[cfg(not(any(test, feature = "testing")))] unsafe { - let ptr = import_kv_exists(key_bytes.as_ptr(), key_bytes.len()); - *(ptr as *const i32) == 1 + let result = import_kv_exists(key_bytes.as_ptr(), key_bytes.len()); + result == 1 } }