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, SignedTransaction,
20 crypto::secp256k1::{recover_signer, recover_signer_unchecked},
21};
22
23const ARBOS_ADDRESS: Address =
25 alloy_primitives::address!("00000000000000000000000000000000000A4B05");
26
27const RETRYABLE_ADDRESS: Address =
29 alloy_primitives::address!("000000000000000000000000000000000000006e");
30
31#[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#[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 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#[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 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 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 pub fn inner(&self) -> &ArbTypedTransaction {
192 &self.transaction
193 }
194
195 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 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
255impl 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
277impl TxHashRef for ArbTransactionSigned {
282 fn tx_hash(&self) -> &TxHash {
283 self.hash.get_or_init(|| self.compute_hash())
284 }
285}
286
287impl SignedTransaction for ArbTransactionSigned {
292 fn recalculate_hash(&self) -> B256 {
293 keccak256(self.encoded_2718())
294 }
295}
296
297impl ArbTransactionSigned {
302 fn recover_signer_inner(
303 &self,
304 strict: bool,
305 ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
306 match &self.transaction {
307 ArbTypedTransaction::Deposit(tx) => Ok(tx.from),
308 ArbTypedTransaction::Unsigned(tx) => Ok(tx.from),
309 ArbTypedTransaction::Contract(tx) => Ok(tx.from),
310 ArbTypedTransaction::Retry(tx) => Ok(tx.from),
311 ArbTypedTransaction::SubmitRetryable(tx) => Ok(tx.from),
312 ArbTypedTransaction::Internal(_) => Ok(ARBOS_ADDRESS),
313 ArbTypedTransaction::Legacy(tx) => {
314 let mut buf = Vec::new();
315 tx.encode_for_signing(&mut buf);
316 if strict {
317 recover_signer(&self.signature, keccak256(&buf))
318 } else {
319 recover_signer_unchecked(&self.signature, keccak256(&buf))
320 }
321 }
322 ArbTypedTransaction::Eip2930(tx) => {
323 let mut buf = Vec::new();
324 tx.encode_for_signing(&mut buf);
325 if strict {
326 recover_signer(&self.signature, keccak256(&buf))
327 } else {
328 recover_signer_unchecked(&self.signature, keccak256(&buf))
329 }
330 }
331 ArbTypedTransaction::Eip1559(tx) => {
332 let mut buf = Vec::new();
333 tx.encode_for_signing(&mut buf);
334 if strict {
335 recover_signer(&self.signature, keccak256(&buf))
336 } else {
337 recover_signer_unchecked(&self.signature, keccak256(&buf))
338 }
339 }
340 ArbTypedTransaction::Eip4844(tx) => {
341 let mut buf = Vec::new();
342 tx.encode_for_signing(&mut buf);
343 if strict {
344 recover_signer(&self.signature, keccak256(&buf))
345 } else {
346 recover_signer_unchecked(&self.signature, keccak256(&buf))
347 }
348 }
349 ArbTypedTransaction::Eip7702(tx) => {
350 let mut buf = Vec::new();
351 tx.encode_for_signing(&mut buf);
352 if strict {
353 recover_signer(&self.signature, keccak256(&buf))
354 } else {
355 recover_signer_unchecked(&self.signature, keccak256(&buf))
356 }
357 }
358 }
359 }
360}
361
362impl alloy_consensus::transaction::SignerRecoverable for ArbTransactionSigned {
363 fn recover_signer(
364 &self,
365 ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
366 if let Some(addr) = self.sender_cache.get() {
367 return Ok(*addr);
368 }
369 let addr = self.recover_signer_inner(true)?;
370 let _ = self.sender_cache.set(addr);
371 Ok(addr)
372 }
373
374 fn recover_signer_unchecked(
375 &self,
376 ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
377 if let Some(addr) = self.sender_cache.get() {
378 return Ok(*addr);
379 }
380 let addr = self.recover_signer_inner(false)?;
381 let _ = self.sender_cache.set(addr);
382 Ok(addr)
383 }
384}
385
386impl Typed2718 for ArbTransactionSigned {
391 fn is_legacy(&self) -> bool {
392 matches!(self.transaction, ArbTypedTransaction::Legacy(_))
393 }
394
395 fn ty(&self) -> u8 {
396 match &self.transaction {
397 ArbTypedTransaction::Legacy(_) => 0u8,
398 ArbTypedTransaction::Deposit(_) => ArbTxType::ArbitrumDepositTx.as_u8(),
399 ArbTypedTransaction::Unsigned(_) => ArbTxType::ArbitrumUnsignedTx.as_u8(),
400 ArbTypedTransaction::Contract(_) => ArbTxType::ArbitrumContractTx.as_u8(),
401 ArbTypedTransaction::Retry(_) => ArbTxType::ArbitrumRetryTx.as_u8(),
402 ArbTypedTransaction::SubmitRetryable(_) => ArbTxType::ArbitrumSubmitRetryableTx.as_u8(),
403 ArbTypedTransaction::Internal(_) => ArbTxType::ArbitrumInternalTx.as_u8(),
404 ArbTypedTransaction::Eip2930(_) => 0x01,
405 ArbTypedTransaction::Eip1559(_) => 0x02,
406 ArbTypedTransaction::Eip4844(_) => 0x03,
407 ArbTypedTransaction::Eip7702(_) => 0x04,
408 }
409 }
410}
411
412impl IsTyped2718 for ArbTransactionSigned {
417 fn is_type(type_id: u8) -> bool {
418 matches!(type_id, 0x01..=0x04) || ArbTxType::from_u8(type_id).is_ok()
420 }
421}
422
423impl Encodable2718 for ArbTransactionSigned {
428 fn type_flag(&self) -> Option<u8> {
429 if self.is_legacy() {
430 None
431 } else {
432 Some(self.ty())
433 }
434 }
435
436 fn encode_2718_len(&self) -> usize {
437 match &self.transaction {
438 ArbTypedTransaction::Legacy(tx) => tx.eip2718_encoded_length(&self.signature),
439 ArbTypedTransaction::Deposit(tx) => tx.length() + 1,
440 ArbTypedTransaction::Unsigned(tx) => tx.length() + 1,
441 ArbTypedTransaction::Contract(tx) => tx.length() + 1,
442 ArbTypedTransaction::Retry(tx) => tx.length() + 1,
443 ArbTypedTransaction::SubmitRetryable(tx) => tx.length() + 1,
444 ArbTypedTransaction::Internal(tx) => tx.length() + 1,
445 ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encoded_length(&self.signature),
446 ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encoded_length(&self.signature),
447 ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encoded_length(&self.signature),
448 ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encoded_length(&self.signature),
449 }
450 }
451
452 fn encode_2718(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
453 match &self.transaction {
454 ArbTypedTransaction::Legacy(tx) => tx.eip2718_encode(&self.signature, out),
455 ArbTypedTransaction::Deposit(tx) => {
456 out.put_u8(ArbTxType::ArbitrumDepositTx.as_u8());
457 tx.encode(out);
458 }
459 ArbTypedTransaction::Unsigned(tx) => {
460 out.put_u8(ArbTxType::ArbitrumUnsignedTx.as_u8());
461 tx.encode(out);
462 }
463 ArbTypedTransaction::Contract(tx) => {
464 out.put_u8(ArbTxType::ArbitrumContractTx.as_u8());
465 tx.encode(out);
466 }
467 ArbTypedTransaction::Retry(tx) => {
468 out.put_u8(ArbTxType::ArbitrumRetryTx.as_u8());
469 tx.encode(out);
470 }
471 ArbTypedTransaction::SubmitRetryable(tx) => {
472 out.put_u8(ArbTxType::ArbitrumSubmitRetryableTx.as_u8());
473 tx.encode(out);
474 }
475 ArbTypedTransaction::Internal(tx) => {
476 out.put_u8(ArbTxType::ArbitrumInternalTx.as_u8());
477 tx.encode(out);
478 }
479 ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encode(&self.signature, out),
480 ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encode(&self.signature, out),
481 ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encode(&self.signature, out),
482 ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encode(&self.signature, out),
483 }
484 }
485}
486
487impl Decodable2718 for ArbTransactionSigned {
492 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
493 if let Ok(kind) = ArbTxType::from_u8(ty) {
495 return Ok(match kind {
496 ArbTxType::ArbitrumDepositTx => {
497 let tx = ArbDepositTx::decode(buf)?;
498 Self::new_unhashed(ArbTypedTransaction::Deposit(tx), Self::zero_sig())
499 }
500 ArbTxType::ArbitrumUnsignedTx => {
501 let tx = ArbUnsignedTx::decode(buf)?;
502 Self::new_unhashed(ArbTypedTransaction::Unsigned(tx), Self::zero_sig())
503 }
504 ArbTxType::ArbitrumContractTx => {
505 let tx = ArbContractTx::decode(buf)?;
506 Self::new_unhashed(ArbTypedTransaction::Contract(tx), Self::zero_sig())
507 }
508 ArbTxType::ArbitrumRetryTx => {
509 let tx = ArbRetryTx::decode(buf)?;
510 Self::new_unhashed(ArbTypedTransaction::Retry(tx), Self::zero_sig())
511 }
512 ArbTxType::ArbitrumSubmitRetryableTx => {
513 let tx = ArbSubmitRetryableTx::decode(buf)?;
514 Self::new_unhashed(ArbTypedTransaction::SubmitRetryable(tx), Self::zero_sig())
515 }
516 ArbTxType::ArbitrumInternalTx => {
517 let tx = ArbInternalTx::decode(buf)?;
518 Self::new_unhashed(ArbTypedTransaction::Internal(tx), Self::zero_sig())
519 }
520 ArbTxType::ArbitrumLegacyTx => return Err(Eip2718Error::UnexpectedType(0x78)),
521 });
522 }
523
524 match alloy_consensus::TxType::try_from(ty).map_err(|_| Eip2718Error::UnexpectedType(ty))? {
526 alloy_consensus::TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
527 alloy_consensus::TxType::Eip2930 => {
528 let (tx, sig) = alloy_consensus::TxEip2930::rlp_decode_with_signature(buf)?;
529 Ok(Self::new_unhashed(ArbTypedTransaction::Eip2930(tx), sig))
530 }
531 alloy_consensus::TxType::Eip1559 => {
532 let (tx, sig) = alloy_consensus::TxEip1559::rlp_decode_with_signature(buf)?;
533 Ok(Self::new_unhashed(ArbTypedTransaction::Eip1559(tx), sig))
534 }
535 alloy_consensus::TxType::Eip4844 => {
536 let (tx, sig) = alloy_consensus::TxEip4844::rlp_decode_with_signature(buf)?;
537 Ok(Self::new_unhashed(ArbTypedTransaction::Eip4844(tx), sig))
538 }
539 alloy_consensus::TxType::Eip7702 => {
540 let (tx, sig) = alloy_consensus::TxEip7702::rlp_decode_with_signature(buf)?;
541 Ok(Self::new_unhashed(ArbTypedTransaction::Eip7702(tx), sig))
542 }
543 }
544 }
545
546 fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
547 let (tx, sig, hash) = TxLegacy::rlp_decode_signed(buf)?.into_parts();
548 let signed_tx = Self::new_unhashed(ArbTypedTransaction::Legacy(tx), sig);
549 signed_tx.hash.get_or_init(|| hash);
550 Ok(signed_tx)
551 }
552}
553
554impl Encodable for ArbTransactionSigned {
559 fn encode(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
560 self.network_encode(out);
561 }
562 fn length(&self) -> usize {
563 let mut payload_length = self.encode_2718_len();
564 if !self.is_legacy() {
565 payload_length += alloy_rlp::Header {
566 list: false,
567 payload_length,
568 }
569 .length();
570 }
571 payload_length
572 }
573}
574
575impl Decodable for ArbTransactionSigned {
576 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
577 Self::network_decode(buf).map_err(Into::into)
578 }
579}
580
581impl ConsensusTx for ArbTransactionSigned {
586 fn chain_id(&self) -> Option<u64> {
587 match &self.transaction {
588 ArbTypedTransaction::Legacy(tx) => tx.chain_id,
589 ArbTypedTransaction::Deposit(tx) => Some(tx.chain_id.to::<u64>()),
590 ArbTypedTransaction::Unsigned(tx) => Some(tx.chain_id.to::<u64>()),
591 ArbTypedTransaction::Contract(tx) => Some(tx.chain_id.to::<u64>()),
592 ArbTypedTransaction::Retry(tx) => Some(tx.chain_id.to::<u64>()),
593 ArbTypedTransaction::SubmitRetryable(tx) => Some(tx.chain_id.to::<u64>()),
594 ArbTypedTransaction::Internal(tx) => Some(tx.chain_id.to::<u64>()),
595 ArbTypedTransaction::Eip2930(tx) => Some(tx.chain_id),
596 ArbTypedTransaction::Eip1559(tx) => Some(tx.chain_id),
597 ArbTypedTransaction::Eip4844(tx) => Some(tx.chain_id),
598 ArbTypedTransaction::Eip7702(tx) => Some(tx.chain_id),
599 }
600 }
601
602 fn nonce(&self) -> u64 {
603 match &self.transaction {
604 ArbTypedTransaction::Legacy(tx) => tx.nonce,
605 ArbTypedTransaction::Deposit(_) => 0,
606 ArbTypedTransaction::Unsigned(tx) => tx.nonce,
607 ArbTypedTransaction::Contract(_) => 0,
608 ArbTypedTransaction::Retry(tx) => tx.nonce,
609 ArbTypedTransaction::SubmitRetryable(_) => 0,
610 ArbTypedTransaction::Internal(_) => 0,
611 ArbTypedTransaction::Eip2930(tx) => tx.nonce,
612 ArbTypedTransaction::Eip1559(tx) => tx.nonce,
613 ArbTypedTransaction::Eip4844(tx) => tx.nonce,
614 ArbTypedTransaction::Eip7702(tx) => tx.nonce,
615 }
616 }
617
618 fn gas_limit(&self) -> u64 {
619 match &self.transaction {
620 ArbTypedTransaction::Legacy(tx) => tx.gas_limit,
621 ArbTypedTransaction::Deposit(_) => 0,
622 ArbTypedTransaction::Unsigned(tx) => tx.gas,
623 ArbTypedTransaction::Contract(tx) => tx.gas,
624 ArbTypedTransaction::Retry(tx) => tx.gas,
625 ArbTypedTransaction::SubmitRetryable(tx) => tx.gas,
626 ArbTypedTransaction::Internal(_) => 0,
627 ArbTypedTransaction::Eip2930(tx) => tx.gas_limit,
628 ArbTypedTransaction::Eip1559(tx) => tx.gas_limit,
629 ArbTypedTransaction::Eip4844(tx) => tx.gas_limit,
630 ArbTypedTransaction::Eip7702(tx) => tx.gas_limit,
631 }
632 }
633
634 fn gas_price(&self) -> Option<u128> {
635 match &self.transaction {
636 ArbTypedTransaction::Legacy(tx) => Some(tx.gas_price),
637 ArbTypedTransaction::Eip2930(tx) => Some(tx.gas_price),
638 _ => None,
639 }
640 }
641
642 fn max_fee_per_gas(&self) -> u128 {
643 match &self.transaction {
644 ArbTypedTransaction::Legacy(tx) => tx.gas_price,
645 ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
646 ArbTypedTransaction::Unsigned(tx) => tx.gas_fee_cap.to::<u128>(),
647 ArbTypedTransaction::Contract(tx) => tx.gas_fee_cap.to::<u128>(),
648 ArbTypedTransaction::Retry(tx) => tx.gas_fee_cap.to::<u128>(),
649 ArbTypedTransaction::SubmitRetryable(tx) => tx.gas_fee_cap.to::<u128>(),
650 ArbTypedTransaction::Eip1559(tx) => tx.max_fee_per_gas,
651 ArbTypedTransaction::Eip4844(tx) => tx.max_fee_per_gas,
652 ArbTypedTransaction::Eip7702(tx) => tx.max_fee_per_gas,
653 _ => 0,
654 }
655 }
656
657 fn max_priority_fee_per_gas(&self) -> Option<u128> {
658 match &self.transaction {
659 ArbTypedTransaction::Eip1559(tx) => Some(tx.max_priority_fee_per_gas),
660 ArbTypedTransaction::Eip4844(tx) => Some(tx.max_priority_fee_per_gas),
661 ArbTypedTransaction::Eip7702(tx) => Some(tx.max_priority_fee_per_gas),
662 _ => None,
664 }
665 }
666
667 fn max_fee_per_blob_gas(&self) -> Option<u128> {
668 match &self.transaction {
669 ArbTypedTransaction::Eip4844(tx) => Some(tx.max_fee_per_blob_gas),
670 _ => None,
671 }
672 }
673
674 fn priority_fee_or_price(&self) -> u128 {
675 match self.max_priority_fee_per_gas() {
676 Some(p) => p,
677 None => self.gas_price().unwrap_or(0),
678 }
679 }
680
681 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
682 let bf = base_fee.unwrap_or(0) as u128;
683 match &self.transaction {
684 ArbTypedTransaction::Legacy(tx) => tx.gas_price,
685 ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
686 ArbTypedTransaction::Eip1559(tx) => core::cmp::min(
687 tx.max_fee_per_gas,
688 bf.saturating_add(tx.max_priority_fee_per_gas),
689 ),
690 ArbTypedTransaction::Eip7702(tx) => core::cmp::min(
691 tx.max_fee_per_gas,
692 bf.saturating_add(tx.max_priority_fee_per_gas),
693 ),
694 ArbTypedTransaction::Eip4844(tx) => core::cmp::min(
695 tx.max_fee_per_gas,
696 bf.saturating_add(tx.max_priority_fee_per_gas),
697 ),
698 _ => bf,
700 }
701 }
702
703 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
704 let bf = base_fee as u128;
705 match &self.transaction {
706 ArbTypedTransaction::Eip1559(tx) => Some(core::cmp::min(
707 tx.max_priority_fee_per_gas,
708 tx.max_fee_per_gas.saturating_sub(bf),
709 )),
710 ArbTypedTransaction::Eip7702(tx) => Some(core::cmp::min(
711 tx.max_priority_fee_per_gas,
712 tx.max_fee_per_gas.saturating_sub(bf),
713 )),
714 ArbTypedTransaction::Eip4844(tx) => Some(core::cmp::min(
715 tx.max_priority_fee_per_gas,
716 tx.max_fee_per_gas.saturating_sub(bf),
717 )),
718 _ => None,
719 }
720 }
721
722 fn is_dynamic_fee(&self) -> bool {
723 !matches!(
724 self.transaction,
725 ArbTypedTransaction::Legacy(_) | ArbTypedTransaction::Eip2930(_)
726 )
727 }
728
729 fn kind(&self) -> TxKind {
730 match &self.transaction {
731 ArbTypedTransaction::Legacy(tx) => tx.to,
732 ArbTypedTransaction::Deposit(tx) => TxKind::Call(tx.to),
733 ArbTypedTransaction::Unsigned(tx) => match tx.to {
734 Some(to) => TxKind::Call(to),
735 None => TxKind::Create,
736 },
737 ArbTypedTransaction::Contract(tx) => match tx.to {
738 Some(to) => TxKind::Call(to),
739 None => TxKind::Create,
740 },
741 ArbTypedTransaction::Retry(tx) => match tx.to {
742 Some(to) => TxKind::Call(to),
743 None => TxKind::Create,
744 },
745 ArbTypedTransaction::SubmitRetryable(_) => TxKind::Call(RETRYABLE_ADDRESS),
746 ArbTypedTransaction::Internal(_) => TxKind::Call(ARBOS_ADDRESS),
747 ArbTypedTransaction::Eip2930(tx) => tx.to,
748 ArbTypedTransaction::Eip1559(tx) => tx.to,
749 ArbTypedTransaction::Eip4844(tx) => TxKind::Call(tx.to),
750 ArbTypedTransaction::Eip7702(tx) => TxKind::Call(tx.to),
751 }
752 }
753
754 fn is_create(&self) -> bool {
755 matches!(self.kind(), TxKind::Create)
756 }
757
758 fn value(&self) -> U256 {
759 match &self.transaction {
760 ArbTypedTransaction::Legacy(tx) => tx.value,
761 ArbTypedTransaction::Deposit(tx) => tx.value,
762 ArbTypedTransaction::Unsigned(tx) => tx.value,
763 ArbTypedTransaction::Contract(tx) => tx.value,
764 ArbTypedTransaction::Retry(tx) => tx.value,
765 ArbTypedTransaction::SubmitRetryable(tx) => tx.retry_value,
766 ArbTypedTransaction::Internal(_) => U256::ZERO,
767 ArbTypedTransaction::Eip2930(tx) => tx.value,
768 ArbTypedTransaction::Eip1559(tx) => tx.value,
769 ArbTypedTransaction::Eip4844(tx) => tx.value,
770 ArbTypedTransaction::Eip7702(tx) => tx.value,
771 }
772 }
773
774 fn input(&self) -> &Bytes {
775 match &self.transaction {
776 ArbTypedTransaction::Legacy(tx) => &tx.input,
777 ArbTypedTransaction::Deposit(_) => self.input_cache.get_or_init(Bytes::new),
778 ArbTypedTransaction::Unsigned(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
779 ArbTypedTransaction::Contract(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
780 ArbTypedTransaction::Retry(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
781 ArbTypedTransaction::SubmitRetryable(tx) => self.input_cache.get_or_init(|| {
782 let sel = arb_alloy_predeploys::selector(
783 arb_alloy_predeploys::SIG_RETRY_SUBMIT_RETRYABLE,
784 );
785 let mut out = Vec::with_capacity(4 + tx.retry_data.len());
786 out.extend_from_slice(&sel);
787 out.extend_from_slice(&tx.retry_data);
788 Bytes::from(out)
789 }),
790 ArbTypedTransaction::Internal(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
791 ArbTypedTransaction::Eip2930(tx) => &tx.input,
792 ArbTypedTransaction::Eip1559(tx) => &tx.input,
793 ArbTypedTransaction::Eip4844(tx) => &tx.input,
794 ArbTypedTransaction::Eip7702(tx) => &tx.input,
795 }
796 }
797
798 fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> {
799 match &self.transaction {
800 ArbTypedTransaction::Eip2930(tx) => Some(&tx.access_list),
801 ArbTypedTransaction::Eip1559(tx) => Some(&tx.access_list),
802 ArbTypedTransaction::Eip4844(tx) => Some(&tx.access_list),
803 ArbTypedTransaction::Eip7702(tx) => Some(&tx.access_list),
804 _ => None,
805 }
806 }
807
808 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
809 None
810 }
811
812 fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
813 match &self.transaction {
814 ArbTypedTransaction::Eip7702(tx) => Some(&tx.authorization_list),
815 _ => None,
816 }
817 }
818}
819
820impl serde::Serialize for ArbTransactionSigned {
825 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
826 where
827 S: serde::Serializer,
828 {
829 use serde::ser::SerializeStruct;
830 let mut state = serializer.serialize_struct("ArbTransactionSigned", 2)?;
831 state.serialize_field("signature", &self.signature)?;
832 state.serialize_field("hash", self.tx_hash())?;
833 state.end()
834 }
835}
836
837impl<'de> serde::Deserialize<'de> for ArbTransactionSigned {
838 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
839 where
840 D: serde::Deserializer<'de>,
841 {
842 #[derive(serde::Deserialize)]
843 struct Helper {
844 signature: Signature,
845 #[serde(default)]
846 transaction_encoded_2718: Option<alloy_primitives::Bytes>,
847 }
848 let helper = Helper::deserialize(deserializer)?;
849 if let Some(encoded) = helper.transaction_encoded_2718 {
850 let mut slice: &[u8] = encoded.as_ref();
851 let parsed = Self::network_decode(&mut slice).map_err(serde::de::Error::custom)?;
852 Ok(parsed)
853 } else {
854 Ok(Self::new_unhashed(
856 ArbTypedTransaction::Legacy(TxLegacy::default()),
857 helper.signature,
858 ))
859 }
860 }
861}
862
863impl reth_primitives_traits::serde_bincode_compat::RlpBincode for ArbTransactionSigned {}
868
869impl reth_codecs::Compact for ArbTransactionSigned {
874 fn to_compact<B>(&self, buf: &mut B) -> usize
875 where
876 B: bytes::BufMut + AsMut<[u8]>,
877 {
878 let encoded = self.encoded_2718();
880 let len = encoded.len() as u32;
881 buf.put_u32(len);
882 buf.put_slice(&encoded);
883 let sig_bytes = self.signature.as_bytes();
885 buf.put_slice(&sig_bytes);
886 0
887 }
888
889 fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
890 use bytes::Buf;
891 let mut slice = buf;
892 let tx_len = slice.get_u32() as usize;
893 let tx_bytes = &slice[..tx_len];
894 slice = &slice[tx_len..];
895
896 let mut tx_buf = tx_bytes;
897 let tx = Self::network_decode(&mut tx_buf).unwrap_or_else(|_| {
898 Self::new_unhashed(
899 ArbTypedTransaction::Legacy(TxLegacy::default()),
900 Signature::new(U256::ZERO, U256::ZERO, false),
901 )
902 });
903
904 if slice.len() >= 65 {
906 let _sig_bytes = &slice[..65];
907 slice = &slice[65..];
908 }
909
910 (tx, slice)
911 }
912}
913
914impl reth_db_api::table::Compress for ArbTransactionSigned {
919 type Compressed = Vec<u8>;
920
921 fn compress_to_buf<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) {
922 let _ = reth_codecs::Compact::to_compact(self, buf);
923 }
924}
925
926impl reth_db_api::table::Decompress for ArbTransactionSigned {
927 fn decompress(value: &[u8]) -> Result<Self, reth_db_api::DatabaseError> {
928 let (obj, _) = reth_codecs::Compact::from_compact(value, value.len());
929 Ok(obj)
930 }
931}
932
933#[derive(Debug, Clone)]
939pub struct SubmitRetryableInfo {
940 pub from: Address,
941 pub deposit_value: U256,
942 pub retry_value: U256,
943 pub gas_fee_cap: U256,
944 pub gas: u64,
945 pub retry_to: Option<Address>,
946 pub retry_data: Vec<u8>,
947 pub beneficiary: Address,
948 pub max_submission_fee: U256,
949 pub fee_refund_addr: Address,
950 pub l1_base_fee: U256,
951 pub request_id: B256,
952}
953
954#[derive(Debug, Clone)]
956pub struct RetryTxInfo {
957 pub from: Address,
958 pub ticket_id: B256,
959 pub refund_to: Address,
960 pub gas_fee_cap: U256,
961 pub max_refund: U256,
962 pub submission_fee_refund: U256,
963}
964
965pub trait ArbTransactionExt {
968 fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
969 None
970 }
971 fn retry_tx_info(&self) -> Option<RetryTxInfo> {
972 None
973 }
974 fn poster_units_for(&self, _level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
977 compute()
978 }
979}
980
981impl ArbTransactionExt for ArbTransactionSigned {
982 fn poster_units_for(&self, level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
983 if let Some(&entry) = self.poster_units_cache.get() {
984 let (cached_level, cached_units) = unpack_poster_units(entry);
985 if cached_level == level {
986 return cached_units;
987 }
988 return compute();
989 }
990 let units = compute();
991 let _ = self.poster_units_cache.set(pack_poster_units(level, units));
992 units
993 }
994
995 fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
996 match &self.transaction {
997 ArbTypedTransaction::SubmitRetryable(tx) => Some(SubmitRetryableInfo {
998 from: tx.from,
999 deposit_value: tx.deposit_value,
1000 retry_value: tx.retry_value,
1001 gas_fee_cap: tx.gas_fee_cap,
1002 gas: tx.gas,
1003 retry_to: tx.retry_to,
1004 retry_data: tx.retry_data.to_vec(),
1005 beneficiary: tx.beneficiary,
1006 max_submission_fee: tx.max_submission_fee,
1007 fee_refund_addr: tx.fee_refund_addr,
1008 l1_base_fee: tx.l1_base_fee,
1009 request_id: tx.request_id,
1010 }),
1011 _ => None,
1012 }
1013 }
1014
1015 fn retry_tx_info(&self) -> Option<RetryTxInfo> {
1016 match &self.transaction {
1017 ArbTypedTransaction::Retry(tx) => Some(RetryTxInfo {
1018 from: tx.from,
1019 ticket_id: tx.ticket_id,
1020 refund_to: tx.refund_to,
1021 gas_fee_cap: tx.gas_fee_cap,
1022 max_refund: tx.max_refund,
1023 submission_fee_refund: tx.submission_fee_refund,
1024 }),
1025 _ => None,
1026 }
1027 }
1028}
1029
1030impl<T> ArbTransactionExt for alloy_consensus::EthereumTxEnvelope<T> {}
1032
1033#[cfg(test)]
1034mod tests {
1035 use super::*;
1036
1037 #[test]
1038 fn roundtrip_unsigned_tx() {
1039 let tx = ArbUnsignedTx {
1040 chain_id: U256::from(42161u64),
1041 from: alloy_primitives::address!("00000000000000000000000000000000000000aa"),
1042 nonce: 7,
1043 gas_fee_cap: U256::from(1_000_000u64),
1044 gas: 21000,
1045 to: Some(alloy_primitives::address!(
1046 "00000000000000000000000000000000000000bb"
1047 )),
1048 value: U256::from(123u64),
1049 data: Vec::new().into(),
1050 };
1051
1052 let mut enc = Vec::with_capacity(1 + tx.length());
1053 enc.push(ArbTxType::ArbitrumUnsignedTx.as_u8());
1054 tx.encode(&mut enc);
1055
1056 let signed =
1057 ArbTransactionSigned::decode_2718_exact(enc.as_slice()).expect("typed decode ok");
1058 assert_eq!(signed.tx_type(), ArbTxTypeLocal::Unsigned);
1059 assert_eq!(signed.chain_id(), Some(42161));
1060 assert_eq!(signed.nonce(), 7);
1061 assert_eq!(signed.gas_limit(), 21000);
1062 assert_eq!(signed.value(), U256::from(123u64));
1063 }
1064
1065 #[test]
1066 fn deposit_tx_has_zero_gas() {
1067 let tx = ArbDepositTx {
1068 chain_id: U256::from(42161u64),
1069 l1_request_id: B256::ZERO,
1070 from: Address::ZERO,
1071 to: Address::ZERO,
1072 value: U256::from(100u64),
1073 };
1074
1075 let signed = ArbTransactionSigned::new_unhashed(
1076 ArbTypedTransaction::Deposit(tx),
1077 ArbTransactionSigned::zero_sig(),
1078 );
1079
1080 assert_eq!(signed.gas_limit(), 0);
1081 assert_eq!(signed.nonce(), 0);
1082 assert_eq!(signed.tx_type(), ArbTxTypeLocal::Deposit);
1083 }
1084}