1use crate::{conditional::MaybeConditionalTransaction, interop::MaybeInteropTransaction};
2use alloy_consensus::{
3 transaction::Recovered, BlobTransactionSidecar, BlobTransactionValidationError, Typed2718,
4};
5use alloy_eips::{eip2930::AccessList, eip7702::SignedAuthorization};
6use alloy_primitives::{Address, Bytes, TxHash, TxKind, B256, U256};
7use alloy_rpc_types_eth::erc4337::TransactionConditional;
8use c_kzg::KzgSettings;
9use core::fmt::Debug;
10use reth_optimism_primitives::OpTransactionSigned;
11use reth_primitives_traits::{InMemorySize, SignedTransaction};
12use reth_transaction_pool::{
13 EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction,
14};
15use std::sync::{
16 atomic::{AtomicU64, Ordering},
17 Arc, OnceLock,
18};
19
20pub(crate) const NO_INTEROP_TX: u64 = 0;
22
23#[derive(Debug, Clone, derive_more::Deref)]
29pub struct OpPooledTransaction<
30 Cons = OpTransactionSigned,
31 Pooled = op_alloy_consensus::OpPooledTransaction,
32> {
33 #[deref]
34 inner: EthPooledTransaction<Cons>,
35 estimated_tx_compressed_size: OnceLock<u64>,
37 _pd: core::marker::PhantomData<Pooled>,
39
40 conditional: Option<Box<TransactionConditional>>,
42
43 interop: Arc<AtomicU64>,
45
46 encoded_2718: OnceLock<Bytes>,
48}
49
50impl<Cons: SignedTransaction, Pooled> OpPooledTransaction<Cons, Pooled> {
51 pub fn new(transaction: Recovered<Cons>, encoded_length: usize) -> Self {
53 Self {
54 inner: EthPooledTransaction::new(transaction, encoded_length),
55 estimated_tx_compressed_size: Default::default(),
56 conditional: None,
57 interop: Arc::new(AtomicU64::new(NO_INTEROP_TX)),
58 _pd: core::marker::PhantomData,
59 encoded_2718: Default::default(),
60 }
61 }
62
63 pub fn estimated_compressed_size(&self) -> u64 {
68 *self
69 .estimated_tx_compressed_size
70 .get_or_init(|| op_alloy_flz::tx_estimated_size_fjord(self.encoded_2718()))
71 }
72
73 pub fn encoded_2718(&self) -> &Bytes {
75 self.encoded_2718.get_or_init(|| self.inner.transaction().encoded_2718().into())
76 }
77
78 pub fn with_conditional(mut self, conditional: TransactionConditional) -> Self {
80 self.conditional = Some(Box::new(conditional));
81 self
82 }
83}
84
85impl<Cons, Pooled> MaybeConditionalTransaction for OpPooledTransaction<Cons, Pooled> {
86 fn set_conditional(&mut self, conditional: TransactionConditional) {
87 self.conditional = Some(Box::new(conditional))
88 }
89
90 fn conditional(&self) -> Option<&TransactionConditional> {
91 self.conditional.as_deref()
92 }
93}
94
95impl<Cons, Pooled> MaybeInteropTransaction for OpPooledTransaction<Cons, Pooled> {
96 fn set_interop_deadline(&self, deadline: u64) {
97 self.interop.store(deadline, Ordering::Relaxed);
98 }
99
100 fn interop_deadline(&self) -> Option<u64> {
101 let interop = self.interop.load(Ordering::Relaxed);
102 if interop > NO_INTEROP_TX {
103 return Some(interop)
104 }
105 None
106 }
107}
108
109impl<Cons, Pooled> PoolTransaction for OpPooledTransaction<Cons, Pooled>
110where
111 Cons: SignedTransaction + From<Pooled>,
112 Pooled: SignedTransaction + TryFrom<Cons, Error: core::error::Error>,
113{
114 type TryFromConsensusError = <Pooled as TryFrom<Cons>>::Error;
115 type Consensus = Cons;
116 type Pooled = Pooled;
117
118 fn clone_into_consensus(&self) -> Recovered<Self::Consensus> {
119 self.inner.transaction().clone()
120 }
121
122 fn into_consensus(self) -> Recovered<Self::Consensus> {
123 self.inner.transaction
124 }
125
126 fn from_pooled(tx: Recovered<Self::Pooled>) -> Self {
127 let encoded_len = tx.encode_2718_len();
128 Self::new(tx.convert(), encoded_len)
129 }
130
131 fn hash(&self) -> &TxHash {
132 self.inner.transaction.tx_hash()
133 }
134
135 fn sender(&self) -> Address {
136 self.inner.transaction.signer()
137 }
138
139 fn sender_ref(&self) -> &Address {
140 self.inner.transaction.signer_ref()
141 }
142
143 fn cost(&self) -> &U256 {
144 &self.inner.cost
145 }
146
147 fn encoded_length(&self) -> usize {
148 self.inner.encoded_length
149 }
150}
151
152impl<Cons: Typed2718, Pooled> Typed2718 for OpPooledTransaction<Cons, Pooled> {
153 fn ty(&self) -> u8 {
154 self.inner.ty()
155 }
156}
157
158impl<Cons: InMemorySize, Pooled> InMemorySize for OpPooledTransaction<Cons, Pooled> {
159 fn size(&self) -> usize {
160 self.inner.size()
161 }
162}
163
164impl<Cons, Pooled> alloy_consensus::Transaction for OpPooledTransaction<Cons, Pooled>
165where
166 Cons: alloy_consensus::Transaction,
167 Pooled: Debug + Send + Sync + 'static,
168{
169 fn chain_id(&self) -> Option<u64> {
170 self.inner.chain_id()
171 }
172
173 fn nonce(&self) -> u64 {
174 self.inner.nonce()
175 }
176
177 fn gas_limit(&self) -> u64 {
178 self.inner.gas_limit()
179 }
180
181 fn gas_price(&self) -> Option<u128> {
182 self.inner.gas_price()
183 }
184
185 fn max_fee_per_gas(&self) -> u128 {
186 self.inner.max_fee_per_gas()
187 }
188
189 fn max_priority_fee_per_gas(&self) -> Option<u128> {
190 self.inner.max_priority_fee_per_gas()
191 }
192
193 fn max_fee_per_blob_gas(&self) -> Option<u128> {
194 self.inner.max_fee_per_blob_gas()
195 }
196
197 fn priority_fee_or_price(&self) -> u128 {
198 self.inner.priority_fee_or_price()
199 }
200
201 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
202 self.inner.effective_gas_price(base_fee)
203 }
204
205 fn is_dynamic_fee(&self) -> bool {
206 self.inner.is_dynamic_fee()
207 }
208
209 fn kind(&self) -> TxKind {
210 self.inner.kind()
211 }
212
213 fn is_create(&self) -> bool {
214 self.inner.is_create()
215 }
216
217 fn value(&self) -> U256 {
218 self.inner.value()
219 }
220
221 fn input(&self) -> &Bytes {
222 self.inner.input()
223 }
224
225 fn access_list(&self) -> Option<&AccessList> {
226 self.inner.access_list()
227 }
228
229 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
230 self.inner.blob_versioned_hashes()
231 }
232
233 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
234 self.inner.authorization_list()
235 }
236}
237
238impl<Cons, Pooled> EthPoolTransaction for OpPooledTransaction<Cons, Pooled>
239where
240 Cons: SignedTransaction + From<Pooled>,
241 Pooled: SignedTransaction + TryFrom<Cons>,
242 <Pooled as TryFrom<Cons>>::Error: core::error::Error,
243{
244 fn take_blob(&mut self) -> EthBlobTransactionSidecar {
245 EthBlobTransactionSidecar::None
246 }
247
248 fn try_into_pooled_eip4844(
249 self,
250 _sidecar: Arc<BlobTransactionSidecar>,
251 ) -> Option<Recovered<Self::Pooled>> {
252 None
253 }
254
255 fn try_from_eip4844(
256 _tx: Recovered<Self::Consensus>,
257 _sidecar: BlobTransactionSidecar,
258 ) -> Option<Self> {
259 None
260 }
261
262 fn validate_blob(
263 &self,
264 _sidecar: &BlobTransactionSidecar,
265 _settings: &KzgSettings,
266 ) -> Result<(), BlobTransactionValidationError> {
267 Err(BlobTransactionValidationError::NotBlobTransaction(self.ty()))
268 }
269}
270
271pub trait OpPooledTx:
274 MaybeConditionalTransaction + MaybeInteropTransaction + PoolTransaction
275{
276}
277impl<T> OpPooledTx for T where
278 T: MaybeConditionalTransaction + MaybeInteropTransaction + PoolTransaction
279{
280}
281
282#[cfg(test)]
283mod tests {
284 use crate::{OpPooledTransaction, OpTransactionValidator};
285 use alloy_consensus::transaction::Recovered;
286 use alloy_eips::eip2718::Encodable2718;
287 use alloy_primitives::{TxKind, U256};
288 use op_alloy_consensus::TxDeposit;
289 use reth_optimism_chainspec::OP_MAINNET;
290 use reth_optimism_primitives::OpTransactionSigned;
291 use reth_provider::test_utils::MockEthProvider;
292 use reth_transaction_pool::{
293 blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder, TransactionOrigin,
294 TransactionValidationOutcome,
295 };
296 #[tokio::test]
297 async fn validate_optimism_transaction() {
298 let client = MockEthProvider::default().with_chain_spec(OP_MAINNET.clone());
299 let validator = EthTransactionValidatorBuilder::new(client)
300 .no_shanghai()
301 .no_cancun()
302 .build(InMemoryBlobStore::default());
303 let validator = OpTransactionValidator::new(validator);
304
305 let origin = TransactionOrigin::External;
306 let signer = Default::default();
307 let deposit_tx = TxDeposit {
308 source_hash: Default::default(),
309 from: signer,
310 to: TxKind::Create,
311 mint: None,
312 value: U256::ZERO,
313 gas_limit: 0,
314 is_system_transaction: false,
315 input: Default::default(),
316 };
317 let signed_tx: OpTransactionSigned = deposit_tx.into();
318 let signed_recovered = Recovered::new_unchecked(signed_tx, signer);
319 let len = signed_recovered.encode_2718_len();
320 let pooled_tx: OpPooledTransaction = OpPooledTransaction::new(signed_recovered, len);
321 let outcome = validator.validate_one(origin, pooled_tx).await;
322
323 let err = match outcome {
324 TransactionValidationOutcome::Invalid(_, err) => err,
325 _ => panic!("Expected invalid transaction"),
326 };
327 assert_eq!(err.to_string(), "transaction type not supported");
328 }
329}