Evolve is an SDK designed to help you build powerful and flexible blockchain applications.
At the heart of Evolve is the AccountCode trait, which specifies how an account responds to execution requests and
queries. Notably, this account is stateless and does not alter its own internal state. You typically do not need to
implement AccountCode manually; Evolve’s macros handle it for you.
In Evolve, every entity is an account. Each account is defined by an identifier (AccountID) and the AccountCode that
governs its behavior. An account’s state is encapsulated behind its unique AccountID.
Accounts in Evolve can be composed into higher-level primitives or “pre-compiles.” The SDK aims to keep its API and trait surfaces minimal and intuitive, allowing even less experienced Rust developers to onboard quickly. Given the evolving nature of blockchain technology, Evolve avoids over-parameterizing its core interfaces; instead, custom logic is addressed by creating specialized accounts.
For example, there is no native “block trait” within the core interfaces. If block-related data needs to be made available, you could create a dedicated block info account that other accounts can query. The same pattern applies to validators or any specialized consensus engine information.
Below is a merged guide on creating an account in Evolve with init, exec, and query methods, along with
details about auto-generated references (AccountRef) and the broader set of collection types available. This
single document covers:
- Core Principles (macro-based trait implementation)
- Collection Types (e.g.,
Item,Map, etc.) - AccountRef Generation (client interface for inter-account communication)
- Step-by-Step Explanation of the example code
Evolve is an SDK designed for building powerful and flexible blockchain applications in Rust.
- Macros for AccountCode: You don’t directly implement
AccountCode; instead, Evolve provides macros—#[init],#[exec], and#[query]—that automatically generate the necessary trait logic. - Everything Is an Account: Each account is defined by an
AccountIDplus the logic (the “code”) behind it. State is isolated perAccountID.
When defining an account’s storage, Evolve enforces the use of collection types—Item, Map, Vector, and
others—to ensure safe and consistent on-chain data handling. You can refer to
the Evolve Collections Documentation (placeholder link) for a deep dive into all
available collection types. In the example below, we use Item<T>, but you can choose a different collection type if
your use case demands it.
By annotating a Rust module with #[account_impl(YourAccountStruct)], Evolve generates a client interface (commonly
referred to as AccountRef) that other accounts or off-chain code can use to interact with the account. Specifically:
initMethod: The reference exposes a function (named after your#[init]method) to deploy and initialize your account.- The signature typically returns
SdkResult<(AccountRef, InitReturnType)>, where:AccountRefis a handle to the newly created account (for subsequent calls).InitReturnTypematches the Rust return type of your init method.
- The signature typically returns
execMethods: Correspond to each#[exec]-annotated method in your code.queryMethods: Correspond to each#[query]-annotated function, enabling read-only calls.
This auto-generation saves a significant amount of boilerplate and ensures consistent, type-safe interaction across different Evolve accounts.
Below, we define a Pool account that also deploys a token (LP token). Users can deposit two different fungible
assets into the pool in exchange for LP tokens, and they can later burn LP tokens to withdraw their share. Notice how
#[init], #[exec], and #[query] methods are used.
use evolve_macros::account_impl;
// Account impl macro defines who is the one implementing the AccountCode trait.
// The AccountCode trait is implemented based on exec/init/query methods
// which are marked using macro attributes.
#[account_impl(Account)]
pub mod pool {
use borsh::{BorshDeserialize, BorshSerialize};
use evolve_collections::item::Item;
use evolve_core::{Environment, ErrorCode, FungibleAsset, SdkResult};
use evolve_fungible_asset::{FungibleAssetInterfaceRef, FungibleAssetMetadata};
use evolve_macros::{exec, init, query};
use evolve_token::account::TokenRef;
const ERR_INVALID_FUNDS_AMOUNT: ErrorCode = ErrorCode::new(0, "invalid funds amount");
const ERR_INVALID_ASSET_ID: ErrorCode = ErrorCode::new(1, "invalid asset id");
#[derive(BorshDeserialize, BorshSerialize)]
pub struct PoolState {
pub asset_one: FungibleAsset,
pub asset_two: FungibleAsset,
}
// Define the account and what it has inside of state.
// Only 'collection' types are allowed (Item, Map, Vector, etc.).
pub struct Account {
// A Ref is an auto-generated client to talk with another account.
// The methods of this Ref are derived from the Query/Exec/Init methods on the account.
lp_token: Item<TokenRef>,
asset_one: Item<FungibleAssetInterfaceRef>,
asset_two: Item<FungibleAssetInterfaceRef>,
}
impl Account {
// The prefixes (0,1,2) must be unique for each field to avoid collisions.
pub const fn new() -> Self {
Account {
lp_token: Item::new(0),
asset_one: Item::new(1),
asset_two: Item::new(2),
}
}
// The #[init(payable)] attribute marks this as the sole initialization method,
// and also allows the method to receive a Vec<FungibleAsset> (funds).
#[init(payable)]
pub fn initialize(
self: &Account,
mut initial_funds: Vec<FungibleAsset>,
env: &mut dyn Environment,
) -> SdkResult<()> {
if initial_funds.len() != 2 {
return Err(ERR_INVALID_FUNDS_AMOUNT);
}
let asset_two = initial_funds.pop().unwrap();
let asset_one = initial_funds.pop().unwrap();
// Initialize an LP token account and get back its reference.
let (lp_token, _) = TokenRef::initialize(
FungibleAssetMetadata {
name: "lp".to_string(),
symbol: "LP".to_string(),
decimals: 0,
icon_url: "".to_string(),
description: "".to_string(),
},
vec![],
Some(env.sender()),
env,
)?;
// Store references to the LP token and underlying assets.
self.lp_token.set(&lp_token, env)?;
self.asset_one
.set(&FungibleAssetInterfaceRef::new(asset_one.asset_id), env)?;
self.asset_two
.set(&FungibleAssetInterfaceRef::new(asset_two.asset_id), env)?;
Ok(())
}
// Marked with #[exec(payable)], meaning this method is called with a mutable Environment
// and can receive fungible assets.
#[exec(payable)]
pub fn deposit(
self: &Account,
mut inputs: Vec<FungibleAsset>,
env: &mut dyn Environment,
) -> SdkResult<()> {
if inputs.len() != 2 {
return Err(ERR_INVALID_FUNDS_AMOUNT);
}
let asset_two = inputs.pop().unwrap();
let asset_one = inputs.pop().unwrap();
// Validate that the correct assets are being deposited.
let asset_one_ref = self.asset_one.get(env)?;
if asset_one_ref.0 != asset_one.asset_id {
return Err(ERR_INVALID_ASSET_ID);
}
let asset_two_ref = self.asset_two.get(env)?;
if asset_two_ref.0 != asset_two.asset_id {
return Err(ERR_INVALID_ASSET_ID);
}
// Compute how many LP tokens to mint for the user (example logic).
let lp_to_user: u128 = 100;
let lp_token = self.lp_token.get(&env)?;
lp_token.mint(env.sender(), lp_to_user, env)?;
Ok(())
}
#[exec(payable)]
pub fn burn(
self: &Account,
mut lp_in: Vec<FungibleAsset>,
env: &mut dyn Environment,
) -> SdkResult<()> {
if lp_in.len() != 1 {
return Err(ERR_INVALID_FUNDS_AMOUNT);
}
let lp = lp_in.pop().unwrap();
// Check that the token being transferred is our own LP token.
if lp.asset_id != env.whoami() {
return Err(ERR_INVALID_ASSET_ID);
}
// (Placeholder) Calculate how many underlying assets to return to the user.
let asset_one_out = 100u128;
let asset_two_out = 100u128;
// Burn the LP tokens and transfer out the underlying assets.
self.lp_token.get(env)?.burn(env.sender(), lp.amount, env)?;
self.asset_one
.get(env)?
.transfer(env.sender(), asset_one_out, env)?;
self.asset_two
.get(env)?
.transfer(env.sender(), asset_two_out, env)?;
Ok(())
}
// Marked with #[query], meaning it's a read-only method that cannot change on-chain state.
#[query]
pub fn pool_state(self: &Account, env: &dyn Environment) -> SdkResult<PoolState> {
let asset_one = self.asset_one.get(env)?;
let asset_two = self.asset_two.get(env)?;
Ok(PoolState {
asset_one: FungibleAsset {
asset_id: asset_one.0,
amount: asset_one.get_balance(env.whoami(), env)?.unwrap_or_default(),
},
asset_two: FungibleAsset {
asset_id: asset_two.0,
amount: asset_two.get_balance(env.whoami(), env)?.unwrap_or_default(),
},
})
}
}
}#[account_impl(Account)]
pub mod pool {
...
}- This annotation informs Evolve to generate an
AccountRef(client interface) for theAccountstruct inside thepoolmodule.
-
initialize(matching your#[init]function)- Usage:
let (pool_ref, init_result) = PoolRef::initialize(...args...)?;
- Returns
(AccountRef, InitReturnType), whereAccountRefis your client handle, andInitReturnTypeis whatever the Rust init method returns (commonlySdkResult<()>).
- Usage:
-
deposit,burn, etc. (matching each#[exec]function)- Usage:
pool_ref.deposit(...args...)?;
- Under the hood, this calls into the on-chain
depositmethod, passing required parameters.
- Usage:
-
pool_state(matching the#[query]function)- Usage:
let state = pool_ref.pool_state()?;
- Invokes the read-only method for retrieving current on-chain data.
- Usage:
-
Macros Reduce Boilerplate
- You only write
#[init],#[exec], and#[query]; Evolve expands these into trait implementations behind the scenes.
- You only write
-
Isolation by Design
- State is accessed exclusively via typed collections (
Item,Map,Vector, etc.), minimizing accidental overlaps.
- State is accessed exclusively via typed collections (
-
Auto-Generated AccountRef
- When you decorate your module with
#[account_impl(Account)], an account reference is created with the **same ** method signatures forinit,exec, andquery. - This reference is how other accounts (or user-facing code) interact with your account.
- When you decorate your module with
-
Flexibility and Extensibility
- Core Evolve interfaces are intentionally minimal. For more specialized behaviors (e.g., block info, validators), you can create additional accounts that each implement specific logic.