Documentation Index
Fetch the complete documentation index at: https://docs.kontor.network/llms.txt
Use this file to discover all available pages before exploring further.
Sigil uses a hierarchical path-based storage model, similar to a filesystem. The storage system provides type-safe accessors through Rust derives that generate methods for reading and writing state.
Storage Derives
StorageRoot - Top-Level Contract Storage
Every contract has exactly one StorageRoot:
#[derive(Clone, Default, StorageRoot)]
struct TokenStorage {
pub ledger: Map<String, Integer>,
}
What it generates:
.init(ctx) method to initialize storage
- Accessible via
ctx.model()
Usage:
fn init(ctx: &ProcContext) {
TokenStorage::default().init(ctx);
}
fn mint(ctx: &ProcContext, n: Integer) {
let ledger = ctx.model().ledger(); // Access via model()
// ...
}
Storage - Nested Structures
For structures nested within your storage root:
#[derive(Clone, Default, Storage)]
struct Account {
pub balance: Integer,
pub owner: String,
pub permissions: Map<String, bool>,
}
What it generates:
- Same accessors as
StorageRoot but without .init()
- Used for nested data structures
Store and Model
#[derive(Store)] - Generates only persistence methods (__set())
#[derive(Model)] - Generates only read/write accessor types (*Model, *WriteModel)
Most contracts only need StorageRoot and Storage.
Type Constraints
Supported Storage Types
Primitive types:
Built-in types:
Integer - Arbitrary precision (256-bit)
Decimal - Arbitrary precision with decimals
ContractAddress - References to other contracts
Structured types:
- Enums and structs with
#[derive(Storage)] or #[derive(Wavey)]
Option<T> where T is a supported type
Map<K, V> where K: ToString + FromString, V is a supported type
Not Directly Supported
Vec - Use Map<u64, T> as workaround:
// Instead of
pub items: Vec<String>,
// Use
pub items: Map<u64, String>,
// Access
fn add_item(ctx: &ProcContext, item: String) {
let items = ctx.model().items();
let len = items.keys().count() as u64;
items.set(len, item);
}
Unsupported primitives - Use alternatives:
- No
u32, u16, u8 → Use u64
- No
f64, f32 → Use Decimal
Working with Basic Fields
#[derive(Clone, Default, StorageRoot)]
struct CounterStorage {
pub count: u64,
pub owner: String,
pub active: bool,
}
impl Guest for Counter {
fn init(ctx: &ProcContext) {
CounterStorage {
count: 0,
owner: ctx.signer().to_string(),
active: true,
}.init(ctx);
}
fn increment(ctx: &ProcContext) {
let current = ctx.model().count(); // Read
ctx.model().set_count(current + 1); // Write
}
fn get_owner(ctx: &ViewContext) -> String {
ctx.model().owner() // Read
}
}
Working with Options
#[derive(Clone, Default, StorageRoot)]
struct ProxyStorage {
contract_address: Option<ContractAddress>,
}
impl Guest for Proxy {
fn get_address(ctx: &ViewContext) -> Option<ContractAddress> {
ctx.model().contract_address() // Returns Option<ContractAddress>
}
fn set_address(ctx: &ProcContext, addr: ContractAddress) {
ctx.model().set_contract_address(Some(addr));
}
fn clear_address(ctx: &ProcContext) {
ctx.model().set_contract_address(None);
}
}
Working with Maps
Maps provide key-value storage:
#[derive(Clone, Default, StorageRoot)]
struct TokenStorage {
pub ledger: Map<String, Integer>,
}
impl Guest for Token {
fn mint(ctx: &ProcContext, n: Integer) {
let ledger = ctx.model().ledger();
let account = ctx.signer().to_string();
// Get (returns Option<V>)
let balance = ledger.get(&account).unwrap_or_default();
// Set
ledger.set(account, balance + n);
}
fn all_holders(ctx: &ViewContext) -> Vec<String> {
// Iterate keys
ctx.model().ledger().keys().collect()
}
}
Map API
For read-only access (ViewContext):
pub trait MapModel<K, V> {
fn get(&self, key: K) -> Option<V>;
fn keys(&self) -> impl Iterator<Item = K>;
}
For read-write access (ProcContext):
pub trait MapWriteModel<K, V> {
fn get(&self, key: K) -> Option<V>;
fn set(&self, key: K, value: V);
fn keys(&self) -> impl Iterator<Item = K>;
}
Map Initialization with Data
fn init(ctx: &ProcContext) {
FibStorage {
cache: Map::new(&[(0, FibValue { value: 0 })]),
}.init(ctx);
}
Nested Structures
Storage structures can be nested arbitrarily deep:
#[derive(Clone, Default, Storage)]
struct Account {
pub balance: Integer,
pub owner: String,
pub other_tenants: Map<String, bool>,
}
#[derive(Clone, Default, StorageRoot)]
struct SharedAccountStorage {
pub accounts: Map<String, Account>,
}
impl Guest for SharedAccount {
fn deposit(ctx: &ProcContext, account_id: String, n: Integer) -> Result<(), Error> {
let accounts = ctx.model().accounts();
let account = accounts.get(&account_id).ok_or(unknown_error())?;
// Access nested fields
let current_balance = account.balance();
// Update nested field
account.set_balance(current_balance + n);
Ok(())
}
fn check_permission(ctx: &ViewContext, account_id: String, user: String) -> bool {
ctx.model()
.accounts()
.get(account_id)
.map(|account| {
// Access nested Map
account.other_tenants().get(user).unwrap_or(false)
})
.unwrap_or(false)
}
}
Updating Nested Fields with Closures
fn deposit(ctx: &ProcContext, account_id: String, n: Integer) -> Result<(), Error> {
let account = ctx.model().accounts().get(account_id).ok_or(unknown_error())?;
// Update a field using a closure
account.update_balance(|b| b + n);
Ok(())
}
Storage Key Scoping
Keys are scoped by path, preventing collisions:
struct Storage {
pub map_a: Map<String, u64>, // path: "map_a.*"
pub map_b: Map<String, u64>, // path: "map_b.*"
}
Keys in map_a and map_b are independent—they can have the same key without collision.
Gas Considerations
Storage operations consume gas:
General guidance:
- Storage writes are more expensive than reads
- Minimize storage writes
- Cache reads when accessing multiple times
- Use lazy evaluation (models load only what you access)
- Map iterations over many keys can be expensive