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/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/README.md b/contract_samples/rust/README.md index 5030eb6..487c6e8 100644 --- a/contract_samples/rust/README.md +++ b/contract_samples/rust/README.md @@ -26,16 +26,15 @@ 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. + +## Unit Testing ```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 +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 ``` -### Testing +### Testnet Deployment Make sure you have `amadeus-cli` installed. Follow the code snippet below to run each example on the testnet. @@ -49,8 +48,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" @@ -66,7 +64,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 @@ -81,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/build_and_validate.sh b/contract_samples/rust/build_and_validate.sh index 0654bc1..6ebf954 100755 --- a/contract_samples/rust/build_and_validate.sh +++ b/contract_samples/rust/build_and_validate.sh @@ -1,19 +1,23 @@ #!/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 +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 +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 -# 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 @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/coin.rs b/contract_samples/rust/examples/coin.rs index 3926b94..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}; fn vault_key(symbol: &Vec) -> Vec { b!("vault:", account_caller(), ":", symbol) @@ -11,43 +13,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..61c05dc 100644 --- a/contract_samples/rust/examples/counter.rs +++ b/contract_samples/rust/examples/counter.rs @@ -1,30 +1,29 @@ #![no_std] #![no_main] + extern crate alloc; +use alloc::vec::Vec; +use alloc::string::String; use amadeus_sdk::*; #[no_mangle] pub extern "C" fn init() { - log("Init called during deployment of contract"); 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/deposit.rs b/contract_samples/rust/examples/deposit.rs index ac4dde1..d48fcb0 100644 --- a/contract_samples/rust/examples/deposit.rs +++ b/contract_samples/rust/examples/deposit.rs @@ -1,57 +1,45 @@ #![no_std] #![no_main] + extern crate alloc; +use alloc::vec::Vec; +use alloc::string::String; use amadeus_sdk::*; -use alloc::{vec::Vec}; 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..cc91d0e 100644 --- a/contract_samples/rust/examples/nft.rs +++ b/contract_samples/rust/examples/nft.rs @@ -1,6 +1,9 @@ #![no_std] #![no_main] + extern crate alloc; +use alloc::vec::Vec; +use alloc::string::String; use amadeus_sdk::*; #[no_mangle] @@ -8,11 +11,9 @@ 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 +21,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 +41,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/examples/showcase.rs b/contract_samples/rust/examples/showcase.rs new file mode 100644 index 0000000..efa9a89 --- /dev/null +++ b/contract_samples/rust/examples/showcase.rs @@ -0,0 +1,190 @@ +#![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; + +#[cfg(any(test, feature = "testing"))] +extern crate std; + +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, +} + +type PlayerWinsMap = MapFlat; +type MatchesMap = Map; +type PlayersMap = Map; + +#[contract_state] +struct Leaderboard { + #[flat] total_matches: i32, + player_wins: PlayerWinsMap, + players: PlayersMap, + tournament: TournamentInfo, +} + +#[contract] +impl Leaderboard { + pub fn increment_total_matches(&mut self) { + *self.total_matches += 1; + } + + 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) { + 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(&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; + } + } + 0 + } + + 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) { + *self.tournament.name = name; + *self.tournament.prize_pool = prize_pool; + } + + pub fn get_tournament_name(&self) -> String { + (*self.tournament.name).clone() + } + + pub fn get_tournament_prize(&self) -> u64 { + *self.tournament.prize_pool + } + + 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(&self, player: String) -> u32 { + if let Some(wins) = self.player_wins.get(&player) { + **wins + } else { + 0 + } + } + + pub fn set_player_wins(&mut self, player: String, wins: u32) { + 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/Cargo.toml b/contract_samples/rust/sdk-macros/Cargo.toml new file mode 100644 index 0000000..c79fd14 --- /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", "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 new file mode 100644 index 0000000..a0319ca --- /dev/null +++ b/contract_samples/rust/sdk-macros/src/lib.rs @@ -0,0 +1,279 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ItemImpl, ItemFn, ImplItem, FnArg, ReturnType, Type, ItemStruct, Fields}; + +#[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 vis = &input.vis; + let attrs = &input.attrs; + + let Fields::Named(ref fields) = input.fields else { + return TokenStream::from(quote! { #input }); + }; + + let is_flat = |f: &syn::Field| -> bool { + f.attrs.iter().any(|attr| attr.path().is_ident("flat")) + }; + + let transformed_fields = fields.named.iter().map(|f| { + let field_name = &f.ident; + let field_vis = &f.vis; + let field_ty = &f.ty; + let filtered_attrs: Vec<_> = f.attrs.iter() + .filter(|attr| !attr.path().is_ident("flat")) + .collect(); + + if is_flat(f) { + quote! { + #(#filtered_attrs)* + #field_vis #field_name: LazyCell<#field_ty> + } + } else { + quote! { + #(#filtered_attrs)* + #field_vis #field_name: #field_ty + } + } + }); + + 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! { + #field: { + let mut key = prefix.clone(); + key.extend_from_slice(#key.as_bytes()); + LazyCell::with_prefix(key) + } + } + } else { + quote! { + #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(); + quote! { self.#field.flush(); } + }); + + TokenStream::from(quote! { + #(#attrs)* + #vis struct #name { + #(#transformed_fields),* + } + + impl ContractState for #name { + fn with_prefix(prefix: alloc::vec::Vec) -> Self { + Self { + #(#init_fields),* + } + } + + fn flush(&self) { + #(#flush_calls)* + } + } + }) +} + +#[proc_macro_attribute] +pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { + 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 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(); + 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 ty = &pat_type.ty; + let ptr = syn::Ident::new(&format!("{}_ptr", quote!(#param)), name.span()); + + 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;), 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 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(); + #return_wrapper + } + } else { + quote! { + #(#deserializations)* + let mut state = <#self_ty as ContractState>::with_prefix(alloc::vec::Vec::new()); + state.#call; + state.flush(); + } + }; + + 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 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 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 = &pat_type.pat; + let ty = &pat_type.ty; + let ptr = syn::Ident::new(&format!("arg{}_ptr", idx), name.span()); + + 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 { + params.extend(quote!(, #ptr: i32)); + call_args.extend(quote!(, #param)); + } else { + params.extend(quote!(#ptr: i32)); + call_args.extend(quote!(#param)); + } + + deserializations.extend(quote! { let #param = #deser; }); + idx += 1; + } + } + + let sig = if idx == 0 { + quote!(#[no_mangle] pub extern "C" fn #name()) + } else { + quote!(#[no_mangle] pub extern "C" fn #name(#params)) + }; + + let call = if has_return { + quote!(ret(#impl_name(#call_args));) + } else { + quote!(#impl_name(#call_args);) + }; + + TokenStream::from(quote! { + #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 new file mode 100644 index 0000000..bedc55c --- /dev/null +++ b/contract_samples/rust/sdk/Cargo.toml @@ -0,0 +1,43 @@ +[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] +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 + +[[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 = "showcase" +path = "../examples/showcase.rs" diff --git a/contract_samples/rust/src/context.rs b/contract_samples/rust/sdk/src/context.rs similarity index 55% rename from contract_samples/rust/src/context.rs rename to contract_samples/rust/sdk/src/context.rs index c49c8f4..a80fda2 100644 --- a/contract_samples/rust/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/src/encoding.rs b/contract_samples/rust/sdk/src/encoding.rs similarity index 96% rename from contract_samples/rust/src/encoding.rs rename to contract_samples/rust/sdk/src/encoding.rs index 1c607c6..9c39bc6 100644 --- a/contract_samples/rust/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 new file mode 100644 index 0000000..43b90cc --- /dev/null +++ b/contract_samples/rust/sdk/src/lib.rs @@ -0,0 +1,543 @@ +#![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::*; +pub use amadeus_sdk_macros::{contract, contract_state}; + +use alloc::vec; + +pub trait ContractState { + fn with_prefix(prefix: Vec) -> Self; + fn flush(&self); +} + +use core::panic::PanicInfo; + +use alloc::{borrow::Cow, vec::Vec, string::String, string::ToString}; + + +#[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; + +#[macro_export] +macro_rules! assert { + ($cond:expr, $msg:expr) => { + if !$cond { + $crate::context::log($msg); + + #[cfg(target_arch = "wasm32")] + core::arch::wasm32::unreachable(); + } + }; +} + +#[macro_export] +macro_rules! abort { + ($msg:expr) => { + { + $crate::context::log($msg); + + #[cfg(all(target_arch = "wasm32", not(test)))] + core::arch::wasm32::unreachable(); + + #[cfg(test)] + panic!($msg); + + #[allow(unreachable_code)] + loop {} + } + }; +} + +#[macro_export] +macro_rules! b { + ( $( $x:expr ),* ) => { + { + let mut temp_vec = alloc::vec::Vec::new(); + $( + temp_vec.extend_from_slice($x.as_ref()); + )* + temp_vec + } + }; +} + +macro_rules! impl_payload_for_ints { + ( $($t:ty),* ) => { + $( + impl Payload for $t { + fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { + Cow::Owned(self.to_string().into_bytes()) + } + } + )* + }; +} + +pub trait Payload { + fn to_payload<'a>(&'a self) -> Cow<'a, [u8]>; +} + +impl Payload for &str { + fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { Cow::Borrowed(self.as_bytes()) } +} + +impl Payload for String { + fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { Cow::Borrowed(self.as_bytes()) } +} + +impl Payload for &[u8] { + fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { Cow::Borrowed(self) } +} + +impl Payload for Vec { + fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { Cow::Borrowed(self.as_slice()) } +} + +impl Payload for &alloc::vec::Vec { + fn to_payload<'a>(&'a self) -> alloc::borrow::Cow<'a, [u8]> { + alloc::borrow::Cow::Borrowed(self.as_slice()) + } +} + +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 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 ContractState for LazyCell { + fn with_prefix(prefix: Vec) -> Self { + Self { + 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(); + *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); + } + + 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); + } +} + +impl LazyCell { + + pub fn with_mut(&mut self, f: F) -> R + where + F: FnOnce(&mut T) -> R + { + if self.value.borrow().is_none() { + let loaded = T::with_prefix(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 loaded = T::with_prefix(self.key.clone()); + *self.value.borrow_mut() = Some(loaded); + } + unsafe { + let ptr = self.value.as_ptr(); + f((*ptr).as_ref().unwrap()) + } + } +} + +use alloc::collections::BTreeMap; + +pub struct MapFlat { + prefix: Vec, + cache: core::cell::UnsafeCell, LazyCell>>, + _phantom: core::marker::PhantomData, +} + +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(), key_bytes.as_ref()) + } + + 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) { + cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); + } else { + return None; + } + } + cache.get(&storage_key) + } + } + + pub fn get_mut(&mut self, key: &K) -> Option<&mut 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) { + cache.insert(storage_key.clone(), LazyCell::with_prefix(storage_key.clone())); + } else { + return None; + } + } + + 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); + unsafe { + (*self.cache.get()).insert(storage_key, cell); + } + } + + pub fn remove(&mut self, key: &K) { + let storage_key = self.build_key(key); + unsafe { + (*self.cache.get()).remove(&storage_key); + } + kv_delete(&storage_key); + } +} + +impl ContractState for MapFlat +where + K: Payload, + V: FromKvBytes + Payload + Default + Clone +{ + fn with_prefix(prefix: Vec) -> Self { + Self { + prefix, + cache: core::cell::UnsafeCell::new(BTreeMap::new()), + _phantom: core::marker::PhantomData, + } + } + + fn flush(&self) { + unsafe { + for cell in (*self.cache.get()).values() { + cell.flush(); + } + } + } +} + +pub struct Map { + prefix: Vec, + 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, + V: ContractState +{ + fn build_key(&self, key: &K) -> Vec { + let key_bytes = key.to_payload(); + b!(self.prefix.as_slice(), key_bytes.as_ref()) + } + + pub fn get(&self, key: K) -> Option<&V> { + let storage_key = self.build_key(&key); + + 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(&storage_key).map(|v| &*(v as *const V)) + } + } + + pub fn get_mut(&mut self, key: K) -> Option<&mut V> { + let storage_key = self.build_key(&key); + + 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, + } + } + } + + 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); + unsafe { + (*self.cache.get()).remove(&storage_key); + } + } +} + +impl ContractState for Map +where + K: Payload, + V: ContractState +{ + fn with_prefix(prefix: Vec) -> Self { + Self { + prefix, + cache: core::cell::UnsafeCell::new(BTreeMap::new()), + _phantom: core::marker::PhantomData, + } + } + + fn flush(&self) { + unsafe { + for value in (*self.cache.get()).values() { + value.flush(); + } + } + } +} diff --git a/contract_samples/rust/src/storage.rs b/contract_samples/rust/sdk/src/storage.rs similarity index 74% rename from contract_samples/rust/src/storage.rs rename to contract_samples/rust/sdk/src/storage.rs index e789a4a..50698b2 100644 --- a/contract_samples/rust/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 { @@ -32,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; @@ -48,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(), @@ -61,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 { @@ -77,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(), @@ -90,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(), @@ -101,9 +128,14 @@ 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 + let result = import_kv_exists(key_bytes.as_ptr(), key_bytes.len()); + result == 1 } } @@ -112,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); @@ -128,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); + }; +} diff --git a/contract_samples/rust/src/lib.rs b/contract_samples/rust/src/lib.rs deleted file mode 100644 index c6b6f69..0000000 --- a/contract_samples/rust/src/lib.rs +++ /dev/null @@ -1,102 +0,0 @@ -#![no_std] -extern crate alloc; - -pub mod context; -pub mod storage; -pub mod encoding; - -pub use context::*; -pub use storage::*; -pub use encoding::*; - -use core::panic::PanicInfo; - -use alloc::{borrow::Cow, vec::Vec, string::String, string::ToString}; - - -#[cfg(not(test))] -#[panic_handler] -fn panic(_: &PanicInfo) -> ! { - loop {} -} - -#[global_allocator] -static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; - -#[macro_export] -macro_rules! assert { - ($cond:expr, $msg:expr) => { - if !$cond { - $crate::context::log($msg); - - #[cfg(target_arch = "wasm32")] - core::arch::wasm32::unreachable(); - } - }; -} - -#[macro_export] -macro_rules! abort { - ($msg:expr) => { - { - $crate::context::log($msg); - - #[cfg(target_arch = "wasm32")] - core::arch::wasm32::unreachable(); - } - }; -} - -#[macro_export] -macro_rules! b { - ( $( $x:expr ),* ) => { - { - let mut temp_vec = alloc::vec::Vec::new(); - $( - temp_vec.extend_from_slice($x.as_ref()); - )* - temp_vec - } - }; -} - -macro_rules! impl_payload_for_ints { - ( $($t:ty),* ) => { - $( - impl Payload for $t { - fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { - Cow::Owned(self.to_string().into_bytes()) - } - } - )* - }; -} - -pub trait Payload { - fn to_payload<'a>(&'a self) -> Cow<'a, [u8]>; -} - -impl Payload for &str { - fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { Cow::Borrowed(self.as_bytes()) } -} - -impl Payload for String { - fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { Cow::Borrowed(self.as_bytes()) } -} - -impl Payload for &[u8] { - fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { Cow::Borrowed(self) } -} - -impl Payload for Vec { - fn to_payload<'a>(&'a self) -> Cow<'a, [u8]> { Cow::Borrowed(self.as_slice()) } -} - -impl Payload for &alloc::vec::Vec { - fn to_payload<'a>(&'a self) -> alloc::borrow::Cow<'a, [u8]> { - alloc::borrow::Cow::Borrowed(self.as_slice()) - } -} - -impl_payload_for_ints!(u8, u16, u32, u64, u128, usize); -impl_payload_for_ints!(i8, i16, i32, i64, i128, isize); 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