# Writing to Solana
Source: https://docs.chain.link/cre/guides/workflow/using-solana-client/onchain-write-go
Last Updated: 2026-07-13

> For the complete documentation index, see [llms.txt](/llms.txt).

This guide walks through your first Solana write from a CRE workflow using the Go SDK. The preferred path is
**generated bindings**: you place your Anchor IDL in the project, run `cre generate-bindings solana`, and call a typed
`WriteReportFrom...` helper. The binding Borsh-encodes your payload, builds the forwarder report, and submits it — you
do not hand-roll encoding.

Onchain, the DON does not call your program directly. It submits a signed report to the **Keystone Forwarder** — a
Chainlink-managed Solana program that verifies DON signatures, then cross-program invokes (CPI) your receiver's
`on_report`. That is the same forwarder model as [EVM Write](/cre/capabilities/evm-read-write); see
[Solana Write](/cre/capabilities/solana-write) for the full flow. Local simulation uses a Devnet stand-in of that
forwarder so you can dry-run without deploying. After you deploy, the live forwarder addresses apply.

For IDL layout, generator output, and what each `WriteReportFrom...` method does under the hood, see
[Generating Solana Bindings](/cre/guides/workflow/using-solana-client/generating-bindings-go).

## Prerequisites

- A CRE Go project initialized with `cre init`. See [Part 1: Project Setup](/cre/getting-started/part-1-project-setup-go) if you are starting from scratch.
- CRE CLI **v1.24.0 or later** (`cre version` / `cre update`) with Solana binding generation.
- A funded Solana Devnet keypair for `CRE_SOLANA_PRIVATE_KEY` (required even for dry-run). Generate one with `solana-keygen new`, fund it with `solana airdrop` (or a transfer), and put the 64-byte base58 keypair in your project `.env`. See [Step 4](#step-4-run-the-simulation).

## What you need: a receiver program

Your Solana program must implement an `on_report` instruction that accepts the payload delivered by the forwarder. The
forwarder CPIs into your program after verifying the DON signatures. Your Anchor IDL's `types` section defines the
struct(s) you will write; the generator turns each into a `WriteReportFrom<StructName>()` method.

This guide uses a minimal `DataStorage` / `UserData` IDL with a receiver program already deployed to Solana Devnet at
`64Q1Xu7AEbc2xgaN21zZU1y1mkaLJNnP5cHFkthGuW31`. You can simulate against it without deploying your own program. Swap in
your IDL and call the generated helpers for your structs when you are ready.

> **NOTE: Forwarder strips leading accounts**
>
> The forwarder passes only indices 2+ of your `accounts` slice to your program's CPI call. Indices 0 (`forwarderState`)
> and 1 (`forwarderAuthority`) are consumed by the forwarder itself.

## Step 1: Add your IDL and generate bindings

1. Save the following as `contracts/solana/src/idl/data_storage.json` (or copy your own Anchor IDL into that folder).

   ```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" }
           ]
         }
       }
     ]
   }
   ```

2. From your project root, generate the bindings:

   ```bash
   cre generate-bindings solana
   ```

   This creates `contracts/solana/src/generated/data_storage/` with a `UserData` struct and
   `WriteReportFromUserData(runtime, input, remainingAccounts, computeConfig)`.

3. Sync dependencies.

   ```bash
   go mod tidy
   ```

## Step 2: Configure the workflow

1. Open (or create) `config.staging.json` and add the Solana Devnet simulation parameters.

   For `cre workflow simulate`, use these mock forwarder program and state accounts for Solana Devnet.
   `receiverProgramId` matches the IDL address above for this example; with your own program, use your Devnet program
   ID.

   ```json
   {
     "schedule": "*/30 * * * * *",
     "chainSelector": "16423721717087811551",
     "receiverProgramId": "64Q1Xu7AEbc2xgaN21zZU1y1mkaLJNnP5cHFkthGuW31",
     "forwarderProgramId": "7kuEAA3mSC1Tz8gQjnvH7bKFda9xSPRRin9SZbH49cNK",
     "forwarderState": "5Tipz3yhTBdVsDbaBxZkrp7Gjf3brGq5SKkxReefPMP7",
     "reportState": "HxzqwQZNyDM3Tvp9nR8v9tgJ5ZZ1Lm8LiJ8kxoyu539K",
     "computeLimit": 200000
   }
   ```

   | Field                | Description                                                                               |
   | -------------------- | ----------------------------------------------------------------------------------------- |
   | `chainSelector`      | Solana Devnet (`"16423721717087811551"`). Store as a string to avoid JSON precision loss. |
   | `receiverProgramId`  | Your receiver program (IDL address for this example)                                      |
   | `forwarderProgramId` | Forwarder program ID used for local simulation (fixed Devnet value)                       |
   | `forwarderState`     | Forwarder state account used for local simulation (fixed Devnet value)                    |
   | `reportState`        | Writable account required by your receiver (DataAccount PDA for this example)             |
   | `computeLimit`       | Solana compute units; must be greater than 0 for simulation                               |

   The workflow derives `forwarderAuthority` at runtime from
   `[]byte("forwarder")`, forwarder state bytes, and receiver program bytes under `forwarderProgramId`. When you deploy
   in [Step 5](#step-5-deploy-the-workflow), keep that derivation and point `forwarderProgramId` / `forwarderState` at
   the live forwarder addresses for your network.

### Simulation vs production forwarder addresses

The forwarder program and state accounts differ between local simulation and production. The config above uses the
**mock forwarder** addresses for `cre workflow simulate`. When you deploy to the DON, you must
replace them with the live Keystone Forwarder addresses for your network.

> **CAUTION: Important: Different addresses for simulation vs production**
>
> The mock forwarder program and state accounts used during simulation are **different** from the production Keystone
> Forwarder accounts used by deployed workflows. After testing with simulation, update `forwarderProgramId` and
> `forwarderState` in your production config to the live forwarder addresses for your network. See
> [Step 5](#step-5-deploy-the-workflow).

#### Simulation (mock forwarder)

These are the mock forwarder addresses for `cre workflow simulate`. They are the values shown in
the `config.staging.json` above:

| Network        | Mock Forwarder Program ID                    | Mock Forwarder State Account                 |
| -------------- | -------------------------------------------- | -------------------------------------------- |
| Solana Devnet  | 7kuEAA3mSC1Tz8gQjnvH7bKFda9xSPRRin9SZbH49cNK | 5Tipz3yhTBdVsDbaBxZkrp7Gjf3brGq5SKkxReefPMP7 |
| Solana Mainnet | 7kuEAA3mSC1Tz8gQjnvH7bKFda9xSPRRin9SZbH49cNK | jhCjuD4Z3V7HeSUChMRpkRwpw6B9yC63mxDMv8SdLNX  |

#### Production (Keystone Forwarder)

For production deployments, use these live Keystone Forwarder addresses:

| Network        | Keystone Forwarder Program ID                | Keystone Forwarder State Account             |
| -------------- | -------------------------------------------- | -------------------------------------------- |
| Solana Devnet  | CXsKEJcs25TQEYU2e5jZ8QTPE3ffMLZhH6BWHrdcCCB5 | 8QoomCQyPSkJ8WopJbX9B4HyvrFzziwvJdU8hZE6DCr9 |
| Solana Mainnet | GFrSSvQXaVGkc6Nrr8y2msie6pivJkR1s2EnDk4et294 | 9FgdPyU28bGMCuJyD34pzw9W7Ys36ziLbT9cbZtsaraV |

The workflow still derives `forwarderAuthority` from `[]byte("forwarder")`, forwarder state bytes, and receiver program bytes under the
production `forwarderProgramId`.

## Step 3: Write the workflow

This step is a complete, pasteable workflow that performs one Solana write on a cron trigger using **generated
bindings**. It reads config, derives the forwarder authority PDA, builds the account list the forwarder expects, and
calls `WriteReportFromUserData()`. Replace `data_storage` / `UserData` with your generated package and struct when you
use your own IDL.

1. Save the following as `my-workflow/workflow.go`.

   A Go project is split across two files: `main.go` is the WASM entry point (generated by `cre init` and unchanged), and
   your workflow logic lives in a separate file such as `workflow.go`. Replace `<project-name>` with your Go module name
   from `go.mod`.

   ```go
   // my-workflow/workflow.go
   package main

   import (
   	"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"
   )

   const defaultComputeLimit uint32 = 200_000

   type Config struct {
   	Schedule           string `json:"schedule"`
   	ChainSelector      uint64 `json:"chainSelector,string"`
   	ReceiverProgramID  string `json:"receiverProgramId"`
   	ForwarderProgramID string `json:"forwarderProgramId"`
   	ForwarderState     string `json:"forwarderState"`
   	ReportState        string `json:"reportState"`
   	ComputeLimit       uint32 `json:"computeLimit"`
   }

   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 deriveForwarderAuthority(forwarderState, receiverProgram, forwarderProgram solanago.PublicKey) (solanago.PublicKey, error) {
   	authority, _, err := solanago.FindProgramAddress(
   		[][]byte{
   			[]byte("forwarder"),
   			forwarderState[:],
   			receiverProgram[:],
   		},
   		forwarderProgram,
   	)
   	return authority, err
   }

   func onCronTrigger(config *Config, runtime cre.Runtime, _ *cron.Payload) (string, error) {
   	client := &solana.Client{ChainSelector: config.ChainSelector}

   	// NewDataStorage uses the program ID baked into the IDL (regenerate bindings when it changes).
   	ds, err := data_storage.NewDataStorage(client)
   	if err != nil {
   		return "", err
   	}

   	forwarderStatePk := solanago.MustPublicKeyFromBase58(config.ForwarderState)
   	receiverPk := solanago.MustPublicKeyFromBase58(config.ReceiverProgramID)
   	forwarderProgramPk := solanago.MustPublicKeyFromBase58(config.ForwarderProgramID)
   	reportStatePk := solanago.MustPublicKeyFromBase58(config.ReportState)

   	forwarderAuthority, err := deriveForwarderAuthority(forwarderStatePk, receiverPk, forwarderProgramPk)
   	if err != nil {
   		return "", err
   	}

   	// Index 0 forwarderState, 1 forwarderAuthority (derived), 2+ reportState.
   	accounts := []*solana.AccountMeta{
   		{PublicKey: forwarderStatePk[:], IsWritable: false},
   		{PublicKey: forwarderAuthority[:], IsWritable: false},
   		{PublicKey: reportStatePk[:], IsWritable: true},
   	}

   	payload := data_storage.UserData{
   		Key:   "price",
   		Value: "500000",
   	}

   	runtime.Logger().Info("Submitting UserData via bindings",
   		"key", payload.Key,
   		"forwarderAuthority", forwarderAuthority.String(),
   	)

   	computeLimit := config.ComputeLimit
   	if computeLimit == 0 {
   		computeLimit = defaultComputeLimit
   	}
   	computeConfig := &solana.ComputeConfig{ComputeLimit: computeLimit}

   	result, err := ds.WriteReportFromUserData(runtime, payload, accounts, computeConfig).Await()
   	if err != nil {
   		return "", err
   	}

   	runtime.Logger().Info("Write completed",
   		"txStatus", result.GetTxStatus().String(),
   		"errorMessage", result.GetErrorMessage(),
   		"txSignature", result.GetTxSignature(),
   	)
   	if msg := result.GetErrorMessage(); msg != "" {
   		return msg, nil
   	}
   	return "Success", nil
   }
   ```

## Step 4: Run the simulation

Use this step to confirm compile, trigger, and the bindings write path with `cre workflow simulate`.

1. Create a `workflow.yaml` in your workflow folder so the CLI can find your staging target.

   ```yaml
   staging-settings:
     user-workflow:
       workflow-name: "my-solana-write"
     workflow-artifacts:
       workflow-path: "./main.go"
       config-path: "./config.staging.json"
       secrets-path: ""
   ```

2. Add a Solana RPC endpoint to your `project.yaml` under your simulation target.

   ```yaml
   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 64-byte base58 keypair even for dry-run. The transmitter account must exist and be
   funded on Solana Devnet. Generate a key with `solana-keygen new`, fund it with `solana airdrop` (or a transfer), and
   put the keypair in `.env`:

   ```bash
   CRE_SOLANA_PRIVATE_KEY=<your-64-byte-base58-keypair>
   ```

4. Run the dry-run simulation.

   ```bash
   cre workflow simulate my-workflow --target staging-settings
   ```

   Confirm the workflow compiles, the cron trigger runs, and your USER LOGs appear. A successful onchain write
   requires deploying to the DON with the production Keystone Forwarder (see [Step 5](#step-5-deploy-the-workflow)).

   ```
   ✓ Workflow compiled
   [SIMULATION] Simulator Initialized
   [SIMULATION] Running trigger trigger=cron-trigger@1.0.0
   [USER LOG] msg="Submitting UserData via bindings" key=price forwarderAuthority=...
   [USER LOG] msg="Write completed" txStatus=... errorMessage=... txSignature=""

   ✓ Workflow Simulation Result:
   ...
   ```

> **NOTE: Simulation error: Custom:6002**
>
> `Custom:6002` is a known limitation of the current simulator and will be patched in a future CLI update.

When you are ready to run on the DON with **your** receiver, continue to [Step 5](#step-5-deploy-the-workflow).

## Step 5: Deploy the workflow

A deployed workflow runs on the DON, which submits reports through the live Keystone Forwarder on Solana Devnet. Prefer
the [private registry](/cre/guides/operations/deploying-to-private-registry-go) for this example (CRE login session; no
Ethereum gas for registry ops). You need [Deploy Access](/cre/account/deploy-access).

You can use the `DataStorage` example receiver as-is — it's deployed on Devnet and ready to accept writes. If you have
your own receiver program, update the IDL address, regenerate bindings, and set `receiverProgramId` to your own value
(Go bindings use the program ID baked into the generated package for the write receiver; config is used to derive the
forwarder authority PDA).

1. Create `config.production.json`. Copy your `receiverProgramId` and `reportState` from the staging config and
   swap in the production Keystone Forwarder addresses:

   ```json
   {
     "schedule": "30 */5 * * * *",
     "chainSelector": "16423721717087811551",
     "receiverProgramId": "64Q1Xu7AEbc2xgaN21zZU1y1mkaLJNnP5cHFkthGuW31",
     "forwarderProgramId": "CXsKEJcs25TQEYU2e5jZ8QTPE3ffMLZhH6BWHrdcCCB5",
     "forwarderState": "8QoomCQyPSkJ8WopJbX9B4HyvrFzziwvJdU8hZE6DCr9",
     "reportState": "HxzqwQZNyDM3Tvp9nR8v9tgJ5ZZ1Lm8LiJ8kxoyu539K",
     "computeLimit": 290000
   }
   ```

   These are the Solana Devnet production Keystone Forwarder addresses. For Mainnet, see the
   [forwarder address table](#production-keystone-forwarder) above. If you are using your own receiver program,
   replace `receiverProgramId` and `reportState` with your own values. The workflow still derives
   `forwarderAuthority` from `[]byte("forwarder")`, forwarder state bytes, and receiver program bytes under that
   `forwarderProgramId`.

2. Point a deploy target at that config and the private registry in `my-workflow/workflow.yaml`.

   ```yaml
   production-settings:
     user-workflow:
       workflow-name: "my-solana-write"
       deployment-registry: "private"
     workflow-artifacts:
       workflow-path: "./main.go"
       config-path: "./config.production.json"
       secrets-path: ""
   ```

3. Add the Solana Devnet RPC under `production-settings` in `project.yaml` (same URL as staging is fine).

4. Deploy from your project root.

   ```bash
   cre workflow deploy my-workflow --target production-settings
   ```

   Expected shape:

   ```
   Deploying Workflow: my-solana-write
     Registry:      private
     ...
   ✓ Workflow registered in private registry
   ...
        Status:           Active
   ```

5. After the cron fires, open the workflow in the [CRE platform](https://app.chain.link/cre/workflows) (or run
   `cre execution list my-solana-write`) and confirm node logs reach the write path:

   ```
   Submitting UserData via bindings key=...
   ```

   If WriteReport returns a Solana `txSignature`, confirm the transaction on
   [Solana Explorer](https://explorer.solana.com/?cluster=devnet) (forwarder as the top-level program, your receiver as
   a CPI).

   Here is an example of a successful write from this guide's `DataStorage` receiver on Solana Devnet:
   [view on Explorer](https://explorer.solana.com/tx/2tfcA2jKvaP3JrVaURQEPuWCLtjV8ZdzMCppCUa9a41LQoG5847MzJNJmwdepzjQHxrJak8Umr4fHebkhBMGTqFM?cluster=devnet).

See [Deploying to the Private Registry](/cre/guides/operations/deploying-to-private-registry-go) for registry details, activate/pause, and CI flags.

## What happens onchain

When the deployed workflow runs on the DON:

1. The DON nodes each execute your workflow and reach consensus on the encoded payload.
2. A DON node calls the Keystone Forwarder program with the signed report and account list.
3. The forwarder verifies all DON signatures.
4. The forwarder CPIs into your program's `on_report` instruction with the decoded payload and receiver accounts.

The transaction on Solana Explorer will show the forwarder program as the initial callee, with your program appearing as a CPI target.

## Next steps

- **[Generating Solana Bindings](/cre/guides/workflow/using-solana-client/generating-bindings-go)**: IDL layout, generator output, and what `WriteReportFrom...` does
- **[Solana Client SDK Reference](/cre/reference/sdk/solana-client-go)**: Full API reference for `solana.Client` and helpers
- **[Solana Write Capability](/cre/capabilities/solana-write)**: Architecture and account layout reference