arb_primitives/
signed_tx.rs

1use alloc::vec::Vec;
2use core::{
3    hash::{Hash, Hasher},
4    ops::Deref,
5};
6
7use alloy_consensus::{
8    SignableTransaction, Transaction as ConsensusTx, TxLegacy, Typed2718,
9    transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx, TxHashRef},
10};
11use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718, IsTyped2718};
12use alloy_primitives::{Address, B256, Bytes, Signature, TxHash, TxKind, U256, keccak256};
13use alloy_rlp::{Decodable, Encodable};
14use arb_alloy_consensus::tx::{
15    ArbContractTx, ArbDepositTx, ArbInternalTx, ArbRetryTx, ArbSubmitRetryableTx, ArbTxType,
16    ArbUnsignedTx,
17};
18use reth_primitives_traits::{
19    InMemorySize,
20    crypto::secp256k1::{recover_signer, recover_signer_unchecked},
21};
22
23/// Internal ArbOS address used as sender for internal transactions.
24const ARBOS_ADDRESS: Address =
25    alloy_primitives::address!("00000000000000000000000000000000000A4B05");
26
27/// Retryable precompile address (0x6e).
28const RETRYABLE_ADDRESS: Address =
29    alloy_primitives::address!("000000000000000000000000000000000000006e");
30
31/// Wraps all supported transaction types (standard Ethereum + Arbitrum-specific).
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum ArbTypedTransaction {
34    Deposit(ArbDepositTx),
35    Unsigned(ArbUnsignedTx),
36    Contract(ArbContractTx),
37    Retry(ArbRetryTx),
38    SubmitRetryable(ArbSubmitRetryableTx),
39    Internal(ArbInternalTx),
40
41    Legacy(TxLegacy),
42    Eip2930(alloy_consensus::TxEip2930),
43    Eip1559(alloy_consensus::TxEip1559),
44    Eip4844(alloy_consensus::TxEip4844),
45    Eip7702(alloy_consensus::TxEip7702),
46}
47
48/// Discriminant for transaction type classification.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum ArbTxTypeLocal {
51    Deposit,
52    Unsigned,
53    Contract,
54    Retry,
55    SubmitRetryable,
56    Internal,
57    Legacy,
58    Eip2930,
59    Eip1559,
60    Eip4844,
61    Eip7702,
62}
63
64impl ArbTxTypeLocal {
65    /// Convert to the EIP-2718 type byte.
66    pub fn as_u8(self) -> u8 {
67        match self {
68            Self::Legacy => 0x00,
69            Self::Eip2930 => 0x01,
70            Self::Eip1559 => 0x02,
71            Self::Eip4844 => 0x03,
72            Self::Eip7702 => 0x04,
73            Self::Deposit => ArbTxType::ArbitrumDepositTx.as_u8(),
74            Self::Unsigned => ArbTxType::ArbitrumUnsignedTx.as_u8(),
75            Self::Contract => ArbTxType::ArbitrumContractTx.as_u8(),
76            Self::Retry => ArbTxType::ArbitrumRetryTx.as_u8(),
77            Self::SubmitRetryable => ArbTxType::ArbitrumSubmitRetryableTx.as_u8(),
78            Self::Internal => ArbTxType::ArbitrumInternalTx.as_u8(),
79        }
80    }
81}
82
83impl Typed2718 for ArbTxTypeLocal {
84    fn is_legacy(&self) -> bool {
85        matches!(self, Self::Legacy)
86    }
87
88    fn ty(&self) -> u8 {
89        self.as_u8()
90    }
91}
92
93impl alloy_consensus::TransactionEnvelope for ArbTransactionSigned {
94    type TxType = ArbTxTypeLocal;
95
96    fn tx_type(&self) -> Self::TxType {
97        match &self.transaction {
98            ArbTypedTransaction::Legacy(_) => ArbTxTypeLocal::Legacy,
99            ArbTypedTransaction::Eip2930(_) => ArbTxTypeLocal::Eip2930,
100            ArbTypedTransaction::Eip1559(_) => ArbTxTypeLocal::Eip1559,
101            ArbTypedTransaction::Eip4844(_) => ArbTxTypeLocal::Eip4844,
102            ArbTypedTransaction::Eip7702(_) => ArbTxTypeLocal::Eip7702,
103            ArbTypedTransaction::Deposit(_) => ArbTxTypeLocal::Deposit,
104            ArbTypedTransaction::Unsigned(_) => ArbTxTypeLocal::Unsigned,
105            ArbTypedTransaction::Contract(_) => ArbTxTypeLocal::Contract,
106            ArbTypedTransaction::Retry(_) => ArbTxTypeLocal::Retry,
107            ArbTypedTransaction::SubmitRetryable(_) => ArbTxTypeLocal::SubmitRetryable,
108            ArbTypedTransaction::Internal(_) => ArbTxTypeLocal::Internal,
109        }
110    }
111}
112
113/// Signed Arbitrum transaction with lazy hash caching.
114#[derive(Clone, Debug, Eq)]
115pub struct ArbTransactionSigned {
116    hash: reth_primitives_traits::sync::OnceLock<TxHash>,
117    signature: Signature,
118    transaction: ArbTypedTransaction,
119    input_cache: reth_primitives_traits::sync::OnceLock<Bytes>,
120    sender_cache: reth_primitives_traits::sync::OnceLock<Address>,
121    /// Cached poster calldata units, packed as `(level as u64) << 56 | (units &
122    /// 0x00FF_FFFF_FFFF_FFFF)`. Brotli compression level is stable across a block, so this
123    /// cache avoids repeated brotli compression of the same tx bytes within that block.
124    poster_units_cache: reth_primitives_traits::sync::OnceLock<u64>,
125}
126
127impl Deref for ArbTransactionSigned {
128    type Target = ArbTypedTransaction;
129    fn deref(&self) -> &Self::Target {
130        &self.transaction
131    }
132}
133
134impl ArbTransactionSigned {
135    pub fn new(transaction: ArbTypedTransaction, signature: Signature, hash: B256) -> Self {
136        Self {
137            hash: hash.into(),
138            signature,
139            transaction,
140            input_cache: Default::default(),
141            sender_cache: Default::default(),
142            poster_units_cache: Default::default(),
143        }
144    }
145
146    pub fn new_unhashed(transaction: ArbTypedTransaction, signature: Signature) -> Self {
147        Self {
148            hash: Default::default(),
149            signature,
150            transaction,
151            input_cache: Default::default(),
152            sender_cache: Default::default(),
153            poster_units_cache: Default::default(),
154        }
155    }
156
157    /// Construct from a signed Ethereum envelope (standard tx types only).
158    pub fn from_envelope(
159        envelope: alloy_consensus::EthereumTxEnvelope<alloy_consensus::TxEip4844>,
160    ) -> Self {
161        use alloy_consensus::EthereumTxEnvelope;
162        match envelope {
163            EthereumTxEnvelope::Legacy(signed) => {
164                let (tx, sig, hash) = signed.into_parts();
165                Self::new(ArbTypedTransaction::Legacy(tx), sig, hash)
166            }
167            EthereumTxEnvelope::Eip2930(signed) => {
168                let (tx, sig, hash) = signed.into_parts();
169                Self::new(ArbTypedTransaction::Eip2930(tx), sig, hash)
170            }
171            EthereumTxEnvelope::Eip1559(signed) => {
172                let (tx, sig, hash) = signed.into_parts();
173                Self::new(ArbTypedTransaction::Eip1559(tx), sig, hash)
174            }
175            EthereumTxEnvelope::Eip4844(signed) => {
176                let (tx, sig, hash) = signed.into_parts();
177                Self::new(ArbTypedTransaction::Eip4844(tx), sig, hash)
178            }
179            EthereumTxEnvelope::Eip7702(signed) => {
180                let (tx, sig, hash) = signed.into_parts();
181                Self::new(ArbTypedTransaction::Eip7702(tx), sig, hash)
182            }
183        }
184    }
185
186    pub const fn signature(&self) -> &Signature {
187        &self.signature
188    }
189
190    /// Returns the inner typed transaction.
191    pub fn inner(&self) -> &ArbTypedTransaction {
192        &self.transaction
193    }
194
195    /// Consume self and return (transaction, signature, hash).
196    pub fn split(self) -> (ArbTypedTransaction, Signature, B256) {
197        let hash = *self.hash.get_or_init(|| self.compute_hash());
198        (self.transaction, self.signature, hash)
199    }
200
201    pub const fn tx_type(&self) -> ArbTxTypeLocal {
202        match &self.transaction {
203            ArbTypedTransaction::Deposit(_) => ArbTxTypeLocal::Deposit,
204            ArbTypedTransaction::Unsigned(_) => ArbTxTypeLocal::Unsigned,
205            ArbTypedTransaction::Contract(_) => ArbTxTypeLocal::Contract,
206            ArbTypedTransaction::Retry(_) => ArbTxTypeLocal::Retry,
207            ArbTypedTransaction::SubmitRetryable(_) => ArbTxTypeLocal::SubmitRetryable,
208            ArbTypedTransaction::Internal(_) => ArbTxTypeLocal::Internal,
209            ArbTypedTransaction::Legacy(_) => ArbTxTypeLocal::Legacy,
210            ArbTypedTransaction::Eip2930(_) => ArbTxTypeLocal::Eip2930,
211            ArbTypedTransaction::Eip1559(_) => ArbTxTypeLocal::Eip1559,
212            ArbTypedTransaction::Eip4844(_) => ArbTxTypeLocal::Eip4844,
213            ArbTypedTransaction::Eip7702(_) => ArbTxTypeLocal::Eip7702,
214        }
215    }
216
217    fn compute_hash(&self) -> B256 {
218        keccak256(self.encoded_2718())
219    }
220
221    fn zero_sig() -> Signature {
222        Signature::new(U256::ZERO, U256::ZERO, false)
223    }
224
225    /// Returns the cached poster calldata units for the given brotli compression level,
226    /// computing them via `compute` on first call. The cache is valid for a single level
227    /// — if called with a different level, returns the freshly computed value without
228    /// updating the cache (callers should avoid mixing levels per-tx).
229    pub fn poster_units_cached<F: FnOnce() -> u64>(&self, level: u64, compute: F) -> u64 {
230        if let Some(&entry) = self.poster_units_cache.get() {
231            let (cached_level, cached_units) = unpack_poster_units(entry);
232            if cached_level == level {
233                return cached_units;
234            }
235            return compute();
236        }
237        let units = compute();
238        let _ = self.poster_units_cache.set(pack_poster_units(level, units));
239        units
240    }
241}
242
243#[inline]
244fn pack_poster_units(level: u64, units: u64) -> u64 {
245    ((level & 0xFF) << 56) | (units & 0x00FF_FFFF_FFFF_FFFF)
246}
247
248#[inline]
249fn unpack_poster_units(packed: u64) -> (u64, u64) {
250    let level = (packed >> 56) & 0xFF;
251    let units = packed & 0x00FF_FFFF_FFFF_FFFF;
252    (level, units)
253}
254
255// ---------------------------------------------------------------------------
256// Hash / PartialEq — identity by tx hash
257// ---------------------------------------------------------------------------
258
259impl Hash for ArbTransactionSigned {
260    fn hash<H: Hasher>(&self, state: &mut H) {
261        self.tx_hash().hash(state)
262    }
263}
264
265impl PartialEq for ArbTransactionSigned {
266    fn eq(&self, other: &Self) -> bool {
267        self.tx_hash() == other.tx_hash()
268    }
269}
270
271impl InMemorySize for ArbTransactionSigned {
272    fn size(&self) -> usize {
273        core::mem::size_of::<TxHash>() + core::mem::size_of::<Signature>()
274    }
275}
276
277// ---------------------------------------------------------------------------
278// TxHashRef — lazy hash initialization
279// ---------------------------------------------------------------------------
280
281impl TxHashRef for ArbTransactionSigned {
282    fn tx_hash(&self) -> &TxHash {
283        self.hash.get_or_init(|| self.compute_hash())
284    }
285}
286
287// ---------------------------------------------------------------------------
288// SignerRecoverable
289// ---------------------------------------------------------------------------
290
291impl ArbTransactionSigned {
292    fn recover_signer_inner(
293        &self,
294        strict: bool,
295    ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
296        match &self.transaction {
297            ArbTypedTransaction::Deposit(tx) => Ok(tx.from),
298            ArbTypedTransaction::Unsigned(tx) => Ok(tx.from),
299            ArbTypedTransaction::Contract(tx) => Ok(tx.from),
300            ArbTypedTransaction::Retry(tx) => Ok(tx.from),
301            ArbTypedTransaction::SubmitRetryable(tx) => Ok(tx.from),
302            ArbTypedTransaction::Internal(_) => Ok(ARBOS_ADDRESS),
303            ArbTypedTransaction::Legacy(tx) => {
304                let mut buf = Vec::new();
305                tx.encode_for_signing(&mut buf);
306                if strict {
307                    recover_signer(&self.signature, keccak256(&buf))
308                } else {
309                    recover_signer_unchecked(&self.signature, keccak256(&buf))
310                }
311            }
312            ArbTypedTransaction::Eip2930(tx) => {
313                let mut buf = Vec::new();
314                tx.encode_for_signing(&mut buf);
315                if strict {
316                    recover_signer(&self.signature, keccak256(&buf))
317                } else {
318                    recover_signer_unchecked(&self.signature, keccak256(&buf))
319                }
320            }
321            ArbTypedTransaction::Eip1559(tx) => {
322                let mut buf = Vec::new();
323                tx.encode_for_signing(&mut buf);
324                if strict {
325                    recover_signer(&self.signature, keccak256(&buf))
326                } else {
327                    recover_signer_unchecked(&self.signature, keccak256(&buf))
328                }
329            }
330            ArbTypedTransaction::Eip4844(tx) => {
331                let mut buf = Vec::new();
332                tx.encode_for_signing(&mut buf);
333                if strict {
334                    recover_signer(&self.signature, keccak256(&buf))
335                } else {
336                    recover_signer_unchecked(&self.signature, keccak256(&buf))
337                }
338            }
339            ArbTypedTransaction::Eip7702(tx) => {
340                let mut buf = Vec::new();
341                tx.encode_for_signing(&mut buf);
342                if strict {
343                    recover_signer(&self.signature, keccak256(&buf))
344                } else {
345                    recover_signer_unchecked(&self.signature, keccak256(&buf))
346                }
347            }
348        }
349    }
350}
351
352impl alloy_consensus::transaction::SignerRecoverable for ArbTransactionSigned {
353    fn recover_signer(
354        &self,
355    ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
356        if let Some(addr) = self.sender_cache.get() {
357            return Ok(*addr);
358        }
359        let addr = self.recover_signer_inner(true)?;
360        let _ = self.sender_cache.set(addr);
361        Ok(addr)
362    }
363
364    fn recover_signer_unchecked(
365        &self,
366    ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
367        if let Some(addr) = self.sender_cache.get() {
368            return Ok(*addr);
369        }
370        let addr = self.recover_signer_inner(false)?;
371        let _ = self.sender_cache.set(addr);
372        Ok(addr)
373    }
374}
375
376// ---------------------------------------------------------------------------
377// Typed2718
378// ---------------------------------------------------------------------------
379
380impl Typed2718 for ArbTransactionSigned {
381    fn is_legacy(&self) -> bool {
382        matches!(self.transaction, ArbTypedTransaction::Legacy(_))
383    }
384
385    fn ty(&self) -> u8 {
386        match &self.transaction {
387            ArbTypedTransaction::Legacy(_) => 0u8,
388            ArbTypedTransaction::Deposit(_) => ArbTxType::ArbitrumDepositTx.as_u8(),
389            ArbTypedTransaction::Unsigned(_) => ArbTxType::ArbitrumUnsignedTx.as_u8(),
390            ArbTypedTransaction::Contract(_) => ArbTxType::ArbitrumContractTx.as_u8(),
391            ArbTypedTransaction::Retry(_) => ArbTxType::ArbitrumRetryTx.as_u8(),
392            ArbTypedTransaction::SubmitRetryable(_) => ArbTxType::ArbitrumSubmitRetryableTx.as_u8(),
393            ArbTypedTransaction::Internal(_) => ArbTxType::ArbitrumInternalTx.as_u8(),
394            ArbTypedTransaction::Eip2930(_) => 0x01,
395            ArbTypedTransaction::Eip1559(_) => 0x02,
396            ArbTypedTransaction::Eip4844(_) => 0x03,
397            ArbTypedTransaction::Eip7702(_) => 0x04,
398        }
399    }
400}
401
402// ---------------------------------------------------------------------------
403// IsTyped2718
404// ---------------------------------------------------------------------------
405
406impl IsTyped2718 for ArbTransactionSigned {
407    fn is_type(type_id: u8) -> bool {
408        // Standard Ethereum types.
409        matches!(type_id, 0x01..=0x04) || ArbTxType::from_u8(type_id).is_ok()
410    }
411}
412
413// ---------------------------------------------------------------------------
414// Encodable2718
415// ---------------------------------------------------------------------------
416
417impl Encodable2718 for ArbTransactionSigned {
418    fn type_flag(&self) -> Option<u8> {
419        if self.is_legacy() {
420            None
421        } else {
422            Some(self.ty())
423        }
424    }
425
426    fn encode_2718_len(&self) -> usize {
427        match &self.transaction {
428            ArbTypedTransaction::Legacy(tx) => tx.eip2718_encoded_length(&self.signature),
429            ArbTypedTransaction::Deposit(tx) => tx.length() + 1,
430            ArbTypedTransaction::Unsigned(tx) => tx.length() + 1,
431            ArbTypedTransaction::Contract(tx) => tx.length() + 1,
432            ArbTypedTransaction::Retry(tx) => tx.length() + 1,
433            ArbTypedTransaction::SubmitRetryable(tx) => tx.length() + 1,
434            ArbTypedTransaction::Internal(tx) => tx.length() + 1,
435            ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encoded_length(&self.signature),
436            ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encoded_length(&self.signature),
437            ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encoded_length(&self.signature),
438            ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encoded_length(&self.signature),
439        }
440    }
441
442    fn encode_2718(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
443        match &self.transaction {
444            ArbTypedTransaction::Legacy(tx) => tx.eip2718_encode(&self.signature, out),
445            ArbTypedTransaction::Deposit(tx) => {
446                out.put_u8(ArbTxType::ArbitrumDepositTx.as_u8());
447                tx.encode(out);
448            }
449            ArbTypedTransaction::Unsigned(tx) => {
450                out.put_u8(ArbTxType::ArbitrumUnsignedTx.as_u8());
451                tx.encode(out);
452            }
453            ArbTypedTransaction::Contract(tx) => {
454                out.put_u8(ArbTxType::ArbitrumContractTx.as_u8());
455                tx.encode(out);
456            }
457            ArbTypedTransaction::Retry(tx) => {
458                out.put_u8(ArbTxType::ArbitrumRetryTx.as_u8());
459                tx.encode(out);
460            }
461            ArbTypedTransaction::SubmitRetryable(tx) => {
462                out.put_u8(ArbTxType::ArbitrumSubmitRetryableTx.as_u8());
463                tx.encode(out);
464            }
465            ArbTypedTransaction::Internal(tx) => {
466                out.put_u8(ArbTxType::ArbitrumInternalTx.as_u8());
467                tx.encode(out);
468            }
469            ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encode(&self.signature, out),
470            ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encode(&self.signature, out),
471            ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encode(&self.signature, out),
472            ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encode(&self.signature, out),
473        }
474    }
475}
476
477// ---------------------------------------------------------------------------
478// Decodable2718
479// ---------------------------------------------------------------------------
480
481impl Decodable2718 for ArbTransactionSigned {
482    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
483        // Try Arbitrum-specific types first.
484        if let Ok(kind) = ArbTxType::from_u8(ty) {
485            return Ok(match kind {
486                ArbTxType::ArbitrumDepositTx => {
487                    let tx = ArbDepositTx::decode(buf)?;
488                    Self::new_unhashed(ArbTypedTransaction::Deposit(tx), Self::zero_sig())
489                }
490                ArbTxType::ArbitrumUnsignedTx => {
491                    let tx = ArbUnsignedTx::decode(buf)?;
492                    Self::new_unhashed(ArbTypedTransaction::Unsigned(tx), Self::zero_sig())
493                }
494                ArbTxType::ArbitrumContractTx => {
495                    let tx = ArbContractTx::decode(buf)?;
496                    Self::new_unhashed(ArbTypedTransaction::Contract(tx), Self::zero_sig())
497                }
498                ArbTxType::ArbitrumRetryTx => {
499                    let tx = ArbRetryTx::decode(buf)?;
500                    Self::new_unhashed(ArbTypedTransaction::Retry(tx), Self::zero_sig())
501                }
502                ArbTxType::ArbitrumSubmitRetryableTx => {
503                    let tx = ArbSubmitRetryableTx::decode(buf)?;
504                    Self::new_unhashed(ArbTypedTransaction::SubmitRetryable(tx), Self::zero_sig())
505                }
506                ArbTxType::ArbitrumInternalTx => {
507                    let tx = ArbInternalTx::decode(buf)?;
508                    Self::new_unhashed(ArbTypedTransaction::Internal(tx), Self::zero_sig())
509                }
510                ArbTxType::ArbitrumLegacyTx => return Err(Eip2718Error::UnexpectedType(0x78)),
511            });
512        }
513
514        // Standard Ethereum typed transactions.
515        match alloy_consensus::TxType::try_from(ty).map_err(|_| Eip2718Error::UnexpectedType(ty))? {
516            alloy_consensus::TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
517            alloy_consensus::TxType::Eip2930 => {
518                let (tx, sig) = alloy_consensus::TxEip2930::rlp_decode_with_signature(buf)?;
519                Ok(Self::new_unhashed(ArbTypedTransaction::Eip2930(tx), sig))
520            }
521            alloy_consensus::TxType::Eip1559 => {
522                let (tx, sig) = alloy_consensus::TxEip1559::rlp_decode_with_signature(buf)?;
523                Ok(Self::new_unhashed(ArbTypedTransaction::Eip1559(tx), sig))
524            }
525            alloy_consensus::TxType::Eip4844 => {
526                let (tx, sig) = alloy_consensus::TxEip4844::rlp_decode_with_signature(buf)?;
527                Ok(Self::new_unhashed(ArbTypedTransaction::Eip4844(tx), sig))
528            }
529            alloy_consensus::TxType::Eip7702 => {
530                let (tx, sig) = alloy_consensus::TxEip7702::rlp_decode_with_signature(buf)?;
531                Ok(Self::new_unhashed(ArbTypedTransaction::Eip7702(tx), sig))
532            }
533        }
534    }
535
536    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
537        let (tx, sig, hash) = TxLegacy::rlp_decode_signed(buf)?.into_parts();
538        let signed_tx = Self::new_unhashed(ArbTypedTransaction::Legacy(tx), sig);
539        signed_tx.hash.get_or_init(|| hash);
540        Ok(signed_tx)
541    }
542}
543
544// ---------------------------------------------------------------------------
545// Encodable / Decodable (RLP network encoding)
546// ---------------------------------------------------------------------------
547
548impl Encodable for ArbTransactionSigned {
549    fn encode(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
550        self.network_encode(out);
551    }
552    fn length(&self) -> usize {
553        let mut payload_length = self.encode_2718_len();
554        if !self.is_legacy() {
555            payload_length += alloy_rlp::Header {
556                list: false,
557                payload_length,
558            }
559            .length();
560        }
561        payload_length
562    }
563}
564
565impl Decodable for ArbTransactionSigned {
566    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
567        Self::network_decode(buf).map_err(Into::into)
568    }
569}
570
571// ---------------------------------------------------------------------------
572// Transaction (alloy_consensus::Transaction)
573// ---------------------------------------------------------------------------
574
575impl ConsensusTx for ArbTransactionSigned {
576    fn chain_id(&self) -> Option<u64> {
577        match &self.transaction {
578            ArbTypedTransaction::Legacy(tx) => tx.chain_id,
579            ArbTypedTransaction::Deposit(tx) => Some(tx.chain_id.to::<u64>()),
580            ArbTypedTransaction::Unsigned(tx) => Some(tx.chain_id.to::<u64>()),
581            ArbTypedTransaction::Contract(tx) => Some(tx.chain_id.to::<u64>()),
582            ArbTypedTransaction::Retry(tx) => Some(tx.chain_id.to::<u64>()),
583            ArbTypedTransaction::SubmitRetryable(tx) => Some(tx.chain_id.to::<u64>()),
584            ArbTypedTransaction::Internal(tx) => Some(tx.chain_id.to::<u64>()),
585            ArbTypedTransaction::Eip2930(tx) => Some(tx.chain_id),
586            ArbTypedTransaction::Eip1559(tx) => Some(tx.chain_id),
587            ArbTypedTransaction::Eip4844(tx) => Some(tx.chain_id),
588            ArbTypedTransaction::Eip7702(tx) => Some(tx.chain_id),
589        }
590    }
591
592    fn nonce(&self) -> u64 {
593        match &self.transaction {
594            ArbTypedTransaction::Legacy(tx) => tx.nonce,
595            ArbTypedTransaction::Deposit(_) => 0,
596            ArbTypedTransaction::Unsigned(tx) => tx.nonce,
597            ArbTypedTransaction::Contract(_) => 0,
598            ArbTypedTransaction::Retry(tx) => tx.nonce,
599            ArbTypedTransaction::SubmitRetryable(_) => 0,
600            ArbTypedTransaction::Internal(_) => 0,
601            ArbTypedTransaction::Eip2930(tx) => tx.nonce,
602            ArbTypedTransaction::Eip1559(tx) => tx.nonce,
603            ArbTypedTransaction::Eip4844(tx) => tx.nonce,
604            ArbTypedTransaction::Eip7702(tx) => tx.nonce,
605        }
606    }
607
608    fn gas_limit(&self) -> u64 {
609        match &self.transaction {
610            ArbTypedTransaction::Legacy(tx) => tx.gas_limit,
611            ArbTypedTransaction::Deposit(_) => 0,
612            ArbTypedTransaction::Unsigned(tx) => tx.gas,
613            ArbTypedTransaction::Contract(tx) => tx.gas,
614            ArbTypedTransaction::Retry(tx) => tx.gas,
615            ArbTypedTransaction::SubmitRetryable(tx) => tx.gas,
616            ArbTypedTransaction::Internal(_) => 0,
617            ArbTypedTransaction::Eip2930(tx) => tx.gas_limit,
618            ArbTypedTransaction::Eip1559(tx) => tx.gas_limit,
619            ArbTypedTransaction::Eip4844(tx) => tx.gas_limit,
620            ArbTypedTransaction::Eip7702(tx) => tx.gas_limit,
621        }
622    }
623
624    fn gas_price(&self) -> Option<u128> {
625        match &self.transaction {
626            ArbTypedTransaction::Legacy(tx) => Some(tx.gas_price),
627            ArbTypedTransaction::Eip2930(tx) => Some(tx.gas_price),
628            _ => None,
629        }
630    }
631
632    fn max_fee_per_gas(&self) -> u128 {
633        match &self.transaction {
634            ArbTypedTransaction::Legacy(tx) => tx.gas_price,
635            ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
636            ArbTypedTransaction::Unsigned(tx) => tx.gas_fee_cap.to::<u128>(),
637            ArbTypedTransaction::Contract(tx) => tx.gas_fee_cap.to::<u128>(),
638            ArbTypedTransaction::Retry(tx) => tx.gas_fee_cap.to::<u128>(),
639            ArbTypedTransaction::SubmitRetryable(tx) => tx.gas_fee_cap.to::<u128>(),
640            ArbTypedTransaction::Eip1559(tx) => tx.max_fee_per_gas,
641            ArbTypedTransaction::Eip4844(tx) => tx.max_fee_per_gas,
642            ArbTypedTransaction::Eip7702(tx) => tx.max_fee_per_gas,
643            _ => 0,
644        }
645    }
646
647    fn max_priority_fee_per_gas(&self) -> Option<u128> {
648        match &self.transaction {
649            ArbTypedTransaction::Eip1559(tx) => Some(tx.max_priority_fee_per_gas),
650            ArbTypedTransaction::Eip4844(tx) => Some(tx.max_priority_fee_per_gas),
651            ArbTypedTransaction::Eip7702(tx) => Some(tx.max_priority_fee_per_gas),
652            // Legacy / 2930 / Arbitrum-internal types have no priority fee.
653            _ => None,
654        }
655    }
656
657    fn max_fee_per_blob_gas(&self) -> Option<u128> {
658        match &self.transaction {
659            ArbTypedTransaction::Eip4844(tx) => Some(tx.max_fee_per_blob_gas),
660            _ => None,
661        }
662    }
663
664    fn priority_fee_or_price(&self) -> u128 {
665        match self.max_priority_fee_per_gas() {
666            Some(p) => p,
667            None => self.gas_price().unwrap_or(0),
668        }
669    }
670
671    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
672        let bf = base_fee.unwrap_or(0) as u128;
673        match &self.transaction {
674            ArbTypedTransaction::Legacy(tx) => tx.gas_price,
675            ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
676            ArbTypedTransaction::Eip1559(tx) => core::cmp::min(
677                tx.max_fee_per_gas,
678                bf.saturating_add(tx.max_priority_fee_per_gas),
679            ),
680            ArbTypedTransaction::Eip7702(tx) => core::cmp::min(
681                tx.max_fee_per_gas,
682                bf.saturating_add(tx.max_priority_fee_per_gas),
683            ),
684            ArbTypedTransaction::Eip4844(tx) => core::cmp::min(
685                tx.max_fee_per_gas,
686                bf.saturating_add(tx.max_priority_fee_per_gas),
687            ),
688            // Arbitrum-internal types: gas price is determined elsewhere.
689            _ => bf,
690        }
691    }
692
693    fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
694        let bf = base_fee as u128;
695        match &self.transaction {
696            ArbTypedTransaction::Eip1559(tx) => Some(core::cmp::min(
697                tx.max_priority_fee_per_gas,
698                tx.max_fee_per_gas.saturating_sub(bf),
699            )),
700            ArbTypedTransaction::Eip7702(tx) => Some(core::cmp::min(
701                tx.max_priority_fee_per_gas,
702                tx.max_fee_per_gas.saturating_sub(bf),
703            )),
704            ArbTypedTransaction::Eip4844(tx) => Some(core::cmp::min(
705                tx.max_priority_fee_per_gas,
706                tx.max_fee_per_gas.saturating_sub(bf),
707            )),
708            _ => None,
709        }
710    }
711
712    fn is_dynamic_fee(&self) -> bool {
713        !matches!(
714            self.transaction,
715            ArbTypedTransaction::Legacy(_) | ArbTypedTransaction::Eip2930(_)
716        )
717    }
718
719    fn kind(&self) -> TxKind {
720        match &self.transaction {
721            ArbTypedTransaction::Legacy(tx) => tx.to,
722            ArbTypedTransaction::Deposit(tx) => TxKind::Call(tx.to),
723            ArbTypedTransaction::Unsigned(tx) => match tx.to {
724                Some(to) => TxKind::Call(to),
725                None => TxKind::Create,
726            },
727            ArbTypedTransaction::Contract(tx) => match tx.to {
728                Some(to) => TxKind::Call(to),
729                None => TxKind::Create,
730            },
731            ArbTypedTransaction::Retry(tx) => match tx.to {
732                Some(to) => TxKind::Call(to),
733                None => TxKind::Create,
734            },
735            ArbTypedTransaction::SubmitRetryable(_) => TxKind::Call(RETRYABLE_ADDRESS),
736            ArbTypedTransaction::Internal(_) => TxKind::Call(ARBOS_ADDRESS),
737            ArbTypedTransaction::Eip2930(tx) => tx.to,
738            ArbTypedTransaction::Eip1559(tx) => tx.to,
739            ArbTypedTransaction::Eip4844(tx) => TxKind::Call(tx.to),
740            ArbTypedTransaction::Eip7702(tx) => TxKind::Call(tx.to),
741        }
742    }
743
744    fn is_create(&self) -> bool {
745        matches!(self.kind(), TxKind::Create)
746    }
747
748    fn value(&self) -> U256 {
749        match &self.transaction {
750            ArbTypedTransaction::Legacy(tx) => tx.value,
751            ArbTypedTransaction::Deposit(tx) => tx.value,
752            ArbTypedTransaction::Unsigned(tx) => tx.value,
753            ArbTypedTransaction::Contract(tx) => tx.value,
754            ArbTypedTransaction::Retry(tx) => tx.value,
755            ArbTypedTransaction::SubmitRetryable(tx) => tx.retry_value,
756            ArbTypedTransaction::Internal(_) => U256::ZERO,
757            ArbTypedTransaction::Eip2930(tx) => tx.value,
758            ArbTypedTransaction::Eip1559(tx) => tx.value,
759            ArbTypedTransaction::Eip4844(tx) => tx.value,
760            ArbTypedTransaction::Eip7702(tx) => tx.value,
761        }
762    }
763
764    fn input(&self) -> &Bytes {
765        match &self.transaction {
766            ArbTypedTransaction::Legacy(tx) => &tx.input,
767            ArbTypedTransaction::Deposit(_) => self.input_cache.get_or_init(Bytes::new),
768            ArbTypedTransaction::Unsigned(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
769            ArbTypedTransaction::Contract(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
770            ArbTypedTransaction::Retry(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
771            ArbTypedTransaction::SubmitRetryable(tx) => self.input_cache.get_or_init(|| {
772                let sel = arb_alloy_predeploys::selector(
773                    arb_alloy_predeploys::SIG_RETRY_SUBMIT_RETRYABLE,
774                );
775                let mut out = Vec::with_capacity(4 + tx.retry_data.len());
776                out.extend_from_slice(&sel);
777                out.extend_from_slice(&tx.retry_data);
778                Bytes::from(out)
779            }),
780            ArbTypedTransaction::Internal(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
781            ArbTypedTransaction::Eip2930(tx) => &tx.input,
782            ArbTypedTransaction::Eip1559(tx) => &tx.input,
783            ArbTypedTransaction::Eip4844(tx) => &tx.input,
784            ArbTypedTransaction::Eip7702(tx) => &tx.input,
785        }
786    }
787
788    fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> {
789        match &self.transaction {
790            ArbTypedTransaction::Eip2930(tx) => Some(&tx.access_list),
791            ArbTypedTransaction::Eip1559(tx) => Some(&tx.access_list),
792            ArbTypedTransaction::Eip4844(tx) => Some(&tx.access_list),
793            ArbTypedTransaction::Eip7702(tx) => Some(&tx.access_list),
794            _ => None,
795        }
796    }
797
798    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
799        None
800    }
801
802    fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
803        match &self.transaction {
804            ArbTypedTransaction::Eip7702(tx) => Some(&tx.authorization_list),
805            _ => None,
806        }
807    }
808}
809
810// ---------------------------------------------------------------------------
811// serde — serialize via 2718 encoding
812// ---------------------------------------------------------------------------
813
814impl serde::Serialize for ArbTransactionSigned {
815    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
816    where
817        S: serde::Serializer,
818    {
819        use serde::ser::SerializeStruct;
820        let mut state = serializer.serialize_struct("ArbTransactionSigned", 2)?;
821        state.serialize_field("signature", &self.signature)?;
822        state.serialize_field("hash", self.tx_hash())?;
823        state.end()
824    }
825}
826
827impl<'de> serde::Deserialize<'de> for ArbTransactionSigned {
828    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
829    where
830        D: serde::Deserializer<'de>,
831    {
832        #[derive(serde::Deserialize)]
833        struct Helper {
834            signature: Signature,
835            #[serde(default)]
836            transaction_encoded_2718: Option<alloy_primitives::Bytes>,
837        }
838        let helper = Helper::deserialize(deserializer)?;
839        if let Some(encoded) = helper.transaction_encoded_2718 {
840            let mut slice: &[u8] = encoded.as_ref();
841            let parsed = Self::network_decode(&mut slice).map_err(serde::de::Error::custom)?;
842            Ok(parsed)
843        } else {
844            // Fallback: return a default-like empty tx (legacy with zero fields).
845            Ok(Self::new_unhashed(
846                ArbTypedTransaction::Legacy(TxLegacy::default()),
847                helper.signature,
848            ))
849        }
850    }
851}
852
853// ---------------------------------------------------------------------------
854// Compact — required by MaybeCompact when reth-codec feature is active
855// ---------------------------------------------------------------------------
856
857impl reth_codecs::Compact for ArbTransactionSigned {
858    fn to_compact<B>(&self, buf: &mut B) -> usize
859    where
860        B: bytes::BufMut + AsMut<[u8]>,
861    {
862        // Simple approach: encode via 2718 and prefix with length.
863        let encoded = self.encoded_2718();
864        let len = encoded.len() as u32;
865        buf.put_u32(len);
866        buf.put_slice(&encoded);
867        // Signature
868        let sig_bytes = self.signature.as_bytes();
869        buf.put_slice(&sig_bytes);
870        0
871    }
872
873    fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
874        use bytes::Buf;
875        let mut slice = buf;
876        let tx_len = slice.get_u32() as usize;
877        let tx_bytes = &slice[..tx_len];
878        slice = &slice[tx_len..];
879
880        let mut tx_buf = tx_bytes;
881        let tx = Self::network_decode(&mut tx_buf).unwrap_or_else(|_| {
882            Self::new_unhashed(
883                ArbTypedTransaction::Legacy(TxLegacy::default()),
884                Signature::new(U256::ZERO, U256::ZERO, false),
885            )
886        });
887
888        // Read signature (65 bytes)
889        if slice.len() >= 65 {
890            let _sig_bytes = &slice[..65];
891            slice = &slice[65..];
892        }
893
894        (tx, slice)
895    }
896}
897
898// ---------------------------------------------------------------------------
899// Compress / Decompress — delegates to Compact for database storage
900// ---------------------------------------------------------------------------
901
902impl reth_db_api::table::Compress for ArbTransactionSigned {
903    type Compressed = Vec<u8>;
904
905    fn compress_to_buf<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) {
906        let _ = reth_codecs::Compact::to_compact(self, buf);
907    }
908}
909
910impl reth_db_api::table::Decompress for ArbTransactionSigned {
911    fn decompress(value: &[u8]) -> Result<Self, reth_codecs::DecompressError> {
912        let (obj, _) = reth_codecs::Compact::from_compact(value, value.len());
913        Ok(obj)
914    }
915}
916
917// ---------------------------------------------------------------------------
918// Arbitrum transaction data extraction
919// ---------------------------------------------------------------------------
920
921/// Data extracted from a SubmitRetryable transaction for processing.
922#[derive(Debug, Clone)]
923pub struct SubmitRetryableInfo {
924    pub from: Address,
925    pub deposit_value: U256,
926    pub retry_value: U256,
927    pub gas_fee_cap: U256,
928    pub gas: u64,
929    pub retry_to: Option<Address>,
930    pub retry_data: Vec<u8>,
931    pub beneficiary: Address,
932    pub max_submission_fee: U256,
933    pub fee_refund_addr: Address,
934    pub l1_base_fee: U256,
935    pub request_id: B256,
936}
937
938/// Data extracted from a RetryTx transaction for processing.
939#[derive(Debug, Clone)]
940pub struct RetryTxInfo {
941    pub from: Address,
942    pub ticket_id: B256,
943    pub refund_to: Address,
944    pub gas_fee_cap: U256,
945    pub max_refund: U256,
946    pub submission_fee_refund: U256,
947}
948
949/// Trait for extracting Arbitrum-specific transaction data beyond the
950/// standard `Transaction` trait.
951pub trait ArbTransactionExt {
952    fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
953        None
954    }
955    fn retry_tx_info(&self) -> Option<RetryTxInfo> {
956        None
957    }
958    /// Compute or return cached poster calldata units for the given brotli level.
959    /// Default impl calls `compute` every time; `ArbTransactionSigned` caches the result.
960    fn poster_units_for(&self, _level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
961        compute()
962    }
963}
964
965impl ArbTransactionExt for ArbTransactionSigned {
966    fn poster_units_for(&self, level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
967        if let Some(&entry) = self.poster_units_cache.get() {
968            let (cached_level, cached_units) = unpack_poster_units(entry);
969            if cached_level == level {
970                return cached_units;
971            }
972            return compute();
973        }
974        let units = compute();
975        let _ = self.poster_units_cache.set(pack_poster_units(level, units));
976        units
977    }
978
979    fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
980        match &self.transaction {
981            ArbTypedTransaction::SubmitRetryable(tx) => Some(SubmitRetryableInfo {
982                from: tx.from,
983                deposit_value: tx.deposit_value,
984                retry_value: tx.retry_value,
985                gas_fee_cap: tx.gas_fee_cap,
986                gas: tx.gas,
987                retry_to: tx.retry_to,
988                retry_data: tx.retry_data.to_vec(),
989                beneficiary: tx.beneficiary,
990                max_submission_fee: tx.max_submission_fee,
991                fee_refund_addr: tx.fee_refund_addr,
992                l1_base_fee: tx.l1_base_fee,
993                request_id: tx.request_id,
994            }),
995            _ => None,
996        }
997    }
998
999    fn retry_tx_info(&self) -> Option<RetryTxInfo> {
1000        match &self.transaction {
1001            ArbTypedTransaction::Retry(tx) => Some(RetryTxInfo {
1002                from: tx.from,
1003                ticket_id: tx.ticket_id,
1004                refund_to: tx.refund_to,
1005                gas_fee_cap: tx.gas_fee_cap,
1006                max_refund: tx.max_refund,
1007                submission_fee_refund: tx.submission_fee_refund,
1008            }),
1009            _ => None,
1010        }
1011    }
1012}
1013
1014/// Standard Ethereum transaction envelopes don't carry retryable data.
1015impl<T> ArbTransactionExt for alloy_consensus::EthereumTxEnvelope<T> {}
1016
1017#[cfg(test)]
1018mod tests {
1019    use super::*;
1020
1021    #[test]
1022    fn roundtrip_unsigned_tx() {
1023        let tx = ArbUnsignedTx {
1024            chain_id: U256::from(42161u64),
1025            from: alloy_primitives::address!("00000000000000000000000000000000000000aa"),
1026            nonce: 7,
1027            gas_fee_cap: U256::from(1_000_000u64),
1028            gas: 21000,
1029            to: Some(alloy_primitives::address!(
1030                "00000000000000000000000000000000000000bb"
1031            )),
1032            value: U256::from(123u64),
1033            data: Vec::new().into(),
1034        };
1035
1036        let mut enc = Vec::with_capacity(1 + tx.length());
1037        enc.push(ArbTxType::ArbitrumUnsignedTx.as_u8());
1038        tx.encode(&mut enc);
1039
1040        let signed =
1041            ArbTransactionSigned::decode_2718_exact(enc.as_slice()).expect("typed decode ok");
1042        assert_eq!(signed.tx_type(), ArbTxTypeLocal::Unsigned);
1043        assert_eq!(signed.chain_id(), Some(42161));
1044        assert_eq!(signed.nonce(), 7);
1045        assert_eq!(signed.gas_limit(), 21000);
1046        assert_eq!(signed.value(), U256::from(123u64));
1047    }
1048
1049    #[test]
1050    fn deposit_tx_has_zero_gas() {
1051        let tx = ArbDepositTx {
1052            chain_id: U256::from(42161u64),
1053            l1_request_id: B256::ZERO,
1054            from: Address::ZERO,
1055            to: Address::ZERO,
1056            value: U256::from(100u64),
1057        };
1058
1059        let signed = ArbTransactionSigned::new_unhashed(
1060            ArbTypedTransaction::Deposit(tx),
1061            ArbTransactionSigned::zero_sig(),
1062        );
1063
1064        assert_eq!(signed.gas_limit(), 0);
1065        assert_eq!(signed.nonce(), 0);
1066        assert_eq!(signed.tx_type(), ArbTxTypeLocal::Deposit);
1067    }
1068}