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
24const ARBOS_ADDRESS: Address =
26 alloy_primitives::address!("00000000000000000000000000000000000A4B05");
27
28const RETRYABLE_ADDRESS: Address =
30 alloy_primitives::address!("000000000000000000000000000000000000006e");
31
32#[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#[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 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#[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 sender_cache: reth_primitives_traits::sync::OnceLock<Address>,
122 poster_units_cache: reth_primitives_traits::sync::OnceLock<u64>,
126}
127
128impl Deref for ArbTransactionSigned {
129 type Target = ArbTypedTransaction;
130 fn deref(&self) -> &Self::Target {
131 &self.transaction
132 }
133}
134
135impl ArbTransactionSigned {
136 pub fn new(transaction: ArbTypedTransaction, signature: Signature, hash: B256) -> Self {
137 Self {
138 hash: hash.into(),
139 signature,
140 transaction,
141 input_cache: Default::default(),
142 sender_cache: Default::default(),
143 poster_units_cache: Default::default(),
144 }
145 }
146
147 pub fn new_unhashed(transaction: ArbTypedTransaction, signature: Signature) -> Self {
148 Self {
149 hash: Default::default(),
150 signature,
151 transaction,
152 input_cache: Default::default(),
153 sender_cache: Default::default(),
154 poster_units_cache: Default::default(),
155 }
156 }
157
158 pub fn from_envelope(
160 envelope: alloy_consensus::EthereumTxEnvelope<alloy_consensus::TxEip4844>,
161 ) -> Self {
162 use alloy_consensus::EthereumTxEnvelope;
163 match envelope {
164 EthereumTxEnvelope::Legacy(signed) => {
165 let (tx, sig, hash) = signed.into_parts();
166 Self::new(ArbTypedTransaction::Legacy(tx), sig, hash)
167 }
168 EthereumTxEnvelope::Eip2930(signed) => {
169 let (tx, sig, hash) = signed.into_parts();
170 Self::new(ArbTypedTransaction::Eip2930(tx), sig, hash)
171 }
172 EthereumTxEnvelope::Eip1559(signed) => {
173 let (tx, sig, hash) = signed.into_parts();
174 Self::new(ArbTypedTransaction::Eip1559(tx), sig, hash)
175 }
176 EthereumTxEnvelope::Eip4844(signed) => {
177 let (tx, sig, hash) = signed.into_parts();
178 Self::new(ArbTypedTransaction::Eip4844(tx), sig, hash)
179 }
180 EthereumTxEnvelope::Eip7702(signed) => {
181 let (tx, sig, hash) = signed.into_parts();
182 Self::new(ArbTypedTransaction::Eip7702(tx), sig, hash)
183 }
184 }
185 }
186
187 pub const fn signature(&self) -> &Signature {
188 &self.signature
189 }
190
191 pub fn inner(&self) -> &ArbTypedTransaction {
193 &self.transaction
194 }
195
196 pub fn split(self) -> (ArbTypedTransaction, Signature, B256) {
198 let hash = *self.hash.get_or_init(|| self.compute_hash());
199 (self.transaction, self.signature, hash)
200 }
201
202 pub const fn tx_type(&self) -> ArbTxTypeLocal {
203 match &self.transaction {
204 ArbTypedTransaction::Deposit(_) => ArbTxTypeLocal::Deposit,
205 ArbTypedTransaction::Unsigned(_) => ArbTxTypeLocal::Unsigned,
206 ArbTypedTransaction::Contract(_) => ArbTxTypeLocal::Contract,
207 ArbTypedTransaction::Retry(_) => ArbTxTypeLocal::Retry,
208 ArbTypedTransaction::SubmitRetryable(_) => ArbTxTypeLocal::SubmitRetryable,
209 ArbTypedTransaction::Internal(_) => ArbTxTypeLocal::Internal,
210 ArbTypedTransaction::Legacy(_) => ArbTxTypeLocal::Legacy,
211 ArbTypedTransaction::Eip2930(_) => ArbTxTypeLocal::Eip2930,
212 ArbTypedTransaction::Eip1559(_) => ArbTxTypeLocal::Eip1559,
213 ArbTypedTransaction::Eip4844(_) => ArbTxTypeLocal::Eip4844,
214 ArbTypedTransaction::Eip7702(_) => ArbTxTypeLocal::Eip7702,
215 }
216 }
217
218 fn compute_hash(&self) -> B256 {
219 keccak256(self.encoded_2718())
220 }
221
222 fn zero_sig() -> Signature {
223 Signature::new(U256::ZERO, U256::ZERO, false)
224 }
225
226 pub fn poster_units_cached<F: FnOnce() -> u64>(&self, level: u64, compute: F) -> u64 {
231 if let Some(&entry) = self.poster_units_cache.get() {
232 let (cached_level, cached_units) = unpack_poster_units(entry);
233 if cached_level == level {
234 return cached_units;
235 }
236 return compute();
237 }
238 let units = compute();
239 let _ = self.poster_units_cache.set(pack_poster_units(level, units));
240 units
241 }
242}
243
244#[inline]
245fn pack_poster_units(level: u64, units: u64) -> u64 {
246 ((level & 0xFF) << 56) | (units & 0x00FF_FFFF_FFFF_FFFF)
247}
248
249#[inline]
250fn unpack_poster_units(packed: u64) -> (u64, u64) {
251 let level = (packed >> 56) & 0xFF;
252 let units = packed & 0x00FF_FFFF_FFFF_FFFF;
253 (level, units)
254}
255
256impl Hash for ArbTransactionSigned {
261 fn hash<H: Hasher>(&self, state: &mut H) {
262 self.tx_hash().hash(state)
263 }
264}
265
266impl PartialEq for ArbTransactionSigned {
267 fn eq(&self, other: &Self) -> bool {
268 self.tx_hash() == other.tx_hash()
269 }
270}
271
272impl InMemorySize for ArbTransactionSigned {
273 fn size(&self) -> usize {
274 core::mem::size_of::<TxHash>() + core::mem::size_of::<Signature>()
275 }
276}
277
278impl TxHashRef for ArbTransactionSigned {
283 fn tx_hash(&self) -> &TxHash {
284 self.hash.get_or_init(|| self.compute_hash())
285 }
286}
287
288impl SignedTransaction for ArbTransactionSigned {
293 fn recalculate_hash(&self) -> B256 {
294 keccak256(self.encoded_2718())
295 }
296}
297
298impl ArbTransactionSigned {
303 fn recover_signer_inner(
304 &self,
305 strict: bool,
306 ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
307 match &self.transaction {
308 ArbTypedTransaction::Deposit(tx) => Ok(tx.from),
309 ArbTypedTransaction::Unsigned(tx) => Ok(tx.from),
310 ArbTypedTransaction::Contract(tx) => Ok(tx.from),
311 ArbTypedTransaction::Retry(tx) => Ok(tx.from),
312 ArbTypedTransaction::SubmitRetryable(tx) => Ok(tx.from),
313 ArbTypedTransaction::Internal(_) => Ok(ARBOS_ADDRESS),
314 ArbTypedTransaction::Legacy(tx) => {
315 let mut buf = Vec::new();
316 tx.encode_for_signing(&mut buf);
317 if strict {
318 recover_signer(&self.signature, keccak256(&buf))
319 } else {
320 recover_signer_unchecked(&self.signature, keccak256(&buf))
321 }
322 }
323 ArbTypedTransaction::Eip2930(tx) => {
324 let mut buf = Vec::new();
325 tx.encode_for_signing(&mut buf);
326 if strict {
327 recover_signer(&self.signature, keccak256(&buf))
328 } else {
329 recover_signer_unchecked(&self.signature, keccak256(&buf))
330 }
331 }
332 ArbTypedTransaction::Eip1559(tx) => {
333 let mut buf = Vec::new();
334 tx.encode_for_signing(&mut buf);
335 if strict {
336 recover_signer(&self.signature, keccak256(&buf))
337 } else {
338 recover_signer_unchecked(&self.signature, keccak256(&buf))
339 }
340 }
341 ArbTypedTransaction::Eip4844(tx) => {
342 let mut buf = Vec::new();
343 tx.encode_for_signing(&mut buf);
344 if strict {
345 recover_signer(&self.signature, keccak256(&buf))
346 } else {
347 recover_signer_unchecked(&self.signature, keccak256(&buf))
348 }
349 }
350 ArbTypedTransaction::Eip7702(tx) => {
351 let mut buf = Vec::new();
352 tx.encode_for_signing(&mut buf);
353 if strict {
354 recover_signer(&self.signature, keccak256(&buf))
355 } else {
356 recover_signer_unchecked(&self.signature, keccak256(&buf))
357 }
358 }
359 }
360 }
361}
362
363impl alloy_consensus::transaction::SignerRecoverable for ArbTransactionSigned {
364 fn recover_signer(
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(true)?;
371 let _ = self.sender_cache.set(addr);
372 Ok(addr)
373 }
374
375 fn recover_signer_unchecked(
376 &self,
377 ) -> Result<Address, reth_primitives_traits::transaction::signed::RecoveryError> {
378 if let Some(addr) = self.sender_cache.get() {
379 return Ok(*addr);
380 }
381 let addr = self.recover_signer_inner(false)?;
382 let _ = self.sender_cache.set(addr);
383 Ok(addr)
384 }
385}
386
387impl Typed2718 for ArbTransactionSigned {
392 fn is_legacy(&self) -> bool {
393 matches!(self.transaction, ArbTypedTransaction::Legacy(_))
394 }
395
396 fn ty(&self) -> u8 {
397 match &self.transaction {
398 ArbTypedTransaction::Legacy(_) => 0u8,
399 ArbTypedTransaction::Deposit(_) => ArbTxType::ArbitrumDepositTx.as_u8(),
400 ArbTypedTransaction::Unsigned(_) => ArbTxType::ArbitrumUnsignedTx.as_u8(),
401 ArbTypedTransaction::Contract(_) => ArbTxType::ArbitrumContractTx.as_u8(),
402 ArbTypedTransaction::Retry(_) => ArbTxType::ArbitrumRetryTx.as_u8(),
403 ArbTypedTransaction::SubmitRetryable(_) => ArbTxType::ArbitrumSubmitRetryableTx.as_u8(),
404 ArbTypedTransaction::Internal(_) => ArbTxType::ArbitrumInternalTx.as_u8(),
405 ArbTypedTransaction::Eip2930(_) => 0x01,
406 ArbTypedTransaction::Eip1559(_) => 0x02,
407 ArbTypedTransaction::Eip4844(_) => 0x03,
408 ArbTypedTransaction::Eip7702(_) => 0x04,
409 }
410 }
411}
412
413impl IsTyped2718 for ArbTransactionSigned {
418 fn is_type(type_id: u8) -> bool {
419 matches!(type_id, 0x01..=0x04) || ArbTxType::from_u8(type_id).is_ok()
421 }
422}
423
424impl Encodable2718 for ArbTransactionSigned {
429 fn type_flag(&self) -> Option<u8> {
430 if self.is_legacy() {
431 None
432 } else {
433 Some(self.ty())
434 }
435 }
436
437 fn encode_2718_len(&self) -> usize {
438 match &self.transaction {
439 ArbTypedTransaction::Legacy(tx) => tx.eip2718_encoded_length(&self.signature),
440 ArbTypedTransaction::Deposit(tx) => tx.length() + 1,
441 ArbTypedTransaction::Unsigned(tx) => tx.length() + 1,
442 ArbTypedTransaction::Contract(tx) => tx.length() + 1,
443 ArbTypedTransaction::Retry(tx) => tx.length() + 1,
444 ArbTypedTransaction::SubmitRetryable(tx) => tx.length() + 1,
445 ArbTypedTransaction::Internal(tx) => tx.length() + 1,
446 ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encoded_length(&self.signature),
447 ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encoded_length(&self.signature),
448 ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encoded_length(&self.signature),
449 ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encoded_length(&self.signature),
450 }
451 }
452
453 fn encode_2718(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
454 match &self.transaction {
455 ArbTypedTransaction::Legacy(tx) => tx.eip2718_encode(&self.signature, out),
456 ArbTypedTransaction::Deposit(tx) => {
457 out.put_u8(ArbTxType::ArbitrumDepositTx.as_u8());
458 tx.encode(out);
459 }
460 ArbTypedTransaction::Unsigned(tx) => {
461 out.put_u8(ArbTxType::ArbitrumUnsignedTx.as_u8());
462 tx.encode(out);
463 }
464 ArbTypedTransaction::Contract(tx) => {
465 out.put_u8(ArbTxType::ArbitrumContractTx.as_u8());
466 tx.encode(out);
467 }
468 ArbTypedTransaction::Retry(tx) => {
469 out.put_u8(ArbTxType::ArbitrumRetryTx.as_u8());
470 tx.encode(out);
471 }
472 ArbTypedTransaction::SubmitRetryable(tx) => {
473 out.put_u8(ArbTxType::ArbitrumSubmitRetryableTx.as_u8());
474 tx.encode(out);
475 }
476 ArbTypedTransaction::Internal(tx) => {
477 out.put_u8(ArbTxType::ArbitrumInternalTx.as_u8());
478 tx.encode(out);
479 }
480 ArbTypedTransaction::Eip2930(tx) => tx.eip2718_encode(&self.signature, out),
481 ArbTypedTransaction::Eip1559(tx) => tx.eip2718_encode(&self.signature, out),
482 ArbTypedTransaction::Eip4844(tx) => tx.eip2718_encode(&self.signature, out),
483 ArbTypedTransaction::Eip7702(tx) => tx.eip2718_encode(&self.signature, out),
484 }
485 }
486}
487
488impl Decodable2718 for ArbTransactionSigned {
493 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
494 if let Ok(kind) = ArbTxType::from_u8(ty) {
496 return Ok(match kind {
497 ArbTxType::ArbitrumDepositTx => {
498 let tx = ArbDepositTx::decode(buf)?;
499 Self::new_unhashed(ArbTypedTransaction::Deposit(tx), Self::zero_sig())
500 }
501 ArbTxType::ArbitrumUnsignedTx => {
502 let tx = ArbUnsignedTx::decode(buf)?;
503 Self::new_unhashed(ArbTypedTransaction::Unsigned(tx), Self::zero_sig())
504 }
505 ArbTxType::ArbitrumContractTx => {
506 let tx = ArbContractTx::decode(buf)?;
507 Self::new_unhashed(ArbTypedTransaction::Contract(tx), Self::zero_sig())
508 }
509 ArbTxType::ArbitrumRetryTx => {
510 let tx = ArbRetryTx::decode(buf)?;
511 Self::new_unhashed(ArbTypedTransaction::Retry(tx), Self::zero_sig())
512 }
513 ArbTxType::ArbitrumSubmitRetryableTx => {
514 let tx = ArbSubmitRetryableTx::decode(buf)?;
515 Self::new_unhashed(ArbTypedTransaction::SubmitRetryable(tx), Self::zero_sig())
516 }
517 ArbTxType::ArbitrumInternalTx => {
518 let tx = ArbInternalTx::decode(buf)?;
519 Self::new_unhashed(ArbTypedTransaction::Internal(tx), Self::zero_sig())
520 }
521 ArbTxType::ArbitrumLegacyTx => return Err(Eip2718Error::UnexpectedType(0x78)),
522 });
523 }
524
525 match alloy_consensus::TxType::try_from(ty).map_err(|_| Eip2718Error::UnexpectedType(ty))? {
527 alloy_consensus::TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
528 alloy_consensus::TxType::Eip2930 => {
529 let (tx, sig) = alloy_consensus::TxEip2930::rlp_decode_with_signature(buf)?;
530 Ok(Self::new_unhashed(ArbTypedTransaction::Eip2930(tx), sig))
531 }
532 alloy_consensus::TxType::Eip1559 => {
533 let (tx, sig) = alloy_consensus::TxEip1559::rlp_decode_with_signature(buf)?;
534 Ok(Self::new_unhashed(ArbTypedTransaction::Eip1559(tx), sig))
535 }
536 alloy_consensus::TxType::Eip4844 => {
537 let (tx, sig) = alloy_consensus::TxEip4844::rlp_decode_with_signature(buf)?;
538 Ok(Self::new_unhashed(ArbTypedTransaction::Eip4844(tx), sig))
539 }
540 alloy_consensus::TxType::Eip7702 => {
541 let (tx, sig) = alloy_consensus::TxEip7702::rlp_decode_with_signature(buf)?;
542 Ok(Self::new_unhashed(ArbTypedTransaction::Eip7702(tx), sig))
543 }
544 }
545 }
546
547 fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
548 let (tx, sig, hash) = TxLegacy::rlp_decode_signed(buf)?.into_parts();
549 let signed_tx = Self::new_unhashed(ArbTypedTransaction::Legacy(tx), sig);
550 signed_tx.hash.get_or_init(|| hash);
551 Ok(signed_tx)
552 }
553}
554
555impl Encodable for ArbTransactionSigned {
560 fn encode(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
561 self.network_encode(out);
562 }
563 fn length(&self) -> usize {
564 let mut payload_length = self.encode_2718_len();
565 if !self.is_legacy() {
566 payload_length += alloy_rlp::Header {
567 list: false,
568 payload_length,
569 }
570 .length();
571 }
572 payload_length
573 }
574}
575
576impl Decodable for ArbTransactionSigned {
577 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
578 Self::network_decode(buf).map_err(Into::into)
579 }
580}
581
582impl ConsensusTx for ArbTransactionSigned {
587 fn chain_id(&self) -> Option<u64> {
588 match &self.transaction {
589 ArbTypedTransaction::Legacy(tx) => tx.chain_id,
590 ArbTypedTransaction::Deposit(tx) => Some(tx.chain_id.to::<u64>()),
591 ArbTypedTransaction::Unsigned(tx) => Some(tx.chain_id.to::<u64>()),
592 ArbTypedTransaction::Contract(tx) => Some(tx.chain_id.to::<u64>()),
593 ArbTypedTransaction::Retry(tx) => Some(tx.chain_id.to::<u64>()),
594 ArbTypedTransaction::SubmitRetryable(tx) => Some(tx.chain_id.to::<u64>()),
595 ArbTypedTransaction::Internal(tx) => Some(tx.chain_id.to::<u64>()),
596 ArbTypedTransaction::Eip2930(tx) => Some(tx.chain_id),
597 ArbTypedTransaction::Eip1559(tx) => Some(tx.chain_id),
598 ArbTypedTransaction::Eip4844(tx) => Some(tx.chain_id),
599 ArbTypedTransaction::Eip7702(tx) => Some(tx.chain_id),
600 }
601 }
602
603 fn nonce(&self) -> u64 {
604 match &self.transaction {
605 ArbTypedTransaction::Legacy(tx) => tx.nonce,
606 ArbTypedTransaction::Deposit(_) => 0,
607 ArbTypedTransaction::Unsigned(tx) => tx.nonce,
608 ArbTypedTransaction::Contract(_) => 0,
609 ArbTypedTransaction::Retry(tx) => tx.nonce,
610 ArbTypedTransaction::SubmitRetryable(_) => 0,
611 ArbTypedTransaction::Internal(_) => 0,
612 ArbTypedTransaction::Eip2930(tx) => tx.nonce,
613 ArbTypedTransaction::Eip1559(tx) => tx.nonce,
614 ArbTypedTransaction::Eip4844(tx) => tx.nonce,
615 ArbTypedTransaction::Eip7702(tx) => tx.nonce,
616 }
617 }
618
619 fn gas_limit(&self) -> u64 {
620 match &self.transaction {
621 ArbTypedTransaction::Legacy(tx) => tx.gas_limit,
622 ArbTypedTransaction::Deposit(_) => 0,
623 ArbTypedTransaction::Unsigned(tx) => tx.gas,
624 ArbTypedTransaction::Contract(tx) => tx.gas,
625 ArbTypedTransaction::Retry(tx) => tx.gas,
626 ArbTypedTransaction::SubmitRetryable(tx) => tx.gas,
627 ArbTypedTransaction::Internal(_) => 0,
628 ArbTypedTransaction::Eip2930(tx) => tx.gas_limit,
629 ArbTypedTransaction::Eip1559(tx) => tx.gas_limit,
630 ArbTypedTransaction::Eip4844(tx) => tx.gas_limit,
631 ArbTypedTransaction::Eip7702(tx) => tx.gas_limit,
632 }
633 }
634
635 fn gas_price(&self) -> Option<u128> {
636 match &self.transaction {
637 ArbTypedTransaction::Legacy(tx) => Some(tx.gas_price),
638 ArbTypedTransaction::Eip2930(tx) => Some(tx.gas_price),
639 _ => None,
640 }
641 }
642
643 fn max_fee_per_gas(&self) -> u128 {
644 match &self.transaction {
645 ArbTypedTransaction::Legacy(tx) => tx.gas_price,
646 ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
647 ArbTypedTransaction::Unsigned(tx) => tx.gas_fee_cap.to::<u128>(),
648 ArbTypedTransaction::Contract(tx) => tx.gas_fee_cap.to::<u128>(),
649 ArbTypedTransaction::Retry(tx) => tx.gas_fee_cap.to::<u128>(),
650 ArbTypedTransaction::SubmitRetryable(tx) => tx.gas_fee_cap.to::<u128>(),
651 ArbTypedTransaction::Eip1559(tx) => tx.max_fee_per_gas,
652 ArbTypedTransaction::Eip4844(tx) => tx.max_fee_per_gas,
653 ArbTypedTransaction::Eip7702(tx) => tx.max_fee_per_gas,
654 _ => 0,
655 }
656 }
657
658 fn max_priority_fee_per_gas(&self) -> Option<u128> {
659 match &self.transaction {
660 ArbTypedTransaction::Eip1559(tx) => Some(tx.max_priority_fee_per_gas),
661 ArbTypedTransaction::Eip4844(tx) => Some(tx.max_priority_fee_per_gas),
662 ArbTypedTransaction::Eip7702(tx) => Some(tx.max_priority_fee_per_gas),
663 _ => None,
665 }
666 }
667
668 fn max_fee_per_blob_gas(&self) -> Option<u128> {
669 match &self.transaction {
670 ArbTypedTransaction::Eip4844(tx) => Some(tx.max_fee_per_blob_gas),
671 _ => None,
672 }
673 }
674
675 fn priority_fee_or_price(&self) -> u128 {
676 match self.max_priority_fee_per_gas() {
677 Some(p) => p,
678 None => self.gas_price().unwrap_or(0),
679 }
680 }
681
682 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
683 let bf = base_fee.unwrap_or(0) as u128;
684 match &self.transaction {
685 ArbTypedTransaction::Legacy(tx) => tx.gas_price,
686 ArbTypedTransaction::Eip2930(tx) => tx.gas_price,
687 ArbTypedTransaction::Eip1559(tx) => core::cmp::min(
688 tx.max_fee_per_gas,
689 bf.saturating_add(tx.max_priority_fee_per_gas),
690 ),
691 ArbTypedTransaction::Eip7702(tx) => core::cmp::min(
692 tx.max_fee_per_gas,
693 bf.saturating_add(tx.max_priority_fee_per_gas),
694 ),
695 ArbTypedTransaction::Eip4844(tx) => core::cmp::min(
696 tx.max_fee_per_gas,
697 bf.saturating_add(tx.max_priority_fee_per_gas),
698 ),
699 _ => bf,
701 }
702 }
703
704 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
705 let bf = base_fee as u128;
706 match &self.transaction {
707 ArbTypedTransaction::Eip1559(tx) => Some(core::cmp::min(
708 tx.max_priority_fee_per_gas,
709 tx.max_fee_per_gas.saturating_sub(bf),
710 )),
711 ArbTypedTransaction::Eip7702(tx) => Some(core::cmp::min(
712 tx.max_priority_fee_per_gas,
713 tx.max_fee_per_gas.saturating_sub(bf),
714 )),
715 ArbTypedTransaction::Eip4844(tx) => Some(core::cmp::min(
716 tx.max_priority_fee_per_gas,
717 tx.max_fee_per_gas.saturating_sub(bf),
718 )),
719 _ => None,
720 }
721 }
722
723 fn is_dynamic_fee(&self) -> bool {
724 !matches!(
725 self.transaction,
726 ArbTypedTransaction::Legacy(_) | ArbTypedTransaction::Eip2930(_)
727 )
728 }
729
730 fn kind(&self) -> TxKind {
731 match &self.transaction {
732 ArbTypedTransaction::Legacy(tx) => tx.to,
733 ArbTypedTransaction::Deposit(tx) => {
734 if tx.to == Address::ZERO {
735 TxKind::Create
736 } else {
737 TxKind::Call(tx.to)
738 }
739 }
740 ArbTypedTransaction::Unsigned(tx) => match tx.to {
741 Some(to) => TxKind::Call(to),
742 None => TxKind::Create,
743 },
744 ArbTypedTransaction::Contract(tx) => match tx.to {
745 Some(to) => TxKind::Call(to),
746 None => TxKind::Create,
747 },
748 ArbTypedTransaction::Retry(tx) => match tx.to {
749 Some(to) => TxKind::Call(to),
750 None => TxKind::Create,
751 },
752 ArbTypedTransaction::SubmitRetryable(_) => TxKind::Call(RETRYABLE_ADDRESS),
753 ArbTypedTransaction::Internal(_) => TxKind::Call(ARBOS_ADDRESS),
754 ArbTypedTransaction::Eip2930(tx) => tx.to,
755 ArbTypedTransaction::Eip1559(tx) => tx.to,
756 ArbTypedTransaction::Eip4844(tx) => TxKind::Call(tx.to),
757 ArbTypedTransaction::Eip7702(tx) => TxKind::Call(tx.to),
758 }
759 }
760
761 fn is_create(&self) -> bool {
762 matches!(self.kind(), TxKind::Create)
763 }
764
765 fn value(&self) -> U256 {
766 match &self.transaction {
767 ArbTypedTransaction::Legacy(tx) => tx.value,
768 ArbTypedTransaction::Deposit(tx) => tx.value,
769 ArbTypedTransaction::Unsigned(tx) => tx.value,
770 ArbTypedTransaction::Contract(tx) => tx.value,
771 ArbTypedTransaction::Retry(tx) => tx.value,
772 ArbTypedTransaction::SubmitRetryable(tx) => tx.retry_value,
773 ArbTypedTransaction::Internal(_) => U256::ZERO,
774 ArbTypedTransaction::Eip2930(tx) => tx.value,
775 ArbTypedTransaction::Eip1559(tx) => tx.value,
776 ArbTypedTransaction::Eip4844(tx) => tx.value,
777 ArbTypedTransaction::Eip7702(tx) => tx.value,
778 }
779 }
780
781 fn input(&self) -> &Bytes {
782 match &self.transaction {
783 ArbTypedTransaction::Legacy(tx) => &tx.input,
784 ArbTypedTransaction::Deposit(_) => self.input_cache.get_or_init(Bytes::new),
785 ArbTypedTransaction::Unsigned(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
786 ArbTypedTransaction::Contract(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
787 ArbTypedTransaction::Retry(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
788 ArbTypedTransaction::SubmitRetryable(tx) => self.input_cache.get_or_init(|| {
789 let sel = arb_alloy_predeploys::selector(
790 arb_alloy_predeploys::SIG_RETRY_SUBMIT_RETRYABLE,
791 );
792 let mut out = Vec::with_capacity(4 + tx.retry_data.len());
793 out.extend_from_slice(&sel);
794 out.extend_from_slice(&tx.retry_data);
795 Bytes::from(out)
796 }),
797 ArbTypedTransaction::Internal(tx) => self.input_cache.get_or_init(|| tx.data.clone()),
798 ArbTypedTransaction::Eip2930(tx) => &tx.input,
799 ArbTypedTransaction::Eip1559(tx) => &tx.input,
800 ArbTypedTransaction::Eip4844(tx) => &tx.input,
801 ArbTypedTransaction::Eip7702(tx) => &tx.input,
802 }
803 }
804
805 fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> {
806 match &self.transaction {
807 ArbTypedTransaction::Eip2930(tx) => Some(&tx.access_list),
808 ArbTypedTransaction::Eip1559(tx) => Some(&tx.access_list),
809 ArbTypedTransaction::Eip4844(tx) => Some(&tx.access_list),
810 ArbTypedTransaction::Eip7702(tx) => Some(&tx.access_list),
811 _ => None,
812 }
813 }
814
815 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
816 None
817 }
818
819 fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
820 match &self.transaction {
821 ArbTypedTransaction::Eip7702(tx) => Some(&tx.authorization_list),
822 _ => None,
823 }
824 }
825}
826
827impl serde::Serialize for ArbTransactionSigned {
832 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
833 where
834 S: serde::Serializer,
835 {
836 use serde::ser::SerializeStruct;
837 let mut state = serializer.serialize_struct("ArbTransactionSigned", 2)?;
838 state.serialize_field("signature", &self.signature)?;
839 state.serialize_field("hash", self.tx_hash())?;
840 state.end()
841 }
842}
843
844impl<'de> serde::Deserialize<'de> for ArbTransactionSigned {
845 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
846 where
847 D: serde::Deserializer<'de>,
848 {
849 #[derive(serde::Deserialize)]
850 struct Helper {
851 signature: Signature,
852 #[serde(default)]
853 transaction_encoded_2718: Option<alloy_primitives::Bytes>,
854 }
855 let helper = Helper::deserialize(deserializer)?;
856 if let Some(encoded) = helper.transaction_encoded_2718 {
857 let mut slice: &[u8] = encoded.as_ref();
858 let parsed = Self::network_decode(&mut slice).map_err(serde::de::Error::custom)?;
859 Ok(parsed)
860 } else {
861 Ok(Self::new_unhashed(
863 ArbTypedTransaction::Legacy(TxLegacy::default()),
864 helper.signature,
865 ))
866 }
867 }
868}
869
870impl reth_primitives_traits::serde_bincode_compat::RlpBincode for ArbTransactionSigned {}
875
876impl reth_codecs::Compact for ArbTransactionSigned {
881 fn to_compact<B>(&self, buf: &mut B) -> usize
882 where
883 B: bytes::BufMut + AsMut<[u8]>,
884 {
885 let encoded = self.encoded_2718();
887 let len = encoded.len() as u32;
888 buf.put_u32(len);
889 buf.put_slice(&encoded);
890 let sig_bytes = self.signature.as_bytes();
892 buf.put_slice(&sig_bytes);
893 0
894 }
895
896 fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
897 use bytes::Buf;
898 let mut slice = buf;
899 let tx_len = slice.get_u32() as usize;
900 let tx_bytes = &slice[..tx_len];
901 slice = &slice[tx_len..];
902
903 let mut tx_buf = tx_bytes;
904 let tx = Self::network_decode(&mut tx_buf).unwrap_or_else(|_| {
905 Self::new_unhashed(
906 ArbTypedTransaction::Legacy(TxLegacy::default()),
907 Signature::new(U256::ZERO, U256::ZERO, false),
908 )
909 });
910
911 if slice.len() >= 65 {
913 let _sig_bytes = &slice[..65];
914 slice = &slice[65..];
915 }
916
917 (tx, slice)
918 }
919}
920
921impl reth_db_api::table::Compress for ArbTransactionSigned {
926 type Compressed = Vec<u8>;
927
928 fn compress_to_buf<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) {
929 let _ = reth_codecs::Compact::to_compact(self, buf);
930 }
931}
932
933impl reth_db_api::table::Decompress for ArbTransactionSigned {
934 fn decompress(value: &[u8]) -> Result<Self, reth_db_api::DatabaseError> {
935 let (obj, _) = reth_codecs::Compact::from_compact(value, value.len());
936 Ok(obj)
937 }
938}
939
940#[derive(Debug, Clone)]
946pub struct SubmitRetryableInfo {
947 pub from: Address,
948 pub deposit_value: U256,
949 pub retry_value: U256,
950 pub gas_fee_cap: U256,
951 pub gas: u64,
952 pub retry_to: Option<Address>,
953 pub retry_data: Vec<u8>,
954 pub beneficiary: Address,
955 pub max_submission_fee: U256,
956 pub fee_refund_addr: Address,
957 pub l1_base_fee: U256,
958 pub request_id: B256,
959}
960
961#[derive(Debug, Clone)]
963pub struct RetryTxInfo {
964 pub from: Address,
965 pub ticket_id: B256,
966 pub refund_to: Address,
967 pub gas_fee_cap: U256,
968 pub max_refund: U256,
969 pub submission_fee_refund: U256,
970}
971
972pub trait ArbTransactionExt {
975 fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
976 None
977 }
978 fn retry_tx_info(&self) -> Option<RetryTxInfo> {
979 None
980 }
981 fn poster_units_for(&self, _level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
984 compute()
985 }
986}
987
988impl ArbTransactionExt for ArbTransactionSigned {
989 fn poster_units_for(&self, level: u64, compute: &mut dyn FnMut() -> u64) -> u64 {
990 if let Some(&entry) = self.poster_units_cache.get() {
991 let (cached_level, cached_units) = unpack_poster_units(entry);
992 if cached_level == level {
993 return cached_units;
994 }
995 return compute();
996 }
997 let units = compute();
998 let _ = self.poster_units_cache.set(pack_poster_units(level, units));
999 units
1000 }
1001
1002 fn submit_retryable_info(&self) -> Option<SubmitRetryableInfo> {
1003 match &self.transaction {
1004 ArbTypedTransaction::SubmitRetryable(tx) => Some(SubmitRetryableInfo {
1005 from: tx.from,
1006 deposit_value: tx.deposit_value,
1007 retry_value: tx.retry_value,
1008 gas_fee_cap: tx.gas_fee_cap,
1009 gas: tx.gas,
1010 retry_to: tx.retry_to,
1011 retry_data: tx.retry_data.to_vec(),
1012 beneficiary: tx.beneficiary,
1013 max_submission_fee: tx.max_submission_fee,
1014 fee_refund_addr: tx.fee_refund_addr,
1015 l1_base_fee: tx.l1_base_fee,
1016 request_id: tx.request_id,
1017 }),
1018 _ => None,
1019 }
1020 }
1021
1022 fn retry_tx_info(&self) -> Option<RetryTxInfo> {
1023 match &self.transaction {
1024 ArbTypedTransaction::Retry(tx) => Some(RetryTxInfo {
1025 from: tx.from,
1026 ticket_id: tx.ticket_id,
1027 refund_to: tx.refund_to,
1028 gas_fee_cap: tx.gas_fee_cap,
1029 max_refund: tx.max_refund,
1030 submission_fee_refund: tx.submission_fee_refund,
1031 }),
1032 _ => None,
1033 }
1034 }
1035}
1036
1037impl<T> ArbTransactionExt for alloy_consensus::EthereumTxEnvelope<T> {}
1039
1040#[cfg(test)]
1041mod tests {
1042 use super::*;
1043
1044 #[test]
1045 fn roundtrip_unsigned_tx() {
1046 let tx = ArbUnsignedTx {
1047 chain_id: U256::from(42161u64),
1048 from: alloy_primitives::address!("00000000000000000000000000000000000000aa"),
1049 nonce: 7,
1050 gas_fee_cap: U256::from(1_000_000u64),
1051 gas: 21000,
1052 to: Some(alloy_primitives::address!(
1053 "00000000000000000000000000000000000000bb"
1054 )),
1055 value: U256::from(123u64),
1056 data: Vec::new().into(),
1057 };
1058
1059 let mut enc = Vec::with_capacity(1 + tx.length());
1060 enc.push(ArbTxType::ArbitrumUnsignedTx.as_u8());
1061 tx.encode(&mut enc);
1062
1063 let signed =
1064 ArbTransactionSigned::decode_2718_exact(enc.as_slice()).expect("typed decode ok");
1065 assert_eq!(signed.tx_type(), ArbTxTypeLocal::Unsigned);
1066 assert_eq!(signed.chain_id(), Some(42161));
1067 assert_eq!(signed.nonce(), 7);
1068 assert_eq!(signed.gas_limit(), 21000);
1069 assert_eq!(signed.value(), U256::from(123u64));
1070 }
1071
1072 #[test]
1073 fn deposit_tx_has_zero_gas() {
1074 let tx = ArbDepositTx {
1075 chain_id: U256::from(42161u64),
1076 l1_request_id: B256::ZERO,
1077 from: Address::ZERO,
1078 to: Address::ZERO,
1079 value: U256::from(100u64),
1080 };
1081
1082 let signed = ArbTransactionSigned::new_unhashed(
1083 ArbTypedTransaction::Deposit(tx),
1084 ArbTransactionSigned::zero_sig(),
1085 );
1086
1087 assert_eq!(signed.gas_limit(), 0);
1088 assert_eq!(signed.nonce(), 0);
1089 assert_eq!(signed.tx_type(), ArbTxTypeLocal::Deposit);
1090 }
1091}