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:
| Collection | Prefixes | Example |
|---|---|---|
Item<T> | 1 | Item::new(0) |
Map<K,V> | 1 | Map::new(0) |
Vector<T> | 2 | Vector::new(0, 1) |
Queue<T> | 2 | Queue::new(0, 1) |
UnorderedMap<K,V> | 4 | UnorderedMap::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 matterStorage 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
| Limit | Value |
|---|---|
| Max key size | 254 bytes |
| Max value size | 1 MB |
| Max overlay entries | 100,000 per transaction |
| Max events | 10,000 per execution |