arb_primitives/
signed_tx.rs

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