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