1use super::{CIP64_TX_TYPE, TxCip64};
2use alloy_consensus::{
3 SignableTransaction, Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, TxLegacy,
4 TxType, Typed2718,
5 crypto::RecoveryError,
6 transaction::{
7 SignerRecoverable, TxEip7702, TxHashRef,
8 eip4844::{TxEip4844Variant, TxEip4844WithSidecar},
9 },
10};
11use alloy_evm::{FromRecoveredTx, FromTxWithEncoded};
12use alloy_network::{
13 AnyRpcTransaction, AnyTxEnvelope, TransactionResponse, eip2718::Encodable2718,
14};
15use alloy_primitives::{Address, B256, Bytes, Signature, TxHash};
16use alloy_rpc_types::ConversionError;
17use revm::context::TxEnv;
18use tempo_primitives::{AASigned, TEMPO_TX_TYPE_ID, TempoSignature, TempoTransaction};
19use tempo_revm::TempoTxEnv;
20
21#[cfg(all(feature = "base", not(feature = "optimism")))]
22use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit};
23
24#[cfg(any(feature = "base", feature = "optimism"))]
25use alloy_consensus::Sealed;
26
27#[cfg(feature = "base")]
28use base_common_consensus::{BaseTxEnvelope, Eip8130Signed, TxEip8130};
29#[cfg(feature = "base")]
30use base_common_evm::EIP8130_TRANSACTION_TYPE;
31#[cfg(feature = "base")]
32use base_common_rpc_types::Transaction;
33
34#[cfg(feature = "optimism")]
35use alloy_consensus::Transaction as _;
36#[cfg(feature = "optimism")]
37use op_alloy_consensus::{
38 DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, PostExecPayload, TxDeposit, TxPostExec,
39};
40
41#[allow(clippy::large_enum_variant)]
45#[derive(Clone, Debug, TransactionEnvelope)]
46#[envelope(
47 tx_type_name = FoundryTxType,
48 typed = FoundryTypedTx,
49)]
50pub enum FoundryTxEnvelope {
51 #[envelope(ty = 0)]
53 Legacy(Signed<TxLegacy>),
54 #[envelope(ty = 1)]
58 Eip2930(Signed<TxEip2930>),
59 #[envelope(ty = 2)]
63 Eip1559(Signed<TxEip1559>),
64 #[envelope(ty = 3)]
68 Eip4844(Signed<TxEip4844Variant>),
69 #[envelope(ty = 4)]
73 Eip7702(Signed<TxEip7702>),
74 #[cfg(any(feature = "base", feature = "optimism"))]
78 #[envelope(ty = 126)]
79 Deposit(Sealed<TxDeposit>),
80 #[cfg(feature = "optimism")]
82 #[envelope(ty = 0x7D)]
83 PostExec(Sealed<TxPostExec>),
84 #[cfg(feature = "base")]
86 #[envelope(ty = 0x79, typed = TxEip8130)]
87 Eip8130(Eip8130Signed),
88 #[envelope(ty = 0x76, typed = TempoTransaction)]
92 Tempo(AASigned),
93 #[envelope(ty = 0x7b)]
95 Celo(Signed<TxCip64>),
96}
97
98impl FoundryTxEnvelope {
99 #[inline]
101 pub const fn is_legacy(&self) -> bool {
102 matches!(self, Self::Legacy(_))
103 }
104
105 #[inline]
107 pub const fn is_eip2930(&self) -> bool {
108 matches!(self, Self::Eip2930(_))
109 }
110
111 #[inline]
113 pub const fn is_eip1559(&self) -> bool {
114 matches!(self, Self::Eip1559(_))
115 }
116
117 #[inline]
119 pub const fn is_eip4844(&self) -> bool {
120 matches!(self, Self::Eip4844(_))
121 }
122
123 #[inline]
125 pub const fn is_eip7702(&self) -> bool {
126 matches!(self, Self::Eip7702(_))
127 }
128
129 #[cfg(any(feature = "base", feature = "optimism"))]
131 #[inline]
132 pub const fn is_deposit(&self) -> bool {
133 matches!(self, Self::Deposit(_))
134 }
135
136 #[cfg(feature = "optimism")]
138 #[inline]
139 pub const fn is_post_exec(&self) -> bool {
140 matches!(self, Self::PostExec(_))
141 }
142
143 #[cfg(feature = "base")]
145 #[inline]
146 pub const fn is_eip8130(&self) -> bool {
147 matches!(self, Self::Eip8130(_))
148 }
149
150 pub fn try_into_eth(self) -> Result<TxEnvelope, Self> {
154 match self {
155 Self::Legacy(tx) => Ok(TxEnvelope::Legacy(tx)),
156 Self::Eip2930(tx) => Ok(TxEnvelope::Eip2930(tx)),
157 Self::Eip1559(tx) => Ok(TxEnvelope::Eip1559(tx)),
158 Self::Eip4844(tx) => Ok(TxEnvelope::Eip4844(tx)),
159 Self::Eip7702(tx) => Ok(TxEnvelope::Eip7702(tx)),
160 #[cfg(any(feature = "base", feature = "optimism"))]
161 Self::Deposit(_) => Err(self),
162 #[cfg(feature = "optimism")]
163 Self::PostExec(_) => Err(self),
164 #[cfg(feature = "base")]
165 Self::Eip8130(_) => Err(self),
166 Self::Tempo(_) | Self::Celo(_) => Err(self),
167 }
168 }
169
170 pub const fn sidecar(&self) -> Option<&TxEip4844WithSidecar> {
171 match self {
172 Self::Eip4844(signed_variant) => match signed_variant.tx() {
173 TxEip4844Variant::TxEip4844WithSidecar(with_sidecar) => Some(with_sidecar),
174 _ => None,
175 },
176 _ => None,
177 }
178 }
179
180 pub fn into_canonical(self) -> Self {
182 match self {
183 Self::Eip4844(tx) => Self::Eip4844(tx.map(TxEip4844Variant::drop_sidecar)),
184 tx => tx,
185 }
186 }
187
188 pub fn hash(&self) -> B256 {
195 *self.tx_hash()
196 }
197
198 pub const fn is_tempo(&self) -> bool {
200 matches!(self, Self::Tempo(_))
201 }
202
203 pub fn has_nonzero_tempo_nonce_key(&self) -> bool {
205 matches!(self, Self::Tempo(tx) if !tx.tx().nonce_key.is_zero())
206 }
207
208 pub fn recover(&self) -> Result<Address, RecoveryError> {
210 Ok(match self {
211 Self::Legacy(tx) => tx.recover_signer()?,
212 Self::Eip2930(tx) => tx.recover_signer()?,
213 Self::Eip1559(tx) => tx.recover_signer()?,
214 Self::Celo(tx) => tx.recover_signer()?,
215 Self::Eip4844(tx) => tx.recover_signer()?,
216 Self::Eip7702(tx) => tx.recover_signer()?,
217 #[cfg(any(feature = "base", feature = "optimism"))]
218 Self::Deposit(tx) => tx.from,
219 #[cfg(feature = "optimism")]
220 Self::PostExec(tx) => tx.inner().signer_address(),
221 #[cfg(feature = "base")]
222 Self::Eip8130(tx) => tx.recover_sender()?,
223 Self::Tempo(tx) => tx.signature().recover_signer(&tx.signature_hash())?,
224 })
225 }
226
227 pub fn encode_rpc_2718(transaction: &AnyRpcTransaction) -> Result<Bytes, ConversionError> {
235 if let AnyTxEnvelope::Ethereum(envelope) = &*transaction.inner.inner {
236 return Ok(envelope.encoded_2718().into());
237 }
238
239 Ok(Self::try_from(transaction.clone())?.encoded_2718().into())
240 }
241}
242
243impl FoundryTxType {
244 pub const fn is_legacy(&self) -> bool {
246 matches!(self, Self::Legacy)
247 }
248
249 pub const fn is_eip2930(&self) -> bool {
251 matches!(self, Self::Eip2930)
252 }
253
254 pub const fn is_eip1559(&self) -> bool {
256 matches!(self, Self::Eip1559)
257 }
258
259 pub const fn is_eip4844(&self) -> bool {
261 matches!(self, Self::Eip4844)
262 }
263
264 pub const fn is_eip7702(&self) -> bool {
266 matches!(self, Self::Eip7702)
267 }
268
269 #[cfg(any(feature = "base", feature = "optimism"))]
271 pub const fn is_deposit(&self) -> bool {
272 matches!(self, Self::Deposit)
273 }
274
275 #[cfg(feature = "optimism")]
277 pub const fn is_post_exec(&self) -> bool {
278 matches!(self, Self::PostExec)
279 }
280
281 #[cfg(feature = "base")]
283 pub const fn is_eip8130(&self) -> bool {
284 matches!(self, Self::Eip8130)
285 }
286
287 pub const fn is_tempo(&self) -> bool {
289 matches!(self, Self::Tempo)
290 }
291
292 pub const fn is_celo(&self) -> bool {
294 matches!(self, Self::Celo)
295 }
296}
297
298impl FoundryTypedTx {
299 pub fn into_impersonated(self) -> FoundryTxEnvelope {
303 let signature = Signature::from_scalars_and_parity(
304 B256::with_last_byte(1),
305 B256::with_last_byte(1),
306 false,
307 );
308 match self {
309 Self::Legacy(tx) => FoundryTxEnvelope::Legacy(tx.into_signed(signature)),
310 Self::Eip2930(tx) => FoundryTxEnvelope::Eip2930(tx.into_signed(signature)),
311 Self::Eip1559(tx) => FoundryTxEnvelope::Eip1559(tx.into_signed(signature)),
312 Self::Celo(tx) => FoundryTxEnvelope::Celo(tx.into_signed(signature)),
313 Self::Eip7702(tx) => FoundryTxEnvelope::Eip7702(tx.into_signed(signature)),
314 Self::Eip4844(tx) => FoundryTxEnvelope::Eip4844(tx.into_signed(signature)),
315 #[cfg(any(feature = "base", feature = "optimism"))]
316 Self::Deposit(tx) => FoundryTxEnvelope::Deposit(Sealed::new(tx)),
317 #[cfg(feature = "optimism")]
318 Self::PostExec(_) => {
319 unreachable!("op post-exec txs should not be impersonated")
320 }
321 #[cfg(feature = "base")]
322 Self::Eip8130(_) => {
323 unreachable!("EIP-8130 requires a signed raw transaction envelope")
324 }
325 Self::Tempo(tx) => {
326 let tempo_sig: TempoSignature = signature.into();
327 FoundryTxEnvelope::Tempo(tx.into_signed(tempo_sig))
328 }
329 }
330 }
331
332 #[cfg(any(feature = "base", feature = "optimism"))]
334 pub const fn is_deposit(&self) -> bool {
335 matches!(self, Self::Deposit(_))
336 }
337
338 #[cfg(feature = "optimism")]
340 pub const fn is_post_exec(&self) -> bool {
341 matches!(self, Self::PostExec(_))
342 }
343
344 #[cfg(feature = "base")]
346 pub const fn is_eip8130(&self) -> bool {
347 matches!(self, Self::Eip8130(_))
348 }
349
350 pub const fn is_tempo(&self) -> bool {
352 matches!(self, Self::Tempo(_))
353 }
354}
355
356impl TxHashRef for FoundryTxEnvelope {
357 fn tx_hash(&self) -> &TxHash {
358 match self {
359 Self::Legacy(t) => t.hash(),
360 Self::Eip2930(t) => t.hash(),
361 Self::Eip1559(t) => t.hash(),
362 Self::Celo(t) => t.hash(),
363 Self::Eip4844(t) => t.hash(),
364 Self::Eip7702(t) => t.hash(),
365 #[cfg(any(feature = "base", feature = "optimism"))]
366 Self::Deposit(t) => t.hash_ref(),
367 #[cfg(feature = "optimism")]
368 Self::PostExec(t) => t.hash_ref(),
369 #[cfg(feature = "base")]
370 Self::Eip8130(t) => t.hash(),
371 Self::Tempo(t) => t.hash(),
372 }
373 }
374}
375
376impl SignerRecoverable for FoundryTxEnvelope {
377 fn recover_signer(&self) -> Result<Address, RecoveryError> {
378 self.recover()
379 }
380
381 fn recover_signer_unchecked(&self) -> Result<Address, RecoveryError> {
382 self.recover()
383 }
384}
385
386impl TryFrom<FoundryTxEnvelope> for TxEnvelope {
387 type Error = FoundryTxEnvelope;
388
389 fn try_from(envelope: FoundryTxEnvelope) -> Result<Self, Self::Error> {
390 envelope.try_into_eth()
391 }
392}
393
394impl From<TxEnvelope> for FoundryTxEnvelope {
395 fn from(tx: TxEnvelope) -> Self {
396 match tx {
397 TxEnvelope::Legacy(tx) => Self::Legacy(tx),
398 TxEnvelope::Eip2930(tx) => Self::Eip2930(tx),
399 TxEnvelope::Eip1559(tx) => Self::Eip1559(tx),
400 TxEnvelope::Eip4844(tx) => Self::Eip4844(tx),
401 TxEnvelope::Eip7702(tx) => Self::Eip7702(tx),
402 }
403 }
404}
405
406impl From<tempo_primitives::TempoTxEnvelope> for FoundryTxEnvelope {
407 fn from(tx: tempo_primitives::TempoTxEnvelope) -> Self {
408 match tx {
409 tempo_primitives::TempoTxEnvelope::Legacy(tx) => Self::Legacy(tx),
410 tempo_primitives::TempoTxEnvelope::Eip2930(tx) => Self::Eip2930(tx),
411 tempo_primitives::TempoTxEnvelope::Eip1559(tx) => Self::Eip1559(tx),
412 tempo_primitives::TempoTxEnvelope::Eip7702(tx) => Self::Eip7702(tx),
413 tempo_primitives::TempoTxEnvelope::AA(tx) => Self::Tempo(tx),
414 }
415 }
416}
417
418impl TryFrom<AnyRpcTransaction> for FoundryTxEnvelope {
419 type Error = ConversionError;
420
421 fn try_from(value: AnyRpcTransaction) -> Result<Self, Self::Error> {
422 #[cfg(feature = "base")]
423 if value.ty() == EIP8130_TRANSACTION_TYPE {
424 let rpc = serde_json::from_value::<Transaction>(
425 serde_json::to_value(&value)
426 .map_err(|err| ConversionError::Custom(err.to_string()))?,
427 )
428 .map_err(|err| ConversionError::Custom(err.to_string()))?;
429 return match rpc.inner.into_inner() {
430 BaseTxEnvelope::Eip8130(tx) => Ok(Self::Eip8130(tx)),
431 _ => Err(ConversionError::Custom("expected Base EIP-8130 transaction".to_string())),
432 };
433 }
434 let transaction = value.into_inner();
435 let from = transaction.from();
436 match transaction.into_inner() {
437 AnyTxEnvelope::Ethereum(tx) => match tx {
438 TxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
439 TxEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
440 TxEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
441 TxEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
442 TxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
443 },
444 AnyTxEnvelope::Unknown(tx) => {
445 if tx.ty() == CIP64_TX_TYPE {
446 let mut fields = tx.inner.fields;
447 fields.insert("hash".into(), serde_json::to_value(tx.hash).unwrap());
448 return fields
449 .deserialize_into::<Signed<TxCip64>>()
450 .map(Self::Celo)
451 .map_err(|err| ConversionError::Custom(err.to_string()));
452 }
453 if tx.ty() == TEMPO_TX_TYPE_ID {
454 let mut fields = tx.inner.fields;
455 fields.insert("hash".into(), serde_json::to_value(tx.hash).unwrap());
456 let tempo_tx = fields.deserialize_into::<AASigned>().map_err(|e| {
457 ConversionError::Custom(format!("Failed to deserialize tempo tx: {e}"))
458 })?;
459 return Ok(Self::Tempo(tempo_tx));
460 }
461
462 #[cfg(all(feature = "base", not(feature = "optimism")))]
463 {
464 let mut tx = tx;
465 if tx.ty() == DEPOSIT_TX_TYPE_ID {
466 tx.inner
467 .fields
468 .insert("from".to_string(), serde_json::to_value(from).unwrap());
469 let deposit =
470 tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|err| {
471 ConversionError::Custom(format!(
472 "Failed to deserialize deposit transaction: {err}"
473 ))
474 })?;
475 return Ok(Self::Deposit(Sealed::new(deposit)));
476 }
477 let tx_type = tx.ty();
478 Err(ConversionError::Custom(format!(
479 "Unknown transaction type: 0x{tx_type:02X}"
480 )))
481 }
482 #[cfg(feature = "optimism")]
483 {
484 let mut tx = tx;
485 let _ = from;
486 if tx.ty() == DEPOSIT_TX_TYPE_ID {
488 tx.inner
489 .fields
490 .insert("from".to_string(), serde_json::to_value(from).unwrap());
491 let deposit_tx =
492 tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|e| {
493 ConversionError::Custom(format!(
494 "Failed to deserialize deposit tx: {e}"
495 ))
496 })?;
497
498 return Ok(Self::Deposit(Sealed::new(deposit_tx)));
499 }
500
501 if tx.ty() == POST_EXEC_TX_TYPE_ID {
502 let input = tx
505 .inner
506 .fields
507 .get_deserialized::<Bytes>("input")
508 .ok_or_else(|| {
509 ConversionError::Custom(
510 "Post-exec tx is missing `input`".to_string(),
511 )
512 })?
513 .map_err(|e| {
514 ConversionError::Custom(format!(
515 "Failed to deserialize post-exec tx `input`: {e}"
516 ))
517 })?;
518 let payload =
519 PostExecPayload::from_rlp_bytes(input.as_ref()).map_err(|e| {
520 ConversionError::Custom(format!(
521 "Failed to decode post-exec tx payload: {e}"
522 ))
523 })?;
524
525 return Ok(Self::PostExec(Sealed::new(TxPostExec::new(payload))));
526 }
527
528 let tx_type = tx.ty();
529 Err(ConversionError::Custom(format!(
530 "Unknown transaction type: 0x{tx_type:02X}"
531 )))
532 }
533 #[cfg(not(any(feature = "base", feature = "optimism")))]
534 {
535 let _ = from;
536 let tx_type = tx.ty();
537 Err(ConversionError::Custom(format!(
538 "Unknown transaction type: 0x{tx_type:02X}"
539 )))
540 }
541 }
542 }
543 }
544}
545
546impl FromRecoveredTx<FoundryTxEnvelope> for TxEnv {
547 fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
548 match tx {
549 FoundryTxEnvelope::Legacy(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
550 FoundryTxEnvelope::Eip2930(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
551 FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
552 FoundryTxEnvelope::Celo(signed_tx) => {
554 Self::from_recovered_tx(&signed_tx.tx().inner, caller)
555 }
556 FoundryTxEnvelope::Eip4844(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
557 FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
558 #[cfg(any(feature = "base", feature = "optimism"))]
559 FoundryTxEnvelope::Deposit(sealed_tx) => {
560 let tx = sealed_tx.inner();
561 Self {
562 tx_type: tx.ty(),
563 caller,
564 gas_limit: tx.gas_limit,
565 kind: tx.to,
566 value: tx.value,
567 data: tx.input.clone(),
568 ..Default::default()
569 }
570 }
571 #[cfg(feature = "optimism")]
572 FoundryTxEnvelope::PostExec(sealed_tx) => {
573 let tx = sealed_tx.inner();
574 Self {
575 tx_type: tx.ty(),
576 caller,
577 kind: tx.kind(),
578 data: tx.input.clone(),
579 ..Default::default()
580 }
581 }
582 #[cfg(feature = "base")]
583 FoundryTxEnvelope::Eip8130(_) => {
584 unreachable!("EIP-8130 transaction in Ethereum context")
585 }
586 FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Ethereum context"),
587 }
588 }
589}
590
591impl FromTxWithEncoded<FoundryTxEnvelope> for TxEnv {
592 fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
593 Self::from_recovered_tx(tx, sender)
594 }
595}
596
597impl FromRecoveredTx<FoundryTxEnvelope> for TempoTxEnv {
598 fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
599 match tx {
600 FoundryTxEnvelope::Legacy(signed_tx) => {
601 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
602 }
603 FoundryTxEnvelope::Eip2930(signed_tx) => {
604 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
605 }
606 FoundryTxEnvelope::Eip1559(signed_tx) => {
607 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
608 }
609 FoundryTxEnvelope::Eip4844(signed_tx) => {
610 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
611 }
612 FoundryTxEnvelope::Eip7702(signed_tx) => {
613 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
614 }
615 #[cfg(any(feature = "base", feature = "optimism"))]
616 FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"),
617 #[cfg(feature = "optimism")]
618 FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"),
619 #[cfg(feature = "base")]
620 FoundryTxEnvelope::Eip8130(_) => {
621 unreachable!("EIP-8130 transaction in Tempo context")
622 }
623 FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller),
624 FoundryTxEnvelope::Celo(_) => unreachable!("CIP-64 transaction in Tempo context"),
625 }
626 }
627}
628
629impl FromTxWithEncoded<FoundryTxEnvelope> for TempoTxEnv {
630 fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
631 Self::from_recovered_tx(tx, sender)
632 }
633}
634
635impl std::fmt::Display for FoundryTxType {
636 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
637 match self {
638 Self::Legacy => write!(f, "legacy"),
639 Self::Eip2930 => write!(f, "eip2930"),
640 Self::Eip1559 => write!(f, "eip1559"),
641 Self::Eip4844 => write!(f, "eip4844"),
642 Self::Eip7702 => write!(f, "eip7702"),
643 #[cfg(any(feature = "base", feature = "optimism"))]
644 Self::Deposit => write!(f, "deposit"),
645 #[cfg(feature = "optimism")]
646 Self::PostExec => write!(f, "post-exec"),
647 #[cfg(feature = "base")]
648 Self::Eip8130 => write!(f, "eip8130"),
649 Self::Tempo => write!(f, "tempo"),
650 Self::Celo => write!(f, "celo"),
651 }
652 }
653}
654
655impl From<TxType> for FoundryTxType {
656 fn from(tx: TxType) -> Self {
657 match tx {
658 TxType::Legacy => Self::Legacy,
659 TxType::Eip2930 => Self::Eip2930,
660 TxType::Eip1559 => Self::Eip1559,
661 TxType::Eip4844 => Self::Eip4844,
662 TxType::Eip7702 => Self::Eip7702,
663 }
664 }
665}
666
667impl From<FoundryTxEnvelope> for FoundryTypedTx {
668 fn from(envelope: FoundryTxEnvelope) -> Self {
669 match envelope {
670 FoundryTxEnvelope::Legacy(signed_tx) => Self::Legacy(signed_tx.strip_signature()),
671 FoundryTxEnvelope::Eip2930(signed_tx) => Self::Eip2930(signed_tx.strip_signature()),
672 FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()),
673 FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()),
674 FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()),
675 #[cfg(any(feature = "base", feature = "optimism"))]
676 FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.unseal()),
677 #[cfg(feature = "optimism")]
678 FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.unseal()),
679 #[cfg(feature = "base")]
680 FoundryTxEnvelope::Eip8130(signed_tx) => Self::Eip8130(signed_tx.into_tx()),
681 FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()),
682 FoundryTxEnvelope::Celo(signed_tx) => Self::Celo(signed_tx.strip_signature()),
683 }
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use alloy_primitives::{TxKind, U256, address, b256, hex};
691 use alloy_rlp::Decodable;
692 use std::str::FromStr;
693
694 fn signed<T>(tx: T) -> Signed<T> {
695 Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO)
696 }
697
698 const ETH_RPC_TX: &str = r#"{"type":"0x0","chainId":"0x1","nonce":"0x15","gasPrice":"0x4a817c800","gas":"0xc350","to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb","value":"0xf3dbb76162000","input":"0x68656c6c6f21","r":"0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea","s":"0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c","v":"0x25","hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b","blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2","blockNumber":"0x5daf3b","transactionIndex":"0x41","from":"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d"}"#;
700
701 #[cfg(feature = "optimism")]
705 const OP_POST_EXEC_RPC_TX: &str = r#"{"blockHash":"0x72edd91c1b181b566e08846b9fe67e3d746c4e6555e6fb81f0d1acd9465f7322","blockNumber":"0x44ee2d","blockTimestamp":"0x6aadaad9","from":"0x0000000000000000000000000000000000000000","gas":"0x0","gasPrice":"0xfb","hash":"0x748fc6eb383fc0f2a92089556f639d4bdb1d363cb50e1be8acae2df338ba6963","input":"0xf83d018344ee2df7c4028207d0c4038207d0c4048207d0c4058207d0c4068207d0c4078207d0c4088207d0c4098207d0c40a8207d0c40b8207d0c40c8207d0","transactionIndex":"0xd","type":"0x7d","value":"0x0"}"#;
706
707 const ARBITRUM_INTERNAL_RPC_TX: &str = r#"{"type":"0x6a","chainId":"0xa4b1","nonce":"0x0","gasPrice":"0x0","gas":"0x0","to":"0x00000000000000000000000000000000000a4b05","value":"0x0","input":"0x6bf6a42d","r":"0x0","s":"0x0","v":"0x0","hash":"0xe5ad4cc44e5cd67a464c038af87169fde2bd475f2c00306bd2d55ca2c5e4452e","blockHash":"0x0ce1511da42af573bac6870ef058d63bc4c8552440e97c149d4d539c482b5f7a","blockNumber":"0x1dc83ddc","transactionIndex":"0x0","from":"0x00000000000000000000000000000000000a4b05"}"#;
709
710 #[test]
711 fn encode_rpc_2718_matches_consensus_encoding() {
712 let tx: AnyRpcTransaction = serde_json::from_str(ETH_RPC_TX).unwrap();
713 let expected = hex!(
714 "f871158504a817c80082c35094f02c1c8e6114b1dbe8937a39260b5b0a374432bb870f3dbb761620008668656c6c6f2125a01b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5feaa04ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c"
715 );
716
717 assert_eq!(FoundryTxEnvelope::encode_rpc_2718(&tx).unwrap(), expected[..]);
718 }
719
720 #[test]
721 fn encode_rpc_2718_rejects_unmodeled_type() {
722 let tx: AnyRpcTransaction = serde_json::from_str(ARBITRUM_INTERNAL_RPC_TX).unwrap();
723
724 assert!(FoundryTxEnvelope::encode_rpc_2718(&tx).is_err());
726 }
727
728 #[cfg(feature = "optimism")]
729 #[test]
730 fn encode_rpc_2718_post_exec_tx() {
731 let tx: AnyRpcTransaction = serde_json::from_str(OP_POST_EXEC_RPC_TX).unwrap();
732
733 let encoded = FoundryTxEnvelope::encode_rpc_2718(&tx).unwrap();
734
735 let expected = hex!(
737 "7df83d018344ee2df7c4028207d0c4038207d0c4048207d0c4058207d0c4068207d0c4078207d0c4088207d0c4098207d0c40a8207d0c40b8207d0c40c8207d0"
738 );
739 assert_eq!(encoded, expected[..]);
740 }
741
742 #[cfg(feature = "optimism")]
745 #[test]
746 fn post_exec_rpc_tx_decodes() {
747 let tx: AnyRpcTransaction = serde_json::from_str(OP_POST_EXEC_RPC_TX).unwrap();
748
749 let envelope = FoundryTxEnvelope::try_from(tx).unwrap();
750 let FoundryTxEnvelope::PostExec(sealed) = &envelope else {
751 panic!("expected a post-exec envelope, got {envelope:?}");
752 };
753
754 assert_eq!(
755 sealed.hash(),
756 b256!("0x748fc6eb383fc0f2a92089556f639d4bdb1d363cb50e1be8acae2df338ba6963")
757 );
758 let payload = &sealed.inner().payload;
760 assert_eq!(payload.block_number, 0x44ee2d);
761 assert_eq!(payload.gas_refund_entries.len(), 11);
762 }
763
764 #[test]
765 fn tx_type_predicates() {
766 assert!(FoundryTxType::Legacy.is_legacy());
767 assert!(FoundryTxType::Eip2930.is_eip2930());
768 assert!(FoundryTxType::Eip1559.is_eip1559());
769 assert!(FoundryTxType::Eip4844.is_eip4844());
770 assert!(FoundryTxType::Eip7702.is_eip7702());
771 assert!(FoundryTxType::Tempo.is_tempo());
772 assert!(!FoundryTxType::Tempo.is_legacy());
773
774 #[cfg(any(feature = "base", feature = "optimism"))]
775 assert!(FoundryTxType::Deposit.is_deposit());
776 #[cfg(feature = "base")]
777 assert!(FoundryTxType::Eip8130.is_eip8130());
778 #[cfg(feature = "optimism")]
779 {
780 assert!(FoundryTxType::PostExec.is_post_exec());
781 assert!(!FoundryTxType::Deposit.is_post_exec());
782 }
783 }
784
785 #[test]
786 fn typed_tx_predicates() {
787 assert!(FoundryTypedTx::Legacy(TxLegacy::default()).is_legacy());
788 assert!(FoundryTypedTx::Eip2930(TxEip2930::default()).is_eip2930());
789 assert!(FoundryTypedTx::Eip1559(TxEip1559::default()).is_eip1559());
790 assert!(
791 FoundryTypedTx::Eip4844(TxEip4844Variant::TxEip4844(Default::default())).is_eip4844()
792 );
793 assert!(FoundryTypedTx::Eip7702(TxEip7702::default()).is_eip7702());
794 assert!(FoundryTypedTx::Tempo(TempoTransaction::default()).is_tempo());
795
796 #[cfg(any(feature = "base", feature = "optimism"))]
797 assert!(FoundryTypedTx::Deposit(TxDeposit::default()).is_deposit());
798 #[cfg(feature = "base")]
799 assert!(FoundryTypedTx::Eip8130(TxEip8130::default()).is_eip8130());
800 #[cfg(feature = "optimism")]
801 {
802 assert!(FoundryTypedTx::PostExec(TxPostExec::default()).is_post_exec());
803 }
804 }
805
806 #[test]
807 fn tx_envelope_predicates() {
808 assert!(FoundryTxEnvelope::Legacy(signed(TxLegacy::default())).is_legacy());
809 assert!(FoundryTxEnvelope::Eip2930(signed(TxEip2930::default())).is_eip2930());
810 assert!(FoundryTxEnvelope::Eip1559(signed(TxEip1559::default())).is_eip1559());
811 assert!(
812 FoundryTxEnvelope::Eip4844(signed(TxEip4844Variant::TxEip4844(Default::default())))
813 .is_eip4844()
814 );
815 assert!(FoundryTxEnvelope::Eip7702(signed(TxEip7702::default())).is_eip7702());
816
817 #[cfg(any(feature = "base", feature = "optimism"))]
818 assert!(FoundryTxEnvelope::Deposit(Sealed::new(TxDeposit::default())).is_deposit());
819 #[cfg(feature = "base")]
820 assert!(
821 FoundryTxEnvelope::Eip8130(Eip8130Signed::new(
822 TxEip8130::default(),
823 Default::default(),
824 Default::default()
825 ))
826 .is_eip8130()
827 );
828 #[cfg(feature = "optimism")]
829 {
830 assert!(FoundryTxEnvelope::PostExec(Sealed::new(TxPostExec::default())).is_post_exec());
831 }
832 }
833
834 #[test]
835 fn impersonated_tx_uses_nonzero_dummy_signature() {
836 let FoundryTxEnvelope::Legacy(tx) =
837 FoundryTypedTx::Legacy(TxLegacy::default()).into_impersonated()
838 else {
839 panic!("expected legacy transaction");
840 };
841
842 assert_eq!(tx.signature().r(), U256::ONE);
843 assert_eq!(tx.signature().s(), U256::ONE);
844 assert!(!tx.signature().v());
845 }
846
847 #[test]
848 fn test_decode_call() {
849 let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
850 let decoded = FoundryTxEnvelope::decode(&mut &bytes_first[..]).unwrap();
851
852 let tx = TxLegacy {
853 nonce: 2u64,
854 gas_price: 1000000000u128,
855 gas_limit: 100000,
856 to: TxKind::Call(Address::from_slice(
857 &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..],
858 )),
859 value: U256::from(1000000000000000u64),
860 input: Bytes::default(),
861 chain_id: Some(4),
862 };
863
864 let signature = Signature::from_str("0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b").unwrap();
865
866 let tx = FoundryTxEnvelope::Legacy(Signed::new_unchecked(
867 tx,
868 signature,
869 b256!("0xa517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34"),
870 ));
871
872 assert_eq!(tx, decoded);
873 }
874
875 #[test]
876 fn test_decode_create_goerli() {
877 let tx_bytes =
879 hex::decode("02f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471")
880 .unwrap();
881 let _decoded = FoundryTxEnvelope::decode(&mut &tx_bytes[..]).unwrap();
882 }
883
884 #[test]
885 fn can_recover_sender() {
886 let bytes = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap();
888
889 let Ok(FoundryTxEnvelope::Eip1559(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
890 panic!("decoding FoundryTxEnvelope failed");
891 };
892
893 assert_eq!(
894 tx.hash(),
895 &b256!("0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f")
896 );
897 assert_eq!(
898 tx.recover_signer().unwrap(),
899 address!("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5")
900 );
901 }
902
903 #[test]
906 fn test_decode_live_4844_tx() {
907 use alloy_primitives::{address, b256};
908
909 let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap();
911 let res = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
912 assert!(res.is_type(3));
913
914 let tx = match res {
915 FoundryTxEnvelope::Eip4844(tx) => tx,
916 _ => unreachable!(),
917 };
918
919 assert_eq!(tx.tx().tx().to, address!("0x11E9CA82A3a762b4B5bd264d4173a242e7a77064"));
920
921 assert_eq!(
922 tx.tx().tx().blob_versioned_hashes,
923 vec![
924 b256!("0x012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a"),
925 b256!("0x0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4"),
926 b256!("0x013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7"),
927 b256!("0x01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1"),
928 b256!("0x011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549")
929 ]
930 );
931
932 let from = tx.recover_signer().unwrap();
933 assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
934 }
935
936 #[test]
937 fn can_recover_sender_not_normalized() {
938 let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();
939
940 let Ok(FoundryTxEnvelope::Legacy(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
941 panic!("decoding FoundryTxEnvelope failed");
942 };
943
944 assert_eq!(tx.tx().input, Bytes::from(b""));
945 assert_eq!(tx.tx().gas_price, 1);
946 assert_eq!(tx.tx().gas_limit, 21000);
947 assert_eq!(tx.tx().nonce, 0);
948 if let TxKind::Call(to) = tx.tx().to {
949 assert_eq!(to, address!("0x095e7baea6a6c7c4c2dfeb977efac326af552d87"));
950 } else {
951 panic!("expected a call transaction");
952 }
953 assert_eq!(tx.tx().value, U256::from(0x0au64));
954 assert_eq!(
955 tx.recover_signer().unwrap(),
956 address!("0f65fe9276bc9a24ae7083ae28e2660ef72df99e")
957 );
958 }
959
960 #[test]
961 fn deser_to_type_tx() {
962 let tx = r#"
963 {
964 "type": "0x2",
965 "chainId": "0x7a69",
966 "nonce": "0x0",
967 "gas": "0x5209",
968 "maxFeePerGas": "0x77359401",
969 "maxPriorityFeePerGas": "0x1",
970 "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
971 "value": "0x0",
972 "accessList": [],
973 "input": "0x",
974 "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
975 "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
976 "yParity": "0x0",
977 "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
978 }"#;
979
980 let _typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
981 }
982
983 #[test]
984 fn test_from_recovered_tx_legacy() {
985 let tx = r#"
986 {
987 "type": "0x0",
988 "chainId": "0x1",
989 "nonce": "0x0",
990 "gas": "0x5208",
991 "gasPrice": "0x1",
992 "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
993 "value": "0x1",
994 "input": "0x",
995 "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
996 "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
997 "v": "0x1b",
998 "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
999 }"#;
1000
1001 let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
1002 let sender = typed_tx.recover().unwrap();
1003
1004 let tx_env = TxEnv::from_recovered_tx(&typed_tx, sender);
1006 assert_eq!(tx_env.caller, sender);
1007 assert_eq!(tx_env.gas_limit, 0x5208);
1008 assert_eq!(tx_env.gas_price, 1);
1009 }
1010
1011 #[test]
1014 fn test_decode_encode_tempo_tx() {
1015 use alloy_primitives::address;
1016 use tempo_primitives::TEMPO_TX_TYPE_ID;
1017
1018 let tx_hash: TxHash =
1019 b256!("0x6d6d8c102064e6dee44abad2024a8b1d37959230baab80e70efbf9b0c739c4fd");
1020
1021 let raw_tx = hex::decode(
1023 "76f9025e82a5bd808502cb4178008302d178f8fcf85c9420c000000000000000000000000000000000000080b844095ea7b3000000000000000000000000dec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000989680f89c94dec000000000000000000000000000000000000080b884f8856c0f00000000000000000000000020c000000000000000000000000000000000000000000000000000000000000020c00000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000097d330c0808080809420c000000000000000000000000000000000000180c0b90133027b98b7a8e6c68d7eac741a52e6fdae0560ce3c16ef5427ad46d7a54d0ed86dd41d000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2238453071464a7a50585167546e645473643649456659457776323173516e626966374c4741776e4b43626b222c226f726967696e223a2268747470733a2f2f74656d706f2d6465782e76657263656c2e617070222c2263726f73734f726967696e223a66616c73657dcfd45c3b19745a42f80b134dcb02a8ba099a0e4e7be1984da54734aa81d8f29f74bb9170ae6d25bd510c83fe35895ee5712efe13980a5edc8094c534e23af85eaacc80b21e45fb11f349424dce3a2f23547f60c0ff2f8bcaede2a247545ce8dd87abf0dbb7a5c9507efae2e43833356651b45ac576c2e61cec4e9c0f41fcbf6e",
1024 )
1025 .unwrap();
1026
1027 let tempo_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
1028
1029 assert!(tempo_tx.is_type(TEMPO_TX_TYPE_ID));
1031
1032 let FoundryTxEnvelope::Tempo(ref aa_signed) = tempo_tx else {
1033 panic!("Expected Tempo transaction");
1034 };
1035
1036 assert_eq!(aa_signed.tx().chain_id, 42429);
1038
1039 assert_eq!(
1041 aa_signed.tx().fee_token,
1042 Some(address!("0x20C0000000000000000000000000000000000001"))
1043 );
1044
1045 assert_eq!(aa_signed.tx().gas_limit, 184696);
1047
1048 assert_eq!(aa_signed.tx().calls.len(), 2);
1050
1051 assert_eq!(tx_hash, tempo_tx.hash());
1053
1054 let mut encoded = Vec::new();
1056 tempo_tx.encode_2718(&mut encoded);
1057 assert_eq!(raw_tx, encoded);
1058
1059 let sender = tempo_tx.recover().unwrap();
1061 assert_eq!(sender, address!("0x566Ff0f4a6114F8072ecDC8A7A8A13d8d0C6B45F"));
1062 }
1063}