Skip to main content

tempo_alloy/rpc/
request.rs

1use alloy_consensus::{Signed, TxEip1559, TxEip2930, TxEip7702, TxLegacy, error::ValueError};
2use alloy_contract::{CallBuilder, CallDecoder};
3use alloy_eips::Typed2718;
4use alloy_primitives::{Address, Bytes, U256};
5use alloy_provider::Provider;
6use alloy_rpc_types_eth::{TransactionRequest, TransactionTrait};
7use serde::{Deserialize, Serialize};
8use tempo_primitives::{
9    AASigned, SignatureType, TempoTransaction, TempoTxEnvelope,
10    transaction::{Call, SignedKeyAuthorization, TempoSignedAuthorization, TempoTypedTransaction},
11};
12
13use crate::TempoNetwork;
14
15/// An Ethereum [`TransactionRequest`] with an optional `fee_token`.
16#[derive(
17    Clone,
18    Debug,
19    Default,
20    PartialEq,
21    Eq,
22    Hash,
23    Serialize,
24    Deserialize,
25    derive_more::Deref,
26    derive_more::DerefMut,
27)]
28#[serde(rename_all = "camelCase")]
29pub struct TempoTransactionRequest {
30    /// Inner [`TransactionRequest`]
31    #[serde(flatten)]
32    #[deref]
33    #[deref_mut]
34    pub inner: TransactionRequest,
35
36    /// Optional fee token preference
37    pub fee_token: Option<Address>,
38
39    /// Optional nonce key for a 2D [`TempoTransaction`].
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub nonce_key: Option<U256>,
42
43    /// Optional calls array, for Tempo transactions.
44    #[serde(default)]
45    pub calls: Vec<Call>,
46
47    /// Optional key type for gas estimation of Tempo transactions.
48    /// Specifies the signature verification algorithm to calculate accurate gas costs.
49    pub key_type: Option<SignatureType>,
50
51    /// Optional key-specific data for gas estimation (e.g., webauthn authenticator data).
52    /// Required when key_type is WebAuthn to calculate calldata gas costs.
53    pub key_data: Option<Bytes>,
54
55    /// Optional access key ID for gas estimation.
56    /// When provided, indicates the transaction uses a Keychain (access key) signature.
57    /// This enables accurate gas estimation for:
58    /// - Keychain signature validation overhead (+3,000 gas)
59    /// - Spending limits enforcement during execution
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub key_id: Option<Address>,
62
63    /// Optional authorization list for Tempo transactions (supports multiple signature types)
64    #[serde(
65        default,
66        skip_serializing_if = "Vec::is_empty",
67        rename = "aaAuthorizationList"
68    )]
69    pub tempo_authorization_list: Vec<TempoSignedAuthorization>,
70
71    /// Key authorization for provisioning an access key (for gas estimation).
72    /// Provide a signed KeyAuthorization when the transaction provisions an access key.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub key_authorization: Option<SignedKeyAuthorization>,
75
76    /// Transaction valid before timestamp in seconds (for expiring nonces, [TIP-1009]).
77    /// Transaction can only be included in a block before this timestamp.
78    ///
79    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
80    #[serde(
81        default,
82        skip_serializing_if = "Option::is_none",
83        with = "alloy_serde::quantity::opt"
84    )]
85    pub valid_before: Option<u64>,
86
87    /// Transaction valid after timestamp in seconds (for expiring nonces, [TIP-1009]).
88    /// Transaction can only be included in a block after this timestamp.
89    ///
90    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
91    #[serde(
92        default,
93        skip_serializing_if = "Option::is_none",
94        with = "alloy_serde::quantity::opt"
95    )]
96    pub valid_after: Option<u64>,
97
98    /// Fee payer signature for sponsored transactions.
99    /// The sponsor signs fee_payer_signature_hash(sender) to commit to paying gas.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub fee_payer_signature: Option<alloy_primitives::Signature>,
102}
103
104impl TempoTransactionRequest {
105    /// Set the fee token for the [`TempoTransaction`] transaction.
106    pub fn set_fee_token(&mut self, fee_token: Address) {
107        self.fee_token = Some(fee_token);
108    }
109
110    /// Builder-pattern method for setting the fee token.
111    pub fn with_fee_token(mut self, fee_token: Address) -> Self {
112        self.fee_token = Some(fee_token);
113        self
114    }
115
116    /// Set the 2D nonce key for the [`TempoTransaction`] transaction.
117    pub fn set_nonce_key(&mut self, nonce_key: U256) {
118        self.nonce_key = Some(nonce_key);
119    }
120
121    /// Builder-pattern method for setting a 2D nonce key for a [`TempoTransaction`].
122    pub fn with_nonce_key(mut self, nonce_key: U256) -> Self {
123        self.nonce_key = Some(nonce_key);
124        self
125    }
126
127    /// Set the valid_before timestamp for expiring nonces ([TIP-1009]).
128    ///
129    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
130    pub fn set_valid_before(&mut self, valid_before: u64) {
131        self.valid_before = Some(valid_before);
132    }
133
134    /// Builder-pattern method for setting valid_before timestamp.
135    pub fn with_valid_before(mut self, valid_before: u64) -> Self {
136        self.valid_before = Some(valid_before);
137        self
138    }
139
140    /// Set the valid_after timestamp for expiring nonces ([TIP-1009]).
141    ///
142    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
143    pub fn set_valid_after(&mut self, valid_after: u64) {
144        self.valid_after = Some(valid_after);
145    }
146
147    /// Builder-pattern method for setting valid_after timestamp.
148    pub fn with_valid_after(mut self, valid_after: u64) -> Self {
149        self.valid_after = Some(valid_after);
150        self
151    }
152
153    /// Set the fee payer signature for sponsored transactions.
154    pub fn set_fee_payer_signature(&mut self, signature: alloy_primitives::Signature) {
155        self.fee_payer_signature = Some(signature);
156    }
157
158    /// Builder-pattern method for setting fee payer signature.
159    pub fn with_fee_payer_signature(mut self, signature: alloy_primitives::Signature) -> Self {
160        self.fee_payer_signature = Some(signature);
161        self
162    }
163
164    /// Attempts to build a [`TempoTransaction`] with the configured fields.
165    pub fn build_aa(self) -> Result<TempoTransaction, ValueError<Self>> {
166        if self.calls.is_empty() && self.inner.to.is_none() {
167            return Err(ValueError::new(
168                self,
169                "Missing 'calls' or 'to' field for Tempo transaction.",
170            ));
171        }
172
173        let Some(nonce) = self.inner.nonce else {
174            return Err(ValueError::new(
175                self,
176                "Missing 'nonce' field for Tempo transaction.",
177            ));
178        };
179        let Some(gas_limit) = self.inner.gas else {
180            return Err(ValueError::new(
181                self,
182                "Missing 'gas_limit' field for Tempo transaction.",
183            ));
184        };
185        let Some(max_fee_per_gas) = self.inner.max_fee_per_gas else {
186            return Err(ValueError::new(
187                self,
188                "Missing 'max_fee_per_gas' field for Tempo transaction.",
189            ));
190        };
191        let Some(max_priority_fee_per_gas) = self.inner.max_priority_fee_per_gas else {
192            return Err(ValueError::new(
193                self,
194                "Missing 'max_priority_fee_per_gas' field for Tempo transaction.",
195            ));
196        };
197
198        let mut calls = self.calls;
199        if let Some(to) = self.inner.to {
200            calls.push(Call {
201                to,
202                value: self.inner.value.unwrap_or_default(),
203                input: self.inner.input.into_input().unwrap_or_default(),
204            });
205        }
206
207        Ok(TempoTransaction {
208            chain_id: self.inner.chain_id.unwrap_or(4217),
209            nonce,
210            fee_payer_signature: self.fee_payer_signature,
211            valid_before: self.valid_before,
212            valid_after: self.valid_after,
213            gas_limit,
214            max_fee_per_gas,
215            max_priority_fee_per_gas,
216            fee_token: self.fee_token,
217            access_list: self.inner.access_list.unwrap_or_default(),
218            calls,
219            tempo_authorization_list: self.tempo_authorization_list,
220            nonce_key: self.nonce_key.unwrap_or_default(),
221            key_authorization: self.key_authorization,
222        })
223    }
224}
225
226impl AsRef<TransactionRequest> for TempoTransactionRequest {
227    fn as_ref(&self) -> &TransactionRequest {
228        &self.inner
229    }
230}
231
232impl AsMut<TransactionRequest> for TempoTransactionRequest {
233    fn as_mut(&mut self) -> &mut TransactionRequest {
234        &mut self.inner
235    }
236}
237
238impl From<TransactionRequest> for TempoTransactionRequest {
239    fn from(value: TransactionRequest) -> Self {
240        Self {
241            inner: value,
242            fee_token: None,
243            ..Default::default()
244        }
245    }
246}
247
248impl From<TempoTransactionRequest> for TransactionRequest {
249    fn from(value: TempoTransactionRequest) -> Self {
250        value.inner
251    }
252}
253
254impl From<TempoTxEnvelope> for TempoTransactionRequest {
255    fn from(value: TempoTxEnvelope) -> Self {
256        match value {
257            TempoTxEnvelope::Legacy(tx) => tx.into(),
258            TempoTxEnvelope::Eip2930(tx) => tx.into(),
259            TempoTxEnvelope::Eip1559(tx) => tx.into(),
260            TempoTxEnvelope::Eip7702(tx) => tx.into(),
261            TempoTxEnvelope::AA(tx) => tx.into(),
262        }
263    }
264}
265
266pub trait FeeToken {
267    fn fee_token(&self) -> Option<Address>;
268}
269
270impl FeeToken for TempoTransaction {
271    fn fee_token(&self) -> Option<Address> {
272        self.fee_token
273    }
274}
275
276impl FeeToken for TxEip7702 {
277    fn fee_token(&self) -> Option<Address> {
278        None
279    }
280}
281
282impl FeeToken for TxEip1559 {
283    fn fee_token(&self) -> Option<Address> {
284        None
285    }
286}
287
288impl FeeToken for TxEip2930 {
289    fn fee_token(&self) -> Option<Address> {
290        None
291    }
292}
293
294impl FeeToken for TxLegacy {
295    fn fee_token(&self) -> Option<Address> {
296        None
297    }
298}
299
300impl<T: TransactionTrait + FeeToken> From<Signed<T>> for TempoTransactionRequest {
301    fn from(value: Signed<T>) -> Self {
302        Self {
303            fee_token: value.tx().fee_token(),
304            inner: TransactionRequest::from_transaction(value),
305            ..Default::default()
306        }
307    }
308}
309
310impl From<TempoTransaction> for TempoTransactionRequest {
311    fn from(tx: TempoTransaction) -> Self {
312        Self {
313            fee_token: tx.fee_token,
314            inner: TransactionRequest {
315                from: None,
316                // AA transactions store their calls in `calls` below.
317                // `to`, `value`, `input` must stay unset to avoid the builder
318                // creating a duplicate call from the envelope fields.
319                to: None,
320                gas: Some(tx.gas_limit()),
321                gas_price: tx.gas_price(),
322                max_fee_per_gas: Some(tx.max_fee_per_gas()),
323                max_priority_fee_per_gas: tx.max_priority_fee_per_gas(),
324                value: None,
325                input: alloy_rpc_types_eth::TransactionInput::default(),
326                nonce: Some(tx.nonce()),
327                chain_id: tx.chain_id(),
328                access_list: tx.access_list().cloned(),
329                max_fee_per_blob_gas: None,
330                blob_versioned_hashes: None,
331                sidecar: None,
332                authorization_list: None,
333                transaction_type: Some(tx.ty()),
334            },
335            calls: tx.calls,
336            tempo_authorization_list: tx.tempo_authorization_list,
337            key_type: None,
338            key_data: None,
339            key_id: None,
340            nonce_key: Some(tx.nonce_key),
341            key_authorization: tx.key_authorization,
342            valid_before: tx.valid_before,
343            valid_after: tx.valid_after,
344            fee_payer_signature: tx.fee_payer_signature,
345        }
346    }
347}
348
349impl From<AASigned> for TempoTransactionRequest {
350    fn from(value: AASigned) -> Self {
351        value.into_parts().0.into()
352    }
353}
354
355impl From<TempoTypedTransaction> for TempoTransactionRequest {
356    fn from(value: TempoTypedTransaction) -> Self {
357        match value {
358            TempoTypedTransaction::Legacy(tx) => Self {
359                inner: tx.into(),
360                fee_token: None,
361                ..Default::default()
362            },
363            TempoTypedTransaction::Eip2930(tx) => Self {
364                inner: tx.into(),
365                fee_token: None,
366                ..Default::default()
367            },
368            TempoTypedTransaction::Eip1559(tx) => Self {
369                inner: tx.into(),
370                fee_token: None,
371                ..Default::default()
372            },
373            TempoTypedTransaction::Eip7702(tx) => Self {
374                inner: tx.into(),
375                fee_token: None,
376                ..Default::default()
377            },
378            TempoTypedTransaction::AA(tx) => tx.into(),
379        }
380    }
381}
382
383/// Extension trait for [`CallBuilder`]
384pub trait TempoCallBuilderExt {
385    /// Sets the `fee_token` field in the [`TempoTransaction`] transaction to the provided value
386    fn fee_token(self, fee_token: Address) -> Self;
387
388    /// Sets the `nonce_key` field in the [`TempoTransaction`] transaction to the provided value
389    fn nonce_key(self, nonce_key: U256) -> Self;
390}
391
392impl<P: Provider<TempoNetwork>, D: CallDecoder> TempoCallBuilderExt
393    for CallBuilder<P, D, TempoNetwork>
394{
395    fn fee_token(self, fee_token: Address) -> Self {
396        self.map(|request| request.with_fee_token(fee_token))
397    }
398
399    fn nonce_key(self, nonce_key: U256) -> Self {
400        self.map(|request| request.with_nonce_key(nonce_key))
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use alloy_primitives::{Bytes, address};
408    use tempo_primitives::transaction::{Call, TEMPO_EXPIRING_NONCE_KEY};
409
410    #[test]
411    fn test_set_valid_before() {
412        let mut request = TempoTransactionRequest::default();
413        assert!(request.valid_before.is_none());
414
415        request.set_valid_before(1234567890);
416        assert_eq!(request.valid_before, Some(1234567890));
417    }
418
419    #[test]
420    fn test_set_valid_after() {
421        let mut request = TempoTransactionRequest::default();
422        assert!(request.valid_after.is_none());
423
424        request.set_valid_after(1234567800);
425        assert_eq!(request.valid_after, Some(1234567800));
426    }
427
428    #[test]
429    fn test_with_valid_before() {
430        let request = TempoTransactionRequest::default().with_valid_before(1234567890);
431        assert_eq!(request.valid_before, Some(1234567890));
432    }
433
434    #[test]
435    fn test_with_valid_after() {
436        let request = TempoTransactionRequest::default().with_valid_after(1234567800);
437        assert_eq!(request.valid_after, Some(1234567800));
438    }
439
440    #[test]
441    fn test_build_aa_with_validity_window() {
442        let request = TempoTransactionRequest::default()
443            .with_nonce_key(TEMPO_EXPIRING_NONCE_KEY)
444            .with_valid_before(1234567890)
445            .with_valid_after(1234567800);
446
447        // Set required fields for build_aa
448        let mut request = request;
449        request.inner.nonce = Some(0);
450        request.inner.gas = Some(21000);
451        request.inner.max_fee_per_gas = Some(1000000000);
452        request.inner.max_priority_fee_per_gas = Some(1000000);
453        request.inner.to = Some(address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into());
454
455        let tx = request.build_aa().expect("should build transaction");
456        assert_eq!(tx.valid_before, Some(1234567890));
457        assert_eq!(tx.valid_after, Some(1234567800));
458        assert_eq!(tx.nonce_key, TEMPO_EXPIRING_NONCE_KEY);
459        assert_eq!(tx.nonce, 0);
460    }
461
462    #[test]
463    fn test_from_tempo_transaction_preserves_validity_window() {
464        let tx = TempoTransaction {
465            chain_id: 1,
466            nonce: 0,
467            fee_payer_signature: None,
468            valid_before: Some(1234567890),
469            valid_after: Some(1234567800),
470            gas_limit: 21000,
471            max_fee_per_gas: 1000000000,
472            max_priority_fee_per_gas: 1000000,
473            fee_token: None,
474            access_list: Default::default(),
475            calls: vec![Call {
476                to: address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into(),
477                value: Default::default(),
478                input: Default::default(),
479            }],
480            tempo_authorization_list: vec![],
481            nonce_key: TEMPO_EXPIRING_NONCE_KEY,
482            key_authorization: None,
483        };
484
485        let request: TempoTransactionRequest = tx.into();
486        assert_eq!(request.valid_before, Some(1234567890));
487        assert_eq!(request.valid_after, Some(1234567800));
488        assert_eq!(request.nonce_key, Some(TEMPO_EXPIRING_NONCE_KEY));
489    }
490
491    #[test]
492    fn test_expiring_nonce_builder_chain() {
493        let request = TempoTransactionRequest::default()
494            .with_nonce_key(TEMPO_EXPIRING_NONCE_KEY)
495            .with_valid_before(1234567890)
496            .with_valid_after(1234567800)
497            .with_fee_token(address!("0x20c0000000000000000000000000000000000000"));
498
499        assert_eq!(request.nonce_key, Some(TEMPO_EXPIRING_NONCE_KEY));
500        assert_eq!(request.valid_before, Some(1234567890));
501        assert_eq!(request.valid_after, Some(1234567800));
502        assert_eq!(
503            request.fee_token,
504            Some(address!("0x20c0000000000000000000000000000000000000"))
505        );
506    }
507
508    #[test]
509    fn test_set_fee_payer_signature() {
510        use alloy_primitives::Signature;
511
512        let mut request = TempoTransactionRequest::default();
513        assert!(request.fee_payer_signature.is_none());
514
515        let sig = Signature::test_signature();
516        request.set_fee_payer_signature(sig);
517        assert!(request.fee_payer_signature.is_some());
518    }
519
520    #[test]
521    fn test_with_fee_payer_signature() {
522        use alloy_primitives::Signature;
523
524        let sig = Signature::test_signature();
525        let request = TempoTransactionRequest::default().with_fee_payer_signature(sig);
526        assert!(request.fee_payer_signature.is_some());
527    }
528
529    #[test]
530    fn test_build_aa_with_fee_payer_signature() {
531        use alloy_primitives::Signature;
532
533        let sig = Signature::test_signature();
534        let mut request = TempoTransactionRequest::default().with_fee_payer_signature(sig);
535
536        request.inner.nonce = Some(0);
537        request.inner.gas = Some(21000);
538        request.inner.max_fee_per_gas = Some(1000000000);
539        request.inner.max_priority_fee_per_gas = Some(1000000);
540        request.inner.to = Some(address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into());
541
542        let tx = request.build_aa().expect("should build transaction");
543        assert_eq!(tx.fee_payer_signature, Some(sig));
544    }
545
546    #[test]
547    fn test_from_tempo_transaction_preserves_fee_payer_signature() {
548        use alloy_primitives::Signature;
549
550        let sig = Signature::test_signature();
551        let tx = TempoTransaction {
552            chain_id: 1,
553            nonce: 0,
554            fee_payer_signature: Some(sig),
555            valid_before: None,
556            valid_after: None,
557            gas_limit: 21000,
558            max_fee_per_gas: 1000000000,
559            max_priority_fee_per_gas: 1000000,
560            fee_token: None,
561            access_list: Default::default(),
562            calls: vec![Call {
563                to: address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into(),
564                value: Default::default(),
565                input: Default::default(),
566            }],
567            tempo_authorization_list: vec![],
568            nonce_key: Default::default(),
569            key_authorization: None,
570        };
571
572        let request: TempoTransactionRequest = tx.into();
573        assert_eq!(request.fee_payer_signature, Some(sig));
574    }
575
576    #[test]
577    fn test_build_aa_preserves_key_authorization() {
578        use tempo_primitives::transaction::{
579            KeyAuthorization, PrimitiveSignature, SignedKeyAuthorization,
580        };
581
582        let key_auth = SignedKeyAuthorization {
583            authorization: KeyAuthorization {
584                chain_id: 4217,
585                key_type: SignatureType::Secp256k1,
586                key_id: address!("0x1111111111111111111111111111111111111111"),
587                expiry: None,
588                limits: None,
589            },
590            signature: PrimitiveSignature::default(),
591        };
592
593        let mut request = TempoTransactionRequest {
594            key_authorization: Some(key_auth.clone()),
595            ..Default::default()
596        };
597        request.inner.nonce = Some(0);
598        request.inner.gas = Some(21000);
599        request.inner.max_fee_per_gas = Some(1000000000);
600        request.inner.max_priority_fee_per_gas = Some(1000000);
601        request.inner.to = Some(address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into());
602
603        let tx = request.build_aa().expect("should build transaction");
604        assert_eq!(
605            tx.key_authorization,
606            Some(key_auth),
607            "build_aa must preserve key_authorization from the request"
608        );
609    }
610
611    #[test]
612    fn test_aa_roundtrip_preserves_count() {
613        let base = TempoTransaction {
614            chain_id: 4217,
615            nonce: 1,
616            gas_limit: 100_000,
617            max_fee_per_gas: 1_000_000_000,
618            max_priority_fee_per_gas: 1_000_000,
619            calls: vec![],
620            ..Default::default()
621        };
622
623        // Regression: single-call AA round-trip must not duplicate the call + preserve.
624        let call = vec![Call {
625            to: address!("0x1111111111111111111111111111111111111111").into(),
626            value: U256::ZERO,
627            input: Bytes::from(vec![0xaa]),
628        }];
629        let mut original = base.clone();
630        original.calls = call.clone();
631
632        let roundtrip = TempoTransactionRequest::from(original)
633            .build_aa()
634            .expect("build_aa should succeed");
635        assert_eq!(
636            roundtrip.calls, call,
637            "single-call AA must not gain extra calls on round-trip"
638        );
639
640        // Regression: multi-call AA round-trip must preserve exact call list.
641        let batch = vec![
642            Call {
643                to: address!("0x1111111111111111111111111111111111111111").into(),
644                value: U256::ZERO,
645                input: Bytes::from(vec![0xaa]),
646            },
647            Call {
648                to: address!("0x2222222222222222222222222222222222222222").into(),
649                value: U256::ZERO,
650                input: Bytes::from(vec![0xbb]),
651            },
652        ];
653        let mut original = base;
654        original.calls = batch.clone();
655
656        let roundtrip = TempoTransactionRequest::from(original)
657            .build_aa()
658            .expect("build_aa should succeed");
659        assert_eq!(
660            roundtrip.calls, batch,
661            "multi-call AA must not gain phantom calls on round-trip"
662        );
663    }
664
665    #[test]
666    #[cfg(feature = "tempo-compat")]
667    fn test_aa_roundtrip_via_tx_env() {
668        use reth_evm::EvmEnv;
669        use reth_rpc_convert::TryIntoTxEnv;
670
671        let calls = vec![
672            Call {
673                to: address!("0x1111111111111111111111111111111111111111").into(),
674                value: U256::ZERO,
675                input: Bytes::from(vec![0xaa]),
676            },
677            Call {
678                to: address!("0x2222222222222222222222222222222222222222").into(),
679                value: U256::ZERO,
680                input: Bytes::from(vec![0xbb]),
681            },
682        ];
683
684        let tx = TempoTransaction {
685            chain_id: 4217,
686            nonce: 1,
687            gas_limit: 100_000,
688            max_fee_per_gas: 1_000_000_000,
689            max_priority_fee_per_gas: 1_000_000,
690            calls: calls.clone(),
691            ..Default::default()
692        };
693
694        let req = TempoTransactionRequest::from(tx);
695
696        let evm_env = EvmEnv::default();
697        let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
698        let aa_calls = tx_env.tempo_tx_env.expect("tempo_tx_env").aa_calls;
699
700        assert_eq!(
701            aa_calls, calls,
702            "roundtrip via try_into_tx_env must preserve exact call list"
703        );
704    }
705}