# Part 3: Reading an Onchain Value
Source: https://docs.chain.link/cre/getting-started/part-3-reading-onchain-value-ts
Last Updated: 2025-11-04

> For the complete documentation index, see [llms.txt](/llms.txt).

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`](/cre/reference/sdk/evm-client) and <a href="https://viem.sh/" target="_blank" rel="noopener noreferrer">Viem</a> 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:

```solidity
// 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.

   ```json
   {
     "schedule": "*/30 * * * * *",
     "apiUrl": "https://api.mathjs.org/v4/?expr=randomInt(1,101)",
     "evms": [
       {
         "storageAddress": "0xa17CF997C28FF154eDBae1422e6a50BeF23927F4",
         "chainName": "ethereum-testnet-sepolia"
       }
     ]
   }
   ```

> **NOTE: About Chain Identifiers**
>
> A **chain selector** is a unique identifier used by Chainlink to specify a blockchain.

Chain selectors can be referenced in multiple formats:

- **String name** (used in config files and `project.yaml`): `"ethereum-testnet-sepolia"`
- **Numeric ID** (used internally by the SDK, returned by `getNetwork()`): `16015286601757825753n`

You only use the string name in your configuration files. The SDK's `getNetwork()` helper converts the string name to the numeric BigInt ID automatically.

For a complete reference showing all supported chains, see the [Chain Selectors reference](/cre/reference/sdk/evm-client#chain-selectors).

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:

   ```yaml
   # 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](https://infura.io/), [Alchemy](https://www.alchemy.com/), or [QuickNode](https://www.quicknode.com/).

> **CAUTION: Protect Your RPC URL**
>
> When you use a dedicated RPC provider, your RPC URLs will contain API keys. To avoid exposing them, you should add
> your `project.yaml` file to your project's `.gitignore` file.

## 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 <a href="https://viem.sh/" target="_blank" rel="noopener noreferrer">Viem</a> 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:

   ```bash
   mkdir -p contracts/abi
   ```

2. **Add the Storage contract ABI**: Create a new file called `Storage.ts` in the `contracts/abi` directory:

   ```bash
   touch contracts/abi/Storage.ts
   ```

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

   ```typescript
   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:

   ```bash
   touch contracts/abi/index.ts
   ```

   Open `contracts/abi/index.ts` and add:

   ```typescript
   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.

Code snippet for onchain-calculator/my-calculator-workflow/main.ts:

```typescript
import {
  CronCapability,
  HTTPClient,
  EVMClient,
  handler,
  consensusMedianAggregation,
  Runner,
  type NodeRuntime,
  type Runtime,
  getNetwork,
  LAST_FINALIZED_BLOCK_NUMBER,
  encodeCallMsg,
  bytesToHex,
} from "@chainlink/cre-sdk"
import { encodeFunctionData, decodeFunctionResult, zeroAddress } from "viem"
import { Storage } from "../contracts/abi"


// EvmConfig defines the configuration for a single EVM chain.
type EvmConfig = {
  storageAddress: string
  chainName: string
}


type Config = {
  schedule: string
  apiUrl: string
  evms: EvmConfig[]
}

type MyResult = {
  finalResult: bigint
}

const initWorkflow = (config: Config) => {
  const cron = new CronCapability()

  return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
}

// fetchMathResult is the function passed to the runInNodeMode helper.
const fetchMathResult = (nodeRuntime: NodeRuntime<Config>): bigint => {
  const httpClient = new HTTPClient()

  const req = {
    url: nodeRuntime.config.apiUrl,
    method: "GET" as const,
  }

  const resp = httpClient.sendRequest(nodeRuntime, req).result()
  const bodyText = new TextDecoder().decode(resp.body)
  const val = BigInt(bodyText.trim())

  return val
}

const onCronTrigger = (runtime: Runtime<Config>): MyResult => {
  // Step 1: Fetch offchain data (from Part 2)
  const offchainValue = runtime.runInNodeMode(fetchMathResult, consensusMedianAggregation())().result()

  runtime.log(`Successfully fetched offchain value: ${offchainValue}`)


  // Get the first EVM configuration from the list.
  const evmConfig = runtime.config.evms[0]

  // Step 2: Read onchain data using the EVM client
  // Convert the human-readable chain name to a chain selector
  const network = getNetwork({
    chainFamily: "evm",
    chainSelectorName: evmConfig.chainName,
  })
  if (!network) {
    throw new Error(`Unknown chain name: ${evmConfig.chainName}`)
  }

  const evmClient = new EVMClient(network.chainSelector.selector)

  // Encode the function call using the Storage ABI
  const callData = encodeFunctionData({
    abi: Storage,
    functionName: "get",
  })

  // Call the contract
  const contractCall = evmClient
    .callContract(runtime, {
      call: encodeCallMsg({
        from: zeroAddress,
        to: evmConfig.storageAddress as `0x${string}`,
        data: callData,
      }),
      blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
    })
    .result()

  // Decode the result
  const onchainValue = decodeFunctionResult({
    abi: Storage,
    functionName: "get",
    data: bytesToHex(contractCall.data),
  }) as bigint

  runtime.log(`Successfully read onchain value: ${onchainValue}`)

  // Step 3: Combine the results
  const finalResult = onchainValue + offchainValue
  runtime.log(`Final calculated result: ${finalResult}`)


  return {
    finalResult,
  }
}

export async function main() {
  const runner = await Runner.newRunner<Config>()
  await runner.run(initWorkflow)
}
```

<Aside type="tip" title="Optional: Add runtime validation">
  If you added Zod validation in Part 1, you can continue using it here. The `Config` type now includes the `evms` array, so update your Zod schema accordingly and pass `configSchema` to `Runner.newRunner<Config>({ configSchema })`. See [Part 1: Optional Runtime Validation](/cre/getting-started/part-1-project-setup#optional-runtime-validation) for details.
</Aside>

**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.

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

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

```bash
Workflow compiled
2025-11-03T19:06:56Z [SIMULATION] Simulator Initialized

2025-11-03T19:06:56Z [SIMULATION] Running trigger trigger=cron-trigger@1.0.0
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](/cre/getting-started/part-4-writing-onchain)**: Learn how to execute an onchain write transaction from your workflow to complete the project.