# 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.
## Try It Now
Clone and run — fetches real Solana blocks, prints every non-vote transaction:
```bash
git clone https://github.com/quiknode-labs/quicknode-cascade && cd quicknode-cascade
cargo run --release --example solana_backfill
```
You'll see:
```
slot=300000000 txs=2541 time=Some(1730981797)
tx=24Bx6JCtMat... fee=5000 success=true
tx=5RrkdQfeNNW... fee=5000 success=true
...
slot=300000001 txs=1907 time=Some(1730981798)
...
```
Try the other examples:
```bash
cargo run --release --example custom_extraction # inner instructions + program stats
cargo run --release --example concurrency_test # proves identical results at any concurrency
cargo run --release --example crash_recovery_test # proves cursor resume works
```
## Use in Your Project
```bash
cargo add quicknode-cascade
```
## Two Approaches to Data
### 1. Custom Extraction — parse raw JSON yourself
Use `on_raw` to get the full JSON-RPC response. Extract whatever you need — inner instructions, program-specific data, rewards, address lookups, anything.
`on_raw` is designed to be the same hook name across all future chain modules.
```rust
use quicknode_cascade::{CascadeRunner, solana, serde_json};
struct MyParser;
impl solana::Plugin for MyParser {
fn name(&self) -> &'static str { "my-parser" }
fn on_raw<'a>(&'a self, slot: u64, raw: &'a serde_json::Value) -> solana::PluginFuture<'a> {
Box::pin(async move {
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(())
})
}
}
// Optional: add tracing_subscriber for framework progress logs (see examples/ for full setup)
fn main() {
CascadeRunner::solana_mainnet()
.auth_token("your-jwt-token")
.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.
```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(())
})
}
}
// Optional: add tracing_subscriber for framework progress logs (see examples/ for full setup)
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. Both together
`on_raw` fires first with raw JSON. Then extraction fires `on_block`, `on_transaction`, etc.
Use `solana::extract_block()` inside `on_raw` if you want the framework's structured data as a starting point alongside your own parsing.
## How It Works
```
┌──────────────────────────────────────────────────────┐
│ CascadeRunner │
│ │
│ Fetch (parallel) ──▶ on_raw(slot, &raw_json) │
│ + retry ↓ your custom extraction │
│ forever ──────────────────────────── │
│ Built-in extraction (optional) │
│ ↓ on_block() │
│ ↓ on_transaction() │
│ ↓ on_token_transfer() │
│ ↓ on_account_activity() │
│ ↓ │
│ Cursor (atomic, crash-safe) │
└──────────────────────────────────────────────────────┘
```
Slots always arrive in order. Kill the process, restart, picks up where it left off.
## Examples
| [solana_backfill](examples/solana_backfill.rs) | `cargo run --release --example solana_backfill` | Block summaries + transaction details |
| [custom_extraction](examples/custom_extraction.rs) | `cargo run --release --example custom_extraction` | Inner instruction counts + top 5 programs per slot |
| [concurrency_test](examples/concurrency_test.rs) | `cargo run --release --example concurrency_test` | Same range at concurrency=1 vs 50 — proves identical results |
| [crash_recovery_test](examples/crash_recovery_test.rs) | `cargo run --release --example crash_recovery_test` | Two-stage backfill proving zero-duplicate cursor resume |
## Running Modes
```rust
// Backfill a slot range
CascadeRunner::solana_mainnet()
.auth_token("your-jwt-token")
.backfill(300_000_000, 300_001_000)
.concurrency(50)
.with_plugin(Box::new(my_plugin))
.run()
// Follow the chain tip in real-time
CascadeRunner::solana_mainnet()
.auth_token("your-jwt-token")
.live()
.with_plugin(Box::new(my_plugin))
.run()
// Follow from a specific slot
CascadeRunner::solana_mainnet()
.auth_token("your-jwt-token")
.live_from(300_000_000)
.with_plugin(Box::new(my_plugin))
.run()
```
## All Options
```rust
CascadeRunner::solana_mainnet() // or ::chain("solana-mainnet")
.auth_token("your-jwt-token") // JWT for Cascade API
.concurrency(50) // parallel workers (default: 10)
.encoding("json") // "json" (structured) or raw
.cursor_file("cursor.json") // resume support (default: cursor.json)
.tip_buffer(100) // slots behind tip for live mode
.source_url("http://custom:8899") // override endpoint
.with_plugin(Box::new(my_plugin)) // register 1+ plugins
.run() // blocks until done or Ctrl-C
```
## Plugin Hooks
```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 */ }
// Raw — fires for EVERY slot, ALL encodings
fn on_raw(&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.
Convenience import: `use quicknode_cascade::solana::prelude::*;`
## Data Types
| `BlockData` | slot, blockhash, parent_slot, block_time, block_height, transaction_count, raw |
| `TransactionData` | slot, tx_index, signature, success, fee, compute_units, is_vote, balances, logs, raw |
| `TokenTransferData` | slot, tx_index, signature, mint, owner, pre/post_amount, decimals |
| `AccountActivityData` | slot, tx_index, signature, account, pre/post_balance, balance_change, is_signer, is_fee_payer |
Types with `raw` carry the full JSON so you can parse anything the framework doesn't extract.
## Reliability
| 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 | Cursor up to 1 batch stale, replay is safe |
Delivery guarantee: **at-least-once**. Design your plugins for idempotent writes.
## Multi-Chain Ready
Solana types live under `quicknode_cascade::solana`. The crate is designed so that when new chains ship, they get their own modules with chain-specific types while sharing the same `on_raw` hook pattern.
```rust
use quicknode_cascade::solana::{Plugin, BlockData};
```
## License
Apache-2.0