Skip to content

Commit 2a76014

Browse files
kariyclaude
andauthored
feat(rpc): add txpool inspection API (#433)
Adds a `txpool_*` JSON-RPC namespace for inspecting the node's local transaction pool, modeled after Ethereum's `txpool_*` methods and= adapted for Starknet transactions. This is primarily intended for debugging and diagnostics. Methods: `txpool_status`, `txpool_content`, `txpool_contentFrom`, `txpool_inspect`. All responses distinguish between `pending` (ready to execute) and `queued` (waiting on a nonce gap) transactions. Katana currently has no queued pool so the `queued` fields are always empty — they exist for forward-compatibility. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 63f5688 commit 2a76014

File tree

12 files changed

+426
-0
lines changed

12 files changed

+426
-0
lines changed

crates/node/config/src/rpc.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub enum RpcModuleKind {
3333
Starknet,
3434
Dev,
3535
Katana,
36+
TxPool,
3637
#[cfg(feature = "cartridge")]
3738
Cartridge,
3839
#[cfg(feature = "tee")]
@@ -106,6 +107,7 @@ impl RpcModulesList {
106107
RpcModuleKind::Starknet,
107108
RpcModuleKind::Katana,
108109
RpcModuleKind::Dev,
110+
RpcModuleKind::TxPool,
109111
#[cfg(feature = "cartridge")]
110112
RpcModuleKind::Cartridge,
111113
#[cfg(feature = "tee")]

crates/node/full/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ impl Node {
203203
rpc_modules.merge(KatanaApiServer::into_rpc(starknet_api.clone()))?;
204204
}
205205

206+
if config.rpc.apis.contains(&RpcModuleKind::TxPool) {
207+
use katana_rpc_api::txpool::TxPoolApiServer;
208+
let api = katana_rpc_server::txpool::TxPoolApi::new(pool.clone());
209+
rpc_modules.merge(TxPoolApiServer::into_rpc(api))?;
210+
}
211+
206212
#[allow(unused_mut)]
207213
let mut rpc_server =
208214
RpcServer::new().metrics(true).health_check(true).cors(cors).module(rpc_modules)?;

crates/node/sequencer/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ where
326326
rpc_modules.merge(DevApiServer::into_rpc(api))?;
327327
}
328328

329+
if config.rpc.apis.contains(&RpcModuleKind::TxPool) {
330+
let api = katana_rpc_server::txpool::TxPoolApi::new(pool.clone());
331+
rpc_modules.merge(katana_rpc_api::txpool::TxPoolApiServer::into_rpc(api))?;
332+
}
333+
329334
// --- build tee api (if configured)
330335
#[cfg(feature = "tee")]
331336
if config.rpc.apis.contains(&RpcModuleKind::Tee) {

crates/pool/pool-api/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ pub trait TransactionPool: Send + Sync {
7474
/// where nonce is the next expected nonce (highest pending nonce + 1).
7575
/// Returns `None` if no pending transactions exist for this account.
7676
fn get_nonce(&self, address: ContractAddress) -> Option<Nonce>;
77+
78+
/// Returns a point-in-time snapshot of all transactions currently in the pool.
79+
fn take_transactions_snapshot(&self) -> Vec<Arc<Self::Transaction>>;
7780
}
7881

7982
// the transaction type is recommended to implement a cheap clone (eg ref-counting) so that it

crates/pool/pool/src/pool.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ where
240240
.max()
241241
.map(|max_nonce| max_nonce + 1)
242242
}
243+
244+
fn take_transactions_snapshot(&self) -> Vec<Arc<T>> {
245+
self.inner.transactions.read().iter().map(|tx| Arc::clone(&tx.tx)).collect()
246+
}
243247
}
244248

245249
impl<T, V, O> Clone for Pool<T, V, O>

crates/rpc/rpc-api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod error;
55
pub mod katana;
66
pub mod starknet;
77
pub mod starknet_ext;
8+
pub mod txpool;
89

910
#[cfg(feature = "cartridge")]
1011
pub mod cartridge;

crates/rpc/rpc-api/src/txpool.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use jsonrpsee::core::RpcResult;
2+
use jsonrpsee::proc_macros::rpc;
3+
use katana_primitives::ContractAddress;
4+
use katana_rpc_types::txpool::{TxPoolContent, TxPoolInspect, TxPoolStatus};
5+
6+
/// Inspection API for the node's local transaction pool.
7+
///
8+
/// This exposes the node's own pending-transaction pool — not a network-wide mempool.
9+
/// Unlike Ethereum, Starknet does not yet have a shared peer-to-peer mempool; each sequencer
10+
/// maintains its own pool of transactions waiting to be included in a block. The
11+
/// transactions visible here are only those that have been submitted to *this* node.
12+
///
13+
/// This API is primarily intended for debugging and diagnostics.
14+
///
15+
/// Modeled after Ethereum's `txpool_*` namespace, adapted for Starknet transactions.
16+
///
17+
/// All responses distinguish between `pending` (ready to execute) and `queued` (waiting on
18+
/// a nonce gap) transactions. Currently Katana has no queued pool — dependent transactions
19+
/// are rejected at submission — so the `queued` fields are always empty/zero. They exist
20+
/// for forward-compatibility.
21+
#[cfg_attr(not(feature = "client"), rpc(server, namespace = "txpool"))]
22+
#[cfg_attr(feature = "client", rpc(client, server, namespace = "txpool"))]
23+
pub trait TxPoolApi {
24+
/// Returns the number of pending and queued transactions in the pool.
25+
///
26+
/// This is a cheap call that avoids snapshotting individual transactions.
27+
#[method(name = "status")]
28+
async fn txpool_status(&self) -> RpcResult<TxPoolStatus>;
29+
30+
/// Returns the full content of the pool, grouped by sender address and nonce.
31+
///
32+
/// Each transaction is represented as a lightweight [`TxPoolTransaction`] containing
33+
/// the hash, nonce, sender, max_fee, and tip. The full transaction can be retrieved
34+
/// via `starknet_getTransactionByHash`.
35+
#[method(name = "content")]
36+
async fn txpool_content(&self) -> RpcResult<TxPoolContent>;
37+
38+
/// Same as `txpool_content` but filtered to a single sender address.
39+
///
40+
/// Returns only the transactions from the given address. The `queued` map is always empty.
41+
#[method(name = "contentFrom")]
42+
async fn txpool_content_from(&self, address: ContractAddress) -> RpcResult<TxPoolContent>;
43+
44+
/// Returns a textual summary of all pooled transactions, grouped by sender and nonce.
45+
///
46+
/// Each transaction is represented as a human-readable string
47+
/// (`hash=0x… nonce=0x… max_fee=… tip=…`) rather than a structured object —
48+
/// useful for quick inspection or logging.
49+
#[method(name = "inspect")]
50+
async fn txpool_inspect(&self) -> RpcResult<TxPoolInspect>;
51+
}

crates/rpc/rpc-server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub mod health;
2929
pub mod metrics;
3030
pub mod permit;
3131
pub mod starknet;
32+
pub mod txpool;
3233

3334
mod logger;
3435
mod utils;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use std::collections::BTreeMap;
2+
3+
use jsonrpsee::core::{async_trait, RpcResult};
4+
use katana_pool::{PoolTransaction, TransactionPool};
5+
use katana_primitives::ContractAddress;
6+
use katana_rpc_api::txpool::TxPoolApiServer;
7+
use katana_rpc_types::txpool::{TxPoolContent, TxPoolInspect, TxPoolStatus, TxPoolTransaction};
8+
9+
/// Handler for the `txpool_*` RPC namespace.
10+
///
11+
/// Operates on the node's local transaction pool (not a network-wide mempool).
12+
/// Generic over any [`TransactionPool`] implementation, so it works with both
13+
/// the sequencer pool ([`TxPool`]) and the full-node pool ([`FullNodePool`]).
14+
#[allow(missing_debug_implementations)]
15+
pub struct TxPoolApi<P> {
16+
pool: P,
17+
}
18+
19+
impl<P> TxPoolApi<P> {
20+
pub fn new(pool: P) -> Self {
21+
Self { pool }
22+
}
23+
}
24+
25+
impl<P: TransactionPool> TxPoolApi<P> {
26+
fn build_content(&self, filter: Option<ContractAddress>) -> TxPoolContent {
27+
let txs = self.pool.take_transactions_snapshot();
28+
let mut pending: BTreeMap<ContractAddress, BTreeMap<_, _>> = BTreeMap::new();
29+
30+
for tx in txs {
31+
let sender = tx.sender();
32+
33+
if let Some(addr) = filter {
34+
if sender != addr {
35+
continue;
36+
}
37+
}
38+
39+
let entry = TxPoolTransaction {
40+
hash: tx.hash(),
41+
nonce: tx.nonce(),
42+
sender,
43+
max_fee: tx.max_fee(),
44+
tip: tx.tip(),
45+
};
46+
47+
pending.entry(sender).or_default().insert(tx.nonce(), entry);
48+
}
49+
50+
TxPoolContent { pending, queued: BTreeMap::new() }
51+
}
52+
}
53+
54+
#[async_trait]
55+
impl<P: TransactionPool + 'static> TxPoolApiServer for TxPoolApi<P> {
56+
async fn txpool_status(&self) -> RpcResult<TxPoolStatus> {
57+
let pending = self.pool.size() as u64;
58+
Ok(TxPoolStatus { pending, queued: 0 })
59+
}
60+
61+
async fn txpool_content(&self) -> RpcResult<TxPoolContent> {
62+
Ok(self.build_content(None))
63+
}
64+
65+
async fn txpool_content_from(&self, address: ContractAddress) -> RpcResult<TxPoolContent> {
66+
Ok(self.build_content(Some(address)))
67+
}
68+
69+
async fn txpool_inspect(&self) -> RpcResult<TxPoolInspect> {
70+
let txs = self.pool.take_transactions_snapshot();
71+
let mut pending: BTreeMap<ContractAddress, BTreeMap<_, _>> = BTreeMap::new();
72+
73+
for tx in txs {
74+
let summary = format!(
75+
"hash={:#x} nonce={:#x} max_fee={} tip={}",
76+
tx.hash(),
77+
tx.nonce(),
78+
tx.max_fee(),
79+
tx.tip(),
80+
);
81+
82+
pending.entry(tx.sender()).or_default().insert(tx.nonce(), summary);
83+
}
84+
85+
Ok(TxPoolInspect { pending, queued: BTreeMap::new() })
86+
}
87+
}

0 commit comments

Comments
 (0)