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 contracts are tested using the testlib crate, which provides a test runtime that simulates the blockchain environment. Tests use the same contract code that runs on-chain, with an in-memory backend for fast iteration.
Test Harness Setup
Test File Structure
use testlib::*;
// Import contract interface (path relative to test directory)
interface!(name = "token", path = "contract/wit");
// Test function with #[testlib::test] attribute
#[testlib::test]
async fn test_token_contract() -> Result<()> {
// 'runtime' is injected automatically
let signer = runtime.identity().await?;
let token = runtime.publish(&signer, "token").await?;
// Test calls (note the double ?? for Result<Result<T, Error>>)
token::mint(runtime, &token, &signer, 100.into()).await??;
let balance = token::balance(runtime, &token, &signer).await?;
assert_eq!(balance, Some(100.into()));
Ok(())
}
The #[testlib::test] Attribute
Syntax:
#[testlib::test]
async fn my_test() -> Result<()> { ... }
#[testlib::test(mode = "regtest")]
async fn my_integration_test() -> Result<()> { ... }
Parameters:
mode - Optional: "regtest" for integration tests against real blockchain
What it provides:
runtime: &mut Runtime - Automatically injected variable
- Test isolation (each test gets fresh state)
- Automatic contract loading from compiled bytecode
Runtime API
impl Runtime {
// Create a new identity (test user)
async fn identity(&mut self) -> Result<Signer>;
// Publish a contract
async fn publish(&mut self, signer: &Signer, name: &str) -> Result<ContractAddress>;
// Get contract's WIT interface
async fn wit(&mut self, contract_address: &ContractAddress) -> Result<String>;
// Issue gas to a signer
async fn issuance(&mut self, signer: &Signer) -> Result<()>;
}
Creating Test Identities
Each test can create multiple users:
#[testlib::test]
async fn test_multi_user() -> Result<()> {
let alice = runtime.identity().await?;
let bob = runtime.identity().await?;
// Both users are funded with gas automatically
let token = runtime.publish(&alice, "token").await?;
token::mint(runtime, &token, &alice, 1000.into()).await?;
token::transfer(runtime, &token, &alice, &bob, 100.into()).await??;
Ok(())
}
Publishing Contracts
#[testlib::test]
async fn test_publish() -> Result<()> {
let deployer = runtime.identity().await?;
// Publishes from example-contracts/target/.../token.wasm.br
let token = runtime.publish(&deployer, "token").await?;
// Contract is initialized (init() is called automatically)
// Returns ContractAddress { name: "token", height: 1, tx_index: 0 }
Ok(())
}
Contract addressing:
- Contracts are identified by
(name, height, tx_index)
- In tests, first published contract is at height 1, tx_index 0
- Second contract is at height 2, tx_index 0, etc.
Calling Contract Functions
Understanding the Double Question Mark
Generated contract functions return Result<Result<T, Error>>:
// Signature of generated function:
pub async fn transfer(
runtime: &mut Runtime,
contract_address: &ContractAddress,
signer: &Signer,
to: &str,
n: Integer
) -> Result<Result<(), Error>>
Why two Results?
- Outer
Result - Runtime errors (network, execution failures)
- Inner
Result - Contract errors (business logic failures)
Usage:
// Option 1: Propagate both errors
token::transfer(runtime, &token, &alice, &bob, 100.into()).await??;
// ^^
// First ? unwraps runtime error
// Second ? unwraps contract error
// Option 2: Handle separately
let result = token::transfer(runtime, &token, &alice, &bob, 100.into()).await?;
match result {
Ok(()) => { /* transfer succeeded */ },
Err(e) => { /* handle contract error */ },
}
Testing View Functions
#[testlib::test]
async fn test_balance_query() -> Result<()> {
let alice = runtime.identity().await?;
let token = runtime.publish(&alice, "token").await?;
// View functions return Option or direct values
let balance = token::balance(runtime, &token, &alice).await?;
assert_eq!(balance, None); // No tokens yet
token::mint(runtime, &token, &alice, 100.into()).await?;
let balance = token::balance(runtime, &token, &alice).await?;
assert_eq!(balance, Some(100.into()));
Ok(())
}
Testing Errors
Expected Business Logic Errors
#[testlib::test]
async fn test_insufficient_funds() -> Result<()> {
let alice = runtime.identity().await?;
let bob = runtime.identity().await?;
let token = runtime.publish(&alice, "token").await?;
// Alice has no tokens, transfer should fail
let result = token::transfer(runtime, &token, &alice, &bob, 100.into()).await?;
assert_eq!(
result,
Err(Error::Message("insufficient funds".to_string()))
);
Ok(())
}
Using is_err_and
let result = risky_operation().await;
assert!(result.is_err_and(|e|
e.root_cause().to_string().contains("expected error message")
));
Testing Cross-Contract Calls
#[testlib::test]
async fn test_fib_with_arith() -> Result<()> {
let signer = runtime.identity().await?;
// Publish both contracts
let arith = runtime.publish(&signer, "arith").await?;
let fib = runtime.publish(&signer, "fib").await?;
// Fib contract calls arith contract internally
let result = fib::fib(runtime, &fib, &signer, arith.clone(), 8).await?;
assert_eq!(result, 21);
Ok(())
}
Testing Re-entrancy Protection
The runtime prevents re-entrancy automatically:
#[testlib::test]
async fn test_reentrancy() -> Result<()> {
let signer = runtime.identity().await?;
let fib = runtime.publish(&signer, "fib").await?;
let arith = runtime.publish(&signer, "arith").await?;
// arith tries to call fib which calls back to arith -> Error
let result = arith::fib(runtime, &arith, &signer, fib.clone(), 9).await;
assert!(result.is_err_and(|e|
e.root_cause().to_string().contains("reentrancy prevented")
));
Ok(())
}
Complete Test Example
use testlib::*;
use tracing::info;
interface!(name = "token", path = "../example-contracts/token/wit");
async fn run_test_token_contract(runtime: &mut Runtime) -> Result<()> {
info!("test_token_contract");
let minter = runtime.identity().await?;
let holder = runtime.identity().await?;
let token = runtime.publish(&minter, "token").await?;
// Mint tokens
token::mint(runtime, &token, &minter, 900.into()).await?;
token::mint(runtime, &token, &minter, 100.into()).await?;
// Check balance
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()))
);
// Successful transfers
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);
Ok(())
}
#[testlib::test]
async fn test_token_contract() -> Result<()> {
run_test_token_contract(runtime).await
}
Integration Tests (Regtest Mode)
Tests can run against a real Bitcoin regtest blockchain:
#[testlib::test(mode = "regtest")]
async fn test_token_on_regtest() -> Result<()> {
// Enable logging
logging::setup();
let alice = runtime.identity().await?;
let token = runtime.publish(&alice, "token").await?;
token::mint(runtime, &token, &alice, 1000.into()).await?;
// State is persisted to real blockchain
Ok(())
}
Regtest mode:
- Runs against actual Bitcoin regtest
- State persists across tests
- Slower but provides full blockchain validation
- Useful for final integration testing before testnet deployment
Running Tests
# Run all tests in a contract
cd example-contracts/token
cargo test
# Run specific test
cargo test test_token_contract
# Run with logging output
cargo test test_token_contract -- --nocapture
# Run integration tests
cargo test test_token_contract_regtest --release
Test Organization
Test directory structure:
test/
├── Cargo.toml
├── build.rs # Compiles contract to WASM
└── src/
└── lib.rs # Test module
Test file (test/src/lib.rs):
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "my_contract", path = "contract/wit");
#[testlib::test]
async fn test_something() -> Result<()> {
// Test code
}
}
Best Practices
1. Test both success and failure cases
// Success path
token::transfer(runtime, &token, &alice, &bob, 10.into()).await??;
// Failure path
let result = token::transfer(runtime, &token, &alice, &bob, 99999.into()).await?;
assert!(result.is_err());
2. Use descriptive test names
#[testlib::test]
async fn test_transfer_fails_with_insufficient_funds() -> Result<()> { ... }
3. Factor out common setup
async fn setup_token_with_balance(runtime: &mut Runtime, amount: Integer) -> Result<(Signer, ContractAddress)> {
let signer = runtime.identity().await?;
let token = runtime.publish(&signer, "token").await?;
token::mint(runtime, &token, &signer, amount).await?;
Ok((signer, token))
}
#[testlib::test]
async fn test_with_setup() -> Result<()> {
let (alice, token) = setup_token_with_balance(runtime, 1000.into()).await?;
// ... test logic
}
Quick Reference
Runtime API
The test runtime is automatically injected via the #[testlib::test] attribute:
runtime.identity() -> Result<Signer>
- Creates a new test identity (user) with automatic gas funding
runtime.publish(&signer, “name”) -> Result<ContractAddress>
- Deploys a contract from compiled bytecode and returns its address
runtime.wit(&contract_address) -> Result<String>
- Retrieves the WIT interface of a deployed contract
Test Attribute
Basic test:
#[testlib::test]
async fn my_test() -> Result<()> {
// 'runtime' is auto-injected
let alice = runtime.identity().await?;
let contract = runtime.publish(&alice, "my-contract").await?;
// ... test code
Ok(())
}
Integration test (Bitcoin regtest):
#[testlib::test(mode = "regtest")]
async fn my_integration_test() -> Result<()> {
// ... test code
}
Interface Macro
Generates type-safe bindings for contract calls:
interface!(name = "token", path = "contract/wit");
Generated functions are async and take runtime, contract_address, and signer parameters:
token::mint(runtime, &token_address, &signer, amount).await??;
Note: For tests, use interface! which generates bindings for any contract address. The import! macro (for fixed addresses) is rarely needed in tests.