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- Custom block number: Use a
BigIntJsonobject for custom finality depths or historical queries
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()
Custom block depths
For use cases requiring fixed confirmation thresholds (e.g., regulatory compliance) or historical state verification, you can specify an exact block number.
Example 1 - Read from a specific historical block:
import { blockNumber } from '@chainlink/cre-sdk'
const historicalBlock = 9767655n
const contractCall = evmClient.callContract(runtime, {
call: encodeCallMsg({...}),
blockNumber: blockNumber(historicalBlock),
}).result()
Example 2 - Read from 500 blocks ago for custom finality:
import { protoBigIntToBigint, blockNumber } from '@chainlink/cre-sdk'
// Get the latest block number
const latestHeader = evmClient.headerByNumber(runtime, {}).result()
if (!latestHeader.header?.blockNumber) {
throw new Error("Failed to get latest block number")
}
// Convert protobuf BigInt to native bigint and calculate custom block
const latestBlockNum = protoBigIntToBigint(latestHeader.header.blockNumber)
const customBlock = latestBlockNum - 500n
// Call the contract at the custom block height
const contractCall = evmClient.callContract(runtime, {
call: encodeCallMsg({...}),
blockNumber: blockNumber(customBlock),
}).result()
Helper functions:
The SDK provides two helper functions for working with block numbers:
-
protoBigIntToBigint(pb)— Converts a protobufBigInt(returned by SDK methods likeheaderByNumber) to a native JavaScriptbigint. Use this when you need to perform arithmetic on block numbers. -
blockNumber(n)— Converts a nativebigint,number, orstringto the protobufBigIntJSON format required by SDK methods. This is an alias forbigintToProtoBigInt.
See Finality and Confidence Levels for more details on when to use custom block depths.
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