Skip to content

Commit 1d6e7fa

Browse files
feat(anvil-polkadot): add transaction pool RPCs 2/2 (#370)
* implement tx_pool inspect * add txpool_inspect RPC test * implement txpool_content * add txpool_content RPC test * implement remove_pool_transactions * add remove_pool_transactions RPC test * Implements sender recovery logic for impersonated transactions in txpool * add impersonation support test for txpool RPCs
1 parent 52c3249 commit 1d6e7fa

File tree

5 files changed

+596
-46
lines changed

5 files changed

+596
-46
lines changed

crates/anvil-polkadot/src/api_server/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub mod error;
1515
pub mod revive_conversions;
1616
mod server;
1717
mod signer;
18+
mod txpool_helpers;
19+
20+
pub use txpool_helpers::TxpoolTransactionInfo;
1821

1922
pub type ApiHandle = mpsc::Sender<ApiRequest>;
2023

crates/anvil-polkadot/src/api_server/server.rs

Lines changed: 87 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ use crate::{
77
ReviveFilter, SubstrateU256, convert_to_generic_transaction,
88
},
99
signer::DevSigner,
10+
txpool_helpers::{
11+
TxpoolTransactionInfo, extract_sender, extract_tx_info, extract_tx_summary,
12+
transaction_matches_eth_hash,
13+
},
1014
},
1115
logging::LoggingManager,
1216
macros::node_info,
1317
substrate_node::{
18+
host::recover_maybe_impersonated_address,
1419
impersonation::ImpersonationManager,
1520
in_mem_rpc::InMemoryRpcClient,
1621
mining_engine::MiningEngine,
@@ -30,13 +35,13 @@ use alloy_primitives::{Address, B256, U64, U256};
3035
use alloy_rpc_types::{
3136
Filter, TransactionRequest,
3237
anvil::{Metadata as AnvilMetadata, MineOptions, NodeEnvironment, NodeInfo},
33-
txpool::TxpoolStatus,
38+
txpool::{TxpoolContent, TxpoolInspect, TxpoolStatus},
3439
};
3540
use alloy_serde::WithOtherFields;
3641
use alloy_trie::{EMPTY_ROOT_HASH, KECCAK_EMPTY, TrieAccount};
3742
use anvil_core::eth::{EthRequest, Params as MineParams};
3843
use anvil_rpc::response::ResponseResult;
39-
use codec::{Decode, DecodeLimit, Encode};
44+
use codec::{Decode, Encode};
4045
use futures::{StreamExt, channel::mpsc};
4146
use indexmap::IndexMap;
4247
use pallet_revive_eth_rpc::{
@@ -65,7 +70,7 @@ use polkadot_sdk::{
6570
use revm::primitives::hardfork::SpecId;
6671
use sqlx::sqlite::SqlitePoolOptions;
6772
use std::{collections::HashSet, sync::Arc, time::Duration};
68-
use substrate_runtime::{Balance, RuntimeCall, UncheckedExtrinsic};
73+
use substrate_runtime::Balance;
6974
use subxt::{
7075
Metadata as SubxtMetadata, OnlineClient, backend::rpc::RpcClient,
7176
client::RuntimeVersion as SubxtRuntimeVersion, config::substrate::H256,
@@ -75,7 +80,6 @@ use subxt_signer::eth::Keypair;
7580
use tokio::try_join;
7681

7782
pub const CLIENT_VERSION: &str = concat!("anvil-polkadot/v", env!("CARGO_PKG_VERSION"));
78-
const MAX_EXTRINSIC_DEPTH: u32 = 256;
7983

8084
pub struct ApiServer {
8185
eth_rpc_client: EthRpcClient,
@@ -339,18 +343,18 @@ impl ApiServer {
339343
self.get_account_info(addr, block).await.to_rpc_result()
340344
}
341345
//------- Transaction Pool ---------
342-
EthRequest::TxPoolStatus(_) => {
343-
node_info!("txpool_status");
344-
self.txpool_status().await.to_rpc_result()
345-
}
346+
EthRequest::TxPoolStatus(_) => self.txpool_status().await.to_rpc_result(),
347+
EthRequest::TxPoolInspect(_) => self.txpool_inspect().await.to_rpc_result(),
348+
EthRequest::TxPoolContent(_) => self.txpool_content().await.to_rpc_result(),
346349
EthRequest::DropAllTransactions() => {
347-
node_info!("anvil_dropAllTransactions");
348350
self.anvil_drop_all_transactions().await.to_rpc_result()
349351
}
350352
EthRequest::DropTransaction(eth_hash) => {
351-
node_info!("anvil_dropTransaction");
352353
self.anvil_drop_transaction(eth_hash).await.to_rpc_result()
353354
}
355+
EthRequest::RemovePoolTransactions(address) => {
356+
self.anvil_remove_pool_transactions(address).await.to_rpc_result()
357+
}
354358
// --- Metadata ---
355359
EthRequest::NodeInfo(_) => self.anvil_node_info().await.to_rpc_result(),
356360
EthRequest::AnvilMetadata(_) => self.anvil_metadata().await.to_rpc_result(),
@@ -1247,12 +1251,58 @@ impl ApiServer {
12471251

12481252
/// Returns transaction pool status
12491253
async fn txpool_status(&self) -> Result<TxpoolStatus> {
1254+
node_info!("txpool_status");
12501255
let pool_status = self.tx_pool.status();
12511256
Ok(TxpoolStatus { pending: pool_status.ready as u64, queued: pool_status.future as u64 })
12521257
}
12531258

1259+
/// Returns a summary of all transactions in the pool
1260+
async fn txpool_inspect(&self) -> Result<TxpoolInspect> {
1261+
node_info!("txpool_inspect");
1262+
let mut inspect = TxpoolInspect::default();
1263+
1264+
for tx in self.tx_pool.ready() {
1265+
if let Some((sender, nonce, summary)) = extract_tx_summary(tx.data()) {
1266+
let entry = inspect.pending.entry(sender).or_default();
1267+
entry.insert(nonce.to_string(), summary);
1268+
}
1269+
}
1270+
1271+
for tx in self.tx_pool.futures() {
1272+
if let Some((sender, nonce, summary)) = extract_tx_summary(tx.data()) {
1273+
let entry = inspect.queued.entry(sender).or_default();
1274+
entry.insert(nonce.to_string(), summary);
1275+
}
1276+
}
1277+
1278+
Ok(inspect)
1279+
}
1280+
1281+
/// Returns full transaction details for all transactions in the pool
1282+
async fn txpool_content(&self) -> Result<TxpoolContent<TxpoolTransactionInfo>> {
1283+
node_info!("txpool_content");
1284+
let mut content = TxpoolContent::default();
1285+
1286+
for tx in self.tx_pool.ready() {
1287+
if let Some((sender, nonce, tx_info)) = extract_tx_info(tx.data()) {
1288+
let entry = content.pending.entry(sender).or_default();
1289+
entry.insert(nonce.to_string(), tx_info);
1290+
}
1291+
}
1292+
1293+
for tx in self.tx_pool.futures() {
1294+
if let Some((sender, nonce, tx_info)) = extract_tx_info(tx.data()) {
1295+
let entry = content.queued.entry(sender).or_default();
1296+
entry.insert(nonce.to_string(), tx_info);
1297+
}
1298+
}
1299+
1300+
Ok(content)
1301+
}
1302+
12541303
/// Drop all transactions from pool
12551304
async fn anvil_drop_all_transactions(&self) -> Result<()> {
1305+
node_info!("anvil_dropAllTransactions");
12561306
let ready_txs = self.tx_pool.ready();
12571307
let future_txs = self.tx_pool.futures();
12581308

@@ -1273,7 +1323,7 @@ impl ApiServer {
12731323

12741324
/// Drop a specific transaction from the pool by its ETH hash
12751325
async fn anvil_drop_transaction(&self, eth_hash: B256) -> Result<Option<B256>> {
1276-
// Search in ready transactions
1326+
node_info!("anvil_dropTransaction");
12771327
for tx in self.tx_pool.ready() {
12781328
if transaction_matches_eth_hash(tx.data(), eth_hash) {
12791329
let mut invalid_txs = IndexMap::new();
@@ -1283,7 +1333,6 @@ impl ApiServer {
12831333
}
12841334
}
12851335

1286-
// Search in future transactions
12871336
for tx in self.tx_pool.futures() {
12881337
if transaction_matches_eth_hash(tx.data(), eth_hash) {
12891338
let mut invalid_txs = IndexMap::new();
@@ -1296,30 +1345,34 @@ impl ApiServer {
12961345
// Transaction not found
12971346
Ok(None)
12981347
}
1299-
}
13001348

1301-
/// Helper function to check if transaction matches ETH hash
1302-
fn transaction_matches_eth_hash(
1303-
tx_data: &Arc<polkadot_sdk::sp_runtime::OpaqueExtrinsic>,
1304-
target_eth_hash: B256,
1305-
) -> bool {
1306-
let encoded = tx_data.encode();
1307-
let Ok(ext) =
1308-
UncheckedExtrinsic::decode_all_with_depth_limit(MAX_EXTRINSIC_DEPTH, &mut &encoded[..])
1309-
else {
1310-
return false;
1311-
};
1349+
/// Remove all transactions from a specific sender address
1350+
async fn anvil_remove_pool_transactions(&self, address: Address) -> Result<()> {
1351+
node_info!("anvil_removePoolTransactions");
1352+
let mut invalid_txs = IndexMap::new();
13121353

1313-
let polkadot_sdk::sp_runtime::generic::UncheckedExtrinsic {
1314-
function: RuntimeCall::Revive(polkadot_sdk::pallet_revive::Call::eth_transact { payload }),
1315-
..
1316-
} = ext.0
1317-
else {
1318-
return false;
1319-
};
1354+
for tx in self.tx_pool.ready() {
1355+
if let Some(sender) = extract_sender(tx.data()) {
1356+
if sender == address {
1357+
invalid_txs.insert(*tx.hash(), None);
1358+
}
1359+
}
1360+
}
1361+
1362+
for tx in self.tx_pool.futures() {
1363+
if let Some(sender) = extract_sender(tx.data()) {
1364+
if sender == address {
1365+
invalid_txs.insert(*tx.hash(), None);
1366+
}
1367+
}
1368+
}
13201369

1321-
let tx_eth_hash = keccak_256(&payload);
1322-
B256::from_slice(&tx_eth_hash) == target_eth_hash
1370+
if !invalid_txs.is_empty() {
1371+
self.tx_pool.report_invalid(None, invalid_txs).await;
1372+
}
1373+
1374+
Ok(())
1375+
}
13231376
}
13241377

13251378
fn new_contract_info(address: &Address, code_hash: H256, nonce: Nonce) -> ContractInfo {
@@ -1437,16 +1490,7 @@ async fn create_revive_rpc_client(
14371490
let receipt_extractor = ReceiptExtractor::new_with_custom_address_recovery(
14381491
api.clone(),
14391492
None,
1440-
Arc::new(|signed_tx: &TransactionSigned| {
1441-
let sig = signed_tx.raw_signature()?;
1442-
if sig[..12] == [0; 12] && sig[32..64] == [0; 32] {
1443-
let mut res = [0; 20];
1444-
res.copy_from_slice(&sig[12..32]);
1445-
Ok(H160::from(res))
1446-
} else {
1447-
signed_tx.recover_eth_address()
1448-
}
1449-
}),
1493+
Arc::new(recover_maybe_impersonated_address),
14501494
)
14511495
.await
14521496
.map_err(|err| Error::ReviveRpc(EthRpcError::ClientError(err)))?;

0 commit comments

Comments
 (0)