Generating Solana Bindings

To interact with a Solana program from your TypeScript workflow, you generate bindings from the program's Anchor IDL. Bindings are typed TypeScript classes that handle Borsh encoding, account hashing, and report submission automatically.

The Solana CRE capability is write-only. For each Anchor struct in your IDL, the generator creates a writeReportFrom<StructName>() method on the binding class. Your workflow calls this method with the struct data and the required accounts; the generated code handles everything else.

The generation process

The CRE CLI reads your IDL files and generates a typed class with the write helpers your workflow needs.

The target language is auto-detected from your project files (presence of package.json picks TypeScript). You can also force TypeScript explicitly with the --language flag:

cre generate-bindings solana --language typescript

Step 1: Add your Anchor IDL

Place your program's Anchor IDL file in my-workflow/contracts/solana/src/idl/. The IDL is a JSON file that Anchor generates when you build your program:

# In your Anchor project
anchor build
# IDL is written to: target/idl/<program-name>.json

Copy or symlink it into your CRE project:

mkdir -p my-workflow/contracts/solana/src/idl
cp /path/to/your-anchor-project/target/idl/my_program.json my-workflow/contracts/solana/src/idl/

Step 2: Generate the bindings

From your workflow directory, run:

cre generate-bindings solana

This reads all .json IDL files in contracts/solana/src/idl/ and generates TypeScript files in contracts/solana/ts/generated/ (relative to your workflow directory). For each IDL, three files are generated:

  • <ProgramName>.ts — The typed binding class with writeReportFrom<StructName>() methods
  • <ProgramName>_mock.ts — A mock for testing without a live network
  • index.ts — A barrel re-exporting everything

Using generated bindings

For onchain writes

For each struct in your IDL's types section, the generator creates a writeReportFrom<StructName>() method that Borsh-encodes your data, builds the forwarder report, and submits it in one step.

Example: A minimal DataStorage IDL

The following IDL is enough to generate a writeReportFromUserData() helper. Save it as my-workflow/contracts/solana/src/idl/data_storage.json.

{
  "address": "64Q1Xu7AEbc2xgaN21zZU1y1mkaLJNnP5cHFkthGuW31",
  "metadata": {
    "name": "data_storage",
    "version": "0.1.0",
    "spec": "0.1.0",
    "description": "Minimal CRE Solana write example IDL"
  },
  "instructions": [
    {
      "name": "on_report",
      "discriminator": [214, 173, 18, 221, 173, 148, 151, 208],
      "accounts": [],
      "args": [
        { "name": "_metadata", "type": "bytes" },
        { "name": "payload", "type": "bytes" }
      ]
    }
  ],
  "accounts": [],
  "events": [],
  "errors": [],
  "types": [
    {
      "name": "UserData",
      "type": {
        "kind": "struct",
        "fields": [
          { "name": "key", "type": "string" },
          { "name": "value", "type": "string" }
        ]
      }
    }
  ]
}

After running cre generate-bindings solana, use the generated DataStorage class in your workflow:

import { SolanaClient, solanaAccountMeta, type Runtime } from "@chainlink/cre-sdk"
import { DataStorage } from "./contracts/solana/ts/generated/DataStorage"

// In your workflow handler...
const client = new SolanaClient(BigInt(config.chainSelector))
const ds = new DataStorage(client)

// remainingAccounts: Index 0 forwarderState, 1 forwarderAuthority, 2+ receiver accounts
const remainingAccounts = [
  solanaAccountMeta(config.forwarderState, false),
  solanaAccountMeta(config.forwarderAuthority, false),
  solanaAccountMeta(config.reportState, true),
]

// Pass the struct data directly — types are derived from the IDL
const result = ds.writeReportFromUserData(runtime, { key: "price", value: "500000" }, remainingAccounts, {
  computeLimit: 200_000,
})

For a full walkthrough including config, simulation, and production deployment, see Writing to Solana.

What the CLI generates

For each IDL file, the generator creates three files in contracts/solana/ts/generated/ (inside your workflow directory):

  • <ProgramName>.ts — The main binding class
  • <ProgramName>_mock.ts — A mock implementation for testing
  • index.ts — A barrel re-exporting everything

What's inside depends on your IDL:

  • For all programs:
    • A typed <PROGRAM>_IDL constant and <PROGRAM>_PROGRAM_ID from the IDL address.
    • A <ProgramName> class with a writeReport(runtime, payload, remainingAccounts, computeConfig?) base method.
  • For onchain writes (each struct in types):
    • A writeReportFrom<StructName>(runtime, input, remainingAccounts, computeConfig?) method.
    • A writeReportFrom<StructName>s(...) variant for Borsh Vec payloads.

You can import from the barrel file to keep imports clean:

import { DataStorage } from "./contracts/solana/ts/generated"

remainingAccounts layout

Every Solana write requires a remainingAccounts array. The Keystone Forwarder (and the Devnet mock forwarder used in simulation) expects:

IndexAccountWritableHow to obtain
0forwarderStateNoProvided by Chainlink for the forwarder deployment (mock values are built into the CLI for Devnet)
1forwarderAuthorityNoPDA derived from ["forwarder", forwarderState, receiverProgram] under the forwarder program ID
2+Receiver accountsDepends on your programDefined by your Anchor program's on_report instruction

The forwarderAuthority PDA derivation in TypeScript using @solana/addresses:

import { getProgramDerivedAddress, address, getAddressEncoder } from "@solana/addresses"

const enc = getAddressEncoder()
const [forwarderAuthority] = await getProgramDerivedAddress({
  programAddress: address(forwarderProgramId),
  seeds: [Buffer.from("forwarder"), enc.encode(address(forwarderState)), enc.encode(address(receiverProgramId))],
})

Best practices

  1. Regenerate when needed: Re-run cre generate-bindings solana whenever your Anchor IDL changes. Do not edit generated files by hand.
  2. Always pass computeConfig: Solana simulation rejects a missing or zero computeLimit. Pass { computeLimit: 200_000 } (or another positive limit within the simulator's CU cap).
  3. Store addresses in config: Put forwarder and account addresses in config.staging.json / config.production.json rather than hard-coding them.
  4. chainSelector as string in config: Store the chain selector as a stringified number in JSON to avoid precision loss, then convert with BigInt() in workflow code.
  5. Use explicit --language in CI: If your project has both go.mod and package.json, pass --language typescript explicitly.

Where to go next

Get the latest Chainlink content straight to your inbox.