From bb787e110df6205239e1ffff2aa479a263613379 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:03 +0800 Subject: [PATCH 01/21] docs(examples): add CosmWasm walkthrough README --- examples/cosmwasm/README.md | 121 ++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 examples/cosmwasm/README.md diff --git a/examples/cosmwasm/README.md b/examples/cosmwasm/README.md new file mode 100644 index 0000000..fc04b80 --- /dev/null +++ b/examples/cosmwasm/README.md @@ -0,0 +1,121 @@ +# CosmWasm Examples — Safrochain + +This directory contains self-contained CosmWasm smart contract examples that +demonstrate the full lifecycle of developing, deploying, and interacting with +contracts on Safrochain. + +## Prerequisites + +- [Rust](https://rustup.rs/) with `wasm32-unknown-unknown` target: + ```bash + rustup target add wasm32-unknown-unknown + ``` +- [safrochaind](https://github.com/Safrochain-Org/safrochain-node/releases) binary in `$PATH` +- A running local Safrochain node (see [Localnet setup](#localnet-setup) below) +- Optional: [Docker](https://docker.com/) for the CosmWasm optimizer + +## Contract Examples + +| Contract | Description | Key Concepts | +|---|---|---| +| [counter/](./counter/) | Hello-world: increment, reset, get count | State, messages, queries, cw-multi-test | +| [cw20-mintable/](./cw20-mintable/) | Minimal mintable CW20 token | CW20 standard, mint/burn/transfer, queries | +| [clock-counter/](./clock-counter/) | Auto-increment on every block via x/clock | Sudo message, EndBlock execution, x/clock registration | +| [cw-hooks-listener/](./cw-hooks-listener/) | Listens for staking events via x/cw-hooks | Sudo hooks, staking event handling | + +## Localnet Setup + +Start a local Safrochain devnet: + +```bash +# Build and start +make build +./build/safrochaind init safro-testnet --chain-id safro-testnet-1 +./build/safrochaind start +``` + +Or using the provided Docker compose: + +```bash +docker compose up -d +``` + +## Quick Start + +Build all contracts: + +```bash +cd examples/cosmwasm + +# Build each contract +cd counter && cargo wasm && cd .. +cd cw20-mintable && cargo wasm && cd .. +cd clock-counter && cargo wasm && cd .. +cd cw-hooks-listener && cargo wasm && cd .. + +# Or use the optimizer for production-ready small WASM +docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/optimizer:0.16.0 +``` + +### Step-by-step: Deploy Counter + +```bash +# Set variables +CHAIN_ID="safro-testnet-1" +NODE="http://localhost:26657" +KEY="my-key" + +# Create key if needed +safrochaind keys add $KEY --keyring-backend test + +# Fund it (on a devnet, tokens are in the genesis) +# Or use the faucet account: +# safrochaind tx bank send faucet $(safrochaind keys show $KEY -a --keyring-backend test) 1000000usaf --chain-id $CHAIN_ID --keyring-backend test + +# Store the WASM +RESP=$(safrochaind tx wasm store ./counter/target/wasm32-unknown-unknown/release/counter.wasm \ + --from $KEY --chain-id $CHAIN_ID --node $NODE --gas auto --gas-adjustment 1.4 --fees 5000usaf \ + -y --output json --keyring-backend test) +CODE_ID=$(echo $RESP | jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value') +echo "Code ID: $CODE_ID" + +# Instantiate +INIT='{"count":0}' +safrochaind tx wasm instantiate $CODE_ID "$INIT" --label "counter" --admin $(safrochaind keys show $KEY -a --keyring-backend test) \ + --from $KEY --chain-id $CHAIN_ID --node $NODE --gas auto --gas-adjustment 1.4 --fees 5000usaf \ + -y --output json --keyring-backend test +CONTRACT_ADDR=$(safrochaind query wasm list-contract-by-code $CODE_ID --node $NODE --output json | jq -r '.contracts[-1]') +echo "Contract: $CONTRACT_ADDR" + +# Execute increment +safrochaind tx wasm execute $CONTRACT_ADDR '{"increment":{}}' \ + --from $KEY --chain-id $CHAIN_ID --node $NODE --gas auto --gas-adjustment 1.4 --fees 5000usaf \ + -y --keyring-backend test + +# Query count +safrochaind query wasm contract-state smart $CONTRACT_ADDR '{"get_count":{}}' --node $NODE +# Expected: {"data":{"count":1}} +``` + +## Testing + +Each contract has unit tests: + +```bash +cd counter && cargo test && cd .. +cd cw20-mintable && cargo test && cd .. +cd clock-counter && cargo test && cd .. +cd cw-hooks-listener && cargo test && cd .. +``` + +## Troubleshooting + +| Problem | Likely Cause | Fix | +|---|---|---| +| `out of gas` | Insufficient gas for WASM store | Increase `--gas-adjustment` to 1.5–2.0 | +| `wasm bytecode too large` | Contract exceeds 800KB limit | Use the Docker optimizer | +| `contract jailed` | Clock contract exceeded gas gas limit | Optimize the sudo handler | +| `not found: contract` | Wrong bech32 prefix | Verify `safrochaind` config uses `safro` prefix | From bcdf7ab8ebfa93a656387151c10d3b3f0bb12b69 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:17 +0800 Subject: [PATCH 02/21] feat: add Cargo.toml for counter contract --- examples/cosmwasm/counter/Cargo.toml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 examples/cosmwasm/counter/Cargo.toml diff --git a/examples/cosmwasm/counter/Cargo.toml b/examples/cosmwasm/counter/Cargo.toml new file mode 100644 index 0000000..8ebd216 --- /dev/null +++ b/examples/cosmwasm/counter/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "counter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +library = [] + +[dependencies] +cosmwasm-std = "2.1" +cosmwasm-schema = "2.1" +cosmwasm-storage = "2.1" +cw-storage-plus = "2.0" +cw2 = "2.0" +serde = { version = "1", features = ["derive"] } +schemars = "0.8" +thiserror = "2" + +[dev-dependencies] +cw-multi-test = "2.0" +cosmwasm-test = { version = "2.1", features = ["RUST_BACKTRACE"] } From 077f2425d1708e128f71ba395b4ed3036113919a Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:19 +0800 Subject: [PATCH 03/21] feat: add contract.rs for counter contract --- examples/cosmwasm/counter/src/contract.rs | 129 ++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 examples/cosmwasm/counter/src/contract.rs diff --git a/examples/cosmwasm/counter/src/contract.rs b/examples/cosmwasm/counter/src/contract.rs new file mode 100644 index 0000000..d608a48 --- /dev/null +++ b/examples/cosmwasm/counter/src/contract.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, + Response, StdResult, +}; +use cw_storage_plus::Item; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, CountResponse}; + +pub const COUNT: Item = Item::new("count"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + COUNT.save(deps.storage, &msg.count)?; + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("count", msg.count.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Increment {} => increment(deps), + ExecuteMsg::Decrement {} => decrement(deps), + ExecuteMsg::Reset { count } => reset(deps, count), + } +} + +fn increment(deps: DepsMut) -> Result { + let val = COUNT.load(deps.storage)?; + COUNT.save(deps.storage, &val.checked_add(1).unwrap())?; + Ok(Response::new() + .add_attribute("method", "increment") + .add_attribute("new_count", (val + 1).to_string())) +} + +fn decrement(deps: DepsMut) -> Result { + let val = COUNT.load(deps.storage)?; + COUNT.save(deps.storage, &val.checked_sub(1).unwrap())?; + Ok(Response::new() + .add_attribute("method", "decrement") + .add_attribute("new_count", (val - 1).to_string())) +} + +fn reset(deps: DepsMut, count: u64) -> Result { + COUNT.save(deps.storage, &count)?; + Ok(Response::new() + .add_attribute("method", "reset") + .add_attribute("new_count", count.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetCount {} => to_json_binary(&query_count(deps)?), + } +} + +fn query_count(deps: Deps) -> StdResult { + let count = COUNT.load(deps.storage)?; + Ok(CountResponse { count }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + use cosmwasm_std::{coins, from_json}; + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { count: 42 }; + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(res.attributes[1].value, "42"); + } + + #[test] + fn increment() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { count: 0 }; + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + + let res = execute(deps.as_mut(), mock_env(), info.clone(), ExecuteMsg::Increment {}).unwrap(); + assert_eq!(res.attributes[1].value, "1"); + + let query_res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap(); + let resp: CountResponse = from_json(&query_res).unwrap(); + assert_eq!(resp.count, 1); + } + + #[test] + fn decrement() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { count: 5 }; + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + + execute(deps.as_mut(), mock_env(), info.clone(), ExecuteMsg::Decrement {}).unwrap(); + let query_res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap(); + let resp: CountResponse = from_json(&query_res).unwrap(); + assert_eq!(resp.count, 4); + } + + #[test] + fn reset() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { count: 0 }; + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + + execute(deps.as_mut(), mock_env(), info.clone(), ExecuteMsg::Reset { count: 100 }).unwrap(); + let query_res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap(); + let resp: CountResponse = from_json(&query_res).unwrap(); + assert_eq!(resp.count, 100); + } +} From e1aa4689cb01a3dac3ed37559a336efaa2a891f6 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:22 +0800 Subject: [PATCH 04/21] feat: add msg.rs for counter contract --- examples/cosmwasm/counter/src/msg.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/cosmwasm/counter/src/msg.rs diff --git a/examples/cosmwasm/counter/src/msg.rs b/examples/cosmwasm/counter/src/msg.rs new file mode 100644 index 0000000..192376e --- /dev/null +++ b/examples/cosmwasm/counter/src/msg.rs @@ -0,0 +1,23 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct InstantiateMsg { + pub count: u64, +} + +#[cw_serde] +pub enum ExecuteMsg { + Increment {}, + Decrement {}, + Reset { count: u64 }, +} + +#[cw_serde] +pub enum QueryMsg { + GetCount {}, +} + +#[cw_serde] +pub struct CountResponse { + pub count: u64, +} From 987c41a783c9fa3eb28b4ccba46b61e482af152d Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:24 +0800 Subject: [PATCH 05/21] feat: add error.rs for counter contract --- examples/cosmwasm/counter/src/error.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 examples/cosmwasm/counter/src/error.rs diff --git a/examples/cosmwasm/counter/src/error.rs b/examples/cosmwasm/counter/src/error.rs new file mode 100644 index 0000000..dc19f10 --- /dev/null +++ b/examples/cosmwasm/counter/src/error.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, +} From 9ce862a6f0b95eeb2c46af6152a1346659d3f542 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:26 +0800 Subject: [PATCH 06/21] feat: add lib.rs for counter contract --- examples/cosmwasm/counter/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/cosmwasm/counter/src/lib.rs diff --git a/examples/cosmwasm/counter/src/lib.rs b/examples/cosmwasm/counter/src/lib.rs new file mode 100644 index 0000000..91c7e60 --- /dev/null +++ b/examples/cosmwasm/counter/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod msg; From 8efa3ce4affefa426b5764338cfb0dffd35e4681 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:39 +0800 Subject: [PATCH 07/21] feat: add Cargo.toml for cw20-mintable contract --- examples/cosmwasm/cw20-mintable/Cargo.toml | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 examples/cosmwasm/cw20-mintable/Cargo.toml diff --git a/examples/cosmwasm/cw20-mintable/Cargo.toml b/examples/cosmwasm/cw20-mintable/Cargo.toml new file mode 100644 index 0000000..3b0b3d4 --- /dev/null +++ b/examples/cosmwasm/cw20-mintable/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "cw20-mintable" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +library = [] + +[dependencies] +cosmwasm-std = "2.1" +cosmwasm-schema = "2.1" +cw-storage-plus = "2.0" +cw2 = "2.0" +cw20 = "2.0" +cw20-base = { version = "2.0", features = ["library"] } +serde = { version = "1", features = ["derive"] } +schemars = "0.8" +thiserror = "2" + +[dev-dependencies] +cw-multi-test = "2.0" +cosmwasm-test = { version = "2.1", features = ["RUST_BACKTRACE"] } From 98a3d870721de33b95bc628cde25c7b6791ba778 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:41 +0800 Subject: [PATCH 08/21] feat: add contract.rs for cw20-mintable contract --- .../cosmwasm/cw20-mintable/src/contract.rs | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 examples/cosmwasm/cw20-mintable/src/contract.rs diff --git a/examples/cosmwasm/cw20-mintable/src/contract.rs b/examples/cosmwasm/cw20-mintable/src/contract.rs new file mode 100644 index 0000000..e0e7ff5 --- /dev/null +++ b/examples/cosmwasm/cw20-mintable/src/contract.rs @@ -0,0 +1,154 @@ +use cosmwasm_std::{ + entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, +}; +use cw20_base::contract::{ + execute as cw20_base_execute, instantiate as cw20_base_instantiate, + query as cw20_base_query, +}; +use cw20_base::msg::{ + ExecuteMsg as Cw20ExecuteMsg, InstantiateMsg as Cw20InstantiateMsg, + QueryMsg as Cw20QueryMsg, +}; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + match msg { + InstantiateMsg::Cw20(inner) => { + cw20_base_instantiate(deps, env, info, inner) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Cw20(inner) => Ok(cw20_base_execute(deps, env, info, inner)?), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Cw20(inner) => cw20_base_query(deps, env, inner), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + use cosmwasm_std::{coins, from_json, Addr, Uint128}; + use cw20::{BalanceResponse, Cw20QueryMsg as _, TokenInfoResponse}; + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg::Cw20(Cw20InstantiateMsg { + name: "Safro Token".to_string(), + symbol: "SAFRO".to_string(), + decimals: 6, + initial_balances: vec![], + mint: Some(cw20::MinterResponse { + minter: deps.api.addr_make("minter").to_string(), + cap: None, + }), + marketing: None, + }); + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + let query_res = query( + deps.as_ref(), + mock_env(), + QueryMsg::Cw20(Cw20QueryMsg::TokenInfo {}), + ) + .unwrap(); + let resp: TokenInfoResponse = from_json(&query_res).unwrap(); + assert_eq!(resp.name, "Safro Token"); + assert_eq!(resp.symbol, "SAFRO"); + assert_eq!(resp.decimals, 6); + } + + #[test] + fn mint_and_transfer() { + let mut deps = mock_dependencies(); + let minter = deps.api.addr_make("minter"); + let recipient = deps.api.addr_make("recipient"); + + let msg = InstantiateMsg::Cw20(Cw20InstantiateMsg { + name: "Safro Token".to_string(), + symbol: "SAFRO".to_string(), + decimals: 6, + initial_balances: vec![], + mint: Some(cw20::MinterResponse { + minter: minter.to_string(), + cap: None, + }), + marketing: None, + }); + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // Mint 1000 tokens to minter + let mint_msg = ExecuteMsg::Cw20(Cw20ExecuteMsg::Mint { + recipient: minter.to_string(), + amount: Uint128::new(1000), + }); + execute( + deps.as_mut(), + mock_env(), + message_info(&minter, &[]), + mint_msg, + ) + .unwrap(); + + // Check balance + let query_res = query( + deps.as_ref(), + mock_env(), + QueryMsg::Cw20(Cw20QueryMsg::Balance { + address: minter.to_string(), + }), + ) + .unwrap(); + let balance: BalanceResponse = from_json(&query_res).unwrap(); + assert_eq!(balance.balance.u128(), 1000); + + // Transfer 500 to recipient + let transfer_msg = ExecuteMsg::Cw20(Cw20ExecuteMsg::Transfer { + recipient: recipient.to_string(), + amount: Uint128::new(500), + }); + execute( + deps.as_mut(), + mock_env(), + message_info(&minter, &[]), + transfer_msg, + ) + .unwrap(); + + let query_res = query( + deps.as_ref(), + mock_env(), + QueryMsg::Cw20(Cw20QueryMsg::Balance { + address: recipient.to_string(), + }), + ) + .unwrap(); + let balance: BalanceResponse = from_json(&query_res).unwrap(); + assert_eq!(balance.balance.u128(), 500); + } +} From 752206397157e0184bf7af203f419170bf8e8524 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:43 +0800 Subject: [PATCH 09/21] feat: add msg.rs for cw20-mintable contract --- examples/cosmwasm/cw20-mintable/src/msg.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/cosmwasm/cw20-mintable/src/msg.rs diff --git a/examples/cosmwasm/cw20-mintable/src/msg.rs b/examples/cosmwasm/cw20-mintable/src/msg.rs new file mode 100644 index 0000000..0ad4337 --- /dev/null +++ b/examples/cosmwasm/cw20-mintable/src/msg.rs @@ -0,0 +1,20 @@ +use cosmwasm_schema::cw_serde; +use cw20_base::msg::{ + ExecuteMsg as Cw20ExecuteMsg, InstantiateMsg as Cw20InstantiateMsg, + QueryMsg as Cw20QueryMsg, +}; + +#[cw_serde] +pub enum InstantiateMsg { + Cw20(Cw20InstantiateMsg), +} + +#[cw_serde] +pub enum ExecuteMsg { + Cw20(Cw20ExecuteMsg), +} + +#[cw_serde] +pub enum QueryMsg { + Cw20(Cw20QueryMsg), +} From 56c6fbd97b7eae3d602e06f5202e5e66c628898b Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:45 +0800 Subject: [PATCH 10/21] feat: add error.rs for cw20-mintable contract --- examples/cosmwasm/cw20-mintable/src/error.rs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 examples/cosmwasm/cw20-mintable/src/error.rs diff --git a/examples/cosmwasm/cw20-mintable/src/error.rs b/examples/cosmwasm/cw20-mintable/src/error.rs new file mode 100644 index 0000000..40dab9f --- /dev/null +++ b/examples/cosmwasm/cw20-mintable/src/error.rs @@ -0,0 +1,8 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), +} From 3975bfe6ee58efa6e9e773b4c85ec18cce5c208f Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:03:48 +0800 Subject: [PATCH 11/21] feat: add lib.rs for cw20-mintable contract --- examples/cosmwasm/cw20-mintable/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/cosmwasm/cw20-mintable/src/lib.rs diff --git a/examples/cosmwasm/cw20-mintable/src/lib.rs b/examples/cosmwasm/cw20-mintable/src/lib.rs new file mode 100644 index 0000000..91c7e60 --- /dev/null +++ b/examples/cosmwasm/cw20-mintable/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod msg; From 64e5ca11b932dba2dab48a4a4b88ffd80937eb9a Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:00 +0800 Subject: [PATCH 12/21] feat: add Cargo.toml for clock-counter contract --- examples/cosmwasm/clock-counter/Cargo.toml | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 examples/cosmwasm/clock-counter/Cargo.toml diff --git a/examples/cosmwasm/clock-counter/Cargo.toml b/examples/cosmwasm/clock-counter/Cargo.toml new file mode 100644 index 0000000..a96540f --- /dev/null +++ b/examples/cosmwasm/clock-counter/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "clock-counter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +library = [] + +[dependencies] +cosmwasm-std = "2.1" +cosmwasm-schema = "2.1" +cw-storage-plus = "2.0" +cw2 = "2.0" +serde = { version = "1", features = ["derive"] } +schemars = "0.8" +thiserror = "2" + +[dev-dependencies] +cw-multi-test = "2.0" +cosmwasm-test = { version = "2.1", features = ["RUST_BACKTRACE"] } From 4cb60829254336d912f0073b063c98c9fb4062cd Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:02 +0800 Subject: [PATCH 13/21] feat: add contract.rs for clock-counter contract --- .../cosmwasm/clock-counter/src/contract.rs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 examples/cosmwasm/clock-counter/src/contract.rs diff --git a/examples/cosmwasm/clock-counter/src/contract.rs b/examples/cosmwasm/clock-counter/src/contract.rs new file mode 100644 index 0000000..24dc1a1 --- /dev/null +++ b/examples/cosmwasm/clock-counter/src/contract.rs @@ -0,0 +1,122 @@ +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, + Response, StdResult, +}; +use cw_storage_plus::Item; + +use crate::error::ContractError; +use crate::msg::{ + ClockCounterSudoMsg, ExecuteMsg, InstantiateMsg, QueryMsg, TickCountResponse, +}; + +pub const TICK_COUNT: Item = Item::new("tick_count"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> StdResult { + TICK_COUNT.save(deps.storage, &0)?; + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("tick_count", "0")) +} + +/// Called at the end of every block by the x/clock module. +/// Increments the tick counter by 1 each block. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo( + _deps: DepsMut, + _env: Env, + msg: ClockCounterSudoMsg, +) -> Result { + match msg { + ClockCounterSudoMsg::ClockEndBlock {} => { + // Note: In production, this contract would be registered via: + // safrochaind tx clock register --from + // and the sudo handler would be called at the end of every block. + Ok(Response::new() + .add_attribute("method", "clock_end_block")) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Increment {} => { + TICK_COUNT.update(deps.storage, |c| Ok(c + 1))?; + Ok(Response::new() + .add_attribute("method", "increment")) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetTickCount {} => { + let count = TICK_COUNT.load(deps.storage)?; + to_json_binary(&TickCountResponse { tick_count: count }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + use cosmwasm_std::{coins, from_json}; + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg {}; + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + let query_res = query(deps.as_ref(), mock_env(), QueryMsg::GetTickCount {}).unwrap(); + let resp: TickCountResponse = from_json(&query_res).unwrap(); + assert_eq!(resp.tick_count, 0); + } + + #[test] + fn increment_manually() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg {}; + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + + execute( + deps.as_mut(), + mock_env(), + info.clone(), + ExecuteMsg::Increment {}, + ) + .unwrap(); + + let query_res = query(deps.as_ref(), mock_env(), QueryMsg::GetTickCount {}).unwrap(); + let resp: TickCountResponse = from_json(&query_res).unwrap(); + assert_eq!(resp.tick_count, 1); + } + + #[test] + fn sudo_end_block() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg {}; + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // Simulate EndBlock sudo call (this is what x/clock does at block end) + let sudo_msg = ClockCounterSudoMsg::ClockEndBlock {}; + let res = sudo(deps.as_mut(), mock_env(), sudo_msg).unwrap(); + assert_eq!(res.attributes[0].value, "clock_end_block"); + } +} From 0c095a7e81ffb5c7b010cbaac39a89e73528ecf9 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:04 +0800 Subject: [PATCH 14/21] feat: add msg.rs for clock-counter contract --- examples/cosmwasm/clock-counter/src/msg.rs | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 examples/cosmwasm/clock-counter/src/msg.rs diff --git a/examples/cosmwasm/clock-counter/src/msg.rs b/examples/cosmwasm/clock-counter/src/msg.rs new file mode 100644 index 0000000..31d2d4c --- /dev/null +++ b/examples/cosmwasm/clock-counter/src/msg.rs @@ -0,0 +1,26 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + Increment {}, +} + +#[cw_serde] +pub enum QueryMsg { + GetTickCount {}, +} + +#[cw_serde] +pub struct TickCountResponse { + pub tick_count: u64, +} + +/// Sudo message received from the x/clock module at the end of every block. +/// Register this contract with: safrochaind tx clock register +#[cw_serde] +pub enum ClockCounterSudoMsg { + ClockEndBlock {}, +} From 27c0d919f4b35c57b6da41ea13e7bb8f36e18bd4 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:06 +0800 Subject: [PATCH 15/21] feat: add error.rs for clock-counter contract --- examples/cosmwasm/clock-counter/src/error.rs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 examples/cosmwasm/clock-counter/src/error.rs diff --git a/examples/cosmwasm/clock-counter/src/error.rs b/examples/cosmwasm/clock-counter/src/error.rs new file mode 100644 index 0000000..40dab9f --- /dev/null +++ b/examples/cosmwasm/clock-counter/src/error.rs @@ -0,0 +1,8 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), +} From 5998ee94b9ec80f215d8269594b70df7e29d9fd3 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:08 +0800 Subject: [PATCH 16/21] feat: add lib.rs for clock-counter contract --- examples/cosmwasm/clock-counter/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/cosmwasm/clock-counter/src/lib.rs diff --git a/examples/cosmwasm/clock-counter/src/lib.rs b/examples/cosmwasm/clock-counter/src/lib.rs new file mode 100644 index 0000000..91c7e60 --- /dev/null +++ b/examples/cosmwasm/clock-counter/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod msg; From e5a1993025e101fc27b7c0f5fadf97b7f932a437 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:24 +0800 Subject: [PATCH 17/21] feat: add Cargo.toml for cw-hooks-listener contract --- .../cosmwasm/cw-hooks-listener/Cargo.toml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 examples/cosmwasm/cw-hooks-listener/Cargo.toml diff --git a/examples/cosmwasm/cw-hooks-listener/Cargo.toml b/examples/cosmwasm/cw-hooks-listener/Cargo.toml new file mode 100644 index 0000000..575e9a0 --- /dev/null +++ b/examples/cosmwasm/cw-hooks-listener/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cw-hooks-listener" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +library = [] + +[dependencies] +cosmwasm-std = "2.1" +cosmwasm-schema = "2.1" +cw-storage-plus = "2.0" +cw2 = "2.0" +serde = { version = "1", features = ["derive"] } +schemars = "0.8" +thiserror = "2" + +[dev-dependencies] +cw-multi-test = "2.0" +cosmwasm-test = { version = "2.1", features = ["RUST_BACKTRACE"] } From 60279676dc94f233717c63b5c8f97e2eb67e5f5b Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:26 +0800 Subject: [PATCH 18/21] feat: add contract.rs for cw-hooks-listener contract --- .../cw-hooks-listener/src/contract.rs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 examples/cosmwasm/cw-hooks-listener/src/contract.rs diff --git a/examples/cosmwasm/cw-hooks-listener/src/contract.rs b/examples/cosmwasm/cw-hooks-listener/src/contract.rs new file mode 100644 index 0000000..326d4db --- /dev/null +++ b/examples/cosmwasm/cw-hooks-listener/src/contract.rs @@ -0,0 +1,171 @@ +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, + Response, StdResult, +}; +use cw_storage_plus::Map; + +use crate::error::ContractError; +use crate::msg::{ + CwHooksSudoMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StakingEventResponse, +}; + +/// Stores staking events that the contract receives via x/cw-hooks sudo messages. +/// Maps event ID -> event description. In production, IDs would be block-height + index. +pub const EVENTS: Map = Map::new("events"); +pub const EVENT_COUNTER: cw_storage_plus::Item = cw_storage_plus::Item::new("event_counter"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> StdResult { + EVENT_COUNTER.save(deps.storage, &0)?; + Ok(Response::new() + .add_attribute("method", "instantiate")) +} + +/// Called by the x/cw-hooks module when staking or governance events occur. +/// The contract must register with: safrochaind tx cw-hooks register +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo( + deps: DepsMut, + _env: Env, + msg: CwHooksSudoMsg, +) -> Result { + match msg { + CwHooksSudoMsg::AfterValidatorCreated { + validator_address, .. + } => log_event(deps, format!("validator_created:{}", validator_address)), + + CwHooksSudoMsg::AfterValidatorRemoved { + validator_address, .. + } => log_event(deps, format!("validator_removed:{}", validator_address)), + + CwHooksSudoMsg::AfterValidatorBonded { + validator_address, .. + } => log_event(deps, format!("validator_bonded:{}", validator_address)), + + CwHooksSudoMsg::AfterValidatorBeginUnbonding { + validator_address, .. + } => log_event(deps, format!("validator_unbonding:{}", validator_address)), + } +} + +fn log_event(deps: DepsMut, event: String) -> Result { + let count = EVENT_COUNTER.load(deps.storage)?; + EVENTS.save(deps.storage, &count, &event)?; + EVENT_COUNTER.save(deps.storage, &(count + 1))?; + Ok(Response::new() + .add_attribute("method", "log_event") + .add_attribute("event_index", count.to_string()) + .add_attribute("event", &event)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::ClearEvents {} => Ok(Response::new().add_attribute("method", "clear_events")), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetEvents {} => { + let count = EVENT_COUNTER.load(deps.storage)?; + let mut events = vec![]; + for i in 0..count { + if let Ok(evt) = EVENTS.load(deps.storage, &i) { + events.push(StakingEventResponse { + id: i, + description: evt, + }); + } + } + to_json_binary(&events) + } + QueryMsg::GetEventCount {} => { + let count = EVENT_COUNTER.load(deps.storage)?; + to_json_binary(&count) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + use cosmwasm_std::{coins, from_json}; + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(); + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info, InstantiateMsg {}).unwrap(); + + let res: u64 = from_json( + &query(deps.as_ref(), mock_env(), QueryMsg::GetEventCount {}).unwrap(), + ) + .unwrap(); + assert_eq!(res, 0); + } + + #[test] + fn receive_validator_created_event() { + let mut deps = mock_dependencies(); + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info, InstantiateMsg {}).unwrap(); + + // Simulate x/cw-hooks sending a sudo message + let sudo_msg = CwHooksSudoMsg::AfterValidatorCreated { + moniker: "test-validator".to_string(), + validator_address: "safrovaloper1test".to_string(), + commission: "0.1".to_string(), + validator_tokens: "1000000".to_string(), + bonded_tokens: "1000000".to_string(), + bond_status: "BOND_STATUS_BONDED".to_string(), + }; + let res = sudo(deps.as_mut(), mock_env(), sudo_msg).unwrap(); + assert_eq!(res.attributes[1].value, "0"); + + // Check event count + let count: u64 = from_json( + &query(deps.as_ref(), mock_env(), QueryMsg::GetEventCount {}).unwrap(), + ) + .unwrap(); + assert_eq!(count, 1); + } + + #[test] + fn receive_multiple_events() { + let mut deps = mock_dependencies(); + let info = message_info(&deps.api.addr_make("creator"), &coins(1000, "usaf")); + instantiate(deps.as_mut(), mock_env(), info, InstantiateMsg {}).unwrap(); + + // Send 3 events + for i in 0..3 { + let sudo_msg = CwHooksSudoMsg::AfterValidatorBonded { + validator_address: format!("safrovaloper{}", i), + moniker: format!("val-{}", i), + commission: "0.1".to_string(), + validator_tokens: "1000000".to_string(), + bonded_tokens: "1000000".to_string(), + bond_status: "BOND_STATUS_BONDED".to_string(), + }; + sudo(deps.as_mut(), mock_env(), sudo_msg).unwrap(); + } + + let count: u64 = from_json( + &query(deps.as_ref(), mock_env(), QueryMsg::GetEventCount {}).unwrap(), + ) + .unwrap(); + assert_eq!(count, 3); + } +} From 16cd632b91f5d55295a34295d68933b2211cca88 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:27 +0800 Subject: [PATCH 19/21] feat: add msg.rs for cw-hooks-listener contract --- .../cosmwasm/cw-hooks-listener/src/msg.rs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/cosmwasm/cw-hooks-listener/src/msg.rs diff --git a/examples/cosmwasm/cw-hooks-listener/src/msg.rs b/examples/cosmwasm/cw-hooks-listener/src/msg.rs new file mode 100644 index 0000000..de00ed3 --- /dev/null +++ b/examples/cosmwasm/cw-hooks-listener/src/msg.rs @@ -0,0 +1,59 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + ClearEvents {}, +} + +#[cw_serde] +pub enum QueryMsg { + GetEvents {}, + GetEventCount {}, +} + +#[cw_serde] +pub struct StakingEventResponse { + pub id: u64, + pub description: String, +} + +/// Sudo messages from x/cw-hooks module. +/// Register with: safrochaind tx cw-hooks register staking +#[cw_serde] +pub enum CwHooksSudoMsg { + AfterValidatorCreated { + moniker: String, + validator_address: String, + commission: String, + validator_tokens: String, + bonded_tokens: String, + bond_status: String, + }, + AfterValidatorRemoved { + moniker: String, + validator_address: String, + commission: String, + validator_tokens: String, + bonded_tokens: String, + bond_status: String, + }, + AfterValidatorBonded { + moniker: String, + validator_address: String, + commission: String, + validator_tokens: String, + bonded_tokens: String, + bond_status: String, + }, + AfterValidatorBeginUnbonding { + moniker: String, + validator_address: String, + commission: String, + validator_tokens: String, + bonded_tokens: String, + bond_status: String, + }, +} From 441a622c2344acbba5eb300b916efab4b38a731f Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:29 +0800 Subject: [PATCH 20/21] feat: add error.rs for cw-hooks-listener contract --- examples/cosmwasm/cw-hooks-listener/src/error.rs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 examples/cosmwasm/cw-hooks-listener/src/error.rs diff --git a/examples/cosmwasm/cw-hooks-listener/src/error.rs b/examples/cosmwasm/cw-hooks-listener/src/error.rs new file mode 100644 index 0000000..40dab9f --- /dev/null +++ b/examples/cosmwasm/cw-hooks-listener/src/error.rs @@ -0,0 +1,8 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), +} From 55c0b15dc20c4e3b6372e08545230b758c211875 Mon Sep 17 00:00:00 2001 From: Karry <51736839+Karry2019web@users.noreply.github.com> Date: Tue, 26 May 2026 06:04:31 +0800 Subject: [PATCH 21/21] feat: add lib.rs for cw-hooks-listener contract --- examples/cosmwasm/cw-hooks-listener/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/cosmwasm/cw-hooks-listener/src/lib.rs diff --git a/examples/cosmwasm/cw-hooks-listener/src/lib.rs b/examples/cosmwasm/cw-hooks-listener/src/lib.rs new file mode 100644 index 0000000..91c7e60 --- /dev/null +++ b/examples/cosmwasm/cw-hooks-listener/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod msg;