Generating Contract Bindings
To interact with a smart contract from your TypeScript workflow, you first need to create bindings. Bindings are type-safe TypeScript classes auto-generated from your contract's ABI. They handle all encoding and decodingโincluding base64 conversion for the CRE SDK wire formatโso you can work directly with native TypeScript types.
How they work depends on whether you are reading from or writing to the chain:
- For onchain reads, bindings provide TypeScript methods that directly mirror your contract's
viewandpurefunctions. - For onchain writes, bindings provide
writeReportFrom<FunctionName>()helpers that ABI-encode your data and submit a signed report. - For event triggers, bindings provide
logTrigger<EventName>()methods that handle topic encoding and return a typed trigger object. The handler receives fully decoded event dataโno manual hex or base64 conversion needed.
This is a one-time code generation step performed using the CRE CLI.
The generation process
The CRE CLI reads your ABI files and generates a typed class with all the methods your workflow needs.
The target language is auto-detected from your project files (presence of package.json picks TypeScript). You can also force TypeScript explicitly with the --language flag:
cre generate-bindings evm --language typescript
Step 1: Add your contract ABI
Place your contract's ABI JSON file into the contracts/evm/src/abi/ directory. For example, to generate bindings for a PriceUpdater contract, create contracts/evm/src/abi/PriceUpdater.abi with your ABI content.
Step 2: Generate the bindings
From your project root, run:
cre generate-bindings evm
This scans all .abi files in contracts/evm/src/abi/ and generates corresponding TypeScript files in contracts/evm/ts/generated/. For each contract, three files are generated:
<ContractName>.tsโ The typed binding class with read, write, and event trigger methods.<ContractName>_mock.tsโ A mock implementation for testing your workflows without deploying contracts.index.tsโ A barrel file that re-exports everything from all generated bindings in the directory.
Each binding class is named after the contract and is imported directly into your workflow.
Using generated bindings
For onchain reads
For view or pure functions, the generator creates methods on the class that call the contract and return the decoded result. These methods do not return a Promise โ they synchronously return the decoded value after the DON reaches consensus.
Example: A simple Storage contract
Create contracts/evm/src/abi/Storage.abi with the following content. This contract is already deployed on Sepolia at 0xa17CF997C28FF154eDBae1422e6a50BeF23927F4 with an initial value of 22, so you can run this example without deploying anything.
[
{
"inputs": [{ "internalType": "uint256", "name": "initialValue", "type": "uint256" }],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "get",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "value",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
}
]
After running cre generate-bindings evm, use the generated Storage class in your workflow:
import { EVMClient, handler, CronCapability, Runner, type Runtime } from "@chainlink/cre-sdk"
import { Storage } from "../contracts/evm/ts/generated/Storage"
type Config = {
schedule: string
storageAddress: string
chainSelector: string
}
const onCronTrigger = (runtime: Runtime<Config>): string => {
const config = runtime.config
// EVMClient takes the chain selector as a bigint directly
const client = new EVMClient(BigInt(config.chainSelector))
const storageContract = new Storage(client, config.storageAddress as `0x${string}`)
// Call the view function โ result is already a decoded bigint
const value = storageContract.get(runtime)
runtime.log(`Storage value: ${value}`)
return value.toString()
}
export const initWorkflow = (config: Config) => {
const cron = new CronCapability()
return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
}
export async function main() {
const runner = await Runner.newRunner<Config>()
await runner.run(initWorkflow)
}
config.staging.json:
{
"schedule": "*/30 * * * * *",
"storageAddress": "0xa17CF997C28FF154eDBae1422e6a50BeF23927F4",
"chainSelector": "16015286601757825753"
}
Run the simulation from your project root:
cre workflow simulate my-workflow
Expected output:
โ Workflow compiled
[SIMULATION] Simulator Initialized
[SIMULATION] Running trigger [email protected]
[USER LOG] Storage value: 22
โ Workflow Simulation Result:
"22"
For onchain writes
For write functions, the generator creates a writeReportFrom<FunctionName>() method that handles ABI encoding, report generation, and submission in one step.
Signaling the generator
To generate write helpers, your ABI must include at least one public or external non-view function. The generated method is named after the function name in your ABI.
Example: A PriceUpdater contract
// contracts/evm/src/abi/PriceUpdater.abi
contract PriceUpdater {
struct PriceData {
uint256 ethPrice;
uint256 btcPrice;
}
function updatePrices(PriceData memory) public {}
}
After running cre generate-bindings evm, you can use the generated class:
import { EVMClient, handler, CronCapability, Runner, type Runtime } from "@chainlink/cre-sdk"
import { PriceUpdater } from "../contracts/evm/ts/generated/PriceUpdater"
type Config = {
schedule: string
proxyAddress: string
chainSelector: string
}
const onCronTrigger = (runtime: Runtime<Config>) => {
const config = runtime.config
const client = new EVMClient(BigInt(config.chainSelector))
const contract = new PriceUpdater(client, config.proxyAddress as `0x${string}`)
// Pass the function arguments directly โ types are derived from the ABI
return contract.writeReportFromUpdatePrices(runtime, { ethPrice: 4000_000000n, btcPrice: 60000_000000n })
}
export const initWorkflow = (config: Config) => {
const cron = new CronCapability()
return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
}
export async function main() {
const runner = await Runner.newRunner<Config>()
await runner.run(initWorkflow)
}
For event logs
The binding generator creates strongly-typed trigger and decoder methods for each event in your ABI. This replaces all manual hexToBase64 and topic encoding โ the generated method handles it automatically.
Example: A contract with a UserAdded event
contract UserDirectory {
event UserAdded(address indexed userAddress, string userName);
function addUser(string calldata userName) external {
emit UserAdded(msg.sender, userName);
}
}
Generated types
For each event, the generator creates two types:
<EventName>Topicsโ Optional filter params (indexed fields only). Pass one or more of these to filter events by specific values.<EventName>Decodedโ All event fields, decoded to their TypeScript types.
// Generated in contracts/evm/ts/generated/UserDirectory.ts
export type UserAddedTopics = {
userAddress?: `0x${string}` // indexed field โ can be used for filtering
}
export type UserAddedDecoded = {
userAddress: `0x${string}`
userName: string
}
Triggering and decoding events
Use the logTrigger<EventName>() method in your initWorkflow function to create a trigger, and the decode<EventName>() method (accessed via the trigger's adapt property) to decode the log data in your handler.
import { EVMClient, handler, Runner, type Runtime } from "@chainlink/cre-sdk"
import { UserDirectory } from "../contracts/evm/ts/generated/UserDirectory"
import type { DecodedLog, UserAddedDecoded } from "../contracts/evm/ts/generated/UserDirectory"
type Config = {
contractAddress: string
chainSelector: string
}
export const initWorkflow = (config: Config) => {
const client = new EVMClient(BigInt(config.chainSelector))
const userDirectory = new UserDirectory(client, config.contractAddress as `0x${string}`)
// Create a trigger for all UserAdded events (no filter)
const userAddedTrigger = userDirectory.logTriggerUserAdded()
// To filter for a specific user address:
// const userAddedTrigger = userDirectory.logTriggerUserAdded([
// { userAddress: "0xabc..." }
// ])
return [handler(userAddedTrigger, onUserAdded)]
}
const onUserAdded = (runtime: Runtime<Config>, log: DecodedLog<UserAddedDecoded>) => {
// log.data is already the typed UserAddedDecoded object โ no manual decoding needed
runtime.log(`New user added! address=${log.data.userAddress} name=${log.data.userName}`)
}
export async function main() {
const runner = await Runner.newRunner<Config>()
await runner.run(initWorkflow)
}
What the CLI generates
For each ABI file, the generator creates three files in contracts/evm/ts/generated/:
<ContractName>.tsโ The main binding class<ContractName>_mock.tsโ A mock implementation for testingindex.tsโ A barrel re-exporting everything, so you can import from a single path
What's inside depends on your ABI:
- For all contracts:
- A typed
<ContractName>ABIconstant for use with viem utilities. - A
<ContractName>class with awriteReport(runtime, callData, gasConfig)base write method. - A
<ContractName>Mocktype with optional function fields for eachview/puremethod (e.g.,get?: () => bigint) plus awriteReportfield, and anew<ContractName>Mock(address, evmMock)factory.
- A typed
- For onchain reads (each
view/purefunction):- A method on the class (e.g.,
get(runtime)) that returns the decoded value directly as a native TypeScript type.
- A method on the class (e.g.,
- For onchain writes (each non-view function):
- A
writeReportFrom<FunctionName>(runtime, args, gasConfig?)method that handles ABI encoding, report generation, and submission in one step.
- A
- For events (each
eventdefinition):<EventName>Topicsand<EventName>Decodedtypes.- A
logTrigger<EventName>(filters?)method that returns a typed trigger object with OR semantics for multiple filters. - A
decode<EventName>(log)method that decodes a rawEVMLogintoDecodedLog<EventDecoded>.
You can import from the barrel file to keep imports clean:
import { Storage, newStorageMock } from "../contracts/evm/ts/generated"
Best practices
- Regenerate when needed: Re-run
cre generate-bindings evmwhenever you update your contract ABIs. Do not edit generated files by hand. - Handle errors: The write and trigger methods will throw if encoding or network calls fail. Wrap them in try/catch blocks in your workflow handlers.
- Use explicit
--languagein CI: If your project has bothgo.modandpackage.json, auto-detection may be ambiguous. Pass--language typescriptexplicitly in CI pipelines. - Organize ABIs: Keep your
.abifiles clearly named incontracts/evm/src/abi/. The file name determines the generated class name.
Where to go next
Now that you know how to generate bindings, you can use them to read data from or write data to your contracts, or trigger workflows from events.