Part 3: Reading an Onchain Value

In the previous part, you successfully fetched data from an offchain API. Now, you will complete the "Onchain Calculator" by reading a value from a smart contract and combining it with your offchain result.

This part of the guide introduces the core pattern for all onchain interactions: contract bindings.

What you'll do

  • Configure your project with a Sepolia RPC URL.
  • Create a new Go package for a contract binding.
  • Use a binding to read a value from a deployed smart contract.
  • Integrate the onchain value into your main workflow logic.

Step 1: The smart contract

For this guide, we will interact with a simple Storage contract that has already been deployed to the Sepolia testnet. All it does is store a single uint256 value.

Here is the Solidity source code for the contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Storage {
  uint256 public value;

  constructor(uint256 initialValue) {
    value = initialValue;
  }

  function get() public view returns (uint256) {
    return value;
  }
}

A version of this contract has been deployed to Sepolia at 0xa17CF997C28FF154eDBae1422e6a50BeF23927F4 with an initialValue of 22.

Step 2: Configure your environment

To interact with a contract on Sepolia, your workflow needs EVM chain details.

  1. Contract address and chain name: Add the deployed contract's address and chain name to your config.staging.json file. We use an evms array to hold the configuration, which makes it easy to add more contracts (or chains) later.

    {
      "schedule": "0 */1 * * * *",
      "apiUrl": "https://api.mathjs.org/v4/?expr=randomInt(1,101)",
      "evms": [
        {
          "storageAddress": "0xa17CF997C28FF154eDBae1422e6a50BeF23927F4",
          "chainName": "ethereum-testnet-sepolia"
        }
      ]
    }
    
  2. RPC URL: For your workflow to interact with the blockchain, it needs an RPC endpoint. The cre init command has already configured a public Sepolia RPC URL in your project.yaml file for convenience. Let's take a look at what was generated:

    Open your project.yaml file at the root of your project. Your staging-settings target should look like this:

    # in onchain-calculator/project.yaml
    staging-settings:
      rpcs:
        - chain-name: ethereum-testnet-sepolia
          url: https://ethereum-sepolia-rpc.publicnode.com
    

    This public RPC endpoint is sufficient for testing and following this guide. However, for production use or higher reliability, you should consider using a dedicated RPC provider like Alchemy or Infura.

Step 3: Create the contract binding

This is the core of onchain interaction. While you could call the generic evm.Client directly from your main workflow logic, this is not recommended. Doing so would require you to manually handle ABI encoding and decoding for every contract call, leading to code that is hard to read and prone to errors.

The recommended pattern is to create a binding: a separate Go package that acts as a type-safe client for your specific smart contract. The binding encapsulates all the low-level encoding/decoding logic, allowing your main workflow to remain clean and focused.

In this step, you will create a binding for the Storage contract.

  1. Add the contract ABI: Create a new file called Storage.abi in the existing abi directory and add the contract's ABI JSON. From your project root (onchain-calculator/), run the following command:

    touch contracts/evm/src/abi/Storage.abi
    

    Open contracts/evm/src/abi/Storage.abi and paste the following ABI:

    [
      {
        "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"
      }
    ]
    
  2. Generate the Go binding: Run the CRE binding generator from your project root (onchain-calculator/):

    cre generate-bindings evm
    

    This command will automatically generate type-safe Go bindings for all ABI files in your contracts/evm/src/abi/ directory. It also automatically adds the required evm capability dependency to your go.mod file. The generated bindings will be placed in contracts/evm/src/generated/.

  3. Verify the generated files: After running the command, you should see two new files in contracts/evm/src/generated/storage/:

    • Storage.go — The main binding that provides a type-safe interface for interacting with your contract
    • Storage_mock.go — A mock implementation for testing workflows without deploying contracts

Step 4: Update your workflow logic

Now you can use your new binding in your main.go file to read the onchain value and complete the calculation.

Replace the entire content of onchain-calculator/my-calculator-workflow/main.go with the version below.

Note: Lines highlighted in green indicate new or modified code compared to Part 2.

onchain-calculator/my-calculator-workflow/main.go
Go
1 //go:build wasip1
2
3 package main
4
5 import (
6 "fmt"
7 "log/slog"
8 "math/big"
9
10 "onchain-calculator/contracts/evm/src/generated/storage"
11
12 "github.com/ethereum/go-ethereum/common"
13 "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
14 "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http"
15 "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
16 "github.com/smartcontractkit/cre-sdk-go/cre"
17 "github.com/smartcontractkit/cre-sdk-go/cre/wasm"
18 )
19
20 // EvmConfig defines the configuration for a single EVM chain.
21 type EvmConfig struct {
22 StorageAddress string `json:"storageAddress"`
23 ChainName string `json:"chainName"`
24 }
25
26 // Config struct now contains a list of EVM configurations.
27 // This makes it consistent with the structure used in Part 4.
28 type Config struct {
29 Schedule string `json:"schedule"`
30 ApiUrl string `json:"apiUrl"`
31 Evms []EvmConfig `json:"evms"`
32 }
33
34 type MyResult struct {
35 FinalResult *big.Int
36 }
37
38 func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
39 return cre.Workflow[*Config]{
40 cre.Handler(cron.Trigger(&cron.Config{Schedule: config.Schedule}), onCronTrigger),
41 }, nil
42 }
43
44 func fetchMathResult(config *Config, logger *slog.Logger, sendRequester *http.SendRequester) (*big.Int, error) {
45 req := &http.Request{Url: config.ApiUrl, Method: "GET"}
46 resp, err := sendRequester.SendRequest(req).Await()
47 if err != nil {
48 return nil, fmt.Errorf("failed to get API response: %w", err)
49 }
50 val, ok := new(big.Int).SetString(string(resp.Body), 10)
51 if !ok {
52 return nil, fmt.Errorf("failed to parse API response into big.Int")
53 }
54 return val, nil
55 }
56
57 func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (*MyResult, error) {
58 logger := runtime.Logger()
59 // Step 1: Fetch offchain data (from Part 2)
60 client := &http.Client{}
61 mathPromise := http.SendRequest(config, runtime, client, fetchMathResult, cre.ConsensusMedianAggregation[*big.Int]())
62 offchainValue, err := mathPromise.Await()
63 if err != nil {
64 return nil, err
65 }
66 logger.Info("Successfully fetched offchain value", "result", offchainValue)
67
68 // Get the first EVM configuration from the list.
69 evmConfig := config.Evms[0]
70
71 // Step 2: Read onchain data using the binding
72 // Convert the human-readable chain name to a numeric chain selector
73 chainSelector, err := evm.ChainSelectorFromName(evmConfig.ChainName)
74 if err != nil {
75 return nil, fmt.Errorf("invalid chain name: %w", err)
76 }
77
78 evmClient := &evm.Client{
79 ChainSelector: chainSelector,
80 }
81
82 storageAddress := common.HexToAddress(evmConfig.StorageAddress)
83
84 storageContract, err := storage.NewStorage(evmClient, storageAddress, nil)
85 if err != nil {
86 return nil, fmt.Errorf("failed to create contract instance: %w", err)
87 }
88
89 onchainValue, err := storageContract.Get(runtime, big.NewInt(-3)).Await() // -3 means finalized block
90 if err != nil {
91 return nil, fmt.Errorf("failed to read onchain value: %w", err)
92 }
93
94 logger.Info("Successfully read onchain value", "result", onchainValue)
95
96 // Step 3: Combine the results
97 finalResult := new(big.Int).Add(onchainValue, offchainValue)
98 logger.Info("Final calculated result", "result", finalResult)
99
100 return &MyResult{
101 FinalResult: finalResult,
102 }, nil
103 }
104
105 func main() {
106 wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
107 }

Step 5: Sync your dependencies

Now that your main.go file has been updated to import the new storage binding package, run go mod tidy to automatically update your project's go.mod and go.sum files.

go mod tidy

Step 6: Run the simulation and review the output

Run the simulation from your project root directory (the onchain-calculator/ folder). Because there is only one trigger defined, the simulator runs it automatically.

cre workflow simulate my-calculator-workflow --target staging-settings

The simulation logs will show the end-to-end execution of your workflow.

Workflow compiled
2025-11-03T22:37:05Z [SIMULATION] Simulator Initialized

2025-11-03T22:37:05Z [SIMULATION] Running trigger trigger=[email protected]
2025-11-03T22:37:05Z [USER LOG] msg="Successfully fetched offchain value" result=53
2025-11-03T22:37:05Z [USER LOG] msg="Successfully read onchain value" result=22
2025-11-03T22:37:05Z [USER LOG] msg="Final calculated result" result=75

Workflow Simulation Result:
 {
  "FinalResult": 75
}

2025-11-03T22:37:05Z [SIMULATION] Execution finished signal received
2025-11-03T22:37:05Z [SIMULATION] Skipping WorkflowEngineV2
  • [USER LOG]: You can now see all three of your logger.Info() calls, showing the offchain value (result=53), the onchain value (result=22), and the final combined result (result=75).
  • [SIMULATION]: These are system-level messages from the simulator showing its internal state.
  • Workflow Simulation Result: This is the final, JSON-formatted return value of your workflow. The FinalResult field contains the sum of the offchain and onchain values (53 + 22 = 75).

You have successfully built a complete CRE workflow that combines offchain and onchain data.

Next Steps

You have successfully read a value from a smart contract and combined it with offchain data. The final step is to write this new result back to the blockchain.

  • Part 4: Writing Onchain: Learn how to execute an onchain write transaction from your workflow to complete the project.

Get the latest Chainlink content straight to your inbox.