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

  1. 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" }
            ]
          }
        }
      ]
    }
    
  2. From your project root, generate the bindings. See Generating Solana Bindings for what this command does in general.

    cre generate-bindings solana --language go
    

    This creates contracts/solana/src/generated/data_storage/ with a DataAccount struct and a Codec.DecodeDataAccount(data []byte) (*DataAccount, error) method, in accounts.go.

  3. 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"
}
FieldDescription
scheduleCron expression for how often to read the account.
chainSelectorSolana Devnet ("16423721717087811551"). Store as a string to avoid JSON precision loss.
dataAccountSpecific 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.

  1. Save the following as my-workflow/workflow.go. Replace <project-name> with your Go module name from go.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

  1. Create a workflow.yaml in 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: ""
    
  2. Add a Solana RPC endpoint to your project.yaml (at your project root, not inside my-workflow/).

    staging-settings:
      rpcs:
        - chain-name: solana-devnet
          url: https://api.devnet.solana.com
    
  3. Set CRE_SOLANA_PRIVATE_KEY in 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_KEY is required too, for the same workflow-owner-derivation reason described there.

  4. Run the simulation.

    cre workflow simulate my-workflow --non-interactive --trigger-index 0 --target staging-settings
    

    The 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

Get the latest Chainlink content straight to your inbox.