quicknode-cascade 0.1.0

Stream blockchain data at scale. Plugin-based framework powered by QuickNode Cascade — start with Solana, more chains coming.
Documentation

quicknode-cascade

Stream blockchain data at scale. Plugin-based framework powered by QuickNode Cascade — an edge-cached block archive served from 300+ global PoPs with sub-50ms latency.

Start with Solana. More chains coming soon.

Install

cargo add quicknode-cascade

Quick Start

use quicknode_cascade::{CascadeRunner, solana};

struct MyIndexer;

impl solana::Plugin for MyIndexer {
    fn name(&self) -> &'static str { "my-indexer" }

    fn on_block<'a>(&'a self, block: &'a solana::BlockData) -> solana::PluginFuture<'a> {
        Box::pin(async move {
            println!("slot {}{} txs", block.slot, block.transaction_count);
            Ok(())
        })
    }

    fn on_transaction<'a>(&'a self, tx: &'a solana::TransactionData) -> solana::PluginFuture<'a> {
        Box::pin(async move {
            if !tx.is_vote {
                println!("  tx {} fee={}", tx.signature, tx.fee);
            }
            Ok(())
        })
    }
}

fn main() {
    CascadeRunner::solana_mainnet()
        .auth_token("your-jwt-token")
        .backfill(300_000_000, 300_001_000)
        .concurrency(50)
        .with_plugin(Box::new(MyIndexer))
        .run()
        .expect("done");
}

That's it. The runner fetches blocks in parallel, extracts structured data, and calls your plugin hooks. You bring your own database, your own schema, your own logic.

CascadeRunner

Builder-pattern runner that handles all parallel fetching, retries, cursor management, and plugin lifecycle.

Chain Selection

use quicknode_cascade::CascadeRunner;

// Solana mainnet
CascadeRunner::solana_mainnet()

// Solana devnet
CascadeRunner::solana_devnet()

// Any chain by name — maps to https://{chain}-cascade.quiknode.io
CascadeRunner::chain("solana-mainnet")

Authentication

CascadeRunner::solana_mainnet()
    .auth_token("your-jwt-token")

The token is sent as Authorization: Bearer <token> on every request.

Running Modes

// Backfill a slot range
.backfill(start_slot, end_slot)

// Follow the chain tip in real-time
.live()

// Follow from a specific slot
.live_from(300_000_000)

Full Configuration

CascadeRunner::solana_mainnet()
    .auth_token("jwt")                    // JWT for Cascade API
    .backfill(300_000_000, 300_001_000)   // slot range to backfill
    .concurrency(50)                      // parallel workers (default: 10)
    .encoding("json")                     // "json" (structured) or raw modes
    .cursor_file("cursor.json")           // resume support (default: cursor.json)
    .tip_buffer(100)                      // slots behind tip for live mode
    .source_url("http://custom:8899")     // override Cascade endpoint
    .with_plugin(Box::new(plugin1))       // register N plugins
    .with_plugin(Box::new(plugin2))       // each sees all events
    .run()                                // blocks until done or SIGTERM

Plugin Trait (Solana)

pub trait Plugin: Send + Sync + 'static {
    fn name(&self) -> &'static str;
    fn on_load(&self) -> PluginFuture<'_> { ... }
    fn on_block(&self, block: &BlockData) -> PluginFuture<'_> { ... }
    fn on_transaction(&self, tx: &TransactionData) -> PluginFuture<'_> { ... }
    fn on_token_transfer(&self, transfer: &TokenTransferData) -> PluginFuture<'_> { ... }
    fn on_account_activity(&self, activity: &AccountActivityData) -> PluginFuture<'_> { ... }
    fn on_skipped_slot(&self, slot: u64) -> PluginFuture<'_> { ... }
    fn on_raw_block(&self, slot: u64, raw: &serde_json::Value) -> PluginFuture<'_> { ... }
    fn on_exit(&self) -> PluginFuture<'_> { ... }
}

All hooks have default no-op implementations. Override only what you need. Plugin errors are logged but never halt the pipeline.

For convenience, import everything with:

use quicknode_cascade::solana::prelude::*;

Data Types

Every data type carries extracted fields plus the original raw JSON for custom parsing:

Type Key Fields
BlockData slot, blockhash, parent_slot, block_time, block_height, transaction_count, raw
TransactionData slot, tx_index, signature, success, fee, compute_units, is_vote, pre/post_balances, log_messages, raw
TokenTransferData slot, tx_index, signature, mint, owner, pre_amount, post_amount, decimals
AccountActivityData slot, tx_index, signature, account, pre/post_balance, balance_change, is_signer, is_fee_payer

Event Order per Slot

1. fetch_block(slot)                    — parallel, retry-forever
2. extract structured data              — BlockData, TransactionData[], etc.
3. on_block(&block_data)                — for each plugin
4. for each transaction:
     on_transaction(&tx)                — for each plugin
     for each token balance change:
       on_token_transfer(&transfer)     — for each plugin
     for each account touched:
       on_account_activity(&activity)   — for each plugin
5. advance cursor

Built-in Plugins

Plugin Description
plugins::StdoutPlugin Prints block JSON to stdout (1 line per block)
plugins::NdjsonPlugin Writes per-type NDJSON files (blocks, transactions, token_transfers, account_activity)

Reliability

Every fetch retries forever. Plugin errors are logged, never fatal. The cursor saves after every batch.

Failure Behavior
Network error / timeout / HTTP 5xx Retry forever with exponential backoff
HTTP 429 (rate limit) Wait 5s, retry forever
Solana-skipped slot (-32004, -32007) on_skipped_slot() called, cursor advances
Plugin hook error Logged, pipeline continues
Plugin on_load error Fatal (fail fast at startup)
Shutdown (SIGTERM / Ctrl-C) on_exit() called for all plugins, cursor saved
Crash (SIGKILL) Cursor may be up to 1 batch stale, replay is safe

Resume

The cursor file (cursor.json) tracks progress. On restart, the runner reads it and skips already-processed slots. Atomic writes (tmp + rename) prevent corruption.

Multi-Chain Design

The crate is designed for multi-chain support. Solana types live under the solana module:

use quicknode_cascade::solana::{Plugin, BlockData, TransactionData};

When new chains are added (Ethereum, etc.), they'll have their own modules with chain-specific plugin traits and data types:

// Future: use quicknode_cascade::ethereum::{Plugin, BlockData, TraceData};

The CascadeRunner::chain() method already maps any chain name to its Cascade endpoint.

License

Apache-2.0