# Writing to Solana
Source: https://docs.chain.link/cre/guides/workflow/using-solana-client/onchain-write-ts
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 TypeScript 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-ts).

## Prerequisites

- A CRE project initialized with `cre init`. See [Part 1: Project Setup](/cre/getting-started/part-1-project-setup-ts) if you are starting from scratch.
- CRE CLI **v1.24.0 or later** (`cre version` / `cre update`) with TypeScript 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 `remainingAccounts` list 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 `my-workflow/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 workflow directory, generate the bindings:

   ```bash
   cd my-workflow
   cre generate-bindings solana
   ```

   This creates `contracts/solana/ts/generated/DataStorage.ts` (plus a mock and `index.ts`) inside your workflow
   folder. The `DataStorage` class includes `writeReportFromUserData(runtime, input, remainingAccounts, computeConfig?)`.

   The generated bindings import from `@solana/codecs`. If you run `tsc --noEmit` outside of `cre workflow simulate`,
   install it in your workflow folder: `npm install @solana/codecs`.

## 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
   `["forwarder", forwarderState, receiverProgram]` 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 `["forwarder", forwarderState, receiverProgram]` 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 `remainingAccounts` list the forwarder
expects, and calls `writeReportFromUserData()`. Replace `DataStorage` / `UserData` with your generated class and struct
when you use your own IDL.

1. Save the following as `my-workflow/main.ts`.

   ```typescript
   // my-workflow/main.ts
   import {
     CronCapability,
     type CronPayload,
     handler,
     Runner,
     type Runtime,
     SolanaClient,
     solanaAccountMeta,
   } from "@chainlink/cre-sdk"
   import { PublicKey } from "@solana/web3.js"
   import { DataStorage, type UserData } from "./contracts/solana/ts/generated/DataStorage"

   type Config = {
     schedule: string
     chainSelector: string
     receiverProgramId: string
     forwarderProgramId: string
     forwarderState: string
     reportState: string
     computeLimit?: number
   }

   const DEFAULT_COMPUTE_LIMIT = 200_000
   const FORWARDER_SEED = Uint8Array.from("forwarder", (c) => c.charCodeAt(0))

   function deriveForwarderAuthority(
     forwarderState: PublicKey,
     receiverProgram: PublicKey,
     forwarderProgram: PublicKey
   ): PublicKey {
     const [authority] = PublicKey.findProgramAddressSync(
       [FORWARDER_SEED, forwarderState.toBytes(), receiverProgram.toBytes()],
       forwarderProgram
     )
     return authority
   }

   const onCronTrigger = (runtime: Runtime<Config>, _trigger: CronPayload): string => {
     const cfg = runtime.config
     const client = new SolanaClient(BigInt(cfg.chainSelector))
     // Program ID defaults to the address baked into the IDL; pass cfg.receiverProgramId to override.
     const receiver = new DataStorage(client, cfg.receiverProgramId)

     const forwarderState = new PublicKey(cfg.forwarderState)
     const receiverProgram = new PublicKey(cfg.receiverProgramId)
     const forwarderProgram = new PublicKey(cfg.forwarderProgramId)
     const reportState = new PublicKey(cfg.reportState)
     const forwarderAuthority = deriveForwarderAuthority(forwarderState, receiverProgram, forwarderProgram)

     // Index 0 forwarderState, 1 forwarderAuthority (derived), 2+ reportState.
     const remainingAccounts = [
       solanaAccountMeta(forwarderState.toBase58(), false),
       solanaAccountMeta(forwarderAuthority.toBase58(), false),
       solanaAccountMeta(reportState.toBase58(), true),
     ]

     const payload: UserData = { key: "price", value: "500000" }

     runtime.log(
       `Submitting UserData via bindings key=${payload.key} forwarderAuthority=${forwarderAuthority.toBase58()}`
     )

     const computeLimit = cfg.computeLimit ?? DEFAULT_COMPUTE_LIMIT
     const out = receiver.writeReportFromUserData(runtime, payload, remainingAccounts, { computeLimit })

     runtime.log(
       `Write completed txStatus=${out.txStatus} errorMessage=${out.errorMessage ?? ""} txSignature=${out.txSignature ?? ""}`
     )
     if (out.errorMessage && out.errorMessage.length > 0) {
       return out.errorMessage
     }
     return "Success"
   }

   export const initWorkflow = (config: Config) => {
     const cron = new CronCapability()
     return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
   }

   export async function main() {
     const runner = await Runner.newRunner<Config>()
     await runner.run(initWorkflow)
   }
   ```

## 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.ts"
       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] Submitting UserData via bindings key=price forwarderAuthority=...
   [USER LOG] Write completed txStatus=0 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-ts) 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, replace `receiverProgramId` and `reportState` with your own values.

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 `["forwarder", forwarderState, receiverProgram]` 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.ts"
       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-ts) 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-ts)**: IDL layout, generator output, and what `writeReportFrom...` does
- **[Solana Client SDK Reference](/cre/reference/sdk/solana-client-ts)**: Full API reference for `SolanaClient` and helpers
- **[Solana Write Capability](/cre/capabilities/solana-write)**: Architecture and account layout reference