Skip to content

Custom Modules

EVMcrispr is extensible through modules. This guide covers how to build your own module using the SDK.

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.json

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,
}) {}

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,
]),
];
},
});

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));
},
});
TypeDescription
addressEthereum address
numberInteger (possibly large)
stringString value
bytesHex-encoded bytes
bytes3232-byte hex value
boolBoolean
arrayArray of values
anyAny type
write-abiFunction signature (state-changing)
read-abiFunction signature (view/pure)
token-symbolToken symbol or address
blockBlock of sub-commands
variableVariable name ($name)
helperHelper reference (@name)
commandCommand reference
expressionExpression to be evaluated
daoDAO identifier
json-pathJSON path expression
{
name: "params",
type: "any",
optional: true, // Argument is not required
rest: true, // Collects remaining arguments as an array
}

After adding commands or helpers, regenerate the import map:

Terminal window
cd modules/my-module
bun ../../packages/sdk/scripts/codegen.ts

This creates src/_generated.ts with lazy imports and metadata.

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
// ...
}
Terminal window
bun run build # Build all packages
bun test:unit # Run tests