Generating Solana Bindings

To interact with a Solana program from your Go workflow, you generate bindings from the program's Anchor IDL. Bindings are typed Go structs and methods 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. 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 provides an automated binding generator that reads Anchor IDLs and creates corresponding Go packages.

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

cre generate-bindings solana --language go

Your project must already be a Go module (go.mod at the project root, as created by cre init). Without go.mod, the generator writes files but then fails when resolving Go dependencies.

Step 1: Add your Anchor IDL

Place your program's Anchor IDL file in 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 it into your CRE project:

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

Step 2: Generate the bindings

From your project root, run:

cre generate-bindings solana

This reads all .json IDL files in contracts/solana/src/idl/ and generates Go packages in contracts/solana/src/generated/. For each IDL, the generator creates a package directory (for example data_storage/) with the binding sources, including:

  • Struct types and WriteReportFrom<StructName>() methods
  • A mock for testing without a live network

After generation, run go mod tidy if you need to sync new dependencies.

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. These methods return a Promise, which you must .Await() to get the result after consensus.

Example: A minimal DataStorage IDL

The following IDL is enough to generate a WriteReportFromUserData() helper. Save it as 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 data_storage package in your workflow:

import (
	solanago "github.com/gagliardetto/solana-go"
	solana "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana"

	"<project-name>/contracts/solana/src/generated/data_storage"
)

// In your workflow handler...
client := &solana.Client{ChainSelector: config.ChainSelector}
ds, err := data_storage.NewDataStorage(client)
if err != nil {
	return "", err
}

// remainingAccounts: Index 0 forwarderState, 1 forwarderAuthority, 2+ receiver accounts
accounts := []*solana.AccountMeta{
	{PublicKey: forwarderStatePk[:], IsWritable: false},
	{PublicKey: forwarderAuthPk[:], IsWritable: false},
	{PublicKey: reportStatePk[:], IsWritable: true},
}

// Pass the struct data directly — types are derived from the IDL
result, err := ds.WriteReportFromUserData(runtime, data_storage.UserData{
	Key:   "price",
	Value: "500000",
}, accounts, &solana.ComputeConfig{ComputeLimit: 200_000}).Await()

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

What the CLI generates

The generator creates a Go package for each IDL file in contracts/solana/src/generated/<program_name>/.

What's inside depends on your IDL:

  • For all programs:
    • A <ProgramName> struct and New<ProgramName>(client) constructor (program ID from the IDL).
    • Codec helpers for encoding structs.
  • For onchain writes (each struct in types):
    • A WriteReportFrom<StructName>(runtime, input, remainingAccounts, computeConfig) method that returns a cre.Promise[*solana.WriteReportReply] you .Await().
    • A WriteReportFrom<StructName>s(...) variant for Borsh Vec payloads.

accounts layout

Every Solana write requires an []*solana.AccountMeta slice. The Keystone Forwarder (and the Devnet mock forwarder used in simulation) expects:

IndexAccountIsWritableHow to obtain
0forwarderStatefalseProvided by Chainlink for the forwarder deployment (mock values are built into the CLI for Devnet)
1forwarderAuthorityfalsePDA derived from ["forwarder", forwarderState, receiverProgram] under the forwarder program ID
2+Receiver accountsDepends on your programDefined by your Anchor program's on_report instruction
FieldTypeDescription
PublicKey[]byte32-byte raw public key (decoded from base58)
IsWritableboolWhether the account is writable

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 nil or zero ComputeLimit. Pass &solana.ComputeConfig{ComputeLimit: 200_000} (or another positive limit within the simulator's CU cap).
  3. Handle errors: Always check errors from constructors and .Await().
  4. Store addresses in config: Put forwarder and account addresses in config.staging.json / config.production.json.
  5. chainSelector as string in config: Use json:"chainSelector,string" on your config struct to avoid precision loss when unmarshalling uint64 values from JSON.

Where to go next

Get the latest Chainlink content straight to your inbox.