Skip to main content

foundry_evm_core/env/
rpc.rs

1//! RPC transaction conversion into concrete execution environments.
2
3use alloy_chains::NamedChain;
4use alloy_consensus::{Transaction as _, Typed2718};
5use alloy_evm::FromRecoveredTx;
6use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse};
7use foundry_evm_networks::celo::CELO_DYNAMIC_FEE_TX_TYPE;
8use revm::context::TxEnv;
9use tempo_alloy::primitives::{TEMPO_TX_TYPE_ID, TempoTxEnvelope};
10use tempo_revm::TempoTxEnv;
11
12/// Trait for converting an [`AnyRpcTransaction`] into a specific `TxEnv`.
13///
14/// Ethereum envelopes delegate to [`FromRecoveredTx`]. Implementations may also explicitly
15/// project compatible network-specific envelopes into their execution environment.
16pub trait FromAnyRpcTransaction: Sized {
17    /// Tries to convert an [`AnyRpcTransaction`] into `Self`.
18    fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result<Self>;
19}
20
21/// Returns the error for a transaction whose envelope `target` cannot represent.
22fn unknown_transaction_type(tx: &AnyRpcTransaction, target: &str) -> eyre::Report {
23    eyre::eyre!("cannot convert unknown transaction type {:#04x} to {target}", tx.inner.inner.ty())
24}
25
26impl FromAnyRpcTransaction for TxEnv {
27    fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result<Self> {
28        if let Some(envelope) = tx.as_envelope() {
29            return Ok(Self::from_recovered_tx(envelope, tx.from()));
30        }
31
32        // CIP-64 transactions have EIP-1559 execution fields plus a Celo-specific fee currency.
33        // Foundry does not model fee payment in TxEnv, but can replay their EVM payload. Preserve
34        // the custom type so revm does not compare the fee-currency price with the native-CELO
35        // base fee. Keep this projection restricted to active Celo chains so an unrelated network
36        // cannot silently acquire semantics for its own type 0x7b envelope.
37        if let AnyTxEnvelope::Unknown(unknown) = &*tx.inner.inner
38            && unknown.ty() == CELO_DYNAMIC_FEE_TX_TYPE
39            && matches!(
40                unknown.chain_id().and_then(NamedChain::from_chain_id),
41                Some(NamedChain::Celo | NamedChain::CeloSepolia)
42            )
43        {
44            return Ok(Self {
45                tx_type: CELO_DYNAMIC_FEE_TX_TYPE,
46                caller: tx.from(),
47                gas_limit: unknown.gas_limit(),
48                gas_price: unknown.max_fee_per_gas(),
49                gas_priority_fee: unknown.max_priority_fee_per_gas(),
50                kind: unknown.kind(),
51                value: unknown.value(),
52                data: unknown.input().clone(),
53                nonce: unknown.nonce(),
54                chain_id: unknown.chain_id(),
55                access_list: unknown.access_list().cloned().unwrap_or_default(),
56                ..Default::default()
57            });
58        }
59
60        Err(unknown_transaction_type(tx, "TxEnv"))
61    }
62}
63
64impl FromAnyRpcTransaction for TempoTxEnv {
65    fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result<Self> {
66        // Rebuild the signed Tempo envelope so Tempo's own conversion populates the transaction
67        // hash and sender-scoped identifier for every type, and the batch calls, nonce key,
68        // validity window, authorizations and fee payer for AA transactions.
69        let envelope = match &*tx.inner.inner {
70            AnyTxEnvelope::Ethereum(envelope) => TempoTxEnvelope::try_from(envelope.clone())
71                .map_err(|_| unknown_transaction_type(tx, "TempoTxEnv"))?,
72            AnyTxEnvelope::Unknown(unknown) if unknown.ty() == TEMPO_TX_TYPE_ID => {
73                serde_json::from_value::<alloy_rpc_types::Transaction<TempoTxEnvelope>>(
74                    serde_json::to_value(tx)?,
75                )
76                .map_err(|err| eyre::eyre!("cannot decode Tempo AA transaction: {err}"))?
77                .inner
78                .into_inner()
79            }
80            AnyTxEnvelope::Unknown(_) => return Err(unknown_transaction_type(tx, "TempoTxEnv")),
81        };
82        Ok(Self::from_recovered_tx(&envelope, tx.from()))
83    }
84}
85
86#[cfg(feature = "base")]
87mod base {
88    use super::*;
89    use base_common_consensus::BaseTxEnvelope;
90    use base_common_evm::{BaseTransaction, EIP8130_TRANSACTION_TYPE};
91    use base_common_rpc_types::Transaction as BaseRpcTransaction;
92
93    impl FromAnyRpcTransaction for BaseTransaction<TxEnv> {
94        fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result<Self> {
95            let envelope = match BaseTxEnvelope::try_from(tx.clone()) {
96                Ok(envelope) => envelope,
97                Err(_) if tx.ty() == EIP8130_TRANSACTION_TYPE => {
98                    let rpc_tx =
99                        serde_json::from_value::<BaseRpcTransaction>(serde_json::to_value(tx)?)
100                            .map_err(|err| {
101                                eyre::eyre!(
102                                    "cannot convert RPC transaction to Base envelope: {err}"
103                                )
104                            })?;
105                    rpc_tx.inner.into_inner()
106                }
107                Err(_) => eyre::bail!("cannot convert transaction to BaseTxEnvelope"),
108            };
109            Ok(Self::from_recovered_tx(&envelope, tx.from()))
110        }
111    }
112}
113
114#[cfg(feature = "optimism")]
115mod optimism {
116    use super::*;
117    use alloy_eips::eip2718::Encodable2718;
118    use alloy_op_evm::OpTx;
119    use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit};
120    use op_revm::OpTransaction;
121
122    impl FromAnyRpcTransaction for OpTx {
123        fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result<Self> {
124            if let Some(envelope) = tx.as_envelope() {
125                return Ok(Self(OpTransaction::<TxEnv> {
126                    base: TxEnv::from_recovered_tx(envelope, tx.from()),
127                    // The L1 data fee is charged off these bytes, and op-revm rejects a
128                    // non-deposit transaction that arrives without them.
129                    enveloped_tx: Some(envelope.encoded_2718().into()),
130                    deposit: Default::default(),
131                }));
132            }
133
134            // Handle OP deposit transactions from `Unknown` envelope variant.
135            if let AnyTxEnvelope::Unknown(unknown) = &*tx.inner.inner
136                && unknown.ty() == DEPOSIT_TX_TYPE_ID
137            {
138                let mut fields = unknown.inner.fields.clone();
139                fields.insert("from".to_string(), serde_json::to_value(tx.from())?);
140                let deposit_tx: TxDeposit = fields
141                    .deserialize_into()
142                    .map_err(|e| eyre::eyre!("failed to deserialize deposit tx: {e}"))?;
143                return Ok(Self::from_recovered_tx(&deposit_tx, deposit_tx.from));
144            }
145
146            Err(unknown_transaction_type(tx, "OpTransaction"))
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use alloy_consensus::{Signed, TxEip1559, transaction::Recovered};
155    use alloy_network::{AnyTxType, UnknownTxEnvelope, UnknownTypedTransaction};
156    use alloy_primitives::{Address, B256, Bytes, Signature, U256, address, b256, bytes};
157    use alloy_rpc_types::{Transaction as RpcTransaction, TransactionInfo};
158    use alloy_serde::WithOtherFields;
159    use alloy_signer::SignerSync;
160    use alloy_signer_local::PrivateKeySigner;
161    use revm::primitives::TxKind;
162    use std::num::NonZeroU64;
163    use tempo_alloy::primitives::{TempoSignature, TempoTransaction, transaction::Call};
164    use tempo_revm::ExecutionContext;
165
166    #[cfg(feature = "base")]
167    use base_common_evm::BaseTransaction;
168
169    fn make_signed_eip1559() -> Signed<TxEip1559> {
170        Signed::new_unchecked(
171            TxEip1559 {
172                chain_id: 1,
173                nonce: 42,
174                gas_limit: 21001,
175                to: TxKind::Call(Address::with_last_byte(0xBB)),
176                value: U256::from(101),
177                ..Default::default()
178            },
179            Signature::new(U256::ZERO, U256::ZERO, false),
180            B256::ZERO,
181        )
182    }
183
184    #[test]
185    fn from_any_rpc_transaction_for_eth() {
186        let from = Address::random();
187        let any_tx = eth_rpc_transaction(from);
188        let tx_env = TxEnv::from_any_rpc_transaction(&any_tx).unwrap();
189
190        assert_eq!(tx_env.caller, from);
191        assert_eq!(tx_env.nonce, 42);
192        assert_eq!(tx_env.gas_limit, 21001);
193        assert_eq!(tx_env.value, U256::from(101));
194        assert_eq!(tx_env.kind, TxKind::Call(Address::with_last_byte(0xBB)));
195    }
196
197    #[cfg(feature = "base")]
198    #[test]
199    fn from_any_rpc_transaction_for_base_eth_envelope() {
200        let from = Address::random();
201        let any_tx = eth_rpc_transaction(from);
202
203        let tx_env = BaseTransaction::<TxEnv>::from_any_rpc_transaction(&any_tx).unwrap();
204        assert_eq!(tx_env.base.caller, from);
205        assert_eq!(tx_env.base.nonce, 42);
206        assert_eq!(tx_env.base.gas_limit, 21001);
207        assert_eq!(tx_env.base.value, U256::from(101));
208        assert!(tx_env.enveloped_tx.is_some());
209    }
210
211    #[test]
212    fn from_any_rpc_transaction_unknown_envelope_errors() {
213        let unknown = AnyTxEnvelope::Unknown(UnknownTxEnvelope {
214            hash: B256::ZERO,
215            inner: UnknownTypedTransaction {
216                ty: AnyTxType(0xFF),
217                fields: Default::default(),
218                memo: Default::default(),
219            },
220        });
221        let from = Address::random();
222        let any_tx = AnyRpcTransaction::new(WithOtherFields::new(RpcTransaction {
223            inner: Recovered::new_unchecked(unknown, from),
224            block_hash: None,
225            block_number: None,
226            transaction_index: None,
227            effective_gas_price: None,
228            block_timestamp: None,
229        }));
230
231        let result = TxEnv::from_any_rpc_transaction(&any_tx).unwrap_err();
232        assert_eq!(result.to_string(), "cannot convert unknown transaction type 0xff to TxEnv");
233    }
234
235    #[test]
236    fn from_any_rpc_transaction_for_celo_dynamic_fee() {
237        let from = Address::with_last_byte(0xAA);
238        let to = Address::with_last_byte(0xBB);
239        let fee_currency = Address::with_last_byte(0xCC);
240        let json = serde_json::json!({
241            "accessList": [],
242            "blockHash": B256::ZERO,
243            "blockNumber": "0x1",
244            "chainId": "0xa4ec",
245            "feeCurrency": fee_currency,
246            "from": from,
247            "gas": "0x5208",
248            "gasPrice": "0x3",
249            "hash": B256::ZERO,
250            "input": "0x1234",
251            "maxFeePerGas": "0x3",
252            "maxPriorityFeePerGas": "0x1",
253            "nonce": "0x2a",
254            "r": B256::ZERO,
255            "s": B256::ZERO,
256            "to": to,
257            "transactionIndex": "0x0",
258            "type": "0x7b",
259            "v": "0x0",
260            "value": "0x65",
261            "yParity": "0x0"
262        });
263        let mut non_celo_json = json.clone();
264        non_celo_json["chainId"] = serde_json::json!("0x1");
265        let non_celo_tx: AnyRpcTransaction = serde_json::from_value(non_celo_json).unwrap();
266        assert!(TxEnv::from_any_rpc_transaction(&non_celo_tx).is_err());
267
268        let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap();
269
270        let tx_env = TxEnv::from_any_rpc_transaction(&any_tx).unwrap();
271
272        assert_eq!(tx_env.tx_type, CELO_DYNAMIC_FEE_TX_TYPE);
273        assert_eq!(tx_env.caller, from);
274        assert_eq!(tx_env.nonce, 42);
275        assert_eq!(tx_env.gas_limit, 21000);
276        assert_eq!(tx_env.gas_price, 3);
277        assert_eq!(tx_env.gas_priority_fee, Some(1));
278        assert_eq!(tx_env.kind, TxKind::Call(to));
279        assert_eq!(tx_env.value, U256::from(101));
280        assert_eq!(tx_env.data, Bytes::from_static(&[0x12, 0x34]));
281        assert_eq!(tx_env.chain_id, Some(42_220));
282    }
283
284    #[test]
285    fn from_any_rpc_transaction_for_tempo_eth_envelope() {
286        let from = Address::random();
287        let signed_tx = make_signed_eip1559();
288        let tempo_envelope = TempoTxEnvelope::Eip1559(signed_tx.clone());
289        let any_tx = eth_rpc_transaction(from);
290
291        let tx_env = TempoTxEnv::from_any_rpc_transaction(&any_tx).unwrap();
292        assert_eq!(tx_env.inner.caller, from);
293        assert_eq!(tx_env.inner.nonce, 42);
294        assert_eq!(tx_env.inner.gas_limit, 21001);
295        assert_eq!(tx_env.inner.value, U256::from(101));
296        assert_eq!(tx_env.fee_token, None);
297        assert_eq!(
298            tx_env.execution_context(),
299            ExecutionContext::Transaction { tx_hash: *signed_tx.hash() }
300        );
301        assert_eq!(tx_env.unique_tx_identifier(), Some(tempo_envelope.unique_tx_identifier(from)));
302        assert!(tx_env.tempo_tx_env.is_none());
303    }
304
305    #[test]
306    fn from_any_rpc_transaction_for_tempo_aa() {
307        let sender = PrivateKeySigner::random();
308        let sponsor = PrivateKeySigner::random();
309        let fee_token = Some(Address::random());
310        let calls = vec![
311            Call {
312                to: TxKind::Call(Address::with_last_byte(0x11)),
313                value: U256::ZERO,
314                input: Bytes::from_static(&[0xaa, 0xbb]),
315            },
316            Call {
317                to: TxKind::Call(Address::with_last_byte(0x22)),
318                value: U256::ZERO,
319                input: Bytes::from_static(&[0xcc, 0xdd]),
320            },
321        ];
322        let mut tempo_tx = TempoTransaction {
323            chain_id: 42431,
324            nonce: 42,
325            gas_limit: 424242,
326            fee_token,
327            nonce_key: U256::from(4242),
328            valid_after: NonZeroU64::new(1800000000),
329            valid_before: NonZeroU64::new(1900000000),
330            calls: calls.clone(),
331            ..Default::default()
332        };
333        tempo_tx.fee_payer_signature = Some(
334            sponsor.sign_hash_sync(&tempo_tx.fee_payer_signature_hash(sender.address())).unwrap(),
335        );
336        let signature = sender.sign_hash_sync(&tempo_tx.signature_hash()).unwrap();
337        let aa_signed = tempo_tx.into_signed(signature.into());
338        let tx_hash = *aa_signed.hash();
339        let unique_tx_identifier = aa_signed.expiring_nonce_hash(sender.address());
340
341        // Round-trip a Tempo RPC transaction through JSON, as `AnyNetwork` providers receive it.
342        let rpc_tx = RpcTransaction::from_transaction(
343            Recovered::new_unchecked(TempoTxEnvelope::AA(aa_signed), sender.address()),
344            TransactionInfo::default(),
345        );
346        let any_tx: AnyRpcTransaction =
347            serde_json::from_value(serde_json::to_value(&rpc_tx).unwrap()).unwrap();
348        assert!((*any_tx.inner.inner).is_unknown());
349
350        let tx_env = TempoTxEnv::from_any_rpc_transaction(&any_tx).unwrap();
351        assert_eq!(tx_env.inner.tx_type, TEMPO_TX_TYPE_ID);
352        assert_eq!(tx_env.inner.caller, sender.address());
353        assert_eq!(tx_env.inner.nonce, 42);
354        assert_eq!(tx_env.inner.gas_limit, 424242);
355        assert_eq!(tx_env.inner.chain_id, Some(42431));
356        assert_eq!(tx_env.fee_token, fee_token);
357        assert_eq!(tx_env.fee_payer, Some(Some(sponsor.address())));
358        assert_eq!(tx_env.execution_context(), ExecutionContext::Transaction { tx_hash });
359        assert_eq!(tx_env.unique_tx_identifier(), Some(unique_tx_identifier));
360
361        let aa = tx_env.tempo_tx_env.as_deref().unwrap();
362        assert_eq!(aa.aa_calls, calls);
363        assert_eq!(aa.nonce_key, U256::from(4242));
364        assert_eq!(aa.valid_after, Some(1800000000));
365        assert_eq!(aa.valid_before, Some(1900000000));
366        assert_eq!(aa.tx_hash, tx_hash);
367    }
368
369    /// Decodes a captured Tempo `eth_getTransactionByHash` result.
370    fn captured_tempo_aa(json: &str) -> TempoTxEnv {
371        let any_tx: AnyRpcTransaction = serde_json::from_str(json).unwrap();
372        let tx_env = TempoTxEnv::from_any_rpc_transaction(&any_tx).unwrap();
373        let aa = tx_env.tempo_tx_env.as_deref().unwrap();
374        // Recovery over the recomputed signing hash only succeeds if every signed field decoded.
375        assert_eq!(aa.signature.recover_signer(&aa.signature_hash).unwrap(), tx_env.inner.caller);
376        assert_eq!(tx_env.inner.tx_type, TEMPO_TX_TYPE_ID);
377        tx_env
378    }
379
380    #[test]
381    fn from_any_rpc_transaction_for_captured_sponsored_tempo_aa_batch() {
382        let tx_env =
383            captured_tempo_aa(include_str!("../../test-data/tempo-aa-sponsored-batch.json"));
384        assert_eq!(tx_env.inner.caller, address!("0x0a0d9bc4dda3a659699ce05153ffe9298f99ce89"));
385        assert_eq!(tx_env.inner.nonce, 0);
386        assert_eq!(tx_env.inner.gas_limit, 1403789);
387        assert_eq!(tx_env.inner.chain_id, Some(4217));
388        assert_eq!(tx_env.fee_token, Some(address!("0x20c0000000000000000000006a37da5c996874be")));
389        assert_eq!(
390            tx_env.fee_payer,
391            Some(Some(address!("0x58aa7ce42e1d13b2919e2ac7e006c4fbc171442c")))
392        );
393        assert_eq!(
394            tx_env.execution_context(),
395            ExecutionContext::Transaction {
396                tx_hash: b256!(
397                    "0xbb23551c35bc2c2539ac3637c7464a22b5fb38bbb9cf39fa1f533ca6c2c981a3"
398                )
399            }
400        );
401        // Expiring nonces are replay-protected by this sender-scoped identifier.
402        assert_eq!(
403            tx_env.channel_open_context_hash(),
404            Some(b256!("0xadb35ee9830a691a8dbd8f42208a1553d9a242b8750a218d841cc78a0dea20ca"))
405        );
406
407        let aa = tx_env.tempo_tx_env.as_deref().unwrap();
408        assert_eq!(
409            aa.aa_calls,
410            [
411                Call {
412                    to: TxKind::Call(address!("0x20c0000000000000000000000000000000000000")),
413                    value: U256::ZERO,
414                    input: bytes!(
415                        "0x095ea7b300000000000000000000000083a1491f3e7f8daab8f787a631334b9ca7a870230000000000000000000000000000000000000000000000000000000077359400"
416                    ),
417                },
418                Call {
419                    to: TxKind::Call(address!("0x83a1491f3e7f8daab8f787a631334b9ca7a87023")),
420                    value: U256::ZERO,
421                    input: bytes!(
422                        "0x6e553f6500000000000000000000000000000000000000000000000000000000773594000000000000000000000000000a0d9bc4dda3a659699ce05153ffe9298f99ce89"
423                    ),
424                },
425            ]
426        );
427        assert_eq!(aa.nonce_key, U256::MAX);
428        assert_eq!(aa.valid_after, Some(542204929));
429        assert_eq!(aa.valid_before, Some(1790813692));
430    }
431
432    #[test]
433    fn from_any_rpc_transaction_for_captured_nonce_lane_tempo_aa() {
434        let tx_env = captured_tempo_aa(include_str!("../../test-data/tempo-aa-nonce-lane.json"));
435        assert_eq!(tx_env.inner.caller, address!("0xfdd1c606b498f5fcaaf27bd318b14caf52e8f6c2"));
436        assert_eq!(tx_env.inner.nonce, 22496);
437        assert_eq!(tx_env.inner.gas_limit, 99408);
438        assert_eq!(tx_env.inner.chain_id, Some(42431));
439        assert_eq!(tx_env.fee_token, None);
440        assert_eq!(tx_env.fee_payer, None);
441        assert_eq!(
442            tx_env.execution_context(),
443            ExecutionContext::Transaction {
444                tx_hash: b256!(
445                    "0x4df0a9b9e859be6c91c00edf8478939d0b91f6f9eb39dd9c8a93946ac70ff740"
446                )
447            }
448        );
449
450        let aa = tx_env.tempo_tx_env.as_deref().unwrap();
451        let [call] = aa.aa_calls.as_slice() else { panic!("expected one call") };
452        assert_eq!(call.to, TxKind::Call(address!("0x5ad0000000000000000000000000000000000003")));
453        assert_eq!(call.value, U256::ZERO);
454        assert_eq!(call.input.len(), 868);
455        assert_eq!(aa.nonce_key, U256::ONE);
456        assert_eq!(aa.valid_after, None);
457        assert_eq!(aa.valid_before, None);
458    }
459
460    #[test]
461    fn from_any_rpc_transaction_for_captured_keychain_tempo_aa() {
462        let tx_env = captured_tempo_aa(include_str!("../../test-data/tempo-aa-keychain.json"));
463        let sender = address!("0x5f704c6c7075acd14ee36527f03b5b5dcb4a966f");
464        let key_id = address!("0x240a31713e851acdc0c590fe45ef72baeff55f91");
465        assert_eq!(tx_env.inner.caller, sender);
466        assert_eq!(tx_env.inner.nonce, 0);
467        assert_eq!(tx_env.inner.gas_limit, 322821);
468        assert_eq!(tx_env.inner.chain_id, Some(42431));
469        assert_eq!(tx_env.fee_token, None);
470        assert_eq!(
471            tx_env.fee_payer,
472            Some(Some(address!("0x133d7736f290fa2758cf0d1e0862f5a93ae1dcbf")))
473        );
474        assert_eq!(
475            tx_env.execution_context(),
476            ExecutionContext::Transaction {
477                tx_hash: b256!(
478                    "0xd4d372ad5cfa4cdf476aeb25d4a76fb80fe2d6169eb9354c544500c0496a19d6"
479                )
480            }
481        );
482        // Expiring nonces are replay-protected by this sender-scoped identifier.
483        assert_eq!(
484            tx_env.channel_open_context_hash(),
485            Some(b256!("0xdb2722c83d8893c1637bbabfe2ad924bcc044653f6c9fa91d472d70ddfa97de3"))
486        );
487
488        let aa = tx_env.tempo_tx_env.as_deref().unwrap();
489        assert_eq!(
490            aa.aa_calls,
491            [Call { to: TxKind::Call(Address::ZERO), value: U256::ZERO, input: Bytes::new() }]
492        );
493        assert_eq!(aa.nonce_key, U256::MAX);
494        assert_eq!(aa.valid_after, Some(949691873));
495        assert_eq!(aa.valid_before, Some(1790811579));
496        let TempoSignature::Keychain(keychain) = &aa.signature else { panic!("expected keychain") };
497        assert_eq!(keychain.user_address, sender);
498        assert_eq!(keychain.key_id(&aa.signature_hash).unwrap(), key_id);
499        let key_authorization = aa.key_authorization.as_ref().unwrap();
500        assert_eq!(key_authorization.key_id, key_id);
501        assert_eq!(key_authorization.recover_signer().unwrap(), sender);
502    }
503
504    #[test]
505    fn from_any_rpc_transaction_for_captured_authorization_list_tempo_aa() {
506        let tx_env =
507            captured_tempo_aa(include_str!("../../test-data/tempo-aa-authorization-list.json"));
508        assert_eq!(tx_env.inner.caller, address!("0xd410bcea80de9214e0e49df7d19c4c5e3db60f2a"));
509        assert_eq!(tx_env.inner.nonce, 0);
510        assert_eq!(tx_env.inner.gas_limit, 2000000);
511        assert_eq!(tx_env.inner.chain_id, Some(42431));
512        assert_eq!(tx_env.fee_token, Some(address!("0x20c0000000000000000000000000000000000000")));
513        assert_eq!(tx_env.fee_payer, None);
514        assert_eq!(
515            tx_env.execution_context(),
516            ExecutionContext::Transaction {
517                tx_hash: b256!(
518                    "0xfff5a6fa344fa9d6d7410374f40b6f711735500892cccdf480670fd7c5adcf93"
519                )
520            }
521        );
522
523        let aa = tx_env.tempo_tx_env.as_deref().unwrap();
524        let call = Call {
525            to: TxKind::Call(address!("0xa98d41b22c17ae4f4a94420a29a3095464c037e3")),
526            value: U256::ZERO,
527            input: Bytes::new(),
528        };
529        assert_eq!(aa.aa_calls, [call.clone(), call]);
530        assert_eq!(aa.nonce_key, U256::ZERO);
531        // Secp256k1, P256 and WebAuthn authorizations, in that order.
532        let delegate = address!("0xaaaaaaaa00000000000000000000000000000000");
533        assert_eq!(
534            aa.tempo_authorization_list
535                .iter()
536                .map(|auth| (auth.address, auth.authority()))
537                .collect::<Vec<_>>(),
538            [
539                (delegate, Some(address!("0xd9aa283bc5643587f9623a0e9683df78588404a8"))),
540                (delegate, Some(address!("0x883b644ccaa219845187fd0932a8ef6e7d484bd6"))),
541                (delegate, Some(address!("0x732c32ec8e029d8989b9d1db9914376b17e20676"))),
542            ]
543        );
544    }
545
546    #[test]
547    fn from_any_rpc_transaction_for_tempo_aa_without_signature_errors() {
548        let mut json: serde_json::Value =
549            serde_json::from_str(include_str!("../../test-data/tempo-aa-sponsored-batch.json"))
550                .unwrap();
551        json.as_object_mut().unwrap().remove("signature");
552        let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap();
553
554        let err = TempoTxEnv::from_any_rpc_transaction(&any_tx).unwrap_err();
555        assert_eq!(
556            err.to_string(),
557            "cannot decode Tempo AA transaction: missing field `signature`"
558        );
559    }
560
561    #[cfg(feature = "optimism")]
562    mod optimism {
563        use super::*;
564        use alloy_consensus::Sealed;
565        use alloy_eips::eip2718::Encodable2718;
566        use alloy_op_evm::OpTx;
567        use op_alloy_consensus::{OpTxEnvelope, TxDeposit, transaction::OpTransactionInfo};
568        use op_alloy_rpc_types::Transaction as OpRpcTransaction;
569
570        #[test]
571        fn from_any_rpc_transaction_for_op() {
572            let from = Address::random();
573            let any_tx = eth_rpc_transaction(from);
574            let expected_base = TxEnv::from_any_rpc_transaction(&any_tx).unwrap();
575
576            let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap();
577            assert_eq!(op_tx_env.base, expected_base);
578            // op-revm charges the L1 data fee off these bytes and rejects a non-deposit
579            // transaction that arrives without them.
580            assert_eq!(
581                op_tx_env.enveloped_tx,
582                Some(any_tx.as_envelope().unwrap().encoded_2718().into())
583            );
584        }
585
586        #[test]
587        fn from_any_rpc_transaction_for_op_deposit() {
588            let from = Address::random();
589            let source_hash = B256::random();
590            let deposit = TxDeposit {
591                source_hash,
592                from,
593                to: TxKind::Call(Address::with_last_byte(0xCC)),
594                mint: 1111,
595                value: U256::from(200),
596                gas_limit: 21000,
597                is_system_transaction: true,
598                input: Default::default(),
599            };
600
601            // Build a concrete OpRpcTransaction, serialize to JSON, deserialize as
602            // AnyRpcTransaction.
603            let op_rpc_tx = OpRpcTransaction::from_transaction(
604                Recovered::new_unchecked(OpTxEnvelope::Deposit(Sealed::new(deposit)), from),
605                OpTransactionInfo::default(),
606            );
607            let json = serde_json::to_value(&op_rpc_tx).unwrap();
608            let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap();
609
610            let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap();
611            assert_eq!(op_tx_env.base.caller, from);
612            assert_eq!(op_tx_env.base.kind, TxKind::Call(Address::with_last_byte(0xCC)));
613            assert_eq!(op_tx_env.base.value, U256::from(200));
614            assert_eq!(op_tx_env.base.gas_limit, 21000);
615            assert_eq!(op_tx_env.deposit.source_hash, source_hash);
616            assert_eq!(op_tx_env.deposit.mint, Some(1111));
617            assert!(op_tx_env.deposit.is_system_transaction);
618        }
619    }
620
621    fn eth_rpc_transaction(from: Address) -> AnyRpcTransaction {
622        RpcTransaction::from_transaction(
623            Recovered::new_unchecked(make_signed_eip1559().into(), from),
624            TransactionInfo::default(),
625        )
626        .into()
627    }
628}