Skip to content

IDL and ABI

IDL (Interface Definition Language) files are the single source of truth for all contract interfaces. They define methods, parameters, return types, structs, and enums in a language-neutral format that drives code generation, ABI encoding, RPC exposure, and tooling discoverability.

The IDL-first approach provides several critical guarantees:

BenefitDescription
Language NeutralityOne definition generates Rust, Zig, TypeScript, Go, and C clients
Deterministic SelectorsMethod selectors computed from signatures, not arbitrary constants
Type SafetyParameter validation at build time and runtime
DiscoverabilityTools can introspect contracts without source code
VersioningBreaking changes detected through IDL comparison
┌─────────────────────────────────────────────────────────────────────────────┐
│ IDL Source │
│ (contracts/*.idl) │
└───────────────────────────────────┬─────────────────────────────────────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Manifest │ │ Code Stubs │ │ On-Chain │
│ (selectors, │ │ (Rust, Zig, │ │ Storage │
│ types) │ │ TS, Go, C) │ │ (UTF-8 IDL) │
└───────┬────────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ JSON Codec │ │ Contract │ │ RPC Query │
│ (encode/ │ │ Binary │ │ contract_idl │
│ decode) │ │ (.elf) │ │ │
└────────────────┘ └────────────────┘ └────────────────┘

IDL files use a simple, declarative syntax:

namespace sft_v1;
/// A token balance with optional lock period.
struct BalanceInfo {
amount: u128;
locked_until: Option<u64>;
}
/// Transfer event emitted on every balance change.
struct TransferEvent {
from: Address;
to: Address;
amount: u128;
}
/// Error codes returned by token operations.
enum TokenError {
InsufficientBalance(Unit);
NotAuthorized(Unit);
BlacklistedAddress(Address);
}
interface SftV1 {
/// Get the total token supply.
fn total_supply() -> u128;
/// Get balance for an account.
fn balance_of(account: Address) -> BalanceInfo;
/// Transfer tokens to another account.
fn transfer(to: Address, amount: u128) -> bool;
/// Mint new tokens (owner only).
fn mint(to: Address, amount: u128) -> bool;
}
TypeDescriptionJSON Encoding
boolBooleantrue / false
u8..u64Unsigned integersNumber
u128Large unsigned integerString (decimal)
i8..i64Signed integersNumber
i128Large signed integerString (decimal)
StringUTF-8 stringString
Address32-byte account addressHex string (0x…)
TypeDescriptionJSON Encoding
Vec<T>Variable-length array[...]
Option<T>Optional valuenull or value
Named structUser-defined record{field: value}
Named enumTagged union{type: "variant", value: payload}
  • Unit struct: Empty payload for enum variants
  • Documentation comments: /// lines become method/type docs
  • Namespace: Must match contract name for discoverability

Method selectors are deterministically computed from the canonical signature:

selector = keccak256(signature)[0..4]
signature = "Interface.method(param1:Type1,param2:Type2)"
IDL TypeCanonical Form
u128u128
Vec<Address>vec<address>
Option<String>option<string>
Named structStruct name (e.g., BalanceInfo)
interface: SftV1
method: transfer(to: Address, amount: u128)
signature: "SftV1.transfer(to:address,amount:u128)"
selector: keccak256(signature)[0..4] = 0x23b872dd

The idl-abi-gen crate generates language-specific code from IDL files.

Terminal window
cargo run -p idl-abi-gen -- \
--idl contracts/sft_v1/sft_v1.idl \
--out-dir contracts/sft_v1/src \
--rust-contract

Generates abi.rs containing:

  • Selector constants
  • Request/response structs
  • Encoding helpers
  • Dispatch router
Terminal window
# TypeScript client
ashen idl codegen --idl sft_v1.idl --lang typescript
# Rust client
ashen idl codegen --idl sft_v1.idl --lang rust
# Go client
ashen idl codegen --idl sft_v1.idl --lang go
# C header
ashen idl codegen --idl sft_v1.idl --lang c

When a contract is deployed, its IDL is stored on-chain alongside the bytecode:

// During deployment
store_contract_idl(&mut backend, &contract_address, idl_bytes);
// Query via RPC
let result = client.contract_idl(&address)?;
let idl_text = result.idl; // Option<String>

A deploy bundle contains three components:

┌───────────────────────────────────────┐
│ Bundle Header (manifest length, etc) │
├───────────────────────────────────────┤
│ Manifest (JSON with metadata) │
├───────────────────────────────────────┤
│ IDL (UTF-8 text) │
├───────────────────────────────────────┤
│ ELF Binary (RISC-V contract code) │
└───────────────────────────────────────┘
Terminal window
# Build contract
cd contracts/sft_v1 && cargo build --release --target riscv64gc-unknown-none-elf
# Create bundle with IDL
ashen contract deploy \
--elf target/riscv64gc-unknown-none-elf/release/sft_v1 \
--idl contracts/sft_v1/sft_v1.idl \
--wait

The node RPC itself is defined by an IDL (src/rpc/node_rpc_v1.idl), enabling the same tooling to work for both:

  1. Contract interaction - Call contract methods via view_call / tx_submit
  2. Node API - Query chain state via status, account, tx_by_hash, etc.

The IdlRpcClient dynamically discovers and invokes RPC methods:

use crate::idl_rpc::IdlRpcClient;
let client = IdlRpcClient::new("http://localhost:3030", None)?;
// List all available methods
for method in client.methods() {
println!("{}: {}", method.qualified_name, method.doc.unwrap_or_default());
}
// Get parameter template for a method
let template = client.param_template("account")?;
// {"address": "0x0000..."}
// Invoke method dynamically
let result = client.call("account", json!({"address": "0x123..."}))?;

Any deployed contract’s IDL can be queried:

Terminal window
# CLI
ashen idl fetch --contract 0x1234...
# RPC
curl -X POST http://localhost:3030/v2/rpc \
-d '{"id":1,"method":"NodeRpcV1.contract_idl","params":{"address":"0x1234..."}}'

The idl-json-codec crate handles bidirectional conversion between JSON and Borsh:

let abi = Abi::from_idl_path(&idl_path)?;
// Build calldata from JSON arguments
let calldata = abi.build_calldata(
Some("SftV1"),
"transfer",
Some(&json!({"to": "0x123...", "amount": "1000"})),
None
)?;
// calldata = [selector (4 bytes)] + [borsh-encoded params]
// Decode return data using method signature
let result = abi.decode_method_result(
Some("SftV1"),
"balance_of",
&return_bytes
)?;
// {"ok": {"amount": "1000", "locked_until": null}}
// or
// {"err": {"Code": {"code": 1, "data": null}}}

All contract returns use a standard envelope:

┌─────────────────────────────────────┐
│ Tag (1 byte) │
│ 0 = Error, 1 = Ok │
├─────────────────────────────────────┤
│ Payload (Borsh-encoded) │
│ Tag=0: ContractError │
│ Tag=1: Return type from IDL │
└─────────────────────────────────────┘

The TUI (Terminal User Interface) uses IDLs for contract discoverability:

Navigate and inspect registered IDLs:

┌─ IDL Explorer: sft_v1 ──────────────────────────────────────────────────────┐
│ Namespace: sft_v1 │
│ │
│ [1] Overview [2] Structs [3] Methods │
│ │
│ ► total_supply() -> u128 │
│ balance_of(account: Address) -> BalanceInfo │
│ transfer(to: Address, amount: u128) -> bool │
│ mint(to: Address, amount: u128) -> bool │
│ burn(amount: u128) -> bool │
│ approve(spender: Address, amount: u128) -> bool │
│ │
│ Press [enter] to invoke selected method │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ User selects contract │
└───────────────────────────────────┬────────────────────────────────────────┘
┌──────────────▼──────────────┐
│ Check local IDL cache │
└──────────────┬──────────────┘
┌─────────────────────┴─────────────────────┐
│ Cache hit │ Cache miss
▼ ▼
┌────────────────┐ ┌────────────────┐
│ Load from │ │ Query RPC │
│ ~/.ashen/ │ │ contract_idl │
│ idl-cache/ │ │ │
└───────┬────────┘ └───────┬────────┘
│ │
│ ▼
│ ┌────────────────┐
│ │ Cache IDL │
│ │ locally │
│ └───────┬────────┘
│ │
└──────────────────┬──────────────────────┘
┌────────────────┐
│ Parse IDL │
│ Display │
│ methods │
└────────────────┘

The IDL system enables AI agents to interact with contracts programmatically:

  1. Method Discovery: List all callable methods with types
  2. Parameter Templates: Generate valid JSON for any method
  3. Validation: Check arguments before submission
  4. Error Decoding: Structured error responses
Terminal window
# List methods (machine-readable)
ashen tui contract methods --rpc http://localhost:3030 --contract 0x123... --json
# Get parameter template
ashen contract inspect --idl sft_v1.idl | jq '.manifest.interfaces[].methods[]'

Check an IDL for errors before deployment:

Terminal window
ashen idl validate --idl contracts/sft_v1/sft_v1.idl

Validates:

  • Syntax correctness
  • Type references resolve
  • No selector collisions
  • Reserved word avoidance

Detect breaking changes between IDL versions:

Terminal window
ashen idl compat \
--old-idl contracts/sft_v1/sft_v1_v1.idl \
--new-idl contracts/sft_v1/sft_v1_v2.idl

Breaking Changes Detected:

  • Method removal
  • Method signature change
  • Struct field removal/type change
  • Enum variant removal

Non-Breaking Changes:

  • New methods added
  • New struct fields (if optional)
  • New enum variants
CommandDescription
ashen idl fetchFetch on-chain IDL for a contract
ashen idl generateGenerate manifest JSON from IDL
ashen idl validateValidate IDL syntax and semantics
ashen idl compatCheck compatibility between IDL versions
ashen idl codegenGenerate client code (TS/Rust/Go/C)
CommandDescription
ashen contract view --idl <path>Call view method with IDL
ashen contract call --idl <path>Execute state-changing call
ashen contract deploy --idl <path>Deploy with on-chain IDL
ashen contract inspect --idl <path>Show IDL manifest
Terminal window
# Add local IDL
ashen tui config idls add --name sft_v1 --path ./contracts/sft_v1/sft_v1.idl
# List registered IDLs
ashen tui config idls list
# Remove IDL
ashen tui config idls remove --name sft_v1
  1. Use descriptive names - BalanceInfo not BI
  2. Document everything - /// comments become API docs
  3. Group related types - Keep request/response pairs together
  4. Version namespaces - sft_v1, sft_v2 for major changes
  5. Keep structs small - Flatten deeply nested structures
  1. Always include IDL - Enables discoverability
  2. Validate before deploy - Catch errors early
  3. Check compatibility - Before upgrading contracts
  4. Cache IDLs locally - Reduce RPC calls in development
  1. Use typed clients - Generate code instead of manual encoding
  2. Validate inputs - Use IdlRpcClient.validate_params()
  3. Handle errors - Decode structured errors for debugging
  4. Log method calls - Include selector for traceability