Writing a Module
EVMcrispr is extensible through modules. This guide covers how to build a TypeScript module with the SDK — the kind that ships with EVMcrispr itself and appears in the Reference section. To publish reusable commands written in EVML itself, see Publishing Modules instead.
Module Structure
Section titled “Module Structure”A module lives in modules/<name>/ and contains:
modules/my-module/ src/ commands/ my-command.ts helpers/ my-helper.ts _generated.ts # Auto-generated by codegen index.ts # Module definition package.jsonDefining a Module
Section titled “Defining a Module”Create src/index.ts:
import { defineModule } from "@evmcrispr/sdk";import { commands, helpers } from "./_generated";
export default class MyModule extends defineModule({ name: "my-module", commands, helpers,}) {}Defining a Command
Section titled “Defining a Command”Commands produce transactions (Action[]). Create src/commands/my-command.ts:
import { defineCommand, encodeAction } from "@evmcrispr/sdk";import type MyModule from "..";
export default defineCommand<MyModule>({ name: "my-command", description: "Do something on-chain.", args: [ { name: "target", type: "address" }, { name: "amount", type: "number" }, ], opts: [ { name: "from", type: "address" }, ], async run(module, { target, amount }, { opts }) { return [ encodeAction(target, "transfer(address,uint256)", [ opts.from ?? target, amount, ]), ]; },});Defining a Helper
Section titled “Defining a Helper”Helpers produce values. Create src/helpers/my-helper.ts:
import { defineHelper } from "@evmcrispr/sdk";import type MyModule from "..";
export default defineHelper<MyModule>({ name: "my-helper", description: "Compute something.", returnType: "number", args: [ { name: "value", type: "number" }, { name: "multiplier", type: "number", optional: true }, ], async run(module, { value, multiplier = 2 }) { return String(BigInt(value) * BigInt(multiplier)); },});Argument Types
Section titled “Argument Types”| Type | Description |
|---|---|
address | Ethereum address |
number | Integer (possibly large) |
string | String value |
bytes | Hex-encoded bytes |
bytes32 | 32-byte hex value |
bool | Boolean |
array | Array of values |
any | Any type |
write-abi | Function signature (state-changing) |
read-abi | Function signature (view/pure) |
token-symbol | Token symbol or address |
block | Block of sub-commands |
variable | Variable name ($name) |
helper | Helper reference (@name) |
command | Command reference |
expression | Expression to be evaluated |
dao | DAO identifier |
json-path | JSON path expression |
Argument Options
Section titled “Argument Options”{ name: "params", type: "any", optional: true, // Argument is not required rest: true, // Collects remaining arguments as an array}Marking Things Experimental
Section titled “Marking Things Experimental”Set "experimental": true in the module's package.json to gate the whole
module, or experimental: true in a defineCommand/defineHelper config
(or on a single option definition) to gate one entry. Experimental items
are excluded from execution, completions, and docs unless the build sets
VITE_PUBLIC_EXPERIMENTAL=true. In hand-written docs, wrap prose about an
experimental feature in an :::experimental block — it is stripped from
builds that don't enable the flag.
Running Codegen
Section titled “Running Codegen”After adding commands or helpers, regenerate the import map:
cd modules/my-modulebun ../../packages/sdk/scripts/codegen.tsThis creates src/_generated.ts with lazy imports and metadata.
Accessing Module State
Section titled “Accessing Module State”Commands and helpers receive the module instance as their first argument. Use it to access shared state:
async run(module, args) { const client = await module.getClient(); // Viem PublicClient const chainId = await module.getChainId(); // Current chain ID // ...}Building and Testing
Section titled “Building and Testing”bun run build # Build all packagesbun test:unit # Run tests