Onchain Read
This guide explains how to read data from a smart contract from within your CRE workflow. The process uses generated bindings and the SDK's evm.Client to create a simple, type-safe developer experience.
The read pattern
Reading from a contract follows a simple pattern:
- Prerequisite - Generate bindings: You must first generate Go bindings for your smart contracts using the CRE CLI. This creates type-safe Go methods that correspond to your contract's
viewandpurefunctions. - Instantiate the binding: In your workflow logic, create an instance of your generated binding.
- Call a read method: Call the desired function on the binding instance, specifying a block number. This is an asynchronous call that immediately returns a
Promise. - Await the result: Call
.Await()on the returned promise to pause execution and wait for the consensus-verified result from the DON.
Step-by-step example
Let's assume you have followed the generating bindings guide and have created a binding for the Storage contract with a get() view returns (uint256) function.
1. The generated binding
After running cre generate-bindings evm, your binding will contain methods that wrap the onchain functions. For the Storage contract's get function, the generated method takes the sdk.Runtime and a block number as arguments:
// In contracts/evm/src/generated/storage/storage.go
func (c Storage) Get(runtime cre.Runtime, blockNumber *big.Int) cre.Promise[*evm.CallContractReply] {
// This method handles ABI encoding, calling the evm.Client,
// and returns a promise for the response.
}
2. The workflow logic
In your main workflow file (main.go), you can now use this binding to read from your contract.
// In your workflow's main.go
import (
"contracts/evm/src/generated/storage" // Import your generated binding
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
"github.com/smartcontractkit/cre-sdk-go/cre"
)
func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (*MyResult, error) {
logger := runtime.Logger()
// 1. Create the EVM client with chain selector
evmClient := &evm.Client{
ChainSelector: config.ChainSelector, // e.g., 16015286601757825753 for Sepolia
}
// 2. Instantiate the contract binding
contractAddress := common.HexToAddress(config.ContractAddress)
storageContract, err := storage.NewStorage(evmClient, contractAddress, nil)
if err != nil {
return nil, fmt.Errorf("failed to create contract instance: %w", err)
}
// 3. Call the read method - it returns the decoded value directly
// See the "Block number options" section below for details on block number parameters
storedValue, err := storageContract.Get(runtime, big.NewInt(-3)).Await() // -3 = finalized block
if err != nil {
logger.Error("Failed to read storage value", "err", err)
return nil, err
}
logger.Info("Successfully read storage value", "value", storedValue.String())
return &MyResult{StoredValue: storedValue}, nil
}
Understanding return types
Generated bindings are designed to be self-documenting. The method signature tells you exactly what type you'll receive, so you don't need to guess or look up the ABIāthe Go type system provides this information directly.
Reading method signatures
When you call a read method on a generated binding, its signature shows you the return type. For example, from the IERC20 binding:
// This method returns a *big.Int
func (c IERC20) TotalSupply(
runtime cre.Runtime,
blockNumber *big.Int,
) cre.Promise[*big.Int] // ā The return type is right here
// This method returns a bool
func (c IERC20) Approve(
runtime cre.Runtime,
args ApproveInput,
blockNumber *big.Int,
) cre.Promise[bool] // ā Returns bool
Solidity-to-Go type mappings
The binding generator follows standard Ethereum conventions:
| Solidity Type | Go Type |
|---|---|
uint8, uint256, etc. | *big.Int |
int8, int256, etc. | *big.Int |
address | common.Address |
bool | bool |
string | string |
bytes, bytes32, etc. | []byte |
struct | Custom Go struct (generated) |
Using your IDE
Modern IDEs will show you the method signature when you hover over a function call or use autocomplete. This makes it easy to see exactly what type you're working with:
// When you type this and hover over TotalSupply, your IDE shows:
value, err := token.TotalSupply(runtime, big.NewInt(-3)).Await()
// ā IDE tooltip: "func TotalSupply(...) cre.Promise[*big.Int]"
// So you know `value` is a *big.Int and can use it directly
Practical usage
Because the type is explicit, you can immediately use the value with confidence:
totalSupply, err := token.TotalSupply(runtime, big.NewInt(-3)).Await()
if err != nil {
return nil, err
}
// You know it's *big.Int, so you can use it in calculations:
doubled := new(big.Int).Mul(totalSupply, big.NewInt(2))
logger.Info("Supply doubled", "result", doubled.String())
Block number options
When calling contract read methods, you must specify a block number. There are two ways to do this:
Using magic numbers
- Finalized block: Use
big.NewInt(-3)to read from the latest finalized block - Latest block: Use
big.NewInt(-2)to read from the latest block - Specific block: Use
big.NewInt(blockNumber)to read from a specific block
Using rpc constants (alternative)
You can also use constants from the go-ethereum/rpc package for better readability:
import (
"math/big"
"github.com/ethereum/go-ethereum/rpc"
)
// For latest block
reqBlockNumber := big.NewInt(rpc.LatestBlockNumber.Int64())
// For finalized block
reqBlockNumber := big.NewInt(rpc.FinalizedBlockNumber.Int64())
Both approaches are equivalent - use whichever you find more readable in your code.
Complete example
This example shows a full, runnable workflow that triggers on a cron schedule and reads a value from the Storage contract.
package main
import (
"contracts/evm/src/generated/storage" // Generated Storage binding
"fmt"
"log/slog"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
"github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
"github.com/smartcontractkit/cre-sdk-go/cre"
"github.com/smartcontractkit/cre-sdk-go/cre/wasm"
)
type Config struct {
ContractAddress string `json:"contractAddress"`
ChainSelector uint64 `json:"chainSelector"`
}
type MyResult struct {
StoredValue *big.Int
}
func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (*MyResult, error) {
logger := runtime.Logger()
// Create EVM client
evmClient := &evm.Client{
ChainSelector: config.ChainSelector,
}
// Create contract instance
contractAddress := common.HexToAddress(config.ContractAddress)
storageContract, err := storage.NewStorage(evmClient, contractAddress, nil)
if err != nil {
return nil, fmt.Errorf("failed to create contract instance: %w", err)
}
// Call contract method - it returns the decoded type directly
storedValue, err := storageContract.Get(runtime, big.NewInt(-3)).Await()
if err != nil {
return nil, fmt.Errorf("failed to read storage value: %w", err)
}
logger.Info("Successfully read storage value", "value", storedValue.String())
return &MyResult{StoredValue: storedValue}, nil
}
func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
return cre.Workflow[*Config]{
cre.Handler(
cron.Trigger(&cron.Config{Schedule: "*/10 * * * * *"}),
onCronTrigger,
),
}, nil
}
func main() {
wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
}
Configuration
Your workflow configuration file (config.json) should include both the contract address and chain selector:
{
"contractAddress": "0xYourContractAddressHere",
"chainSelector": 16015286601757825753
}
You pass this file to the simulator using the --config flag: cre workflow simulate --config config.json main.go