Documentation

Contract SDK

The norn-sdk crate provides the building blocks for writing Norn loom smart contracts in Rust, targeting wasm32-unknown-unknown.

Overview

Contracts export three entry points: init(), execute(ptr, len), and query(ptr, len). The #[norn_contract] proc macro generates all boilerplate — you just write struct fields and annotated methods.

Setup

Add these dependencies to your contract's Cargo.toml:

[dependencies]
norn-sdk = { path = "../../norn-sdk" }
norn-sdk-macros = { path = "../../norn-sdk-macros" }
borsh = { version = "1", features = ["derive"] }
 
[lib]
crate-type = ["cdylib"]

Add the Wasm target:

rustup target add wasm32-unknown-unknown

Quick Start: Counter Contract

#![no_std]
 
extern crate alloc;
 
use norn_sdk::prelude::*;
 
#[norn_contract]
pub struct Counter {
    value: u64,
}
 
#[norn_contract]
impl Counter {
    #[init]
    pub fn new(_ctx: &Context) -> Self {
        Counter { value: 0 }
    }
 
    #[execute]
    pub fn increment(&mut self, _ctx: &Context) -> ContractResult {
        self.value += 1;
        Ok(Response::with_action("increment").set_data(&self.value))
    }
 
    #[execute]
    pub fn decrement(&mut self, _ctx: &Context) -> ContractResult {
        ensure!(self.value > 0, "counter is already zero");
        self.value -= 1;
        Ok(Response::with_action("decrement").set_data(&self.value))
    }
 
    #[execute]
    pub fn reset(&mut self, _ctx: &Context) -> ContractResult {
        self.value = 0;
        Ok(Response::with_action("reset").set_data(&self.value))
    }
 
    #[query]
    pub fn get_value(&self, _ctx: &Context) -> ContractResult {
        ok(self.value)
    }
}

The #[norn_contract] Macro

Apply #[norn_contract] to both a struct and its impl block:

On a struct — automatically derives BorshSerialize and BorshDeserialize:

#[norn_contract]
pub struct Counter { value: u64 }

On an impl block — generates dispatch enums, the Contract trait implementation, and the norn_entry! call from annotated methods:

  • #[init] — constructor (exactly one required, must return Self)
  • #[execute] — state-changing operation (&mut self, &Context, ...)
  • #[query] — read-only operation (&self, &Context, ...)
  • Unmarked methods are kept as internal helpers

Extra parameters on #[execute] and #[query] methods become fields in auto-generated message enums.

Entry Point Signatures

MarkerSignaturePurpose
#[init]fn name(ctx: &Context) -> SelfCalled once when bytecode is uploaded. Returns the initial contract state.
#[execute]fn name(&mut self, ctx: &Context, ...) -> ContractResultCalled for state-mutating operations.
#[query]fn name(&self, ctx: &Context, ...) -> ContractResultCalled for read-only queries.

ContractResult is defined as Result<Response, ContractError>.

Context

The Context struct provides access to the runtime environment:

// Get the transaction sender's address
let sender: Address = ctx.sender();
 
// Get current block height and timestamp
let height: u64 = ctx.block_height();
let time: u64 = ctx.timestamp();
 
// Assert sender is the expected address
ctx.require_sender(&expected_address)?;
 
// Emit a log message
ctx.log("something happened");
 
// Transfer tokens between accounts
ctx.transfer(&from, &to, &token_id, amount);
 
// Cross-contract call
let result: Option<Vec<u8>> = ctx.call_contract_raw(&target_loom_id, &input_bytes);

Storage Primitives

Storage primitives call host functions directly — they do not take a ctx parameter.

Item<T>

A single stored value.

const OWNER: Item<Address> = Item::new("owner");
 
// Save (returns Result)
OWNER.save(&address)?;
 
// Save in init (panics on failure, avoids .unwrap() noise)
OWNER.init(&address);
 
// Load (returns Result<T, ContractError> — errors if not found)
let owner: Address = OWNER.load()?;
 
// Load with default (returns T, never errors)
let owner: Address = OWNER.load_or(ZERO_ADDRESS);
let owner: Address = OWNER.load_or_default();
 
// Check existence
if OWNER.exists() { /* ... */ }
 
// Remove
OWNER.remove();
 
// Update in place
OWNER.update(|val| Ok(val + 1))?;

Map<K, V>

A key-value map.

const BALANCES: Map<Address, u128> = Map::new("balances");
 
// Save
BALANCES.save(&address, &amount)?;
 
// Save in init (panics on failure)
BALANCES.init(&address, &amount);
 
// Load (returns Result<V, ContractError>)
let balance: u128 = BALANCES.load(&address)?;
 
// Load with default
let balance: u128 = BALANCES.load_or(&address, 0u128);
 
// Check existence
if BALANCES.has(&address) { /* ... */ }
 
// Remove
BALANCES.remove(&address);
 
// Update in place
BALANCES.update(&address, |bal| Ok(bal + amount))?;
 
// Update with default if absent
BALANCES.update_or(&address, 0u128, |bal| Ok(bal + amount))?;

IndexedMap<K, V>

A map that tracks all keys for iteration.

const HOLDERS: IndexedMap<Address, u128> = IndexedMap::new("holders");
 
// Save, load, remove — same as Map
HOLDERS.save(&addr, &1000u128)?;
 
// Iteration support
let all_keys: Vec<Address> = HOLDERS.keys();
let page: Vec<(Address, u128)> = HOLDERS.range(0, 10);
let count: u64 = HOLDERS.len();

Response Builder

Build responses using the Response type:

// Simple response
Ok(Response::new())
 
// Response with an action attribute
Ok(Response::with_action("transfer"))
 
// Response with data and events
Ok(Response::with_action("transfer")
    .add_attribute("from", sender_hex)
    .add_address("to", &to_address)
    .add_u128("amount", amount)
    .set_data(&balance)
    .add_event(Event::new("Transfer")
        .add_attribute("from", sender_hex)
        .add_address("to", &to_address)
        .add_u128("amount", amount)))

Use the ok() helper to serialize a value directly into a response:

// Returns Ok(Response { data: borsh(value), .. })
ok(self.value)

Guard Macros

ensure!(amount > 0, "amount must be positive");
ensure!(amount > 0, ContractError::Custom("amount must be positive".into()));
ensure_eq!(sender, owner, ContractError::Unauthorized);
ensure_ne!(from, to, ContractError::Custom("cannot self-transfer".into()));

Standard Library (stdlib)

The SDK includes composable mixins for common patterns:

Ownable

Ownership management with transfer capability.

Pausable

Emergency pause/unpause functionality.

Norn20

Full NT-1 token implementation with mint, burn, transfer, and allowance support. Use Response::merge() to compose stdlib responses with your own:

let stdlib_resp = Norn20::mint(&to, amount)?;
Ok(Response::with_action("mint").merge(stdlib_resp))

Cross-Contract Calls

Contracts can call other contracts:

let result: Option<Vec<u8>> = ctx.call_contract_raw(&target_loom_id, &input_bytes);

Maximum call depth is 8. Re-entrancy (calling back into the same contract) is detected and rejected.

Building

cargo build --target wasm32-unknown-unknown --release \
  --manifest-path examples/counter/Cargo.toml

The compiled .wasm file will be at target/wasm32-unknown-unknown/release/counter.wasm.

Testing

Use TestEnv for native testing without a Wasm runtime:

#[cfg(test)]
mod tests {
    use super::*;
    use norn_sdk::testing::*;
 
    #[test]
    fn test_increment() {
        let env = TestEnv::new();
        let mut counter = Counter::new(&env.ctx());
 
        let resp = counter.increment(&env.ctx()).unwrap();
        assert_attribute(&resp, "action", "increment");
        assert_data::<u64>(&resp, &1);
    }
 
    #[test]
    fn test_decrement_at_zero_fails() {
        let env = TestEnv::new();
        let mut counter = Counter::new(&env.ctx());
 
        let err = counter.decrement(&env.ctx()).unwrap_err();
        assert_eq!(err.message(), "counter is already zero");
    }
 
    #[test]
    fn test_with_sender() {
        let env = TestEnv::new().with_sender(ALICE);
        let vault = TokenVault::new(&env.ctx());
 
        // Change sender mid-test
        env.set_sender(BOB);
        let err = vault.withdraw(&env.ctx(), BOB, 50).unwrap_err();
        assert_eq!(err, ContractError::Unauthorized);
    }
}

TestEnv API

// Create and configure
let env = TestEnv::new()
    .with_sender(ALICE)           // set sender address
    .with_block_height(42)        // set block height
    .with_timestamp(1700000000);  // set timestamp
 
// Get a Context for passing to contract methods
let ctx = env.ctx();
 
// Change state mid-test (non-consuming)
env.set_sender(BOB);
env.set_block_height(100);
env.set_timestamp(1700001000);
 
// Inspect captured side effects
let logs: Vec<String> = env.logs();
let events = env.events();
let transfers = env.transfers();
env.clear_logs();

Test Address Constants

The SDK provides named constants for test addresses:

use norn_sdk::testing::{ALICE, BOB, CHARLIE, DAVE};
// ALICE = [1u8; 20], BOB = [2u8; 20], etc.

Assertion Helpers

// Assert response contains an attribute
assert_attribute(&resp, "action", "deposit");
 
// Assert response data deserializes to expected value
assert_data::<u128>(&resp, &500);
 
// Deserialize response data manually
let info: VaultInfo = from_response(&resp).unwrap();
 
// Assert error message contains substring
assert_err_contains(&err, "must be positive");
 
// Assert response contains an event
assert_event(&resp, "Transfer");
assert_event_attribute(&resp, "Transfer", "amount", "100");