Migrate from Carbon 1.x to 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.
On this page
- Checklist
- Toolchain
- Dependencies
- Datasources not in 2.
0 - Regenerate decoders with the 2.
0 CLI - Transaction source configuration
- RpcBlockSubscribe
- RpcBlockCrawler
- HeliusWebsocket transaction subscriptions
- Account deletions
- The account_
deletions_ tracked argument is gone - Zero-lamport accounts are now always deletions
- Custom datasources
- Datasource configuration changes
- LaserStream config
- Yellowstone client reconnection is opt-in
- SPL Token account helpers
- Transaction V1
- Other behaviour changes
- Verify the migration
- Common problems
- expected `solana_
account:: Account`, found a different `solana_ account:: Account` - this function takes 9 arguments but 10 arguments were supplied
- this function takes 8 arguments but 7 arguments were supplied on LaserStreamClientConfig::
new - no associated function or constant named `decode` found for struct `Token`
- Failed to decode transaction in the logs of an RPC block datasource
- Next steps
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 and the crates themselves.
Checklist#
- Rust 1.96.1 or newer, 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. - Remove Jetstreamer and Jito Shredstream if you use them; they aren't on the 2.0 line.
- Regenerate decoders with
@sevenlabs-hq/.carbon-cli@2. 0. 0 - Request Base64 and transaction version 1 in RPC block and Helius Atlas configs you build yourself.
- Remove
account_fromdeletions_ tracked YellowstoneGrpcGeyserClient,LaserStreamGeyserClient,HeliusWebsocketandStreamMessageClient. - Handle zero-lamport accounts as deletions; give
AccountDeletionits newaccountfield. - Use
into_in custom datasources.update() - Add
from_slottoLaserStreamClientConfig::.new - Opt into Yellowstone reconnect if you want client-side replay.
- Replace
Mint::,decode Token::anddecode Multisig::.decode - Match
VersionedMessage::wherever you match on message versions.V1 - Update dashboards for sanitized Prometheus names if you used custom metric names with
.or-.
Toolchain#
Carbon 2.0 crates declare rust-version = "1.; 1.x built with Rust 1.88.
rustup update stable
rustc --versionIf your project pins a toolchain, update it:
[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).
[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_ (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:
cargo update
cargo tree -e normal --depth 1 | grep -E "solana|yellowstone|carbon"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.0Every 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:
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 graphDon'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.
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 -y @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./idl.json --out-dir ./decoders/my-program --name my-programpnpm dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./idl.json --out-dir ./decoders/my-program --name my-programbunx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./idl.json --out-dir ./decoders/my-program --name my-programyarn dlx @sevenlabs-hq/carbon-cli@2.0.0 parse --idl ./idl.json --out-dir ./decoders/my-program --name my-programparse deletes --out-dir first, so commit or move any hand edits before you run it. Other CLI changes:
scaffoldnow rejects--with-graphql truewith--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-discriminatoroverrides the default Anchor tag). - With
--with-base58 true, pubkeys in instruction metadata serialize as base58 too.
See Generate a decoder and the CLI reference.
Transaction source configuration#
Carbon 2.0 reconstructs transactions from binary encodings only and needs max_ to receive V1 transactions. Configs you pass yourself must say so. RpcTransactionCrawler and the Helius GTFA datasource set both internally.
RpcBlockSubscribe#
// 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()
}),
);// 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#
// 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_.
HeliusWebsocket transaction subscriptions#
The Helius SDK's TransactionSubscribeOptions:: asks for jsonParsed and version 0. Override both:
// 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_; add helius = "2. to use the SDK types.
Account deletions#
The account_ deletions_ tracked argument is gone#
In 1.x these four constructors took an Arc< of accounts to report closures for. Remove the argument.
YellowstoneGrpcGeyserClient::
// 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,
);// 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::
// 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,
);// Carbon 2.0
let datasource = LaserStreamGeyserClient::new(
endpoint,
api_key,
Some(CommitmentLevel::Confirmed),
account_filters,
transaction_filters,
BlockFilters::default(),
config,
);HeliusWebsocket::
// Carbon 1.x
let datasource = HeliusWebsocket::new(api_key, filters, Arc::new(RwLock::new(HashSet::new())), Cluster::MainnetBeta);// Carbon 2.0
let datasource = HeliusWebsocket::new(api_key, filters, Cluster::MainnetBeta);StreamMessageClient::
// Carbon 1.x
let datasource = StreamMessageClient::new(Arc::new(RwLock::new(HashSet::new())), rx);// Carbon 2.0
let datasource = StreamMessageClient::new(rx);The matching public account_ 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:: | Update:: |
| Same, pubkey not tracked | Dropped | Update:: |
| Zero lamports, other owner or non-empty data | Update:: | Update:: |
| Non-zero lamports | Update:: | Update:: |
Consequences:
- Account pipes (
.) no longer receive zero-lamport accounts. If a processor handled closures by checkingaccount(. . ) lamports == 0, move that logic to an.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.account_ deletions(. . ) AccountDeletioncarries the final account state in a newaccount: Accountfield. Itsowneranddataare whatever the source reported for the closing update, so check them before you rely on them.
If you construct AccountDeletion yourself, add the field:
// Carbon 1.x
let deletion = AccountDeletion { pubkey, slot, transaction_signature: None };// Carbon 2.0
let deletion = AccountDeletion { pubkey, account, slot, transaction_signature: None };Custom datasources#
Build an AccountUpdate and call into_ instead of choosing between Update:: and Update:: yourself, so your datasource applies the same rule as the built-in ones:
// Carbon 1.x
let update = Update::Account(AccountUpdate { pubkey, account, slot, transaction_signature: None });// Carbon 2.0
let update = AccountUpdate { pubkey, account, slot, transaction_signature: None }.into_update();See Write a custom datasource.
Datasource configuration changes#
LaserStream config#
LaserStreamClientConfig:: takes a new last argument, from_. Pass None to keep the previous behaviour, or a slot to start the stream from it.
// 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
);// 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:: also works and sets from_.
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:
// 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:: keeps its six arguments.
SPL Token account helpers#
carbon-token-program-decoder no longer has Mint::, Token:: and Multisig::. Decoding through TokenProgramDecoder in a pipeline needs no change. If you called them directly, unpack with the SPL layout and convert:
// Carbon 1.x
let token = carbon_token_program_decoder::accounts::token::Token::decode(data);// 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_ has a third variant.
// 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.is served asblock-crawler. fetch. failed rpc_). Built-in names were already valid; update dashboards that query custom metrics withblock_ crawler_ fetch_ failed .or-in their names. - Short log events decode.
decode_no longer skipslog_ events 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
stackHeightstop 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
Packlayout.
Verify the migration#
cargo buildwith no duplicate Solana crates (cargo tree -d).- Run against a known slot range and compare decoded counts with your 1.x indexer. Expect more
AccountDeletions and fewer zero-lamport account updates. - If you index V1 transactions, check that some arrive:
VersionedMessage::inV1 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 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:: call still passes account_. Remove it. The same error with other counts comes from LaserStreamGeyserClient:: (7), HeliusWebsocket:: (3) and StreamMessageClient:: (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_ 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:: and max_.