一键导入
evolve-modules
// Write modules for the Evolve SDK using AccountCode trait and account_impl macro. Use when creating modules, writing account code, using AccountState, storage prefixes, or developing new blockchain modules.
// Write modules for the Evolve SDK using AccountCode trait and account_impl macro. Use when creating modules, writing account code, using AccountState, storage prefixes, or developing new blockchain modules.
Use the Evolve execution debugger for trace recording, replay, and time-travel debugging. Use when debugging failing tests, recording execution traces, stepping through state changes, or analyzing execution flow.
Create custom Evolve blockchain nodes by composing storage, STF, mempool, RPC, and gRPC components. Use when the user wants to create a new node binary, build a custom chain, compose node components, or asks about evd/testapp architecture.
Write property-based tests for Evolve SDK using proptest. Use when writing property tests, invariant testing, fuzz testing, test generators, or finding edge cases.
Use the Evolve deterministic simulator for testing with controlled time, storage, and randomness. Use when creating simulations, seed-based testing, fault injection, stress testing, or deterministic test reproduction.
Overview of Evolve SDK testing infrastructure including TestApp, SimTestApp, MockEnv, and transaction generators. Use when writing tests, setting up test harnesses, using test infrastructure, or choosing between testing approaches.
Analyze State Transition Function implementations for correctness, threading issues, non-determinism, and simplification opportunities. Use when analyzing STF code, checking threading model, finding non-determinism sources, or reviewing execution layer.
| name | evolve-modules |
| description | Write modules for the Evolve SDK using AccountCode trait and account_impl macro. Use when creating modules, writing account code, using AccountState, storage prefixes, or developing new blockchain modules. |
Modules in Evolve are stateless code executors that implement the AccountCode trait. They interact with blockchain state through the Environment interface and are composed via the #[account_impl] macro.
Full documentation: docs/module-system/
A minimal module using #[derive(AccountState)] for compile-time storage validation:
use evolve_core::account_impl;
#[account_impl(MyModule)]
pub mod account {
use evolve_collections::{item::Item, map::Map};
use evolve_core::{AccountId, Environment, EnvironmentQuery, SdkResult};
use evolve_macros::{exec, init, query};
#[derive(evolve_core::AccountState)]
pub struct MyModule {
#[storage(0)]
pub owner: Item<AccountId>,
#[storage(1)]
pub data: Map<AccountId, u64>,
}
impl MyModule {
#[init]
pub fn initialize(
&self,
owner: AccountId,
env: &mut dyn Environment,
) -> SdkResult<()> {
self.owner.set(&owner, env)?;
Ok(())
}
#[exec]
pub fn set_data(
&self,
key: AccountId,
value: u64,
env: &mut dyn Environment,
) -> SdkResult<()> {
self.data.set(&key, &value, env)?;
Ok(())
}
#[query]
pub fn get_data(
&self,
key: AccountId,
env: &mut dyn EnvironmentQuery,
) -> SdkResult<Option<u64>> {
self.data.may_get(&key, env)
}
}
}
The #[derive(AccountState)] macro:
new() and Default implementations automatically| Marker | Environment | Purpose | Generated Message |
|---|---|---|---|
#[init] | &mut dyn Environment | One-time initialization | InitializeMsg |
#[exec] | &mut dyn Environment | State mutations | <FnName>Msg |
#[query] | &mut dyn EnvironmentQuery | Read-only | <FnName>Msg |
#[payable] | Add to #[exec] | Accept fungible assets | - |
Use #[storage(n)] to assign unique prefix bytes (validated at compile time):
use evolve_collections::{item::Item, map::Map};
#[derive(evolve_core::AccountState)]
pub struct MyModule {
#[storage(0)]
pub config: Item<Config>, // single value
#[storage(1)]
pub balances: Map<AccountId, u128>, // key-value
}
Vector and UnorderedMap require multiple prefixes. Use manual initialization:
pub struct ComplexModule {
history: Vector<Event>, // needs 2 prefixes
validators: UnorderedMap<AccountId, Validator>, // needs 4 prefixes
}
impl ComplexModule {
pub const fn new() -> Self {
Self {
history: Vector::new(0, 1),
validators: UnorderedMap::new(2, 3, 4, 5),
}
}
}
Use #[skip_storage] for fields that don't need storage prefixes:
#[derive(evolve_core::AccountState)]
pub struct MyModule {
#[storage(0)]
pub data: Item<Data>,
#[skip_storage]
pub helper: MyHelper, // Initialized with Type::new()
}
// Item<T>
item.set(&value, env)?; // write
item.get(env)?; // read (panics if missing)
item.may_get(env)?; // read -> Option<T>
item.update(|v| Ok(v + 1), env)?; // atomic update
// Map<K, V>
map.set(&key, &value, env)?;
map.get(&key, env)?; // panics if missing
map.may_get(&key, env)?; // -> Option<V>
map.remove(&key, env)?;
map.update(&key, |v| Ok(v.unwrap_or(0) + 1), env)?;
// Vector<T>
vector.push(&value, env)?;
vector.get(index, env)?;
vector.len(env)?;
vector.pop(env)?;
Use env.emit_event() to emit events from module functions:
use evolve_core::events_api::Event;
#[exec]
pub fn transfer(
&self,
to: AccountId,
amount: u128,
env: &mut dyn Environment,
) -> SdkResult<()> {
// ... transfer logic ...
// Emit event with name and borsh-serializable data
env.emit_event("transfer", &TransferEvent {
from: env.sender(),
to,
amount,
})?;
Ok(())
}
Events are collected during execution and included in the transaction result.
Define custom errors with define_error!:
use evolve_core::{define_error, ERR_UNAUTHORIZED};
define_error!(ERR_INSUFFICIENT_BALANCE, 0x1, "insufficient balance");
define_error!(ERR_ALREADY_EXISTS, 0x2, "already exists");
#[exec]
pub fn withdraw(&self, amount: u128, env: &mut dyn Environment) -> SdkResult<()> {
let balance = self.balances.may_get(&env.sender(), env)?.unwrap_or(0);
if balance < amount {
return Err(ERR_INSUFFICIENT_BALANCE);
}
// ...
}
Check sender for privileged operations:
#[exec]
pub fn admin_action(&self, env: &mut dyn Environment) -> SdkResult<()> {
if env.sender() != self.owner.get(env)? {
return Err(ERR_UNAUTHORIZED);
}
// privileged logic
Ok(())
}
For system-only operations:
use evolve_core::RUNTIME_ACCOUNT_ID;
#[exec]
pub fn system_only(&self, env: &mut dyn Environment) -> SdkResult<()> {
if env.sender() != RUNTIME_ACCOUNT_ID {
return Err(ERR_UNAUTHORIZED);
}
Ok(())
}
Separate authorization from logic:
// Internal: no auth checks
pub fn mint_unchecked(
&self,
recipient: AccountId,
amount: u128,
env: &mut dyn Environment,
) -> SdkResult<()> {
self.balances.update(&recipient, |b| Ok(b.unwrap_or(0) + amount), env)?;
self.total_supply.update(|s| Ok(s.unwrap_or(0) + amount), env)?;
Ok(())
}
// External: with auth
#[exec]
pub fn mint(
&self,
recipient: AccountId,
amount: u128,
env: &mut dyn Environment,
) -> SdkResult<()> {
if self.supply_manager.get(env)? != Some(env.sender()) {
return Err(ERR_UNAUTHORIZED);
}
self.mint_unchecked(recipient, amount, env)
}
The macro generates a Ref wrapper for type-safe calls:
use evolve_token::account::TokenRef;
#[exec]
pub fn do_transfer(&self, env: &mut dyn Environment) -> SdkResult<()> {
let token = TokenRef::from(self.token_id.get(env)?);
token.transfer(recipient, amount, env)?;
Ok(())
}
Or use raw InvokeRequest for flexibility:
use evolve_core::InvokeRequest;
use evolve_fungible_asset::TransferMsg;
let request = InvokeRequest::new(&TransferMsg { to: recipient, amount })?;
env.do_exec(token_id, &request, vec![])?;
Implement standard interfaces for composability:
use evolve_fungible_asset::{FungibleAssetInterface, FungibleAssetMetadata};
impl FungibleAssetInterface for Token {
#[exec]
fn transfer(&self, to: AccountId, amount: u128, env: &mut dyn Environment) -> SdkResult<()> {
// implementation
}
#[query]
fn get_balance(&self, account: AccountId, env: &mut dyn EnvironmentQuery) -> SdkResult<Option<u128>> {
self.balances.may_get(&account, env)
}
#[query]
fn metadata(&self, env: &mut dyn EnvironmentQuery) -> SdkResult<FungibleAssetMetadata> {
self.metadata.get(env)
}
#[query]
fn total_supply(&self, env: &mut dyn EnvironmentQuery) -> SdkResult<u128> {
self.total_supply.get(env)
}
}
#[cfg(test)]
mod tests {
use super::account::MyModule;
use evolve_core::AccountId;
use evolve_testing::MockEnv;
#[test]
fn test_basic_flow() {
let contract = AccountId::new(1);
let sender = AccountId::new(2);
let mut env = MockEnv::new(contract, sender);
let module = MyModule::default();
module.initialize(sender, &mut env).unwrap();
module.set_data(AccountId::new(3), 42, &mut env).unwrap();
let value = module.get_data(AccountId::new(3), &mut env).unwrap();
assert_eq!(value, Some(42));
}
#[test]
fn test_unauthorized() {
let contract = AccountId::new(1);
let owner = AccountId::new(2);
let mut env = MockEnv::new(contract, owner);
let module = MyModule::default();
module.initialize(owner, &mut env).unwrap();
// Change sender to non-owner
env = env.with_sender(AccountId::new(999));
let result = module.admin_action(&mut env);
assert!(result.is_err());
}
}
use testapp::{TestApp, GenesisAccounts};
#[test]
fn test_with_full_stf() {
let mut app = TestApp::new();
let accounts = app.accounts();
app.system_exec_as(accounts.alice, |env| {
// Interact with modules through refs
let token = TokenRef::from(accounts.atom);
token.transfer(accounts.bob, 100, env)
}).unwrap();
app.next_block();
}
All module code must be deterministic. These patterns are banned:
| Pattern | Why | Alternative |
|---|---|---|
HashMap, HashSet | Non-deterministic iteration | BTreeMap or evolve_collections |
std::time | Varies between nodes | BlockInfo module |
rand | Non-deterministic | Derive from chain state |
f32, f64 | Platform-dependent | evolve_math::FixedPoint |
The workspace has clippy lints configured (.clippy.toml) to warn on these.
See docs/module-system/determinism.md for full details.
#[derive(AccountState)] - Compile-time storage prefix validation#[storage(n)] values - Each field gets unique byte prefixenv.sender() appropriatelychecked_add, checked_sub to prevent overflowmay_get()?.ok_or(ERR_...)? not unwrap()crates/app/sdk/macros/src/lib.rs - #[account_impl] and #[derive(AccountState)] macroscrates/app/sdk/core/src/lib.rs - Core traits (AccountCode, Environment)crates/app/sdk/collections/src/ - Storage collectionscrates/app/sdk/x/token/src/lib.rs - Reference implementation (Token)crates/app/sdk/x/scheduler/src/lib.rs - Block scheduling hooks (Scheduler)docs/module-system/ - Full documentation (architecture, storage, errors, testing, determinism)