reth_seismic_txpool/
transaction.rs

1use alloy_consensus::{transaction::Recovered, BlobTransactionValidationError, Typed2718};
2use alloy_eips::{
3    eip2930::AccessList, eip7594::BlobTransactionSidecarVariant, eip7702::SignedAuthorization,
4    Encodable2718,
5};
6use alloy_primitives::{Address, Bytes, TxHash, TxKind, B256, U256};
7use c_kzg::KzgSettings;
8use core::fmt::Debug;
9use reth_primitives_traits::{InMemorySize, SignedTransaction};
10use reth_seismic_primitives::SeismicTransactionSigned;
11use reth_transaction_pool::{
12    EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction,
13};
14use seismic_alloy_consensus::SeismicTxEnvelope;
15use std::sync::Arc;
16
17/// Pool Transaction for Seismic.
18#[derive(Debug, Clone, derive_more::Deref)]
19pub struct SeismicPooledTransaction<Cons = SeismicTransactionSigned, Pooled = SeismicTxEnvelope> {
20    #[deref]
21    inner: EthPooledTransaction<Cons>,
22    /// The pooled transaction type.
23    _pd: core::marker::PhantomData<Pooled>,
24}
25
26impl<Cons: SignedTransaction, Pooled> SeismicPooledTransaction<Cons, Pooled> {
27    /// Create a new [`SeismicPooledTransaction`].
28    pub fn new(transaction: Recovered<Cons>, encoded_length: usize) -> Self {
29        Self {
30            inner: EthPooledTransaction::new(transaction, encoded_length),
31            _pd: core::marker::PhantomData,
32        }
33    }
34}
35
36impl<Cons, Pooled> PoolTransaction for SeismicPooledTransaction<Cons, Pooled>
37where
38    Cons: SignedTransaction + From<Pooled>,
39    Pooled: SignedTransaction + TryFrom<Cons, Error: core::error::Error>,
40{
41    type TryFromConsensusError = <Pooled as TryFrom<Cons>>::Error;
42    type Consensus = Cons;
43    type Pooled = Pooled;
44
45    fn hash(&self) -> &TxHash {
46        self.inner.transaction.tx_hash()
47    }
48
49    fn sender(&self) -> Address {
50        self.inner.transaction.signer()
51    }
52
53    fn sender_ref(&self) -> &Address {
54        self.inner.transaction.signer_ref()
55    }
56
57    fn cost(&self) -> &U256 {
58        &self.inner.cost
59    }
60
61    fn encoded_length(&self) -> usize {
62        self.inner.encoded_length
63    }
64
65    fn clone_into_consensus(&self) -> Recovered<Self::Consensus> {
66        self.inner.transaction().clone()
67    }
68
69    fn into_consensus(self) -> Recovered<Self::Consensus> {
70        self.inner.transaction
71    }
72
73    fn from_pooled(tx: Recovered<Self::Pooled>) -> Self {
74        let encoded_len = tx.encode_2718_len();
75        Self::new(tx.convert(), encoded_len)
76    }
77}
78
79impl<Cons: Typed2718, Pooled> Typed2718 for SeismicPooledTransaction<Cons, Pooled> {
80    fn ty(&self) -> u8 {
81        self.inner.ty()
82    }
83}
84
85impl<Cons: InMemorySize, Pooled> InMemorySize for SeismicPooledTransaction<Cons, Pooled> {
86    fn size(&self) -> usize {
87        self.inner.size()
88    }
89}
90
91impl<Cons, Pooled> alloy_consensus::Transaction for SeismicPooledTransaction<Cons, Pooled>
92where
93    Cons: alloy_consensus::Transaction + SignedTransaction, // Ensure Cons has the methods
94    Pooled: Debug + Send + Sync + 'static,                  /* From Optimism example, for
95                                                             * completeness */
96{
97    fn chain_id(&self) -> Option<u64> {
98        self.inner.chain_id()
99    }
100    fn nonce(&self) -> u64 {
101        self.inner.nonce()
102    }
103    fn gas_limit(&self) -> u64 {
104        self.inner.gas_limit()
105    }
106    fn gas_price(&self) -> Option<u128> {
107        self.inner.gas_price()
108    }
109    fn max_fee_per_gas(&self) -> u128 {
110        self.inner.max_fee_per_gas()
111    }
112    fn max_priority_fee_per_gas(&self) -> Option<u128> {
113        self.inner.max_priority_fee_per_gas()
114    }
115    fn max_fee_per_blob_gas(&self) -> Option<u128> {
116        self.inner.max_fee_per_blob_gas()
117    }
118    fn value(&self) -> U256 {
119        self.inner.value()
120    }
121    fn input(&self) -> &Bytes {
122        self.inner.input()
123    }
124    fn access_list(&self) -> Option<&AccessList> {
125        self.inner.access_list()
126    }
127    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
128        self.inner.blob_versioned_hashes()
129    }
130    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
131        self.inner.authorization_list()
132    }
133    fn priority_fee_or_price(&self) -> u128 {
134        self.inner.priority_fee_or_price()
135    }
136    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
137        self.inner.effective_gas_price(base_fee)
138    }
139    fn is_dynamic_fee(&self) -> bool {
140        self.inner.is_dynamic_fee()
141    }
142    fn kind(&self) -> TxKind {
143        self.inner.kind()
144    }
145    fn is_create(&self) -> bool {
146        self.inner.is_create()
147    }
148}
149
150impl<Cons, Pooled> EthPoolTransaction for SeismicPooledTransaction<Cons, Pooled>
151where
152    Cons: SignedTransaction + From<Pooled>,
153    Pooled: SignedTransaction + TryFrom<Cons>,
154    <Pooled as TryFrom<Cons>>::Error: core::error::Error,
155{
156    fn take_blob(&mut self) -> EthBlobTransactionSidecar {
157        EthBlobTransactionSidecar::None
158    }
159
160    fn try_into_pooled_eip4844(
161        self,
162        _sidecar: Arc<BlobTransactionSidecarVariant>,
163    ) -> Option<Recovered<Self::Pooled>> {
164        None
165    }
166
167    fn try_from_eip4844(
168        _tx: Recovered<Self::Consensus>,
169        _sidecar: BlobTransactionSidecarVariant,
170    ) -> Option<Self> {
171        None
172    }
173
174    fn validate_blob(
175        &self,
176        _sidecar: &BlobTransactionSidecarVariant,
177        _settings: &KzgSettings,
178    ) -> Result<(), BlobTransactionValidationError> {
179        Err(BlobTransactionValidationError::NotBlobTransaction(self.ty()))
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use crate::SeismicPooledTransaction;
186    use alloy_consensus::transaction::Recovered;
187    use alloy_eips::eip2718::Encodable2718;
188    use reth_primitives_traits::transaction::error::InvalidTransactionError;
189    use reth_provider::test_utils::MockEthProvider;
190    use reth_seismic_chainspec::SEISMIC_MAINNET;
191    use reth_seismic_primitives::test_utils::get_signed_seismic_tx;
192    use reth_transaction_pool::{
193        blobstore::InMemoryBlobStore, error::InvalidPoolTransactionError,
194        validate::EthTransactionValidatorBuilder, TransactionOrigin, TransactionValidationOutcome,
195    };
196
197    #[tokio::test]
198    async fn validate_seismic_transaction() {
199        // setup validator
200        let client = MockEthProvider::default().with_chain_spec(SEISMIC_MAINNET.clone());
201        let validator = EthTransactionValidatorBuilder::new(client)
202            .no_shanghai()
203            .no_cancun()
204            .build(InMemoryBlobStore::default());
205
206        // check that a SeismicTypedTransaction::Seismic is valid
207        let origin = TransactionOrigin::External;
208        let signer = Default::default();
209        let signed_seismic_tx = get_signed_seismic_tx();
210        let signed_recovered = Recovered::new_unchecked(signed_seismic_tx, signer);
211        let len = signed_recovered.encode_2718_len();
212        let pooled_tx: SeismicPooledTransaction =
213            SeismicPooledTransaction::new(signed_recovered, len);
214
215        let outcome = validator.validate_one(origin, pooled_tx);
216
217        match outcome {
218            TransactionValidationOutcome::Invalid(
219                _,
220                InvalidPoolTransactionError::Consensus(InvalidTransactionError::InsufficientFunds(
221                    _,
222                )),
223            ) => {
224                // expected since the client (MockEthProvider) state does not have funds for any
225                // accounts account balance is one of the last things checked in
226                // validate_one, so getting that far good news
227            }
228            _ => panic!("Did not get expected outcome, got: {:?}", outcome),
229        }
230    }
231}