1. Concepts

Updates

The Update enum that datasources send into a pipeline, its four variants, UpdateType, zero-lamport account deletions, Transaction V1 and block details.

On this page

Every piece of data a datasource produces enters the pipeline as a carbon_core::datasource::Update. You meet this type when you write a custom datasource, and its fields show up again in the metadata your processors receive. Full field-by-field details are in the updates reference.

The Update enum#

Rust
pub enum Update {
    Account(AccountUpdate),
    Transaction(Box<TransactionUpdate>),
    AccountDeletion(AccountDeletion),
    BlockDetails(BlockDetails),
}
VariantMeansDelivered to
AccountAn account's state at a slot: a write, or a read from a snapshot or getProgramAccounts.account pipes, through their decoder
TransactionA transaction with its execution metadata.instruction pipes (one call per decoded instruction) and transaction pipes
AccountDeletionAn account that was closed: its lamports reached zero.account_deletions pipes
BlockDetailsMetadata about a block.block_details pipes

The pipeline routes each variant only to pipes of the matching kind; Architecture gives the exact order.

Account updates#

Rust
pub struct AccountUpdate {
    pub pubkey: Pubkey,
    pub account: Account,
    pub slot: u64,
    pub transaction_signature: Option<Signature>,
}

account is a solana_account::Account with lamports, data, owner, executable and rent_epoch. transaction_signature is the transaction that wrote the account, when the source knows it: Geyser streams usually set it; getProgramAccounts and snapshots don't.

Account deletions#

Solana has no explicit "account closed" event. A program closes an account by moving all its lamports out, and the runtime then removes it. Carbon 2.0 reports every account write with zero lamports as an AccountDeletion:

Rust
pub struct AccountDeletion {
    pub pubkey: Pubkey,
    pub account: Account, // the final state the source reported
    pub slot: u64,
    pub transaction_signature: Option<Signature>,
}

Datasources do this conversion with AccountUpdate::into_update, which returns Update::AccountDeletion when account.lamports == 0 and Update::Account otherwise. The Yellowstone gRPC, Helius LaserStream, Helius Atlas websocket, RPC program subscribe and stream message datasources all use it. Call it in your own datasources too, so that deletions are classified in one place:

Rust
let update = AccountUpdate { pubkey, account, slot, transaction_signature }.into_update();

Because deletions are a separate variant, an account pipe never sees a zero-lamport account from these datasources. Register an account_deletions processor to handle closures. The final account keeps the owner and data the source reported, so you can tell which program the account belonged to.

Transactions#

Rust
pub struct TransactionUpdate {
    pub signature: Signature,
    pub transaction: VersionedTransaction,
    pub meta: TransactionStatusMeta,
    pub is_vote: bool,
    pub slot: u64,
    pub index: Option<u64>,
    pub block_time: Option<i64>,
    pub block_hash: Option<Hash>,
}
  • transaction is the signed transaction; meta is its execution result: status, fee, balances, log messages, inner instructions, loaded addresses and so on.
  • index is the transaction's position in its block. Yellowstone, LaserStream, RPC block subscribe, the block crawler and Helius GTFA set it; the transaction crawler and Helius Atlas websockets leave it None.
  • block_time is a Unix timestamp in seconds, when the source provides it.

The pipeline turns each TransactionUpdate into a shared TransactionMetadata and the list of its instructions before any processor runs.

Transaction V1#

The Agave 4.2 Solana crates add the V1 transaction format alongside legacy and v0 transactions. transaction.message is a VersionedMessage, and Carbon 2.0 handles all three variants (Legacy, V0 and V1) when it extracts instructions and their accounts.

Datasources must request V1 transactions and a binary encoding, because Carbon can't rebuild a transaction from a Json or JsonParsed payload. The RPC transaction crawler and the Helius GTFA datasource set this internally. When you pass your own RpcBlockSubscribeConfig or RpcBlockConfig, set it yourself:

Rust
RpcBlockSubscribeConfig {
    encoding: Some(UiTransactionEncoding::Base64),
    transaction_details: Some(TransactionDetails::Full),
    max_supported_transaction_version: Some(1),
    ..RpcBlockSubscribeConfig::default()
}

Block details#

Rust
pub struct BlockDetails {
    pub slot: u64,
    pub block_hash: Option<Hash>,
    pub previous_block_hash: Option<Hash>,
    pub rewards: Option<Rewards>,
    pub num_reward_partitions: Option<u64>,
    pub block_time: Option<i64>,
    pub block_height: Option<u64>,
}

Among the Carbon 2.0 datasources, only RPC block subscribe sends BlockDetails: one per block, before that block's transactions. A custom datasource can send them too.

UpdateType#

UpdateType names the variants without their data:

Rust
pub enum UpdateType {
    AccountUpdate,
    Transaction,
    AccountDeletion,
    BlockDetails,
}

Update::update_type() returns the UpdateType of an update, and Datasource::update_types() returns the types a datasource declares it sends. In 2.0.0 the pipeline doesn't read update_types() or check updates against it, so treat it as documentation. It isn't always complete: RPC block subscribe declares only Transaction but also sends BlockDetails.

Example#

This program builds the same token account twice, once funded and once with zero lamports, and shows how into_update classifies each.

Terminal
cargo add carbon-core@2.0.0 solana-account@4.3.2 solana-pubkey@4.2.1
src/main.rs
use {
    carbon_core::datasource::{AccountUpdate, Update},
    solana_account::Account,
    solana_pubkey::Pubkey,
};

fn main() {
    let token_program = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");

    for lamports in [2_039_280, 0] {
        let update = AccountUpdate {
            pubkey: Pubkey::new_unique(),
            account: Account {
                lamports,
                data: vec![0; 165],
                owner: token_program,
                executable: false,
                rent_epoch: u64::MAX,
            },
            slot: 371_000_000,
            transaction_signature: None,
        };

        match update.into_update() {
            Update::Account(update) => {
                println!("account update: {} lamports", update.account.lamports)
            }
            Update::AccountDeletion(deletion) => println!(
                "account deletion: owner {}, {} bytes of final data",
                deletion.account.owner,
                deletion.account.data.len()
            ),
            other => println!("{:?}", other.update_type()),
        }
    }
}
Terminal
cargo run
Output
account update: 2039280 lamports
account deletion: owner TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA, 165 bytes of final data

Next steps#

Your endpoints

Code samples use these instead of the placeholders, so you can copy and run them as they are. They’re stored only in this browser and never sent anywhere.