Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Storage

Evolve provides type-safe, collision-free storage primitives for module state.

Storage Patterns

There are two patterns for defining storage:

Static Storage (Simple)

For straightforward modules, use static storage with byte prefixes:

use evolve_collections::{Item, Map};
 
static CONFIG: Item<Config> = Item::new(b"config");
static BALANCES: Map<AccountId, u128> = Map::new(b"bal");

AccountState Derive (Recommended)

For compile-time validation and struct-based organization:

#[derive(evolve_core::AccountState)]
pub struct MyModule {
    #[storage(0)]
    pub counter: Item<u64>,
    #[storage(1)]
    pub balances: Map<AccountId, u128>,
    #[storage(2)]
    pub metadata: Item<String>,
}

Benefits of AccountState:

  • Compile-time validation - Duplicate prefixes are caught at build time
  • Refactor safety - Reordering fields doesn't change storage layout
  • Migration safety - Adding new fields doesn't corrupt existing data

Multi-Prefix Collections

Some collections require multiple prefixes:

CollectionPrefixesExample
Item<T>1Item::new(0)
Map<K,V>1Map::new(0)
Vector<T>2Vector::new(0, 1)
Queue<T>2Queue::new(0, 1)
UnorderedMap<K,V>4UnorderedMap::new(0, 1, 2, 3)

For multi-prefix collections, use manual initialization:

pub struct ComplexModule {
    validators: UnorderedMap<AccountId, Validator>,
    changes: Vector<Change>,
}
 
impl ComplexModule {
    pub const fn new() -> Self {
        Self {
            validators: UnorderedMap::new(0, 1, 2, 3),
            changes: Vector::new(4, 5),
        }
    }
}

Collection Types

Item

Single value storage:

use evolve_collections::Item;
 
static CONFIG: Item<Config> = Item::new(b"config");
 
// Set value
CONFIG.set(env, Config { admin, paused: false })?;
 
// Get value
let config = CONFIG.get(env)?.unwrap_or_default();
 
// Check existence
if CONFIG.exists(env)? {
    // ...
}
 
// Remove value
CONFIG.remove(env)?;

Map

Key-value mapping with deterministic iteration:

use evolve_collections::Map;
 
static BALANCES: Map<AccountId, u128> = Map::new(b"bal");
 
// Set value
BALANCES.set(env, account, 1000)?;
 
// Get value
let balance = BALANCES.get(env, account)?.unwrap_or(0);
 
// Update atomically
BALANCES.update(env, account, |balance| {
    Ok(balance.unwrap_or(0) + amount)
})?;
 
// Remove entry
BALANCES.remove(env, account)?;
 
// Iterate (deterministic order)
for (key, value) in BALANCES.iter(env)? {
    // ...
}

Vector

Dynamic ordered array:

use evolve_collections::Vector;
 
static HISTORY: Vector<Event> = Vector::new(b"hist");
 
// Push to end
HISTORY.push(env, event)?;
 
// Get by index
let event = HISTORY.get(env, 0)?;
 
// Get length
let len = HISTORY.len(env)?;
 
// Pop from end
let last = HISTORY.pop(env)?;
 
// Iterate
for event in HISTORY.iter(env)? {
    // ...
}

Queue

FIFO queue for processing:

use evolve_collections::Queue;
 
static PENDING: Queue<Task> = Queue::new(b"pending");
 
// Enqueue (add to back)
PENDING.enqueue(env, task)?;
 
// Dequeue (remove from front)
let task = PENDING.dequeue(env)?;
 
// Peek at front
let next = PENDING.peek(env)?;
 
// Check if empty
if PENDING.is_empty(env)? {
    // ...
}

UnorderedMap

Hash-based mapping for large datasets:

use evolve_collections::UnorderedMap;
 
static METADATA: UnorderedMap<TokenId, Metadata> = UnorderedMap::new(b"meta");
 
// Same API as Map, but optimized for:
// - Large datasets
// - Random access patterns
// - When iteration order doesn't matter

Storage Prefixes

Each collection needs a unique prefix to avoid collisions:

// Good: unique prefixes
static BALANCES: Map<AccountId, u128> = Map::new(b"bal");
static ALLOWANCES: Map<(AccountId, AccountId), u128> = Map::new(b"allow");
 
// Bad: collision!
static A: Map<u64, u64> = Map::new(b"data");
static B: Map<u64, u64> = Map::new(b"data"); // Same prefix!

Serialization

Types must implement Borsh serialization:

use borsh::{BorshSerialize, BorshDeserialize};
 
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Config {
    pub admin: AccountId,
    pub fee_rate: u64,
}

Best Practices

Use Typed Keys

// Good: type-safe key
#[derive(BorshSerialize, BorshDeserialize)]
pub struct AllowanceKey {
    owner: AccountId,
    spender: AccountId,
}
 
static ALLOWANCES: Map<AllowanceKey, u128> = Map::new(b"allow");
 
// Also good: tuple key
static ALLOWANCES: Map<(AccountId, AccountId), u128> = Map::new(b"allow");

Batch Updates

// Bad: multiple writes
for account in accounts {
    BALANCES.set(env, account, amount)?;
}
 
// Better: use update for atomic operations
BALANCES.update(env, account, |bal| Ok(bal.unwrap_or(0) + amount))?;

Lazy Loading

// Only load what you need
let balance = BALANCES.get(env, specific_account)?;
 
// Avoid loading everything
// let all = BALANCES.iter(env)?.collect::<Vec<_>>();

Helper Fields

For stateless helper types (like EventsEmitter), use #[skip_storage]:

#[derive(evolve_core::AccountState)]
pub struct MyModule {
    #[storage(0)]
    pub data: Item<Data>,
    #[skip_storage]
    pub events: EventsEmitter,  // Initialized with Type::new()
}

Size Limits

LimitValue
Max key size254 bytes
Max value size1 MB
Max overlay entries100,000 per transaction
Max events10,000 per execution