quicknode-cascade 0.1.2

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, just data.

Add to Your Project

cargo add quicknode-cascade
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");
}

You implement the hooks. The framework handles parallel fetching, retries, ordering, and crash-safe cursors. Bring your own database.

How It Works

┌─────────────────────────────────────────────────────────┐
│  CascadeRunner                                          │
│                                                         │
│  ┌──────────┐    ┌───────────┐    ┌──────────────────┐  │
│  │ Fetch    │───▶│ Extract   │───▶│ Your Plugin      │  │
│  │ parallel │    │ zero-copy │    │   on_block()     │  │
│  │ + retry  │    │           │    │   on_transaction()│  │
│  │ forever  │    │ BlockData │    │   on_token_...() │  │
│  │          │    │ TxData    │    │   on_account_..()│  │
│  └──────────┘    │ Tokens    │    └──────────────────┘  │
│       │          │ Accounts  │             │            │
│       │          └───────────┘             │            │
│       │                                    ▼            │
│       │                          ┌──────────────────┐   │
│       └─────────────────────────▶│ Cursor (atomic)  │   │
│                                  │ crash-safe resume │   │
│                                  └──────────────────┘   │
└─────────────────────────────────────────────────────────┘

Slots always arrive in order, even though they're fetched in parallel. Cursor saves after every batch. Kill the process, restart, it picks up exactly where it left off.

Examples

Example What it does Run it
solana_backfill Fetch blocks, print summaries cargo run --release --example solana_backfill
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

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

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 */ }

    // Data events (fired in order per slot)
    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<'_> { ... }
    fn on_raw_block(&self, slot: u64, raw: &Value) -> PluginFuture<'_> { ... }
}

For convenience: use quicknode_cascade::solana::prelude::*;

Data Types

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

Every type with a raw field carries the full JSON so you can parse anything the framework doesn't extract.

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. On crash recovery, some slots may replay. 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