# Migrate from Carbon 1.x to 2.0 (Carbon 2.0)

> Upgrade from Carbon 1.x to 2.0: a checklist with before/after code for the toolchain, Solana crates, transaction configs, account deletions and the CLI.

Carbon 2.0 is the Agave 4 compatibility release: it moves to the Agave 4.2 Solana crates, supports Transaction V1 end to end, and delivers account closures the same way from every datasource. The pipeline, processor and decoder APIs are unchanged, so most indexers only need dependency bumps and a few constructor changes. This page lists every breaking change from 1.x, from the [2.0.0 changelog](https://github.com/sevenlabs-hq/carbon/blob/v2.0.0/CHANGELOG.md) and the crates themselves.

## Checklist

- [ ] [Rust 1.96.1 or newer](#toolchain), Node.js 20.18.0 or newer for the CLI.
- [ ] [Every Carbon crate at `2.0.0`, Solana crates on the Agave 4.2 cohort](#dependencies).
- [ ] [Remove Jetstreamer and Jito Shredstream](#datasources-not-in-20) if you use them; they aren't on the 2.0 line.
- [ ] [Regenerate decoders](#regenerate-decoders-with-the-20-cli) with `@sevenlabs-hq/carbon-cli@2.0.0`.
- [ ] [Request Base64 and transaction version 1](#transaction-source-configuration) in RPC block and Helius Atlas configs you build yourself.
- [ ] [Remove `account_deletions_tracked`](#account-deletions) from `YellowstoneGrpcGeyserClient`, `LaserStreamGeyserClient`, `HeliusWebsocket` and `StreamMessageClient`.
- [ ] [Handle zero-lamport accounts as deletions](#zero-lamport-accounts-are-now-always-deletions); give `AccountDeletion` its new `account` field.
- [ ] [Use `into_update()` in custom datasources](#custom-datasources).
- [ ] [Add `from_slot` to `LaserStreamClientConfig::new`](#laserstream-config).
- [ ] [Opt into Yellowstone reconnect](#yellowstone-client-reconnection-is-opt-in) if you want client-side replay.
- [ ] [Replace `Mint::decode`, `Token::decode` and `Multisig::decode`](#spl-token-account-helpers).
- [ ] [Match `VersionedMessage::V1`](#transaction-v1) wherever you match on message versions.
- [ ] [Update dashboards for sanitized Prometheus names](#other-behaviour-changes) if you used custom metric names with `.` or `-`.

## Toolchain

Carbon 2.0 crates declare `rust-version = "1.96.1"`; 1.x built with Rust 1.88.

```sh
rustup update stable
rustc --version
```

If your project pins a toolchain, update it:

`rust-toolchain.toml`:

```diff
 [toolchain]
-channel = "1.88.0"
+channel = "1.96.1"
```

The CLI and its renderer need Node.js 20.18.0 or newer (the 1.x-era CLI, 0.12.0, accepted Node 18).

## Dependencies

Bump every Carbon crate you declare to `2.0.0`, and every Solana crate you declare directly to the version Carbon 2.0 uses. Carbon 1.x used the Solana 3.x crates (`solana-pubkey` 3, `solana-client` 3, `yellowstone-grpc-proto` 10).

`Cargo.toml`:

```diff
 [dependencies]
-carbon-core = "1.0.0"
-carbon-yellowstone-grpc-datasource = "1.0.0"
-carbon-pump-swap-decoder = "1.0.0"
-solana-pubkey = "3.0.0"
-solana-client = "3.0.0"
-yellowstone-grpc-proto = "10.0.0"
+carbon-core = "2.0.0"
+carbon-yellowstone-grpc-datasource = "2.0.0"
+carbon-pump-swap-decoder = "2.0.0"
+solana-pubkey = "4.2.1"
+solana-client = "=4.2.2"
+yellowstone-grpc-proto = { version = "=12.7.0", default-features = false }
```

The versions Carbon 2.0 pins, for the crates you're most likely to declare:

| Crate                                                                  | Version            |
| ---------------------------------------------------------------------- | ------------------ |
| `solana-pubkey`                                                        | `4.2.1`            |
| `solana-account`                                                       | `4.3.2`            |
| `solana-signature`                                                     | `3.4.1`            |
| `solana-instruction`                                                   | `3.4.1`            |
| `solana-message`                                                       | `4.4.1`            |
| `solana-hash`                                                          | `4.5.0`            |
| `solana-transaction`                                                   | `4.1.6`            |
| `solana-commitment-config`                                             | `3.1.1`            |
| `solana-client`, `solana-transaction-status`, `solana-account-decoder` | `=4.2.2`           |
| `yellowstone-grpc-proto`                                               | `=12.7.0`          |
| `yellowstone-grpc-client`                                              | `=13.5.0`          |
| `helius`                                                               | `2.0.0`            |
| `sqlx` / `sqlx_migrator` (with `postgres`)                             | `0.8.6` / `0.17.0` |
| `juniper` / `axum` (with `graphql`)                                    | `0.16` / `0.8`     |

Then update and list the versions your own crate depends on directly:

```sh
cargo update
cargo tree -e normal --depth 1 | grep -E "solana|yellowstone|carbon"
```

Output:

```text
my-indexer v0.1.0 (/home/you/my-indexer)
├── carbon-core v2.0.0
├── carbon-meteora-dlmm-decoder v2.0.0
├── carbon-prometheus-metrics v2.0.0
├── carbon-yellowstone-grpc-datasource v2.0.0
├── solana-hash v4.5.0
├── solana-message v4.4.1
├── solana-pubkey v4.2.1
├── solana-signature v3.4.1
├── solana-transaction v4.1.6
├── solana-transaction-status v4.2.2
├── yellowstone-grpc-client v13.5.0
└── yellowstone-grpc-proto v12.7.0
```

Every Carbon crate must be `2.0.0`, and every `solana-*` crate you list must match the table. Don't use `cargo tree -d` as the check: Agave 4.2's own crates already pull a second copy of some type crates (`solana-account` 3.4.0 and `solana-pubkey` 3.0.0, through `solana-config-interface`), which is expected and harmless.

A Solana type crate on the wrong major version fails where a Carbon type meets yours:

Output:

```text
error[E0308]: mismatched types
   |  expected `solana_account::Account`, found a different `solana_account::Account`
note: there are multiple different versions of crate `solana_account` in the dependency graph
```

Don't mix Carbon 1.x and 2.0 crates in one graph either.

### Datasources not in 2.0

The 2.0 release publishes twelve datasource crates. `carbon-jito-shredstream-grpc-datasource` (no Transaction V1 support yet) and `carbon-jetstreamer-datasource` (not yet on the Solana v4 stack) stay on 1.x and can't be used in a Carbon 2.0 dependency graph. Keep indexers that depend on them on 1.x for now, or move them to [another datasource](https://carbon.sevenlabs.org/datasources.md).

## Regenerate decoders with the 2.0 CLI

Decoders generated by earlier CLI versions depend on Carbon 1.x crates. Regenerate them from the same IDL with the 2.0 CLI. The CLI's version on npm is `2.0.0` (the changelog calls this release of the CLI, renderer and version-registry packages 0.13; npm has no 0.13).

**npx**

```sh
npx -y @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./idl.json --out-dir ./decoders/my-program --name my-program
```

**pnpm**

```sh
pnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./idl.json --out-dir ./decoders/my-program --name my-program
```

**bun**

```sh
bunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./idl.json --out-dir ./decoders/my-program --name my-program
```

**yarn**

```sh
yarn dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./idl.json --out-dir ./decoders/my-program --name my-program
```

`parse` deletes `--out-dir` first, so commit or move any hand edits before you run it. Other CLI changes:

- `scaffold` now rejects `--with-graphql true` with `--with-postgres false`: the generated GraphQL layer reads the Postgres tables.
- Codama IDLs: native Codama events are rendered, and event-CPI discriminators are exported (`--event-cpi-discriminator` overrides the default Anchor tag).
- With `--with-base58 true`, pubkeys in instruction metadata serialize as base58 too.

See [Generate a decoder](https://carbon.sevenlabs.org/guides/generate-a-decoder.md) and the [CLI reference](https://carbon.sevenlabs.org/reference/cli.md).

## Transaction source configuration

Carbon 2.0 reconstructs transactions from binary encodings only and needs `max_supported_transaction_version: Some(1)` to receive V1 transactions. Configs you pass yourself must say so. `RpcTransactionCrawler` and the Helius GTFA datasource set both internally.

### `RpcBlockSubscribe`

```rust
// Carbon 1.x
let filters = Filters::new(
    RpcBlockSubscribeFilter::MentionsAccountOrProgram(program_id.to_string()),
    Some(RpcBlockSubscribeConfig {
        encoding: Some(UiTransactionEncoding::Base64),
        transaction_details: Some(TransactionDetails::Full),
        max_supported_transaction_version: Some(0),
        ..RpcBlockSubscribeConfig::default()
    }),
);
```

```rust
// Carbon 2.0
let filters = Filters::new(
    RpcBlockSubscribeFilter::MentionsAccountOrProgram(program_id.to_string()),
    Some(RpcBlockSubscribeConfig {
        encoding: Some(UiTransactionEncoding::Base64),
        transaction_details: Some(TransactionDetails::Full),
        max_supported_transaction_version: Some(1),
        ..RpcBlockSubscribeConfig::default()
    }),
);
let datasource = RpcBlockSubscribe::new(rpc_ws_url, filters);
```

If you requested `Json` or `JsonParsed`, switch to `Base64`: Carbon can't rebuild the original transaction from them.

### `RpcBlockCrawler`

```rust
// Carbon 2.0
let datasource = RpcBlockCrawler::new(
    rpc_url,
    467_000_000,       // start_slot
    Some(467_000_100), // end_slot
    None,              // block_interval
    RpcBlockConfig {
        encoding: Some(UiTransactionEncoding::Base64),
        transaction_details: Some(TransactionDetails::Full),
        max_supported_transaction_version: Some(1),
        ..RpcBlockConfig::default()
    },
    None, // max_concurrent_requests
    None, // channel_buffer_size
);
```

The 1.x code is the same with `max_supported_transaction_version: Some(0)`.

### `HeliusWebsocket` transaction subscriptions

The Helius SDK's `TransactionSubscribeOptions::default()` asks for `jsonParsed` and version 0. Override both:

```rust
// Carbon 2.0
use helius::types::{
    Cluster, RpcTransactionsConfig, TransactionSubscribeFilter, TransactionSubscribeOptions,
    UiEnhancedTransactionEncoding,
};

let filters = Filters::new(
    vec![],
    Some(RpcTransactionsConfig {
        filter: TransactionSubscribeFilter {
            account_include: Some(vec![program_id.to_string()]),
            ..TransactionSubscribeFilter::default()
        },
        options: TransactionSubscribeOptions {
            encoding: Some(UiEnhancedTransactionEncoding::Base64),
            max_supported_transaction_version: Some(1),
            ..TransactionSubscribeOptions::default()
        },
    }),
)?;
let datasource = HeliusWebsocket::new(api_key, filters, Cluster::MainnetBeta);
```

`Filters` here is `carbon_helius_atlas_ws_datasource::Filters`; add `helius = "2.0.0"` to use the SDK types.

## Account deletions

### The `account_deletions_tracked` argument is gone

In 1.x these four constructors took an `Arc<RwLock<HashSet<Pubkey>>>` of accounts to report closures for. Remove the argument.

**`YellowstoneGrpcGeyserClient::new`**

```rust
// Carbon 1.x
let datasource = YellowstoneGrpcGeyserClient::new(
    endpoint,
    x_token,
    Some(CommitmentLevel::Confirmed),
    account_filters,
    transaction_filters,
    BlockFilters::default(),
    Arc::new(RwLock::new(HashSet::new())), // account_deletions_tracked
    YellowstoneGrpcClientConfig::default(),
    None,
    None,
);
```

```rust
// Carbon 2.0
let datasource = YellowstoneGrpcGeyserClient::new(
    endpoint,
    x_token,
    Some(CommitmentLevel::Confirmed),
    account_filters,
    transaction_filters,
    BlockFilters::default(),
    YellowstoneGrpcClientConfig::default(),
    None, // disconnect_notifier
    None, // stream_timeout
);
```

**`LaserStreamGeyserClient::new`**

```rust
// Carbon 1.x
let datasource = LaserStreamGeyserClient::new(
    endpoint,
    api_key,
    Some(CommitmentLevel::Confirmed),
    account_filters,
    transaction_filters,
    BlockFilters::default(),
    Arc::new(RwLock::new(HashSet::new())), // account_deletions_tracked
    config,
);
```

```rust
// Carbon 2.0
let datasource = LaserStreamGeyserClient::new(
    endpoint,
    api_key,
    Some(CommitmentLevel::Confirmed),
    account_filters,
    transaction_filters,
    BlockFilters::default(),
    config,
);
```

**`HeliusWebsocket::new`**

```rust
// Carbon 1.x
let datasource = HeliusWebsocket::new(api_key, filters, Arc::new(RwLock::new(HashSet::new())), Cluster::MainnetBeta);
```

```rust
// Carbon 2.0
let datasource = HeliusWebsocket::new(api_key, filters, Cluster::MainnetBeta);
```

**`StreamMessageClient::new`**

```rust
// Carbon 1.x
let datasource = StreamMessageClient::new(Arc::new(RwLock::new(HashSet::new())), rx);
```

```rust
// Carbon 2.0
let datasource = StreamMessageClient::new(rx);
```

The matching public `account_deletions_tracked` fields were removed too.

### Zero-lamport accounts are now always deletions

|                                                                                   | Carbon 1.x                | Carbon 2.0                |
| --------------------------------------------------------------------------------- | ------------------------- | ------------------------- |
| Zero-lamport update for a tracked pubkey, empty data, owned by the System Program | `Update::AccountDeletion` | `Update::AccountDeletion` |
| Same, pubkey not tracked                                                          | Dropped                   | `Update::AccountDeletion` |
| Zero lamports, other owner or non-empty data                                      | `Update::Account`         | `Update::AccountDeletion` |
| Non-zero lamports                                                                 | `Update::Account`         | `Update::Account`         |

Consequences:

- Account pipes (`.account(..)`) no longer receive zero-lamport accounts. If a processor handled closures by checking `lamports == 0`, move that logic to an `.account_deletions(..)` processor.
- `.account_deletions(..)` processors now receive a deletion for every closed account the datasource sees, not only tracked ones. Filter by owner or pubkey in the processor if you only care about some.
- `AccountDeletion` carries the final account state in a new `account: Account` field. Its `owner` and `data` are whatever the source reported for the closing update, so check them before you rely on them.

If you construct `AccountDeletion` yourself, add the field:

```rust
// Carbon 1.x
let deletion = AccountDeletion { pubkey, slot, transaction_signature: None };
```

```rust
// Carbon 2.0
let deletion = AccountDeletion { pubkey, account, slot, transaction_signature: None };
```

### Custom datasources

Build an `AccountUpdate` and call `into_update()` instead of choosing between `Update::Account` and `Update::AccountDeletion` yourself, so your datasource applies the same rule as the built-in ones:

```rust
// Carbon 1.x
let update = Update::Account(AccountUpdate { pubkey, account, slot, transaction_signature: None });
```

```rust
// Carbon 2.0
let update = AccountUpdate { pubkey, account, slot, transaction_signature: None }.into_update();
```

See [Write a custom datasource](https://carbon.sevenlabs.org/guides/custom-datasource.md#emit-account-updates).

## Datasource configuration changes

### LaserStream config

`LaserStreamClientConfig::new` takes a new last argument, `from_slot: Option<u64>`. Pass `None` to keep the previous behaviour, or a slot to start the stream from it.

```rust
// Carbon 1.x
let config = LaserStreamClientConfig::new(
    None,                              // compression
    Some(Duration::from_secs(15)),     // connect_timeout
    Some(Duration::from_secs(15)),     // timeout
    None,                              // max_decoding_message_size
    None,                              // tls_config
    None,                              // tcp_nodelay
    true,                              // replay_enabled
);
```

```rust
// Carbon 2.0
let config = LaserStreamClientConfig::new(
    None,                              // compression
    Some(Duration::from_secs(15)),     // connect_timeout
    Some(Duration::from_secs(15)),     // timeout
    None,                              // max_decoding_message_size
    None,                              // tls_config
    None,                              // tcp_nodelay
    true,                              // replay_enabled
    None,                              // from_slot
);
```

`LaserStreamClientConfig::default()` also works and sets `from_slot: None`.

### Yellowstone client reconnection is opt-in

`YellowstoneGrpcClientConfig` has a new `reconnect` field, off by default, so nothing changes unless you opt in. With it, the gRPC client reconnects and replays missed data inside the stream, and Carbon no longer sees (or reports) the disconnect. Use it only if your provider supports replay for the range you need:

```rust
// Carbon 2.0, optional; add yellowstone-grpc-client = "=13.5.0"
use yellowstone_grpc_client::ReconnectConfig;

let config = YellowstoneGrpcClientConfig::default().with_reconnect(ReconnectConfig::default());
```

If you build `YellowstoneGrpcClientConfig` with a struct literal, add `reconnect: None`. `YellowstoneGrpcClientConfig::new(..)` keeps its six arguments.

## SPL Token account helpers

`carbon-token-program-decoder` no longer has `Mint::decode`, `Token::decode` and `Multisig::decode`. Decoding through `TokenProgramDecoder` in a pipeline needs no change. If you called them directly, unpack with the SPL layout and convert:

```rust
// Carbon 1.x
let token = carbon_token_program_decoder::accounts::token::Token::decode(data);
```

```rust
// Carbon 2.0; add solana-program-pack = "3.1.0" and spl-token-interface = "3.0.0"
use solana_program_pack::Pack;

let token = spl_token_interface::state::Account::unpack(data)
    .ok()
    .map(carbon_token_program_decoder::accounts::token::Token::from);
let mint = spl_token_interface::state::Mint::unpack(data)
    .ok()
    .map(carbon_token_program_decoder::accounts::mint::Mint::from);
let multisig = spl_token_interface::state::Multisig::unpack(data)
    .ok()
    .map(carbon_token_program_decoder::accounts::multisig::Multisig::from);
```

`unpack` also checks that the account is initialized, which the 1.x `decode` (a length check plus borsh) didn't.

## Transaction V1

Agave 4.2 adds V1 transactions, and Carbon 2.0 decodes their instructions, inner instructions and account metadata like legacy and V0. Your code only changes if it matches on the message version: `solana_message::VersionedMessage` has a third variant.

```rust
// Carbon 2.0
match &input.metadata.transaction_metadata.message {
    VersionedMessage::Legacy(_) => "legacy",
    VersionedMessage::V0(_) => "v0",
    VersionedMessage::V1(_) => "v1",
}
```

## Other behaviour changes

These don't need code changes but can change what you see:

- **Prometheus metric names are sanitized.** Characters other than letters, digits, `_` and `:` become `_` (`rpc.block-crawler.fetch.failed` is served as `rpc_block_crawler_fetch_failed`). Built-in names were already valid; update dashboards that query custom metrics with `.` or `-` in their names.
- **Short log events decode.** `decode_log_events` no longer skips `Program data:` payloads shorter than 8 bytes, so events with short discriminators are returned.
- **Invalid CPI stack heights are skipped.** Inner instructions with a missing or out-of-range `stackHeight` stop that inner-instruction group with a warning instead of producing a wrong tree.
- **Yellowstone** backs off between failed subscribe attempts (100 ms doubling to 3 s) and keeps its filters on ping resubscribes.
- **Token Program accounts** decode with the SPL `Pack` layout.

## Verify the migration

1. `cargo build` with no duplicate Solana crates (`cargo tree -d`).
2. Run against a known slot range and compare decoded counts with your 1.x indexer. Expect more `AccountDeletion`s and fewer zero-lamport account updates.
3. If you index V1 transactions, check that some arrive: `VersionedMessage::V1` in `transaction_metadata.message`.

## Common problems

### ``expected `solana_account::Account`, found a different `solana_account::Account` ``

A Solana type crate you depend on is still on its 1.x-era major version (here `solana-account` 3), or a Carbon 1.x crate is left in the graph. Bump it to the version in the [table](#dependencies) and run `cargo update`. `solana-pubkey` 3 and 4 share one `Pubkey` type, so pubkeys rarely show this error; accounts, transactions and messages do.

### `this function takes 9 arguments but 10 arguments were supplied`

A 1.x `YellowstoneGrpcGeyserClient::new` call still passes `account_deletions_tracked`. Remove it. The same error with other counts comes from `LaserStreamGeyserClient::new` (7), `HeliusWebsocket::new` (3) and `StreamMessageClient::new` (1).

### `this function takes 8 arguments but 7 arguments were supplied` on `LaserStreamClientConfig::new`

Add the trailing `from_slot` argument (`None`).

### ``no associated function or constant named `decode` found for struct `Token` ``

Use `spl_token_interface::state::Account::unpack` and `Token::from`, as shown above.

### `Failed to decode transaction` in the logs of an RPC block datasource

Your config still requests `Json`/`JsonParsed` or version 0. Set `UiTransactionEncoding::Base64` and `max_supported_transaction_version: Some(1)`.

## Next steps

- [Changelog](https://carbon.sevenlabs.org/changelog.md): Everything in 2.0.0.
- [Installation](https://carbon.sevenlabs.org/installation.md): Toolchain, crates and features for Carbon 2.0.
- [Datasources](https://carbon.sevenlabs.org/datasources.md): The twelve datasources in Carbon 2.0.

---

Carbon 2.0.0 documentation · Web: https://carbon.sevenlabs.org/guides/migrate-to-v2 · Last updated 2026-11-09 · Index of all pages: https://carbon.sevenlabs.org/llms.txt
