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 EVM interactions using the TypeScript SDK's EVMClient and Viem for type-safe contract interactions.

What you'll do

  • Configure your project with a Sepolia RPC URL.
  • Use the EVM client with Viem 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": "*/30 * * * * *",
      "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 Infura, Alchemy, or QuickNode.

Step 3: Create the contract ABI file

To interact with the Storage contract in a type-safe and maintainable way, you'll create an ABI file that defines the contract's interface.

The TypeScript SDK uses Viem for EVM interactions, which provides excellent TypeScript type inference when you define ABIs as TypeScript modules.

  1. Create the ABI directory: From your project root (onchain-calculator/), create the contracts/abi directory:

    mkdir -p contracts/abi
    
  2. Add the Storage contract ABI: Create a new file called Storage.ts in the contracts/abi directory:

    touch contracts/abi/Storage.ts
    

    Open contracts/abi/Storage.ts and paste the following ABI definition:

    export const Storage = [
      {
        inputs: [],
        name: "get",
        outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
        stateMutability: "view",
        type: "function",
      },
    ] as const
    

    The as const assertion is importantโ€”it tells TypeScript to infer the most specific type possible, which enables Viem's type-safe contract interactions.

  3. Create an index file: To make imports cleaner, create an index.ts file that exports all your ABIs:

    touch contracts/abi/index.ts
    

    Open contracts/abi/index.ts and add:

    export { Storage } from "./Storage"
    

    This allows you to import ABIs using: import { Storage } from "../contracts/abi".

Step 4: Update your workflow logic

Now that you have the ABI defined, you can import and use it in your workflow. Replace the entire content of onchain-calculator/my-calculator-workflow/main.ts with the version below.

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

onchain-calculator/my-calculator-workflow/main.ts
Typescript
1 import {
2 cre,
3 consensusMedianAggregation,
4 Runner,
5 type NodeRuntime,
6 type Runtime,
7 getNetwork,
8 LAST_FINALIZED_BLOCK_NUMBER,
9 encodeCallMsg,
10 bytesToHex,
11 } from "@chainlink/cre-sdk"
12 import { encodeFunctionData, decodeFunctionResult, zeroAddress } from "viem"
13 import { Storage } from "../contracts/abi"
14 โ€‹
15 // EvmConfig defines the configuration for a single EVM chain.
16 type EvmConfig = {
17 storageAddress: string
18 chainName: string
19 }
20 โ€‹
21 type Config = {
22 schedule: string
23 apiUrl: string
24 evms: EvmConfig[]
25 }
26 โ€‹
27 type MyResult = {
28 finalResult: bigint
29 }
30 โ€‹
31 const initWorkflow = (config: Config) => {
32 const cron = new cre.capabilities.CronCapability()
33 โ€‹
34 return [cre.handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
35 }
36 โ€‹
37 // fetchMathResult is the function passed to the runInNodeMode helper.
38 const fetchMathResult = (nodeRuntime: NodeRuntime<Config>): bigint => {
39 const httpClient = new cre.capabilities.HTTPClient()
40 โ€‹
41 const req = {
42 url: nodeRuntime.config.apiUrl,
43 method: "GET" as const,
44 }
45 โ€‹
46 const resp = httpClient.sendRequest(nodeRuntime, req).result()
47 const bodyText = new TextDecoder().decode(resp.body)
48 const val = BigInt(bodyText.trim())
49 โ€‹
50 return val
51 }
52 โ€‹
53 const onCronTrigger = (runtime: Runtime<Config>): MyResult => {
54 // Step 1: Fetch offchain data (from Part 2)
55 const offchainValue = runtime.runInNodeMode(fetchMathResult, consensusMedianAggregation())().result()
56 โ€‹
57 runtime.log(`Successfully fetched offchain value: ${offchainValue}`)
58 โ€‹
59 // Get the first EVM configuration from the list.
60 const evmConfig = runtime.config.evms[0]
61 โ€‹
62 // Step 2: Read onchain data using the EVM client
63 // Convert the human-readable chain name to a chain selector
64 const network = getNetwork({
65 chainFamily: "evm",
66 chainSelectorName: evmConfig.chainName,
67 isTestnet: true,
68 })
69 if (!network) {
70 throw new Error(`Unknown chain name: ${evmConfig.chainName}`)
71 }
72 โ€‹
73 const evmClient = new cre.capabilities.EVMClient(network.chainSelector.selector)
74 โ€‹
75 // Encode the function call using the Storage ABI
76 const callData = encodeFunctionData({
77 abi: Storage,
78 functionName: "get",
79 })
80 โ€‹
81 // Call the contract
82 const contractCall = evmClient
83 .callContract(runtime, {
84 call: encodeCallMsg({
85 from: zeroAddress,
86 to: evmConfig.storageAddress as `0x${string}`,
87 data: callData,
88 }),
89 blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
90 })
91 .result()
92 โ€‹
93 // Decode the result
94 const onchainValue = decodeFunctionResult({
95 abi: Storage,
96 functionName: "get",
97 data: bytesToHex(contractCall.data),
98 }) as bigint
99 โ€‹
100 runtime.log(`Successfully read onchain value: ${onchainValue}`)
101 โ€‹
102 // Step 3: Combine the results
103 const finalResult = onchainValue + offchainValue
104 runtime.log(`Final calculated result: ${finalResult}`)
105 โ€‹
106 return {
107 finalResult,
108 }
109 }
110 โ€‹
111 export async function main() {
112 const runner = await Runner.newRunner<Config>()
113 await runner.run(initWorkflow)
114 }
115 โ€‹
116 main()
117 โ€‹

Key TypeScript SDK features:

  • getNetwork(): Converts a human-readable chain name to a numeric chain selector
  • EVMClient: The EVM capability client for interacting with blockchains
  • encodeFunctionData(): From Viem, encodes a function call with type-safe parameters
  • callContract(): EVMClient method for calling view/pure functions on a contract
  • bytesToHex(): Converts Uint8Array response data to hex string for Viem
  • decodeFunctionResult(): From Viem, decodes the contract call response with type inference
  • LAST_FINALIZED_BLOCK_NUMBER: Constant for reading from the most recent finalized block
  • Native bigint: TypeScript's built-in big integer type (no external library needed)

Step 5: 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-03T19:06:56Z [SIMULATION] Simulator Initialized

2025-11-03T19:06:56Z [SIMULATION] Running trigger trigger=[email protected]
2025-11-03T19:06:56Z [USER LOG] Successfully fetched offchain value: 55
2025-11-03T19:06:56Z [USER LOG] Successfully read onchain value: 22
2025-11-03T19:06:56Z [USER LOG] Final calculated result: 77

Workflow Simulation Result:
 {
  "finalResult": 77
}

2025-11-03T19:06:56Z [SIMULATION] Execution finished signal received
2025-11-03T19:06:56Z [SIMULATION] Skipping WorkflowEngineV2
  • [USER LOG]: You can now see all three of your runtime.log() calls, showing the offchain value (55), the onchain value (22), and the final combined result (77).
  • [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 (55 + 22 = 77).

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.