Skip to main content

quicknode_cascade/solana/
plugin.rs

1//! Solana plugin trait and PluginFuture type alias.
2//!
3//! Implement the [`Plugin`] trait to observe Solana block and transaction events.
4//! Register plugins with [`CascadeRunner::with_plugin()`](crate::CascadeRunner::with_plugin).
5//!
6//! Two approaches:
7//! - **Custom extraction**: implement [`on_raw`](Plugin::on_raw) and parse the raw JSON yourself.
8//!   Use [`extract_block()`](super::extract_block) as a utility if you want structured data too.
9//! - **Built-in extraction**: implement [`on_block`](Plugin::on_block),
10//!   [`on_transaction`](Plugin::on_transaction), etc. and let the framework extract for you.
11//!
12//! Both can be combined — `on_raw` fires first, then the structured hooks.
13
14use std::future::Future;
15use std::pin::Pin;
16
17use super::types::{
18    AccountActivityData, BlockData, TokenTransferData, TransactionData,
19};
20
21/// The return type for all plugin hooks. A boxed async future that returns
22/// `Result<(), Box<dyn Error>>`. Use `Box::pin(async move { ... })` to create one.
23pub type PluginFuture<'a> =
24    Pin<Box<dyn Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send + 'a>>;
25
26/// Observe Solana block and transaction events.
27///
28/// Two approaches to consuming data:
29///
30/// **1. Custom extraction** — implement [`on_raw`](Self::on_raw) and parse the raw
31/// JSON-RPC response yourself. Use [`extract_block()`](super::extract_block)
32/// as an optional utility.
33///
34/// **2. Built-in extraction** — implement [`on_block`](Self::on_block),
35/// [`on_transaction`](Self::on_transaction), etc. The framework extracts structured data
36/// and calls your hooks.
37///
38/// Both can be combined: `on_raw` fires first with the raw JSON, then the framework
39/// extracts and calls the structured hooks.
40///
41/// # Example: Custom extraction
42///
43/// ```rust,no_run
44/// use quicknode_cascade::solana::{Plugin, PluginFuture};
45///
46/// struct RawProcessor;
47///
48/// impl Plugin for RawProcessor {
49///     fn name(&self) -> &'static str { "raw" }
50///
51///     fn on_raw<'a>(&'a self, slot: u64, raw: &'a serde_json::Value) -> PluginFuture<'a> {
52///         Box::pin(async move {
53///             let txs = raw.get("transactions").and_then(|v| v.as_array());
54///             println!("slot {} has {} txs", slot, txs.map_or(0, |t| t.len()));
55///             Ok(())
56///         })
57///     }
58/// }
59/// ```
60///
61/// # Example: Built-in extraction
62///
63/// ```rust,no_run
64/// use quicknode_cascade::solana::{Plugin, PluginFuture, BlockData, TransactionData};
65///
66/// struct MyIndexer;
67///
68/// impl Plugin for MyIndexer {
69///     fn name(&self) -> &'static str { "my-indexer" }
70///
71///     fn on_block<'a>(&'a self, block: &'a BlockData) -> PluginFuture<'a> {
72///         Box::pin(async move {
73///             println!("slot {} — {} txs", block.slot, block.transaction_count);
74///             Ok(())
75///         })
76///     }
77///
78///     fn on_transaction<'a>(&'a self, tx: &'a TransactionData) -> PluginFuture<'a> {
79///         Box::pin(async move {
80///             if !tx.is_vote {
81///                 println!("  tx {}", tx.signature);
82///             }
83///             Ok(())
84///         })
85///     }
86/// }
87/// ```
88pub trait Plugin: Send + Sync + 'static {
89    /// Human-friendly name for this plugin (used in logs).
90    fn name(&self) -> &'static str;
91
92    /// Called once when the runner starts, before any data is fetched.
93    /// Use this to create tables, open connections, or validate config.
94    fn on_load<'a>(&'a self) -> PluginFuture<'a> {
95        Box::pin(async { Ok(()) })
96    }
97
98    /// Called for every non-skipped slot with the full raw JSON-RPC response.
99    ///
100    /// This fires BEFORE the built-in extraction hooks (`on_block`, `on_transaction`, etc.).
101    /// Use this for custom extraction — parse the JSON however you need.
102    ///
103    /// The name `on_raw` is consistent across all chain modules so multi-chain
104    /// code follows the same pattern everywhere.
105    ///
106    /// If you also want the framework's structured data from within your custom
107    /// extraction, call [`extract_block()`](super::extract_block) on a clone of the JSON.
108    fn on_raw<'a>(&'a self, _slot: u64, _raw: &'a serde_json::Value) -> PluginFuture<'a> {
109        Box::pin(async { Ok(()) })
110    }
111
112    /// Called for each block. Fired once per slot, before transactions.
113    fn on_block<'a>(&'a self, _block: &'a BlockData) -> PluginFuture<'a> {
114        Box::pin(async { Ok(()) })
115    }
116
117    /// Called for each transaction within a block.
118    /// Fired in order of `tx_index` within the slot.
119    fn on_transaction<'a>(&'a self, _tx: &'a TransactionData) -> PluginFuture<'a> {
120        Box::pin(async { Ok(()) })
121    }
122
123    /// Called for each token balance change within a transaction.
124    /// Only fired when the balance actually changed (pre != post).
125    fn on_token_transfer<'a>(&'a self, _transfer: &'a TokenTransferData) -> PluginFuture<'a> {
126        Box::pin(async { Ok(()) })
127    }
128
129    /// Called for each account touched by a transaction (SOL balance changes).
130    fn on_account_activity<'a>(&'a self, _activity: &'a AccountActivityData) -> PluginFuture<'a> {
131        Box::pin(async { Ok(()) })
132    }
133
134    /// Called when a slot was skipped by the Solana leader (no block produced).
135    fn on_skipped_slot<'a>(&'a self, _slot: u64) -> PluginFuture<'a> {
136        Box::pin(async { Ok(()) })
137    }
138
139    /// Called once when the runner is shutting down. Flush buffers, close connections.
140    fn on_exit<'a>(&'a self) -> PluginFuture<'a> {
141        Box::pin(async { Ok(()) })
142    }
143}