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.
This example demonstrates persistent storage, user balances, transactions, and error handling for a fungible token.
WIT Interface
Exports functions for a fungible token with minting, burning, transfers, and balance queries:
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{core-context, view-context, proc-context, signer};
use kontor:built-in/error.{error};
use kontor:built-in/numbers.{integer};
record balance {
key: string,
value: integer,
}
export init: func(ctx: borrow<proc-context>);
export mint: func(ctx: borrow<proc-context>, n: integer) -> result<_, error>;
export burn: func(ctx: borrow<proc-context>, n: integer) -> result<_, error>;
export transfer: func(ctx: borrow<proc-context>, to: string, n: integer) -> result<_, error>;
export balance: func(ctx: borrow<view-context>, acc: string) -> option<integer>;
export balances: func(ctx: borrow<view-context>) -> list<balance>;
export total-supply: func(ctx: borrow<view-context>) -> integer;
}
Rust Implementation
The token contract uses a Map to store account balances, with helper functions for validation and a total supply tracker:
use stdlib::*;
contract!(name = "token");
const BURNER: &str = "burn";
#[derive(Clone, Default, StorageRoot)]
struct TokenStorage {
pub ledger: Map<String, Integer>,
pub total_supply: Integer,
}
fn assert_gt_zero(n: Integer) -> Result<(), Error> {
if n <= 0.into() {
return Err(Error::Message("Amount must be positive".to_string()));
}
Ok(())
}
impl Guest for Token {
fn init(ctx: &ProcContext) {
TokenStorage::default().init(ctx);
}
fn mint(ctx: &ProcContext, n: Integer) -> Result<(), Error> {
assert_gt_zero(n)?;
let to = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let balance = ledger.get(&to).unwrap_or_default();
ledger.set(to, balance.add(n)?);
ctx.model().try_update_total_supply(|t| t.add(n))?;
Ok(())
}
fn burn(ctx: &ProcContext, n: Integer) -> Result<(), Error> {
Self::transfer(ctx, BURNER.to_string(), n)?;
ctx.model().try_update_total_supply(|t| t.sub(n))?;
Ok(())
}
fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> {
assert_gt_zero(n)?;
let from = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let from_balance = ledger.get(&from).unwrap_or_default();
let to_balance = ledger.get(&to).unwrap_or_default();
if from_balance < n {
return Err(Error::Message("insufficient funds".to_string()));
}
ledger.set(from, from_balance.sub(n)?);
ledger.set(to, to_balance.add(n)?);
Ok(())
}
fn balance(ctx: &ViewContext, acc: String) -> Option<Integer> {
ctx.model().ledger().get(acc)
}
fn balances(ctx: &ViewContext) -> Vec<Balance> {
ctx.model()
.ledger()
.keys()
.filter_map(|k| {
if [BURNER.to_string()].contains(&k) {
None
} else {
Some(Balance {
value: ctx.model().ledger().get(&k).unwrap_or_default(),
key: k,
})
}
})
.collect()
}
fn total_supply(ctx: &ViewContext) -> Integer {
ctx.model().total_supply()
}
}
Key points:
- Storage accessed via
ctx.model(), not a separate storage() function
- Map operations:
ledger.get(&key) and ledger.set(key, value) - no extra ctx parameter
- Errors use
Error::Message(...to_string()) variant
- Uses checked arithmetic (
.add(n)?, .sub(n)?) for overflow safety
- Helper validation function (
assert_gt_zero)
- Total supply tracking with
try_update_* closures
Testing
The test demonstrates minting, transferring, and error handling with proper use of the double ?? operator for functions returning Result:
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "token");
#[testlib::test]
async fn test_contract() -> Result<()> {
let minter = runtime.identity().await?;
let holder = runtime.identity().await?;
let token = runtime.publish(&minter, "token").await?;
token::mint(runtime, &token, &minter, 900.into()).await??;
token::mint(runtime, &token, &minter, 100.into()).await??;
let result = token::balance(runtime, &token, &minter).await?;
assert_eq!(result, Some(1000.into()));
// Test insufficient funds error
let result = token::transfer(runtime, &token, &holder, &minter, 123.into()).await?;
assert_eq!(
result,
Err(Error::Message("insufficient funds".to_string()))
);
token::transfer(runtime, &token, &minter, &holder, 40.into()).await??;
token::transfer(runtime, &token, &minter, &holder, 2.into()).await??;
let result = token::balance(runtime, &token, &holder).await?;
assert_eq!(result, Some(42.into()));
let result = token::balance(runtime, &token, &minter).await?;
assert_eq!(result, Some(958.into()));
// Test non-existent account
let result = token::balance(runtime, &token, "foo").await?;
assert_eq!(result, None);
// Test balances list and total supply
let balances = token::balances(runtime, &token).await?;
assert_eq!(balances.len(), 2);
let total = balances
.iter()
.fold(Integer::from(0), |acc, x| acc + x.value);
assert_eq!(total, token::total_supply(runtime, &token).await?);
Ok(())
}
}
Key points:
- Uses
interface! to generate bindings, not import!
#[testlib::test] auto-injects runtime variable
runtime.identity() creates test users
runtime.publish() deploys the contract
- Functions returning
Result use ?? to unwrap both the runtime Result and contract Result
- Integers created with
.into() or Integer::from()