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:

  1. 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 view and pure functions.
  2. Instantiate the binding: In your workflow logic, create an instance of your generated binding.
  3. 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.
  4. 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 TypeGo Type
uint8, uint256, etc.*big.Int
int8, int256, etc.*big.Int
addresscommon.Address
boolbool
stringstring
bytes, bytes32, etc.[]byte
structCustom 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

Get the latest Chainlink content straight to your inbox.