Onchain Read
This guide explains how to read data from a Solana account from within your CRE workflow. Reading is deliberately
split into two steps: fetch the raw account bytes with SolanaClient's
getAccountInfoWithOpts, then decode them into
your account's type with the generated decode<AccountName>Account(). Fetching is generic. It works the same way
for any account on any program. Decoding is schema-specific, so it has to be generated from your program's IDL. Keeping
them separate is what lets one fetch call work for every account type you read.
Prerequisites
- A CRE project initialized with
cre init. See Part 1: Project Setup if you are starting from scratch. - The latest version of the CRE CLI (
cre version/cre update); v1.33.0 or later is required for TypeScript Solana read binding generation. - A funded Solana Devnet keypair for
CRE_SOLANA_PRIVATE_KEY(required for every Solana simulation, including a read-only workflow). See Step 4.
What you need: an account to read
This guide reads a DataAccount owned by the data_storage program, deployed to Solana Devnet at
64Q1Xu7AEbc2xgaN21zZU1y1mkaLJNnP5cHFkthGuW31. DataAccount is a simple key/value record:
// data_storage (abridged)
#[account]
pub struct DataAccount {
pub key: String,
pub value: String,
}
The account already exists and is initialized at a fixed PDA,
HxzqwQZNyDM3Tvp9nR8v9tgJ5ZZ1Lm8LiJ8kxoyu539K
(derived from the program with seed "data"), so you do not need to deploy or initialize anything to follow this
guide.
Step 1: Add the IDL and generate bindings
-
Save the following as
contracts/solana/src/idl/data_storage.json.{ "address": "64Q1Xu7AEbc2xgaN21zZU1y1mkaLJNnP5cHFkthGuW31", "metadata": { "name": "data_storage", "version": "0.1.0", "spec": "0.1.0", "description": "CRE Solana Write example receiver with a persistent DataAccount PDA, reused for the Onchain Read docs guide" }, "instructions": [ { "name": "initialize", "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], "accounts": [ { "name": "data_account", "writable": true, "pda": { "seeds": [{ "kind": "const", "value": [100, 97, 116, 97] }] } }, { "name": "payer", "writable": true, "signer": true }, { "name": "system_program", "address": "11111111111111111111111111111111" } ], "args": [] }, { "name": "on_report", "discriminator": [214, 173, 18, 221, 173, 148, 151, 208], "accounts": [ { "name": "state" }, { "name": "forwarder_authority", "signer": true }, { "name": "data_account", "writable": true, "pda": { "seeds": [{ "kind": "const", "value": [100, 97, 116, 97] }] } } ], "args": [ { "name": "metadata", "type": "bytes" }, { "name": "payload", "type": "bytes" } ] } ], "accounts": [ { "name": "DataAccount", "discriminator": [85, 240, 182, 158, 76, 7, 18, 233] } ], "events": [], "errors": [ { "code": 6000, "name": "InvalidPayload", "msg": "Failed to deserialize payload as UserData" } ], "types": [ { "name": "DataAccount", "type": { "kind": "struct", "fields": [ { "name": "key", "type": "string" }, { "name": "value", "type": "string" } ] } } ] } -
From your workflow directory, generate the bindings. See Generating Solana Bindings for what this command does in general.
cd my-workflow cre generate-bindings solanaThis creates
contracts/solana/ts/generated/DataStorage.ts(plus a mock andindex.ts), exporting adecodeDataAccountAccount(data: Uint8Array): DataAccountfunction.
Step 2: Configure the workflow
Open (or create) config.staging.json. Every read-only workflow needs a schedule and the chain to read from. The
dataAccount field is specific to this example: it is not a general CRE or Solana concept, just a config value this
guide uses to point at the one DataAccount PDA it reads. A workflow that reads a different account, or several,
would name and structure this differently.
{
"schedule": "*/30 * * * * *",
"chainSelector": "16423721717087811551",
"dataAccount": "HxzqwQZNyDM3Tvp9nR8v9tgJ5ZZ1Lm8LiJ8kxoyu539K"
}
| Field | Description |
|---|---|
schedule | Cron expression for how often to read the account. |
chainSelector | Solana Devnet ("16423721717087811551"). Store as a string to avoid JSON precision loss. |
dataAccount | Specific to this example: the base58 address of the DataAccount PDA to read. |
Step 3: Write the workflow
This step is a complete, pasteable workflow that reads DataAccount on a cron schedule and logs its decoded
key/value.
-
Save the following as
my-workflow/main.ts.// my-workflow/main.ts import { bytesToBase64, CronCapability, handler, Runner, SolanaClient, solanaAddressToBytes, type Runtime, } from "@chainlink/cre-sdk" import { decodeDataAccountAccount } from "./contracts/solana/ts/generated/DataStorage" type Config = { schedule: string chainSelector: string dataAccount: string } const onCronTrigger = (runtime: Runtime<Config>) => { const config = runtime.config const client = new SolanaClient(BigInt(config.chainSelector)) const reply = client .getAccountInfoWithOpts(runtime, { account: bytesToBase64(solanaAddressToBytes(config.dataAccount)), opts: { // Base64 is required for accounts over 128 bytes; DataAccount's fixed layout is 528 bytes. encoding: "ENCODING_TYPE_BASE64", commitment: "COMMITMENT_TYPE_CONFIRMED", }, }) .result() if (!reply.value) { throw new Error(`DataAccount ${config.dataAccount} does not exist`) } if (reply.value.data?.body.case !== "raw") { throw new Error(`expected raw-encoded account data, got ${reply.value.data?.body.case}`) } const data = decodeDataAccountAccount(reply.value.data.body.value) runtime.log(`DataAccount key=${data.key} value=${data.value} lamports=${reply.value.lamports}`) return "Success" } export const initWorkflow = (config: Config) => { const cron = new CronCapability() return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)] } export async function main() { const runner = await Runner.newRunner<Config>() await runner.run(initWorkflow) }
Step 4: Run the simulation
-
Create a
workflow.yamlin your workflow folder.staging-settings: user-workflow: workflow-name: "my-solana-onchain-read" workflow-artifacts: workflow-path: "./main.ts" config-path: "./config.staging.json" secrets-path: "" -
Add a Solana RPC endpoint to your
project.yaml(at your project root, not insidemy-workflow/).staging-settings: rpcs: - chain-name: solana-devnet url: https://api.devnet.solana.com -
Set
CRE_SOLANA_PRIVATE_KEYin your project.env. Solana simulation requires a funded 64-byte base58 keypair for every run, even a read-only workflow. See Writing to Solana: Step 4 for how to generate and fund one.CRE_ETH_PRIVATE_KEYis required too, for the same workflow-owner-derivation reason described there. -
Run the simulation.
cre workflow simulate my-workflow --non-interactive --trigger-index 0 --target staging-settingsThe simulator runs your cron handler once, reads the live account from Devnet, and decodes it:
✓ Workflow compiled [SIMULATION] Simulator Initialized [SIMULATION] Running trigger trigger=cron-trigger@1.0.0 [USER LOG] DataAccount key=price value=500000 lamports=4565760 ✓ Workflow Simulation Result: "Success"
Deploying the workflow
Once your handler behaves as expected in simulation, deploy it the same way as any other CRE workflow. See Deploying to the Private Registry. A deployed read workflow runs on the schedule you configured, with the DON reaching consensus on each read.
Next steps
- Solana Client SDK Reference: Full API reference for
SolanaClient's read methods - Solana Log Trigger: React to Solana events instead of polling on a schedule
- Writing to Solana: Submit CRE workflow results to a Solana program
- The Solana Read & Write Capabilities: Conceptual overview of how Solana reads reach consensus