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 withwriteReportFrom<StructName>()methods<ProgramName>_mock.ts— A mock for testing without a live networkindex.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 testingindex.ts— A barrel re-exporting everything
What's inside depends on your IDL:
- For all programs:
- A typed
<PROGRAM>_IDLconstant and<PROGRAM>_PROGRAM_IDfrom the IDL address. - A
<ProgramName>class with awriteReport(runtime, payload, remainingAccounts, computeConfig?)base method.
- A typed
- For onchain writes (each struct in
types):- A
writeReportFrom<StructName>(runtime, input, remainingAccounts, computeConfig?)method. - A
writeReportFrom<StructName>s(...)variant for BorshVecpayloads.
- A
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:
| Index | Account | Writable | How to obtain |
|---|---|---|---|
| 0 | forwarderState | No | Provided by Chainlink for the forwarder deployment (mock values are built into the CLI for Devnet) |
| 1 | forwarderAuthority | No | PDA derived from ["forwarder", forwarderState, receiverProgram] under the forwarder program ID |
| 2+ | Receiver accounts | Depends on your program | Defined 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
- Regenerate when needed: Re-run
cre generate-bindings solanawhenever your Anchor IDL changes. Do not edit generated files by hand. - Always pass
computeConfig: Solana simulation rejects a missing or zerocomputeLimit. Pass{ computeLimit: 200_000 }(or another positive limit within the simulator's CU cap). - Store addresses in config: Put forwarder and account addresses in
config.staging.json/config.production.jsonrather than hard-coding them. chainSelectoras string in config: Store the chain selector as a stringified number in JSON to avoid precision loss, then convert withBigInt()in workflow code.- Use explicit
--languagein CI: If your project has bothgo.modandpackage.json, pass--language typescriptexplicitly.
Where to go next
- Writing to Solana — Full write walkthrough (deploy, broadcast, production forwarder addresses)
- Solana Client SDK Reference — Full API reference for
SolanaClientand helper functions