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 tempo_tx = tx.inner.fields.deserialize_into::<AASigned>().map_err(|e| {
455 ConversionError::Custom(format!("Failed to deserialize tempo tx: {e}"))
456 })?;
457 return Ok(Self::Tempo(tempo_tx));
458 }
459
460 #[cfg(all(feature = "base", not(feature = "optimism")))]
461 {
462 let mut tx = tx;
463 if tx.ty() == DEPOSIT_TX_TYPE_ID {
464 tx.inner
465 .fields
466 .insert("from".to_string(), serde_json::to_value(from).unwrap());
467 let deposit =
468 tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|err| {
469 ConversionError::Custom(format!(
470 "Failed to deserialize deposit transaction: {err}"
471 ))
472 })?;
473 return Ok(Self::Deposit(Sealed::new(deposit)));
474 }
475 let tx_type = tx.ty();
476 Err(ConversionError::Custom(format!(
477 "Unknown transaction type: 0x{tx_type:02X}"
478 )))
479 }
480 #[cfg(feature = "optimism")]
481 {
482 let mut tx = tx;
483 let _ = from;
484 if tx.ty() == DEPOSIT_TX_TYPE_ID {
486 tx.inner
487 .fields
488 .insert("from".to_string(), serde_json::to_value(from).unwrap());
489 let deposit_tx =
490 tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|e| {
491 ConversionError::Custom(format!(
492 "Failed to deserialize deposit tx: {e}"
493 ))
494 })?;
495
496 return Ok(Self::Deposit(Sealed::new(deposit_tx)));
497 }
498
499 if tx.ty() == POST_EXEC_TX_TYPE_ID {
500 let input = tx
503 .inner
504 .fields
505 .get_deserialized::<Bytes>("input")
506 .ok_or_else(|| {
507 ConversionError::Custom(
508 "Post-exec tx is missing `input`".to_string(),
509 )
510 })?
511 .map_err(|e| {
512 ConversionError::Custom(format!(
513 "Failed to deserialize post-exec tx `input`: {e}"
514 ))
515 })?;
516 let payload =
517 PostExecPayload::from_rlp_bytes(input.as_ref()).map_err(|e| {
518 ConversionError::Custom(format!(
519 "Failed to decode post-exec tx payload: {e}"
520 ))
521 })?;
522
523 return Ok(Self::PostExec(Sealed::new(TxPostExec::new(payload))));
524 }
525
526 let tx_type = tx.ty();
527 Err(ConversionError::Custom(format!(
528 "Unknown transaction type: 0x{tx_type:02X}"
529 )))
530 }
531 #[cfg(not(any(feature = "base", feature = "optimism")))]
532 {
533 let _ = from;
534 let tx_type = tx.ty();
535 Err(ConversionError::Custom(format!(
536 "Unknown transaction type: 0x{tx_type:02X}"
537 )))
538 }
539 }
540 }
541 }
542}
543
544impl FromRecoveredTx<FoundryTxEnvelope> for TxEnv {
545 fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
546 match tx {
547 FoundryTxEnvelope::Legacy(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
548 FoundryTxEnvelope::Eip2930(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
549 FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
550 FoundryTxEnvelope::Celo(signed_tx) => {
552 Self::from_recovered_tx(&signed_tx.tx().inner, caller)
553 }
554 FoundryTxEnvelope::Eip4844(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
555 FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
556 #[cfg(any(feature = "base", feature = "optimism"))]
557 FoundryTxEnvelope::Deposit(sealed_tx) => {
558 let tx = sealed_tx.inner();
559 Self {
560 tx_type: tx.ty(),
561 caller,
562 gas_limit: tx.gas_limit,
563 kind: tx.to,
564 value: tx.value,
565 data: tx.input.clone(),
566 ..Default::default()
567 }
568 }
569 #[cfg(feature = "optimism")]
570 FoundryTxEnvelope::PostExec(sealed_tx) => {
571 let tx = sealed_tx.inner();
572 Self {
573 tx_type: tx.ty(),
574 caller,
575 kind: tx.kind(),
576 data: tx.input.clone(),
577 ..Default::default()
578 }
579 }
580 #[cfg(feature = "base")]
581 FoundryTxEnvelope::Eip8130(_) => {
582 unreachable!("EIP-8130 transaction in Ethereum context")
583 }
584 FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Ethereum context"),
585 }
586 }
587}
588
589impl FromTxWithEncoded<FoundryTxEnvelope> for TxEnv {
590 fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
591 Self::from_recovered_tx(tx, sender)
592 }
593}
594
595impl FromRecoveredTx<FoundryTxEnvelope> for TempoTxEnv {
596 fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
597 match tx {
598 FoundryTxEnvelope::Legacy(signed_tx) => {
599 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
600 }
601 FoundryTxEnvelope::Eip2930(signed_tx) => {
602 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
603 }
604 FoundryTxEnvelope::Eip1559(signed_tx) => {
605 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
606 }
607 FoundryTxEnvelope::Eip4844(signed_tx) => {
608 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
609 }
610 FoundryTxEnvelope::Eip7702(signed_tx) => {
611 Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
612 }
613 #[cfg(any(feature = "base", feature = "optimism"))]
614 FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"),
615 #[cfg(feature = "optimism")]
616 FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"),
617 #[cfg(feature = "base")]
618 FoundryTxEnvelope::Eip8130(_) => {
619 unreachable!("EIP-8130 transaction in Tempo context")
620 }
621 FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller),
622 FoundryTxEnvelope::Celo(_) => unreachable!("CIP-64 transaction in Tempo context"),
623 }
624 }
625}
626
627impl FromTxWithEncoded<FoundryTxEnvelope> for TempoTxEnv {
628 fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
629 Self::from_recovered_tx(tx, sender)
630 }
631}
632
633impl std::fmt::Display for FoundryTxType {
634 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
635 match self {
636 Self::Legacy => write!(f, "legacy"),
637 Self::Eip2930 => write!(f, "eip2930"),
638 Self::Eip1559 => write!(f, "eip1559"),
639 Self::Eip4844 => write!(f, "eip4844"),
640 Self::Eip7702 => write!(f, "eip7702"),
641 #[cfg(any(feature = "base", feature = "optimism"))]
642 Self::Deposit => write!(f, "deposit"),
643 #[cfg(feature = "optimism")]
644 Self::PostExec => write!(f, "post-exec"),
645 #[cfg(feature = "base")]
646 Self::Eip8130 => write!(f, "eip8130"),
647 Self::Tempo => write!(f, "tempo"),
648 Self::Celo => write!(f, "celo"),
649 }
650 }
651}
652
653impl From<TxType> for FoundryTxType {
654 fn from(tx: TxType) -> Self {
655 match tx {
656 TxType::Legacy => Self::Legacy,
657 TxType::Eip2930 => Self::Eip2930,
658 TxType::Eip1559 => Self::Eip1559,
659 TxType::Eip4844 => Self::Eip4844,
660 TxType::Eip7702 => Self::Eip7702,
661 }
662 }
663}
664
665impl From<FoundryTxEnvelope> for FoundryTypedTx {
666 fn from(envelope: FoundryTxEnvelope) -> Self {
667 match envelope {
668 FoundryTxEnvelope::Legacy(signed_tx) => Self::Legacy(signed_tx.strip_signature()),
669 FoundryTxEnvelope::Eip2930(signed_tx) => Self::Eip2930(signed_tx.strip_signature()),
670 FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()),
671 FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()),
672 FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()),
673 #[cfg(any(feature = "base", feature = "optimism"))]
674 FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.unseal()),
675 #[cfg(feature = "optimism")]
676 FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.unseal()),
677 #[cfg(feature = "base")]
678 FoundryTxEnvelope::Eip8130(signed_tx) => Self::Eip8130(signed_tx.into_tx()),
679 FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()),
680 FoundryTxEnvelope::Celo(signed_tx) => Self::Celo(signed_tx.strip_signature()),
681 }
682 }
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688 use alloy_primitives::{TxKind, U256, address, b256, hex};
689 use alloy_rlp::Decodable;
690 use std::str::FromStr;
691
692 fn signed<T>(tx: T) -> Signed<T> {
693 Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO)
694 }
695
696 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"}"#;
698
699 #[cfg(feature = "optimism")]
703 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"}"#;
704
705 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"}"#;
707
708 #[test]
709 fn encode_rpc_2718_matches_consensus_encoding() {
710 let tx: AnyRpcTransaction = serde_json::from_str(ETH_RPC_TX).unwrap();
711 let expected = hex!(
712 "f871158504a817c80082c35094f02c1c8e6114b1dbe8937a39260b5b0a374432bb870f3dbb761620008668656c6c6f2125a01b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5feaa04ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c"
713 );
714
715 assert_eq!(FoundryTxEnvelope::encode_rpc_2718(&tx).unwrap(), expected[..]);
716 }
717
718 #[test]
719 fn encode_rpc_2718_rejects_unmodeled_type() {
720 let tx: AnyRpcTransaction = serde_json::from_str(ARBITRUM_INTERNAL_RPC_TX).unwrap();
721
722 assert!(FoundryTxEnvelope::encode_rpc_2718(&tx).is_err());
724 }
725
726 #[cfg(feature = "optimism")]
727 #[test]
728 fn encode_rpc_2718_post_exec_tx() {
729 let tx: AnyRpcTransaction = serde_json::from_str(OP_POST_EXEC_RPC_TX).unwrap();
730
731 let encoded = FoundryTxEnvelope::encode_rpc_2718(&tx).unwrap();
732
733 let expected = hex!(
735 "7df83d018344ee2df7c4028207d0c4038207d0c4048207d0c4058207d0c4068207d0c4078207d0c4088207d0c4098207d0c40a8207d0c40b8207d0c40c8207d0"
736 );
737 assert_eq!(encoded, expected[..]);
738 }
739
740 #[cfg(feature = "optimism")]
743 #[test]
744 fn post_exec_rpc_tx_decodes() {
745 let tx: AnyRpcTransaction = serde_json::from_str(OP_POST_EXEC_RPC_TX).unwrap();
746
747 let envelope = FoundryTxEnvelope::try_from(tx).unwrap();
748 let FoundryTxEnvelope::PostExec(sealed) = &envelope else {
749 panic!("expected a post-exec envelope, got {envelope:?}");
750 };
751
752 assert_eq!(
753 sealed.hash(),
754 b256!("0x748fc6eb383fc0f2a92089556f639d4bdb1d363cb50e1be8acae2df338ba6963")
755 );
756 let payload = &sealed.inner().payload;
758 assert_eq!(payload.block_number, 0x44ee2d);
759 assert_eq!(payload.gas_refund_entries.len(), 11);
760 }
761
762 #[test]
763 fn tx_type_predicates() {
764 assert!(FoundryTxType::Legacy.is_legacy());
765 assert!(FoundryTxType::Eip2930.is_eip2930());
766 assert!(FoundryTxType::Eip1559.is_eip1559());
767 assert!(FoundryTxType::Eip4844.is_eip4844());
768 assert!(FoundryTxType::Eip7702.is_eip7702());
769 assert!(FoundryTxType::Tempo.is_tempo());
770 assert!(!FoundryTxType::Tempo.is_legacy());
771
772 #[cfg(any(feature = "base", feature = "optimism"))]
773 assert!(FoundryTxType::Deposit.is_deposit());
774 #[cfg(feature = "base")]
775 assert!(FoundryTxType::Eip8130.is_eip8130());
776 #[cfg(feature = "optimism")]
777 {
778 assert!(FoundryTxType::PostExec.is_post_exec());
779 assert!(!FoundryTxType::Deposit.is_post_exec());
780 }
781 }
782
783 #[test]
784 fn typed_tx_predicates() {
785 assert!(FoundryTypedTx::Legacy(TxLegacy::default()).is_legacy());
786 assert!(FoundryTypedTx::Eip2930(TxEip2930::default()).is_eip2930());
787 assert!(FoundryTypedTx::Eip1559(TxEip1559::default()).is_eip1559());
788 assert!(
789 FoundryTypedTx::Eip4844(TxEip4844Variant::TxEip4844(Default::default())).is_eip4844()
790 );
791 assert!(FoundryTypedTx::Eip7702(TxEip7702::default()).is_eip7702());
792 assert!(FoundryTypedTx::Tempo(TempoTransaction::default()).is_tempo());
793
794 #[cfg(any(feature = "base", feature = "optimism"))]
795 assert!(FoundryTypedTx::Deposit(TxDeposit::default()).is_deposit());
796 #[cfg(feature = "base")]
797 assert!(FoundryTypedTx::Eip8130(TxEip8130::default()).is_eip8130());
798 #[cfg(feature = "optimism")]
799 {
800 assert!(FoundryTypedTx::PostExec(TxPostExec::default()).is_post_exec());
801 }
802 }
803
804 #[test]
805 fn tx_envelope_predicates() {
806 assert!(FoundryTxEnvelope::Legacy(signed(TxLegacy::default())).is_legacy());
807 assert!(FoundryTxEnvelope::Eip2930(signed(TxEip2930::default())).is_eip2930());
808 assert!(FoundryTxEnvelope::Eip1559(signed(TxEip1559::default())).is_eip1559());
809 assert!(
810 FoundryTxEnvelope::Eip4844(signed(TxEip4844Variant::TxEip4844(Default::default())))
811 .is_eip4844()
812 );
813 assert!(FoundryTxEnvelope::Eip7702(signed(TxEip7702::default())).is_eip7702());
814
815 #[cfg(any(feature = "base", feature = "optimism"))]
816 assert!(FoundryTxEnvelope::Deposit(Sealed::new(TxDeposit::default())).is_deposit());
817 #[cfg(feature = "base")]
818 assert!(
819 FoundryTxEnvelope::Eip8130(Eip8130Signed::new(
820 TxEip8130::default(),
821 Default::default(),
822 Default::default()
823 ))
824 .is_eip8130()
825 );
826 #[cfg(feature = "optimism")]
827 {
828 assert!(FoundryTxEnvelope::PostExec(Sealed::new(TxPostExec::default())).is_post_exec());
829 }
830 }
831
832 #[test]
833 fn impersonated_tx_uses_nonzero_dummy_signature() {
834 let FoundryTxEnvelope::Legacy(tx) =
835 FoundryTypedTx::Legacy(TxLegacy::default()).into_impersonated()
836 else {
837 panic!("expected legacy transaction");
838 };
839
840 assert_eq!(tx.signature().r(), U256::ONE);
841 assert_eq!(tx.signature().s(), U256::ONE);
842 assert!(!tx.signature().v());
843 }
844
845 #[test]
846 fn test_decode_call() {
847 let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
848 let decoded = FoundryTxEnvelope::decode(&mut &bytes_first[..]).unwrap();
849
850 let tx = TxLegacy {
851 nonce: 2u64,
852 gas_price: 1000000000u128,
853 gas_limit: 100000,
854 to: TxKind::Call(Address::from_slice(
855 &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..],
856 )),
857 value: U256::from(1000000000000000u64),
858 input: Bytes::default(),
859 chain_id: Some(4),
860 };
861
862 let signature = Signature::from_str("0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b").unwrap();
863
864 let tx = FoundryTxEnvelope::Legacy(Signed::new_unchecked(
865 tx,
866 signature,
867 b256!("0xa517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34"),
868 ));
869
870 assert_eq!(tx, decoded);
871 }
872
873 #[test]
874 fn test_decode_create_goerli() {
875 let tx_bytes =
877 hex::decode("02f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471")
878 .unwrap();
879 let _decoded = FoundryTxEnvelope::decode(&mut &tx_bytes[..]).unwrap();
880 }
881
882 #[test]
883 fn can_recover_sender() {
884 let bytes = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap();
886
887 let Ok(FoundryTxEnvelope::Eip1559(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
888 panic!("decoding FoundryTxEnvelope failed");
889 };
890
891 assert_eq!(
892 tx.hash(),
893 &b256!("0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f")
894 );
895 assert_eq!(
896 tx.recover_signer().unwrap(),
897 address!("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5")
898 );
899 }
900
901 #[test]
904 fn test_decode_live_4844_tx() {
905 use alloy_primitives::{address, b256};
906
907 let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap();
909 let res = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
910 assert!(res.is_type(3));
911
912 let tx = match res {
913 FoundryTxEnvelope::Eip4844(tx) => tx,
914 _ => unreachable!(),
915 };
916
917 assert_eq!(tx.tx().tx().to, address!("0x11E9CA82A3a762b4B5bd264d4173a242e7a77064"));
918
919 assert_eq!(
920 tx.tx().tx().blob_versioned_hashes,
921 vec![
922 b256!("0x012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a"),
923 b256!("0x0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4"),
924 b256!("0x013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7"),
925 b256!("0x01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1"),
926 b256!("0x011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549")
927 ]
928 );
929
930 let from = tx.recover_signer().unwrap();
931 assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
932 }
933
934 #[test]
935 fn can_recover_sender_not_normalized() {
936 let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();
937
938 let Ok(FoundryTxEnvelope::Legacy(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
939 panic!("decoding FoundryTxEnvelope failed");
940 };
941
942 assert_eq!(tx.tx().input, Bytes::from(b""));
943 assert_eq!(tx.tx().gas_price, 1);
944 assert_eq!(tx.tx().gas_limit, 21000);
945 assert_eq!(tx.tx().nonce, 0);
946 if let TxKind::Call(to) = tx.tx().to {
947 assert_eq!(to, address!("0x095e7baea6a6c7c4c2dfeb977efac326af552d87"));
948 } else {
949 panic!("expected a call transaction");
950 }
951 assert_eq!(tx.tx().value, U256::from(0x0au64));
952 assert_eq!(
953 tx.recover_signer().unwrap(),
954 address!("0f65fe9276bc9a24ae7083ae28e2660ef72df99e")
955 );
956 }
957
958 #[test]
959 fn deser_to_type_tx() {
960 let tx = r#"
961 {
962 "type": "0x2",
963 "chainId": "0x7a69",
964 "nonce": "0x0",
965 "gas": "0x5209",
966 "maxFeePerGas": "0x77359401",
967 "maxPriorityFeePerGas": "0x1",
968 "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
969 "value": "0x0",
970 "accessList": [],
971 "input": "0x",
972 "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
973 "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
974 "yParity": "0x0",
975 "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
976 }"#;
977
978 let _typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
979 }
980
981 #[test]
982 fn test_from_recovered_tx_legacy() {
983 let tx = r#"
984 {
985 "type": "0x0",
986 "chainId": "0x1",
987 "nonce": "0x0",
988 "gas": "0x5208",
989 "gasPrice": "0x1",
990 "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
991 "value": "0x1",
992 "input": "0x",
993 "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
994 "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
995 "v": "0x1b",
996 "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
997 }"#;
998
999 let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
1000 let sender = typed_tx.recover().unwrap();
1001
1002 let tx_env = TxEnv::from_recovered_tx(&typed_tx, sender);
1004 assert_eq!(tx_env.caller, sender);
1005 assert_eq!(tx_env.gas_limit, 0x5208);
1006 assert_eq!(tx_env.gas_price, 1);
1007 }
1008
1009 #[test]
1012 fn test_decode_encode_tempo_tx() {
1013 use alloy_primitives::address;
1014 use tempo_primitives::TEMPO_TX_TYPE_ID;
1015
1016 let tx_hash: TxHash =
1017 b256!("0x6d6d8c102064e6dee44abad2024a8b1d37959230baab80e70efbf9b0c739c4fd");
1018
1019 let raw_tx = hex::decode(
1021 "76f9025e82a5bd808502cb4178008302d178f8fcf85c9420c000000000000000000000000000000000000080b844095ea7b3000000000000000000000000dec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000989680f89c94dec000000000000000000000000000000000000080b884f8856c0f00000000000000000000000020c000000000000000000000000000000000000000000000000000000000000020c00000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000097d330c0808080809420c000000000000000000000000000000000000180c0b90133027b98b7a8e6c68d7eac741a52e6fdae0560ce3c16ef5427ad46d7a54d0ed86dd41d000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2238453071464a7a50585167546e645473643649456659457776323173516e626966374c4741776e4b43626b222c226f726967696e223a2268747470733a2f2f74656d706f2d6465782e76657263656c2e617070222c2263726f73734f726967696e223a66616c73657dcfd45c3b19745a42f80b134dcb02a8ba099a0e4e7be1984da54734aa81d8f29f74bb9170ae6d25bd510c83fe35895ee5712efe13980a5edc8094c534e23af85eaacc80b21e45fb11f349424dce3a2f23547f60c0ff2f8bcaede2a247545ce8dd87abf0dbb7a5c9507efae2e43833356651b45ac576c2e61cec4e9c0f41fcbf6e",
1022 )
1023 .unwrap();
1024
1025 let tempo_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
1026
1027 assert!(tempo_tx.is_type(TEMPO_TX_TYPE_ID));
1029
1030 let FoundryTxEnvelope::Tempo(ref aa_signed) = tempo_tx else {
1031 panic!("Expected Tempo transaction");
1032 };
1033
1034 assert_eq!(aa_signed.tx().chain_id, 42429);
1036
1037 assert_eq!(
1039 aa_signed.tx().fee_token,
1040 Some(address!("0x20C0000000000000000000000000000000000001"))
1041 );
1042
1043 assert_eq!(aa_signed.tx().gas_limit, 184696);
1045
1046 assert_eq!(aa_signed.tx().calls.len(), 2);
1048
1049 assert_eq!(tx_hash, tempo_tx.hash());
1051
1052 let mut encoded = Vec::new();
1054 tempo_tx.encode_2718(&mut encoded);
1055 assert_eq!(raw_tx, encoded);
1056
1057 let sender = tempo_tx.recover().unwrap();
1059 assert_eq!(sender, address!("0x566Ff0f4a6114F8072ecDC8A7A8A13d8d0C6B45F"));
1060 }
1061}