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 solana.Client's
GetAccountInfoWithOpts, then decode them into your
account's Go type with the generated Codec.Decode<AccountName>(). 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 Go 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 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 project root, generate the bindings. See Generating Solana Bindings for what this command does in general.
cre generate-bindings solana --language goThis creates
contracts/solana/src/generated/data_storage/with aDataAccountstruct and aCodec.DecodeDataAccount(data []byte) (*DataAccount, error)method, inaccounts.go. -
Sync dependencies.
go mod tidy
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/workflow.go. Replace<project-name>with your Go module name fromgo.mod.// my-workflow/workflow.go package main import ( "fmt" "log/slog" solanago "github.com/gagliardetto/solana-go" solana "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana" "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" "github.com/smartcontractkit/cre-sdk-go/cre" "<project-name>/contracts/solana/src/generated/data_storage" ) type Config struct { Schedule string `json:"schedule"` ChainSelector uint64 `json:"chainSelector,string"` DataAccount string `json:"dataAccount"` } func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) { cronTrigger := cron.Trigger(&cron.Config{Schedule: config.Schedule}) return cre.Workflow[*Config]{ cre.Handler(cronTrigger, onCronTrigger), }, nil } func onCronTrigger(config *Config, runtime cre.Runtime, _ *cron.Payload) (string, error) { logger := runtime.Logger() client := &solana.Client{ChainSelector: config.ChainSelector} // NewDataStorage gives you Codec.DecodeDataAccount to parse the raw bytes below. ds, err := data_storage.NewDataStorage(client) if err != nil { return "", err } dataAccountPk := solanago.MustPublicKeyFromBase58(config.DataAccount) reply, err := client.GetAccountInfoWithOpts(runtime, &solana.GetAccountInfoWithOptsRequest{ Account: dataAccountPk[:], Opts: &solana.GetAccountInfoOpts{ // Base64 is required for accounts over 128 bytes; DataAccount's fixed layout is 528 bytes. Encoding: solana.EncodingType_ENCODING_TYPE_BASE64, Commitment: solana.CommitmentType_COMMITMENT_TYPE_CONFIRMED, }, }).Await() if err != nil { return "", fmt.Errorf("failed to read DataAccount: %w", err) } if reply.GetValue() == nil { return "", fmt.Errorf("DataAccount %s does not exist", config.DataAccount) } data, err := ds.Codec.DecodeDataAccount(reply.GetValue().GetData().GetRaw()) if err != nil { return "", fmt.Errorf("failed to decode DataAccount: %w", err) } logger.Info("DataAccount", "key", data.Key, "value", data.Value, "lamports", reply.GetValue().GetLamports(), ) return "Success", nil }
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.go" 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] msg=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
solana.Client'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