Custom Modules
EVMcrispr is extensible through modules. This guide covers how to build your own module using the SDK.
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}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