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}