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](https://cascade.quiknode.io) — edge-cached block archive, 300+ PoPs, sub-50ms latency worldwide.

## 30-Second Start

```bash
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

```bash
cargo add quicknode-cascade
```

```rust
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`]examples/solana_backfill.rs | Fetch blocks, print summaries | `cargo run --release --example solana_backfill` |
| [`crash_recovery_test`]examples/crash_recovery_test.rs | Two-stage backfill proving cursor resume | `cargo run --release --example crash_recovery_test` |

## Running Modes

```rust
// 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

```rust
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.

```rust
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:

```rust
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