quicknode-cascade 0.2.0

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

quicknode-cascade

Stream Solana data at insane speed. One crate. Parallel. Consistent. Crash-safe.

Powered by QuickNode Cascade — edge-cached block archive, 300+ PoPs, sub-50ms latency worldwide.

30-Second Start

git clone https://github.com/quiknode-labs/quicknode-cascade
cd quicknode-cascade
cargo run --release --example solana_backfill

That fetches real Solana blocks and prints every non-vote transaction. No config, no setup.

Add to Your Project

cargo add quicknode-cascade

Two Approaches

You choose how to consume the data. Use one or both.

1. Custom Extraction — Parse Raw JSON Yourself

Implement on_slot and get the full raw getBlock JSON-RPC response. Parse it however you need — inner instructions, program-specific data, rewards, address lookups, anything.

use quicknode_cascade::{CascadeRunner, solana};

struct MyParser;

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

    fn on_slot<'a>(&'a self, slot: u64, raw: &'a serde_json::Value) -> solana::PluginFuture<'a> {
        Box::pin(async move {
            // Full raw JSON — extract whatever you need
            let txs = raw.get("transactions").and_then(|v| v.as_array());
            let rewards = raw.get("rewards").and_then(|v| v.as_array());
            println!("slot {}{} txs, {} rewards",
                slot,
                txs.map_or(0, |t| t.len()),
                rewards.map_or(0, |r| r.len()),
            );
            Ok(())
        })
    }
}

fn main() {
    CascadeRunner::solana_mainnet()
        .backfill(300_000_000, 300_000_010)
        .concurrency(10)
        .with_plugin(Box::new(MyParser))
        .run()
        .expect("done");
}

2. Built-in Extraction — Let the Framework Do It

Implement on_block, on_transaction, etc. The framework extracts structured data and calls your hooks.

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!("  {} 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");
}

3. Combine Both

on_slot fires first with raw JSON. Then the built-in extraction fires on_block, on_transaction, etc. Use solana::extract_block() as a utility if you want structured data alongside your custom parsing.

fn on_slot<'a>(&'a self, slot: u64, raw: &'a serde_json::Value) -> solana::PluginFuture<'a> {
    Box::pin(async move {
        // Framework's extraction as a starting point
        let extracted = solana::extract_block(slot, raw.clone());
        println!("{} txs", extracted.transactions.len());

        // PLUS your own custom parsing from the same raw JSON
        if let Some(rewards) = raw.get("rewards").and_then(|v| v.as_array()) {
            println!("{} rewards", rewards.len());
        }
        Ok(())
    })
}

How It Works

┌─────────────────────────────────────────────────────────┐
│  CascadeRunner                                          │
│                                                         │
│  ┌──────────┐    ┌───────────────────────────────────┐  │
│  │ Fetch    │───▶│  on_slot(slot, &raw_json)         │  │
│  │ parallel │    │  ↓ your custom extraction          │  │
│  │ + retry  │    ├───────────────────────────────────┤  │
│  │ forever  │    │  Built-in extraction (optional)   │  │
│  │          │    │  ↓ on_block()                     │  │
│  └──────────┘    │  ↓ on_transaction()               │  │
│       │          │  ↓ on_token_transfer()            │  │
│       │          │  ↓ on_account_activity()          │  │
│       │          └───────────────────────────────────┘  │
│       │                         │                       │
│       └─────────────────────────▼───────────────────┐   │
│                          ┌──────────────────┐       │   │
│                          │ Cursor (atomic)  │       │   │
│                          │ crash-safe resume │       │   │
│                          └──────────────────┘       │   │
└─────────────────────────────────────────────────────────┘

Slots always arrive in order. Cursor saves after every batch. Kill the process, restart, picks up where it left off.

Examples

Example What it does Run it
solana_backfill Built-in extraction: blocks + transactions cargo run --release --example solana_backfill
custom_extraction Custom extraction: inner instructions + program stats cargo run --release --example custom_extraction
crash_recovery_test Two-stage backfill proving cursor resume cargo run --release --example crash_recovery_test

Running Modes

// Backfill a range of slots
CascadeRunner::solana_mainnet()
    .backfill(300_000_000, 300_001_000)
    .concurrency(50)
    .run()

// Follow the chain tip in real-time
CascadeRunner::solana_mainnet()
    .live()
    .run()

// Follow from a specific slot
CascadeRunner::solana_mainnet()
    .live_from(300_000_000)
    .run()

Configuration

CascadeRunner::solana_mainnet()
    .auth_token("jwt")                   // JWT for Cascade API
    .concurrency(50)                     // parallel workers (default: 10)
    .encoding("json")                    // "json" (structured) or raw
    .cursor_file("cursor.json")          // resume support
    .tip_buffer(100)                     // slots behind tip (live mode)
    .source_url("http://custom:8899")    // override endpoint
    .with_plugin(Box::new(my_plugin))    // register plugins
    .run()

Plugin Hooks

impl solana::Plugin for MyPlugin {
    fn name(&self) -> &'static str;

    // Lifecycle
    fn on_load(&self) -> PluginFuture<'_>  { /* open connections */ }
    fn on_exit(&self) -> PluginFuture<'_>  { /* flush, close */ }

    // Raw — fires for EVERY slot, ALL encodings
    fn on_slot(&self, slot: u64, raw: &Value) -> PluginFuture<'_> { ... }

    // Structured — fires after extraction (json encoding only)
    fn on_block(&self, block: &BlockData) -> PluginFuture<'_> { ... }
    fn on_transaction(&self, tx: &TransactionData) -> PluginFuture<'_> { ... }
    fn on_token_transfer(&self, t: &TokenTransferData) -> PluginFuture<'_> { ... }
    fn on_account_activity(&self, a: &AccountActivityData) -> PluginFuture<'_> { ... }
    fn on_skipped_slot(&self, slot: u64) -> PluginFuture<'_> { ... }
}

All hooks default to no-op. Override only what you need.

Extraction Utility

solana::extract_block() is public — call it from on_slot if you want the framework's structured data as a starting point:

let extracted = solana::extract_block(slot, raw_json.clone());
// extracted.block, extracted.transactions, extracted.token_transfers, extracted.account_activity

Reliability

What happens What the framework does
Network error / timeout / 5xx Retry forever, exponential backoff
HTTP 429 Wait 5s, retry forever
Skipped slot on_skipped_slot(), cursor advances
Plugin error Log it, keep going
on_load error Fail fast, clean up loaded plugins
Ctrl-C / SIGTERM on_exit() all plugins, save cursor
SIGKILL (crash) Cursor up to 1 batch stale, replay is safe

Delivery guarantee: at-least-once. Design your plugins for idempotent writes.

Multi-Chain

Solana types live under quicknode_cascade::solana. When new chains ship, they get their own modules:

use quicknode_cascade::solana::{Plugin, BlockData};         // now
// use quicknode_cascade::ethereum::{Plugin, BlockData};    // future

CascadeRunner::chain("solana-mainnet") maps to https://solana-mainnet-cascade.quiknode.io. Same pattern for any chain.

License

Apache-2.0