# quicknode-cascade
Stream blockchain data at scale. Plugin-based framework powered by [QuickNode Cascade](https://cascade.quiknode.io) — an edge-cached block archive served from 300+ global PoPs with sub-50ms latency.
Start with **Solana**. More chains coming soon.
## Install
```bash
cargo add quicknode-cascade
```
## Quick Start
```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!(" 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
```rust
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
```rust
CascadeRunner::solana_mainnet()
.auth_token("your-jwt-token")
```
The token is sent as `Authorization: Bearer <token>` on every request.
### Running Modes
```rust
// 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
```rust
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)
```rust
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:
```rust
use quicknode_cascade::solana::prelude::*;
```
## Data Types
Every data type carries extracted fields plus the original `raw` JSON for custom parsing:
| `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:
```rust
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:
```rust
// 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