Skip to main content
Full working code: The complete source code for this example is available in example-contracts/token/
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", path = "contract/wit");

    #[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()