1use crate::tt_2d_pool::{AA2dTransactionId, AASequenceId};
2use alloy_consensus::{BlobTransactionValidationError, Transaction, transaction::TxHashRef};
3use alloy_eips::{
4 eip2718::{Encodable2718, Typed2718},
5 eip2930::AccessList,
6 eip4844::env_settings::KzgSettings,
7 eip7594::BlobTransactionSidecarVariant,
8 eip7702::SignedAuthorization,
9};
10use alloy_evm::FromRecoveredTx;
11use alloy_primitives::{Address, B256, Bytes, TxHash, TxKind, U256, bytes};
12use reth_evm::execute::WithTxEnv;
13use reth_primitives_traits::{InMemorySize, Recovered};
14use reth_transaction_pool::{
15 EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction,
16 error::PoolTransactionError,
17};
18use std::{
19 convert::Infallible,
20 fmt::Debug,
21 sync::{Arc, OnceLock},
22};
23use tempo_precompiles::nonce::NonceManager;
24use tempo_primitives::{TempoTxEnvelope, transaction::calc_gas_balance_spending};
25use tempo_revm::TempoTxEnv;
26use thiserror::Error;
27
28#[derive(Debug, Clone)]
32pub struct TempoPooledTransaction {
33 inner: EthPooledTransaction<TempoTxEnvelope>,
34 is_payment: bool,
36 nonce_key_slot: OnceLock<Option<U256>>,
38 tx_env: OnceLock<TempoTxEnv>,
40}
41
42impl TempoPooledTransaction {
43 pub fn new(transaction: Recovered<TempoTxEnvelope>) -> Self {
45 let is_payment = transaction.is_payment();
46 Self {
47 inner: EthPooledTransaction {
48 cost: calc_gas_balance_spending(
49 transaction.gas_limit(),
50 transaction.max_fee_per_gas(),
51 )
52 .saturating_add(transaction.value()),
53 encoded_length: transaction.encode_2718_len(),
54 blob_sidecar: EthBlobTransactionSidecar::None,
55 transaction,
56 },
57 is_payment,
58 nonce_key_slot: OnceLock::new(),
59 tx_env: OnceLock::new(),
60 }
61 }
62
63 pub fn fee_token_cost(&self) -> U256 {
65 self.inner.cost - self.inner.value()
66 }
67
68 pub fn inner(&self) -> &Recovered<TempoTxEnvelope> {
70 &self.inner.transaction
71 }
72
73 pub fn is_aa(&self) -> bool {
75 self.inner().is_aa()
76 }
77
78 pub fn nonce_key(&self) -> Option<U256> {
80 self.inner.transaction.nonce_key()
81 }
82
83 pub fn nonce_key_slot(&self) -> Option<U256> {
85 *self.nonce_key_slot.get_or_init(|| {
86 let nonce_key = self.nonce_key()?;
87 let sender = self.sender();
88 let slot = NonceManager::new().nonces.at(sender).at(nonce_key).slot();
89 Some(slot)
90 })
91 }
92
93 pub fn is_payment(&self) -> bool {
97 self.is_payment
98 }
99
100 pub(crate) fn is_aa_2d(&self) -> bool {
103 self.inner
104 .transaction
105 .as_aa()
106 .map(|tx| !tx.tx().nonce_key.is_zero())
107 .unwrap_or(false)
108 }
109
110 pub(crate) fn aa_transaction_id(&self) -> Option<AA2dTransactionId> {
112 let nonce_key = self.nonce_key()?;
113 let sender = AASequenceId {
114 address: self.sender(),
115 nonce_key,
116 };
117 Some(AA2dTransactionId {
118 seq_id: sender,
119 nonce: self.nonce(),
120 })
121 }
122
123 fn tx_env_slow(&self) -> TempoTxEnv {
125 TempoTxEnv::from_recovered_tx(self.inner().inner(), self.sender())
126 }
127
128 pub fn prepare_tx_env(&self) {
133 self.tx_env.get_or_init(|| self.tx_env_slow());
134 }
135
136 pub fn into_with_tx_env(mut self) -> WithTxEnv<TempoTxEnv, Recovered<TempoTxEnvelope>> {
141 let tx_env = self.tx_env.take().unwrap_or_else(|| self.tx_env_slow());
142 WithTxEnv {
143 tx_env,
144 tx: Arc::new(self.inner.transaction),
145 }
146 }
147}
148
149#[derive(Debug, Error)]
150pub enum TempoPoolTransactionError {
151 #[error(
152 "Transaction exceeds non payment gas limit, please see https://docs.tempo.xyz/errors/tx/ExceedsNonPaymentLimit for more"
153 )]
154 ExceedsNonPaymentLimit,
155
156 #[error(
157 "Invalid fee token: {0}, please see https://docs.tempo.xyz/errors/tx/InvalidFeeToken for more"
158 )]
159 InvalidFeeToken(Address),
160
161 #[error("No fee token preference configured")]
162 MissingFeeToken,
163
164 #[error(
165 "'valid_before' {valid_before} is too close to current time (min allowed: {min_allowed})"
166 )]
167 InvalidValidBefore { valid_before: u64, min_allowed: u64 },
168
169 #[error("'valid_after' {valid_after} is too far in the future (max allowed: {max_allowed})")]
170 InvalidValidAfter { valid_after: u64, max_allowed: u64 },
171
172 #[error(
173 "Keychain signature validation failed: {0}, please see https://docs.tempo.xyz/errors/tx/Keychain for more"
174 )]
175 Keychain(&'static str),
176
177 #[error(
178 "Native transfers are not supported, if you were trying to transfer a stablecoin, please call TIP20::Transfer"
179 )]
180 NonZeroValue,
181
182 #[error("Tempo Transaction with subblock nonce key prefix aren't supported in the pool")]
184 SubblockNonceKey,
185
186 #[error("Fee payer {fee_payer} is blacklisted by fee token: {fee_token}")]
188 BlackListedFeePayer {
189 fee_token: Address,
190 fee_payer: Address,
191 },
192
193 #[error(
196 "Insufficient liquidity for fee token: {0}, please see https://docs.tempo.xyz/protocol/fees for more"
197 )]
198 InsufficientLiquidity(Address),
199}
200
201impl PoolTransactionError for TempoPoolTransactionError {
202 fn is_bad_transaction(&self) -> bool {
203 match self {
204 Self::ExceedsNonPaymentLimit
205 | Self::InvalidFeeToken(_)
206 | Self::MissingFeeToken
207 | Self::BlackListedFeePayer { .. }
208 | Self::InvalidValidBefore { .. }
209 | Self::InvalidValidAfter { .. }
210 | Self::Keychain(_)
211 | Self::InsufficientLiquidity(_) => false,
212 Self::NonZeroValue | Self::SubblockNonceKey => true,
213 }
214 }
215
216 fn as_any(&self) -> &dyn std::any::Any {
217 self
218 }
219}
220
221impl InMemorySize for TempoPooledTransaction {
222 fn size(&self) -> usize {
223 self.inner.size()
224 }
225}
226
227impl Typed2718 for TempoPooledTransaction {
228 fn ty(&self) -> u8 {
229 self.inner.transaction.ty()
230 }
231}
232
233impl Encodable2718 for TempoPooledTransaction {
234 fn type_flag(&self) -> Option<u8> {
235 self.inner.transaction.type_flag()
236 }
237
238 fn encode_2718_len(&self) -> usize {
239 self.inner.transaction.encode_2718_len()
240 }
241
242 fn encode_2718(&self, out: &mut dyn bytes::BufMut) {
243 self.inner.transaction.encode_2718(out)
244 }
245}
246
247impl PoolTransaction for TempoPooledTransaction {
248 type TryFromConsensusError = Infallible;
249 type Consensus = TempoTxEnvelope;
250 type Pooled = TempoTxEnvelope;
251
252 fn clone_into_consensus(&self) -> Recovered<Self::Consensus> {
253 self.inner.transaction.clone()
254 }
255
256 fn into_consensus(self) -> Recovered<Self::Consensus> {
257 self.inner.transaction
258 }
259
260 fn from_pooled(tx: Recovered<Self::Pooled>) -> Self {
261 Self::new(tx)
262 }
263
264 fn hash(&self) -> &TxHash {
265 self.inner.transaction.tx_hash()
266 }
267
268 fn sender(&self) -> Address {
269 self.inner.transaction.signer()
270 }
271
272 fn sender_ref(&self) -> &Address {
273 self.inner.transaction.signer_ref()
274 }
275
276 fn cost(&self) -> &U256 {
277 &U256::ZERO
278 }
279
280 fn encoded_length(&self) -> usize {
281 self.inner.encoded_length
282 }
283
284 fn requires_nonce_check(&self) -> bool {
285 self.inner
286 .transaction()
287 .as_aa()
288 .map(|tx| {
289 tx.tx().nonce_key.is_zero()
291 })
292 .unwrap_or(true)
293 }
294}
295
296impl alloy_consensus::Transaction for TempoPooledTransaction {
297 fn chain_id(&self) -> Option<u64> {
298 self.inner.chain_id()
299 }
300
301 fn nonce(&self) -> u64 {
302 self.inner.nonce()
303 }
304
305 fn gas_limit(&self) -> u64 {
306 self.inner.gas_limit()
307 }
308
309 fn gas_price(&self) -> Option<u128> {
310 self.inner.gas_price()
311 }
312
313 fn max_fee_per_gas(&self) -> u128 {
314 self.inner.max_fee_per_gas()
315 }
316
317 fn max_priority_fee_per_gas(&self) -> Option<u128> {
318 self.inner.max_priority_fee_per_gas()
319 }
320
321 fn max_fee_per_blob_gas(&self) -> Option<u128> {
322 self.inner.max_fee_per_blob_gas()
323 }
324
325 fn priority_fee_or_price(&self) -> u128 {
326 self.inner.priority_fee_or_price()
327 }
328
329 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
330 self.inner.effective_gas_price(base_fee)
331 }
332
333 fn is_dynamic_fee(&self) -> bool {
334 self.inner.is_dynamic_fee()
335 }
336
337 fn kind(&self) -> TxKind {
338 self.inner.kind()
339 }
340
341 fn is_create(&self) -> bool {
342 self.inner.is_create()
343 }
344
345 fn value(&self) -> U256 {
346 self.inner.value()
347 }
348
349 fn input(&self) -> &Bytes {
350 self.inner.input()
351 }
352
353 fn access_list(&self) -> Option<&AccessList> {
354 self.inner.access_list()
355 }
356
357 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
358 self.inner.blob_versioned_hashes()
359 }
360
361 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
362 self.inner.authorization_list()
363 }
364}
365
366impl EthPoolTransaction for TempoPooledTransaction {
367 fn take_blob(&mut self) -> EthBlobTransactionSidecar {
368 EthBlobTransactionSidecar::None
369 }
370
371 fn try_into_pooled_eip4844(
372 self,
373 _sidecar: Arc<BlobTransactionSidecarVariant>,
374 ) -> Option<Recovered<Self::Pooled>> {
375 None
376 }
377
378 fn try_from_eip4844(
379 _tx: Recovered<Self::Consensus>,
380 _sidecar: BlobTransactionSidecarVariant,
381 ) -> Option<Self> {
382 None
383 }
384
385 fn validate_blob(
386 &self,
387 _sidecar: &BlobTransactionSidecarVariant,
388 _settings: &KzgSettings,
389 ) -> Result<(), BlobTransactionValidationError> {
390 Err(BlobTransactionValidationError::NotBlobTransaction(
391 self.ty(),
392 ))
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use alloy_primitives::address;
400 use tempo_primitives::TxFeeToken;
401
402 #[test]
403 fn test_payment_classification_caching() {
404 let payment_addr = address!("20c0000000000000000000000000000000000001");
406 let tx = TxFeeToken {
407 to: TxKind::Call(payment_addr),
408 gas_limit: 21000,
409 ..Default::default()
410 };
411
412 let envelope = TempoTxEnvelope::FeeToken(alloy_consensus::Signed::new_unchecked(
413 tx,
414 alloy_primitives::Signature::test_signature(),
415 alloy_primitives::B256::ZERO,
416 ));
417
418 let recovered = Recovered::new_unchecked(
419 envelope,
420 address!("0000000000000000000000000000000000000001"),
421 );
422
423 let pooled_tx = TempoPooledTransaction::new(recovered);
425 assert!(pooled_tx.is_payment());
426 }
427}