Skip to main content

anvil_polkadot/api_server/
txpool_helpers.rs

1//! Helper functions for txpool RPC methods
2//!
3//! This module contains utilities for extracting transaction information from
4//! Substrate extrinsics, including support for impersonated transactions with
5//! fake signatures.
6
7use alloy_primitives::{Address, B256, U256, keccak256};
8use alloy_rpc_types::txpool::TxpoolInspectSummary;
9use codec::{DecodeLimit, Encode};
10use polkadot_sdk::{
11    pallet_revive::evm::TransactionSigned,
12    sp_core::{self, H256},
13};
14use serde::{Deserialize, Serialize};
15use std::sync::Arc;
16use substrate_runtime::{RuntimeCall, UncheckedExtrinsic};
17
18use crate::substrate_node::host::recover_maybe_impersonated_address;
19
20const MAX_EXTRINSIC_DEPTH: u32 = 256;
21
22/// Transaction info for txpool RPCs with Option fields to match Anvil's null values
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TxpoolTransactionInfo {
25    pub hash: H256,
26    pub block_hash: Option<H256>,
27    pub block_number: Option<sp_core::U256>,
28    pub transaction_index: Option<sp_core::U256>,
29    pub from: sp_core::H160,
30    pub transaction_signed: TransactionSigned,
31}
32
33/// Decode extrinsic into ETH transaction payload and signed transaction
34pub(super) fn decode_eth_transaction(
35    tx_data: &Arc<polkadot_sdk::sp_runtime::OpaqueExtrinsic>,
36) -> Option<(Vec<u8>, TransactionSigned)> {
37    let encoded = tx_data.encode();
38    let ext =
39        UncheckedExtrinsic::decode_all_with_depth_limit(MAX_EXTRINSIC_DEPTH, &mut &encoded[..])
40            .ok()?;
41
42    let polkadot_sdk::sp_runtime::generic::UncheckedExtrinsic {
43        function: RuntimeCall::Revive(polkadot_sdk::pallet_revive::Call::eth_transact { payload }),
44        ..
45    } = ext.0
46    else {
47        return None;
48    };
49
50    let signed_tx = TransactionSigned::decode(&payload).ok()?;
51
52    Some((payload, signed_tx))
53}
54
55/// Check if transaction matches ETH hash
56pub(super) fn transaction_matches_eth_hash(
57    tx_data: &Arc<polkadot_sdk::sp_runtime::OpaqueExtrinsic>,
58    target_eth_hash: B256,
59) -> bool {
60    let Some((payload, _signed_tx)) = decode_eth_transaction(tx_data) else {
61        return false;
62    };
63
64    let tx_eth_hash = keccak256(&payload);
65    B256::from_slice(tx_eth_hash.as_ref()) == target_eth_hash
66}
67
68/// Fields extracted from an Ethereum transaction
69pub(super) struct TransactionFields {
70    pub nonce: sp_core::U256,
71    pub to: Option<sp_core::H160>,
72    pub value: sp_core::U256,
73    pub gas: sp_core::U256,
74    pub gas_price: sp_core::U256,
75}
76
77/// Extract fields from ETH transaction
78fn extract_tx_fields(signed_tx: &TransactionSigned) -> TransactionFields {
79    match signed_tx {
80        TransactionSigned::TransactionLegacySigned(tx) => {
81            let t = &tx.transaction_legacy_unsigned;
82            TransactionFields {
83                nonce: t.nonce,
84                to: t.to,
85                value: t.value,
86                gas: t.gas,
87                gas_price: t.gas_price,
88            }
89        }
90        TransactionSigned::Transaction2930Signed(tx) => {
91            let t = &tx.transaction_2930_unsigned;
92            TransactionFields {
93                nonce: t.nonce,
94                to: t.to,
95                value: t.value,
96                gas: t.gas,
97                gas_price: t.gas_price,
98            }
99        }
100        TransactionSigned::Transaction1559Signed(tx) => {
101            let t = &tx.transaction_1559_unsigned;
102            TransactionFields {
103                nonce: t.nonce,
104                to: t.to,
105                value: t.value,
106                gas: t.gas,
107                gas_price: t.max_fee_per_gas,
108            }
109        }
110        TransactionSigned::Transaction4844Signed(tx) => {
111            let t = &tx.transaction_4844_unsigned;
112            TransactionFields {
113                nonce: t.nonce,
114                to: Some(t.to),
115                value: t.value,
116                gas: t.gas,
117                gas_price: t.max_fee_per_gas,
118            }
119        }
120        TransactionSigned::Transaction7702Signed(tx) => {
121            let t = &tx.transaction_7702_unsigned;
122            TransactionFields {
123                nonce: t.nonce,
124                to: Some(t.to),
125                value: t.value,
126                gas: t.gas,
127                gas_price: t.max_fee_per_gas,
128            }
129        }
130    }
131}
132
133/// Extract transaction summary from extrinsic
134pub(super) fn extract_tx_summary(
135    tx_data: &Arc<polkadot_sdk::sp_runtime::OpaqueExtrinsic>,
136) -> Option<(Address, u64, TxpoolInspectSummary)> {
137    let (_payload, signed_tx) = decode_eth_transaction(tx_data)?;
138
139    let from = recover_maybe_impersonated_address(&signed_tx).ok()?;
140    let sender = Address::from_slice(from.as_bytes());
141
142    let fields = extract_tx_fields(&signed_tx);
143
144    let to_addr = fields.to.map(|addr| Address::from_slice(addr.as_bytes()));
145    let value_u256 = U256::from_limbs(fields.value.0);
146    let gas_u64 = fields.gas.as_u64();
147    let gas_price_u128 = fields.gas_price.as_u128();
148    let nonce_u64 = fields.nonce.as_u64();
149
150    Some((
151        sender,
152        nonce_u64,
153        TxpoolInspectSummary {
154            to: to_addr,
155            value: value_u256,
156            gas: gas_u64,
157            gas_price: gas_price_u128,
158        },
159    ))
160}
161
162/// Extract full transaction info from extrinsic
163pub(super) fn extract_tx_info(
164    tx_data: &Arc<polkadot_sdk::sp_runtime::OpaqueExtrinsic>,
165) -> Option<(Address, u64, TxpoolTransactionInfo)> {
166    let (payload, signed_tx) = decode_eth_transaction(tx_data)?;
167
168    let eth_hash = keccak256(&payload);
169    let eth_hash_h256 = H256::from_slice(eth_hash.as_ref());
170
171    let from = recover_maybe_impersonated_address(&signed_tx).ok()?;
172    let sender = Address::from_slice(from.as_bytes());
173
174    let fields = extract_tx_fields(&signed_tx);
175    let nonce_u64 = fields.nonce.as_u64();
176
177    let tx_info = TxpoolTransactionInfo {
178        hash: eth_hash_h256,
179        block_hash: None,
180        block_number: None,
181        transaction_index: None,
182        from,
183        transaction_signed: signed_tx,
184    };
185
186    Some((sender, nonce_u64, tx_info))
187}
188
189/// Extract sender address from extrinsic as Alloy Address type.
190/// Helper for `anvil_remove_pool_transactions` to compare sender addresses.
191pub(super) fn extract_sender(
192    tx_data: &Arc<polkadot_sdk::sp_runtime::OpaqueExtrinsic>,
193) -> Option<Address> {
194    let (_payload, signed_tx) = decode_eth_transaction(tx_data)?;
195
196    let from = recover_maybe_impersonated_address(&signed_tx).ok()?;
197    let sender = Address::from_slice(from.as_bytes());
198
199    Some(sender)
200}