Onchain Read
This guide explains how to read data from a smart contract from within your CRE workflow. The TypeScript SDK uses viem for ABI handling and the SDK's EVMClient to create a type-safe developer experience.
The read pattern
Reading from a contract follows this pattern:
- Define your contract ABI: Create a TypeScript file with your contract's ABI using viem's
parseAbi(inline) or store it incontracts/abi/for complex workflows - Get network information: Use the SDK's
getNetwork()helper to look up chain selector and other network details - Instantiate the EVM Client: Create an
EVMClientinstance with the chain selector - Encode the function call: Use viem's
encodeFunctionData()to ABI-encode your function call - Encode the call message: Use
encodeCallMsg()to create a properly formatted call message withfrom,to, anddata - Call the contract: Use
callContract(runtime, {...})to execute the read operation - Decode the result: Use viem's
decodeFunctionResult()to decode the returned data - Await the result: Call
.result()on the returned object to get the consensus-verified result
Step-by-step example
Let's read a value from a simple Storage contract with a get() view returns (uint256) function.
1. Define the contract ABI
For simple contracts, you can define the ABI inline using viem's parseAbi:
import { parseAbi } from "viem"
const storageAbi = parseAbi(["function get() view returns (uint256)"])
For complex workflows with multiple contracts, it's recommended to create separate ABI files in a contracts/abi/ directory. See Part 3 of the Getting Started guide for an example of this pattern.
2. The workflow logic
Here's a complete example of reading from a Storage contract:
import {
cre,
getNetwork,
encodeCallMsg,
bytesToHex,
LAST_FINALIZED_BLOCK_NUMBER,
type Runtime,
Runner,
} from "@chainlink/cre-sdk"
import { type Address, encodeFunctionData, decodeFunctionResult, parseAbi, zeroAddress } from "viem"
import { z } from "zod"
// Define config schema with Zod
const configSchema = z.object({
contractAddress: z.string(),
chainSelectorName: z.string(),
})
type Config = z.infer<typeof configSchema>
// Define the Storage contract ABI
const storageAbi = parseAbi(["function get() view returns (uint256)"])
const onCronTrigger = (runtime: Runtime<Config>): string => {
// Get network information
const network = getNetwork({
chainFamily: "evm",
chainSelectorName: runtime.config.chainName,
isTestnet: true,
})
if (!network) {
throw new Error(`Network not found: ${runtime.config.chainSelectorName}`)
}
// Create EVM client with chain selector
const evmClient = new cre.capabilities.EVMClient(network.chainSelector.selector)
// Encode the function call
const callData = encodeFunctionData({
abi: storageAbi,
functionName: "get",
args: [], // No arguments for this function
})
// Call the contract
const contractCall = evmClient
.callContract(runtime, {
call: encodeCallMsg({
from: zeroAddress,
to: runtime.config.contractAddress as Address,
data: callData,
}),
blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
})
.result()
// Decode the result (convert Uint8Array to hex string for viem)
const storedValue = decodeFunctionResult({
abi: storageAbi,
functionName: "get",
data: bytesToHex(contractCall.data),
})
runtime.log(`Successfully read storage value: ${storedValue.toString()}`)
return storedValue.toString()
}
const initWorkflow = (config: Config) => {
return [
cre.handler(
new cre.capabilities.CronCapability().trigger({
schedule: "*/10 * * * * *", // Every 10 seconds
}),
onCronTrigger
),
]
}
export async function main() {
const runner = await Runner.newRunner<Config>()
await runner.run(initWorkflow)
}
main()
Understanding the components
Network lookup with getNetwork()
The SDK provides a getNetwork() helper that looks up network information by name:
const network = getNetwork({
chainFamily: "evm",
chainSelectorName: "ethereum-testnet-sepolia",
isTestnet: true,
})
// Returns network info including:
// - chainSelector.selector (numeric ID)
// - name
// - chainType
See the EVM Client SDK Reference for all available networks.
Block number options
When calling callContract(), you can specify which block to read from:
LAST_FINALIZED_BLOCK_NUMBER: Read from the last finalized block (recommended for production)LATEST_BLOCK_NUMBER: Read from the latest block- Specific block: Use an object with
{ absVal: "base64EncodedNumber", sign: "1" }format
import { LAST_FINALIZED_BLOCK_NUMBER, LATEST_BLOCK_NUMBER } from "@chainlink/cre-sdk"
// Read from finalized block (most common)
const contractCall = evmClient.callContract(runtime, {
call: encodeCallMsg({...}),
blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
}).result()
// Or read from latest block
const contractCall = evmClient.callContract(runtime, {
call: encodeCallMsg({...}),
blockNumber: LATEST_BLOCK_NUMBER,
}).result()
Encoding call messages with encodeCallMsg()
The encodeCallMsg() helper converts your hex-formatted call data into the base64 format required by the EVM capability:
import { encodeCallMsg } from "@chainlink/cre-sdk"
import { zeroAddress } from "viem"
const callMsg = encodeCallMsg({
from: zeroAddress, // Caller address (typically zeroAddress for view functions)
to: "0xYourContractAddress", // Contract address
data: callData, // ABI-encoded function call from encodeFunctionData()
})
This helper is required because the underlying EVM capability expects addresses and data in base64 format, not hex.
ABI encoding/decoding with viem
The TypeScript SDK relies on viem for all ABI operations:
encodeFunctionData(): Encodes a function call into bytesdecodeFunctionResult(): Decodes the returned bytes into TypeScript typesparseAbi(): Parses human-readable ABI strings into typed ABI objects
The .result() pattern
All CRE capability calls return objects with a .result() method. Calling .result() blocks execution synchronously (within the WASM environment) and waits for the consensus-verified result.
// This returns an object with a .result() method
const callObject = evmClient.callContract(runtime, {
call: encodeCallMsg({...}),
blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
})
// This blocks and returns the actual result
const contractCall = callObject.result()
This pattern is consistent across all SDK capabilities (EVM, HTTP, etc.).
Solidity-to-TypeScript type mappings
Viem automatically handles type conversions:
| Solidity Type | TypeScript Type |
|---|---|
uint8, uint256, etc. | bigint |
int8, int256, etc. | bigint |
address | string |
bool | boolean |
string | string |
bytes, bytes32, etc. | Uint8Array |
Complete example with configuration
Here's a full runnable workflow with external configuration:
Main workflow file (main.ts)
import {
cre,
getNetwork,
encodeCallMsg,
bytesToHex,
LAST_FINALIZED_BLOCK_NUMBER,
type Runtime,
Runner,
} from "@chainlink/cre-sdk"
import { type Address, encodeFunctionData, decodeFunctionResult, parseAbi, zeroAddress } from "viem"
import { z } from "zod"
const configSchema = z.object({
contractAddress: z.string(),
chainSelectorName: z.string(),
})
type Config = z.infer<typeof configSchema>
const storageAbi = parseAbi(["function get() view returns (uint256)"])
const onCronTrigger = (runtime: Runtime<Config>): string => {
const network = getNetwork({
chainFamily: "evm",
chainSelectorName: runtime.config.chainSelectorName,
isTestnet: true,
})
if (!network) {
throw new Error(`Network not found: ${runtime.config.chainSelectorName}`)
}
const evmClient = new cre.capabilities.EVMClient(network.chainSelector.selector)
const callData = encodeFunctionData({
abi: storageAbi,
functionName: "get",
args: [],
})
const contractCall = evmClient
.callContract(runtime, {
call: encodeCallMsg({
from: zeroAddress,
to: runtime.config.contractAddress as Address,
data: callData,
}),
blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
})
.result()
const storedValue = decodeFunctionResult({
abi: storageAbi,
functionName: "get",
data: bytesToHex(contractCall.data),
})
runtime.log(`Storage value: ${storedValue.toString()}`)
return storedValue.toString()
}
const initWorkflow = (config: Config) => {
return [
cre.handler(
new cre.capabilities.CronCapability().trigger({
schedule: "*/10 * * * * *",
}),
onCronTrigger
),
]
}
export async function main() {
const runner = await Runner.newRunner<Config>()
await runner.run(initWorkflow)
}
main()
Configuration file (config.json)
{
"contractAddress": "0xa17CF997C28FF154eDBae1422e6a50BeF23927F4",
"chainSelectorName": "ethereum-testnet-sepolia"
}
Working with complex ABIs
For workflows with multiple contracts or complex ABIs, organize them in separate files:
Contract ABI file (contracts/abi/Storage.ts)
import { parseAbi } from "viem"
export const Storage = parseAbi(["function get() view returns (uint256)", "function set(uint256 value) external"])
Export file (contracts/abi/index.ts)
export { Storage } from "./Storage"
Import in workflow
import { Storage } from "../contracts/abi"
const callData = encodeFunctionData({
abi: Storage,
functionName: "get",
args: [],
})
This pattern provides better organization, reusability, and type safety across your workflow.
Next steps
- Learn how to write data to contracts
- Explore the EVM Client SDK Reference for all available methods
- See Part 3 and Part 4 of the Getting Started guide for more examples