Skip to main content

tempo_alloy/rpc/
reth_compat.rs

1use crate::rpc::{TempoHeaderResponse, TempoTransactionRequest};
2use alloy_consensus::{EthereumTxEnvelope, TxEip4844, error::ValueError};
3use alloy_network::{NetworkTransactionBuilder, TxSigner};
4use alloy_primitives::{Address, B256, Bytes, Signature};
5use core::num::NonZeroU64;
6use reth_evm::EvmEnv;
7use reth_primitives_traits::SealedHeader;
8use reth_rpc_convert::{
9    FromConsensusHeader, SignTxRequestError, SignableTxRequest, TryIntoSimTx, TryIntoTxEnv,
10};
11use reth_rpc_eth_types::EthApiError;
12use tempo_chainspec::hardfork::TempoHardfork;
13use tempo_evm::TempoBlockEnv;
14use tempo_primitives::{
15    SignatureType, TempoHeader, TempoSignature, TempoTxEnvelope, TempoTxType,
16    transaction::{Call, RecoveredTempoAuthorization},
17};
18use tempo_revm::{TempoBatchCallEnv, TempoTxEnv};
19
20/// Non-zero transaction identifier used only for RPC simulations.
21///
22/// RPC requests are not final signed transactions, so gas filling and other request normalization
23/// can make a simulated signing payload differ from the eventual submitted transaction. Use a
24/// fixed sentinel instead of deriving a misleading future channel id from the simulated payload.
25const RPC_SIMULATION_UNIQUE_TX_IDENTIFIER: B256 = B256::new(*b"TEMPO_RPC_SIMULATION_MPP_CONTEXT");
26
27impl TryIntoSimTx<TempoTxEnvelope> for TempoTransactionRequest {
28    fn try_into_sim_tx(self) -> Result<TempoTxEnvelope, ValueError<Self>> {
29        match self.output_tx_type() {
30            TempoTxType::AA => {
31                let tx = self.build_aa()?;
32
33                // Create an empty signature for the transaction.
34                let signature = TempoSignature::default();
35
36                Ok(tx.into_signed(signature).into())
37            }
38            TempoTxType::Legacy
39            | TempoTxType::Eip2930
40            | TempoTxType::Eip1559
41            | TempoTxType::Eip7702 => {
42                let Self {
43                    inner,
44                    fee_token,
45                    nonce_key,
46                    calls,
47                    key_type,
48                    key_data,
49                    key_id,
50                    tempo_authorization_list,
51                    key_authorization,
52                    valid_before,
53                    valid_after,
54                    fee_payer_signature,
55                } = self;
56                let envelope = match TryIntoSimTx::<EthereumTxEnvelope<TxEip4844>>::try_into_sim_tx(
57                    inner.clone(),
58                ) {
59                    Ok(inner) => inner,
60                    Err(e) => {
61                        return Err(e.map(|inner| Self {
62                            inner,
63                            fee_token,
64                            nonce_key,
65                            calls,
66                            key_type,
67                            key_data,
68                            key_id,
69                            tempo_authorization_list,
70                            key_authorization,
71                            valid_before,
72                            valid_after,
73                            fee_payer_signature,
74                        }));
75                    }
76                };
77
78                Ok(envelope.try_into().map_err(
79                    |e: ValueError<EthereumTxEnvelope<TxEip4844>>| {
80                        e.map(|_inner| Self {
81                            inner,
82                            fee_token,
83                            nonce_key,
84                            calls,
85                            key_type,
86                            key_data,
87                            key_id,
88                            tempo_authorization_list,
89                            key_authorization,
90                            valid_before,
91                            valid_after,
92                            fee_payer_signature,
93                        })
94                    },
95                )?)
96            }
97        }
98    }
99}
100
101impl TryIntoTxEnv<TempoTxEnv, TempoHardfork, TempoBlockEnv> for TempoTransactionRequest {
102    type Err = EthApiError;
103
104    fn try_into_tx_env(
105        self,
106        evm_env: &EvmEnv<TempoHardfork, TempoBlockEnv>,
107    ) -> Result<TempoTxEnv, Self::Err> {
108        let caller_addr = self.inner.from.unwrap_or_default();
109
110        let fee_payer = if self.fee_payer_signature.is_some() {
111            // Try to recover the fee payer address from the signature.
112            // If recovery fails (e.g. dummy signature during gas estimation / fill),
113            // keep it unresolved so estimation/fill can continue with sender-paid semantics.
114            let recovered = self
115                .clone()
116                .build_aa()
117                .ok()
118                .and_then(|tx| tx.recover_fee_payer(caller_addr).ok());
119            Some(recovered)
120        } else {
121            None
122        };
123
124        let Self {
125            inner,
126            fee_token,
127            calls,
128            key_type,
129            key_data,
130            key_id,
131            tempo_authorization_list,
132            nonce_key,
133            key_authorization,
134            valid_before,
135            valid_after,
136            fee_payer_signature: _,
137        } = self;
138
139        Ok(TempoTxEnv {
140            fee_token,
141            is_system_tx: false,
142            unique_tx_identifier: Some(RPC_SIMULATION_UNIQUE_TX_IDENTIFIER),
143            fee_payer,
144            tempo_tx_env: if !calls.is_empty()
145                || !tempo_authorization_list.is_empty()
146                || nonce_key.is_some()
147                || key_authorization.is_some()
148                || key_id.is_some()
149                || fee_payer.is_some()
150                || valid_before.is_some()
151                || valid_after.is_some()
152            {
153                // Create mock signature for gas estimation
154                // If key_type is not provided, default to secp256k1
155                // For Keychain signatures, use the caller's address as the root key address
156                let key_type = key_type.unwrap_or(SignatureType::Secp256k1);
157                let mock_signature = create_mock_tempo_sig(
158                    &key_type,
159                    key_data.as_ref(),
160                    key_id,
161                    caller_addr,
162                    evm_env.spec_id().is_t1c(),
163                );
164
165                let mut calls = calls;
166                if let Some(to) = &inner.to {
167                    calls.push(Call {
168                        to: *to,
169                        value: inner.value.unwrap_or_default(),
170                        input: inner.input.clone().into_input().unwrap_or_default(),
171                    });
172                }
173                if calls.is_empty() {
174                    return Err(EthApiError::InvalidParams("empty calls list".to_string()));
175                }
176
177                Some(Box::new(TempoBatchCallEnv {
178                    aa_calls: calls,
179                    signature: mock_signature,
180                    tempo_authorization_list: tempo_authorization_list
181                        .into_iter()
182                        .map(RecoveredTempoAuthorization::new)
183                        .collect(),
184                    nonce_key: nonce_key.unwrap_or_default(),
185                    key_authorization,
186                    signature_hash: B256::ZERO,
187                    tx_hash: B256::ZERO,
188                    valid_before: valid_before.map(NonZeroU64::get),
189                    valid_after: valid_after.map(NonZeroU64::get),
190                    subblock_transaction: false,
191                    override_key_id: key_id,
192                    expiring_nonce_idx: None,
193                }))
194            } else {
195                None
196            },
197            inner: inner.try_into_tx_env(evm_env)?,
198        })
199    }
200}
201
202/// Creates a mock AA signature for gas estimation based on key type hints
203///
204/// - `key_type`: The primitive signature type (secp256k1, P256, WebAuthn)
205/// - `key_data`: Type-specific data (e.g., WebAuthn size)
206/// - `key_id`: If Some, wraps the signature in a Keychain wrapper (+3,000 gas for key validation)
207/// - `caller_addr`: The transaction caller address (used as root key address for Keychain)
208/// - `is_t1c`: Whether T1C is active — determines keychain signature version (V1 pre-T1C, V2 post-T1C)
209fn create_mock_tempo_sig(
210    key_type: &SignatureType,
211    key_data: Option<&Bytes>,
212    key_id: Option<Address>,
213    caller_addr: alloy_primitives::Address,
214    is_t1c: bool,
215) -> TempoSignature {
216    use tempo_primitives::transaction::tt_signature::{KeychainSignature, TempoSignature};
217
218    let inner_sig = create_mock_primitive_signature(key_type, key_data.cloned());
219
220    if key_id.is_some() {
221        // For Keychain signatures, the root_key_address is the caller (account owner).
222        let keychain_sig = if is_t1c {
223            KeychainSignature::new(caller_addr, inner_sig)
224        } else {
225            KeychainSignature::new_v1(caller_addr, inner_sig)
226        };
227        TempoSignature::Keychain(keychain_sig)
228    } else {
229        TempoSignature::Primitive(inner_sig)
230    }
231}
232
233/// Creates a mock primitive signature for gas estimation
234fn create_mock_primitive_signature(
235    sig_type: &SignatureType,
236    key_data: Option<Bytes>,
237) -> tempo_primitives::transaction::tt_signature::PrimitiveSignature {
238    use tempo_primitives::transaction::tt_signature::{
239        P256SignatureWithPreHash, PrimitiveSignature, WebAuthnSignature,
240    };
241
242    match sig_type {
243        SignatureType::Secp256k1 => {
244            // Create a dummy secp256k1 signature (65 bytes)
245            PrimitiveSignature::Secp256k1(Signature::new(
246                alloy_primitives::U256::ZERO,
247                alloy_primitives::U256::ZERO,
248                false,
249            ))
250        }
251        SignatureType::P256 => {
252            // Create a dummy P256 signature
253            PrimitiveSignature::P256(P256SignatureWithPreHash {
254                r: alloy_primitives::B256::ZERO,
255                s: alloy_primitives::B256::ZERO,
256                pub_key_x: alloy_primitives::B256::ZERO,
257                pub_key_y: alloy_primitives::B256::ZERO,
258                pre_hash: false,
259            })
260        }
261        SignatureType::WebAuthn => {
262            // Create a dummy WebAuthn signature with the specified size
263            // key_data contains the total size of webauthn_data (excluding 128 bytes for public keys)
264            // Default: 800 bytes if no key_data provided
265
266            // Base clientDataJSON template (50 bytes): {"type":"webauthn.get","challenge":"","origin":""}
267            // Authenticator data (37 bytes): 32 rpIdHash + 1 flags + 4 signCount
268            // Minimum total: 87 bytes
269            const BASE_CLIENT_JSON: &str = r#"{"type":"webauthn.get","challenge":"","origin":""}"#;
270            const AUTH_DATA_SIZE: usize = 37;
271            const MIN_WEBAUTHN_SIZE: usize = AUTH_DATA_SIZE + BASE_CLIENT_JSON.len(); // 87 bytes
272            const DEFAULT_WEBAUTHN_SIZE: usize = 800; // Default when no key_data provided
273            const MAX_WEBAUTHN_SIZE: usize = 8192; // Maximum realistic WebAuthn signature size
274
275            // Parse size from key_data, or use default
276            let size = if let Some(data) = key_data.as_ref() {
277                match data.len() {
278                    1 => data[0] as usize,
279                    2 => u16::from_be_bytes([data[0], data[1]]) as usize,
280                    4 => u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize,
281                    _ => DEFAULT_WEBAUTHN_SIZE, // Fallback default
282                }
283            } else {
284                DEFAULT_WEBAUTHN_SIZE // Default size when no key_data provided
285            };
286
287            // Clamp size to safe bounds to prevent DoS via unbounded allocation
288            let size = size.clamp(MIN_WEBAUTHN_SIZE, MAX_WEBAUTHN_SIZE);
289
290            // Construct authenticatorData (37 bytes)
291            let mut webauthn_data = vec![0u8; AUTH_DATA_SIZE];
292            webauthn_data[32] = 0x01; // UP flag set
293
294            // Construct clientDataJSON with padding in origin field if needed
295            let additional_bytes = size - MIN_WEBAUTHN_SIZE;
296            let client_json = if additional_bytes > 0 {
297                // Add padding bytes to origin field
298                // {"type":"webauthn.get","challenge":"","origin":"XXXXX"}
299                let padding = "x".repeat(additional_bytes);
300                format!(r#"{{"type":"webauthn.get","challenge":"","origin":"{padding}"}}"#,)
301            } else {
302                BASE_CLIENT_JSON.to_string()
303            };
304
305            webauthn_data.extend_from_slice(client_json.as_bytes());
306            let webauthn_data = Bytes::from(webauthn_data);
307
308            PrimitiveSignature::WebAuthn(WebAuthnSignature {
309                webauthn_data,
310                r: alloy_primitives::B256::ZERO,
311                s: alloy_primitives::B256::ZERO,
312                pub_key_x: alloy_primitives::B256::ZERO,
313                pub_key_y: alloy_primitives::B256::ZERO,
314            })
315        }
316    }
317}
318
319impl SignableTxRequest<TempoTxEnvelope> for TempoTransactionRequest {
320    async fn try_build_and_sign(
321        self,
322        signer: impl TxSigner<Signature> + Send,
323    ) -> Result<TempoTxEnvelope, SignTxRequestError> {
324        if self.output_tx_type() == TempoTxType::AA {
325            let mut tx = self
326                .build_aa()
327                .map_err(|_| SignTxRequestError::InvalidTransactionRequest)?;
328            let signature = signer.sign_transaction(&mut tx).await?;
329            Ok(tx.into_signed(signature.into()).into())
330        } else {
331            SignableTxRequest::<TempoTxEnvelope>::try_build_and_sign(self.inner, signer).await
332        }
333    }
334}
335
336impl FromConsensusHeader<TempoHeader> for TempoHeaderResponse {
337    fn from_consensus_header(header: SealedHeader<TempoHeader>, block_size: usize) -> Self {
338        Self {
339            timestamp_millis: header.timestamp_millis(),
340            inner: FromConsensusHeader::from_consensus_header(header, block_size),
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use alloy_primitives::{TxKind, address};
349    use alloy_rpc_types_eth::TransactionRequest;
350    use alloy_signer::SignerSync;
351    use alloy_signer_local::PrivateKeySigner;
352    use reth_rpc_convert::TryIntoTxEnv;
353    use tempo_primitives::{
354        TempoTransaction,
355        transaction::{Call, FEE_PAYER_SIGNATURE_MARKER, tt_signature::PrimitiveSignature},
356    };
357
358    #[test]
359    fn test_estimate_gas_when_calls_set() {
360        let existing_call = Call {
361            to: TxKind::Call(address!("0x1111111111111111111111111111111111111111")),
362            value: alloy_primitives::U256::from(1),
363            input: Bytes::from(vec![0xaa]),
364        };
365
366        let req = TempoTransactionRequest {
367            inner: TransactionRequest {
368                to: Some(TxKind::Call(address!(
369                    "0x2222222222222222222222222222222222222222"
370                ))),
371                value: Some(alloy_primitives::U256::from(2)),
372                input: alloy_rpc_types_eth::TransactionInput::new(Bytes::from(vec![0xbb])),
373                nonce: Some(0),
374                gas: Some(100_000),
375                max_fee_per_gas: Some(1_000_000_000),
376                max_priority_fee_per_gas: Some(1_000_000),
377                ..Default::default()
378            },
379            calls: vec![existing_call],
380            nonce_key: Some(alloy_primitives::U256::ZERO),
381            ..Default::default()
382        };
383
384        let built_calls = req.clone().build_aa().expect("build_aa").calls;
385
386        let evm_env = EvmEnv::default();
387        let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
388        let estimated_calls = tx_env.tempo_tx_env.expect("tempo_tx_env").aa_calls;
389
390        assert_eq!(estimated_calls, built_calls);
391    }
392
393    #[test]
394    fn test_try_into_tx_env_sets_channel_open_context_hash_for_rpc_simulation() {
395        let sender = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
396        let target = address!("0x2222222222222222222222222222222222222222");
397
398        let req = TempoTransactionRequest {
399            inner: TransactionRequest {
400                from: Some(sender),
401                to: Some(TxKind::Call(target)),
402                nonce: Some(0),
403                gas: Some(100_000),
404                max_fee_per_gas: Some(1_000_000_000),
405                max_priority_fee_per_gas: Some(1_000_000),
406                chain_id: Some(4217),
407                ..Default::default()
408            },
409            ..Default::default()
410        };
411
412        let evm_env = EvmEnv::default();
413        let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
414
415        assert_eq!(
416            tx_env.channel_open_context_hash(),
417            Some(RPC_SIMULATION_UNIQUE_TX_IDENTIFIER)
418        );
419        assert_ne!(
420            tx_env.channel_open_context_hash(),
421            Some(B256::ZERO),
422            "RPC simulations must seed a non-zero context hash so TIP20ChannelReserve.open() does not treat it as unset"
423        );
424    }
425
426    #[test]
427    fn test_webauthn_size_clamped_to_max() {
428        // Attempt to create a signature with u32::MAX size (would be ~4GB without fix)
429        let malicious_key_data = Bytes::from(0xFFFFFFFFu32.to_be_bytes().to_vec());
430        let sig =
431            create_mock_primitive_signature(&SignatureType::WebAuthn, Some(malicious_key_data));
432
433        // Extract webauthn_data and verify it's clamped to MAX_WEBAUTHN_SIZE (8192)
434        let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
435            panic!("Expected WebAuthn signature");
436        };
437
438        // The webauthn_data should be at most MAX_WEBAUTHN_SIZE bytes
439        assert!(
440            webauthn_sig.webauthn_data.len() <= 8192,
441            "WebAuthn data size {} exceeds maximum 8192",
442            webauthn_sig.webauthn_data.len()
443        );
444    }
445
446    #[test]
447    fn test_webauthn_size_respects_minimum() {
448        // Attempt to create a signature with size 0
449        let key_data = Bytes::from(vec![0u8]);
450        let sig = create_mock_primitive_signature(&SignatureType::WebAuthn, Some(key_data));
451
452        let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
453            panic!("Expected WebAuthn signature");
454        };
455
456        // Should be at least MIN_WEBAUTHN_SIZE (87 bytes)
457        assert!(
458            webauthn_sig.webauthn_data.len() >= 87,
459            "WebAuthn data size {} is below minimum 87",
460            webauthn_sig.webauthn_data.len()
461        );
462    }
463
464    #[test]
465    fn test_webauthn_default_size() {
466        // No key_data should use default size (800)
467        let sig = create_mock_primitive_signature(&SignatureType::WebAuthn, None);
468
469        let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
470            panic!("Expected WebAuthn signature");
471        };
472
473        // Default is 800 bytes
474        assert_eq!(webauthn_sig.webauthn_data.len(), 800);
475    }
476
477    #[test]
478    fn test_estimate_gas_fee_payer_signature_only_produces_aa_env() {
479        let sponsor = PrivateKeySigner::random();
480        let sender = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
481        let target = address!("0x2222222222222222222222222222222222222222");
482
483        // Build a TempoTransaction so we can compute fee_payer_signature_hash
484        let tx = TempoTransaction {
485            chain_id: 4217,
486            nonce: 0,
487            fee_payer_signature: None,
488            valid_before: None,
489            valid_after: None,
490            gas_limit: 100_000,
491            max_fee_per_gas: 1_000_000_000,
492            max_priority_fee_per_gas: 1_000_000,
493            fee_token: None,
494            access_list: Default::default(),
495            calls: vec![Call {
496                to: target.into(),
497                value: Default::default(),
498                input: Default::default(),
499            }],
500            tempo_authorization_list: vec![],
501            nonce_key: Default::default(),
502            key_authorization: None,
503        };
504        let hash = tx.fee_payer_signature_hash(sender);
505        let fee_payer_sig = sponsor.sign_hash_sync(&hash).expect("sign");
506
507        // Request with ONLY fee_payer_signature as the Tempo-specific field
508        let req = TempoTransactionRequest {
509            inner: TransactionRequest {
510                from: Some(sender),
511                to: Some(TxKind::Call(target)),
512                nonce: Some(0),
513                gas: Some(100_000),
514                max_fee_per_gas: Some(1_000_000_000),
515                max_priority_fee_per_gas: Some(1_000_000),
516                chain_id: Some(4217),
517                ..Default::default()
518            },
519            fee_payer_signature: Some(fee_payer_sig),
520            ..Default::default()
521        };
522
523        let evm_env = EvmEnv::default();
524        let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
525
526        assert!(
527            tx_env.tempo_tx_env.is_some(),
528            "fee_payer_signature alone must produce an AA tx env"
529        );
530        assert_eq!(
531            tx_env.fee_payer,
532            Some(Some(sponsor.address())),
533            "fee_payer should recover sponsor address"
534        );
535    }
536
537    #[test]
538    fn test_aa_roundtrip_via_tx_env() {
539        use alloy_primitives::U256;
540
541        let calls = vec![
542            Call {
543                to: address!("0x1111111111111111111111111111111111111111").into(),
544                value: U256::ZERO,
545                input: Bytes::from(vec![0xaa]),
546            },
547            Call {
548                to: address!("0x2222222222222222222222222222222222222222").into(),
549                value: U256::ZERO,
550                input: Bytes::from(vec![0xbb]),
551            },
552        ];
553
554        let tx = TempoTransaction {
555            chain_id: 4217,
556            nonce: 1,
557            gas_limit: 100_000,
558            max_fee_per_gas: 1_000_000_000,
559            max_priority_fee_per_gas: 1_000_000,
560            calls: calls.clone(),
561            ..Default::default()
562        };
563
564        let req: TempoTransactionRequest = tx.into();
565
566        let evm_env = EvmEnv::default();
567        let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
568        let aa_calls = tx_env.tempo_tx_env.expect("tempo_tx_env").aa_calls;
569
570        assert_eq!(
571            aa_calls, calls,
572            "roundtrip via try_into_tx_env must preserve exact call list"
573        );
574    }
575
576    #[test]
577    fn test_estimate_gas_invalid_fee_payer_signature_keeps_unresolved_fee_payer() {
578        let sender = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
579        let target = address!("0x2222222222222222222222222222222222222222");
580
581        let req = TempoTransactionRequest {
582            inner: TransactionRequest {
583                from: Some(sender),
584                to: Some(TxKind::Call(target)),
585                nonce: Some(0),
586                gas: Some(100_000),
587                max_fee_per_gas: Some(1_000_000_000),
588                max_priority_fee_per_gas: Some(1_000_000),
589                chain_id: Some(4217),
590                ..Default::default()
591            },
592            fee_payer_signature: Some(FEE_PAYER_SIGNATURE_MARKER),
593            ..Default::default()
594        };
595
596        let evm_env = EvmEnv::default();
597        let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
598
599        assert!(
600            tx_env.tempo_tx_env.is_some(),
601            "fee_payer_signature alone must produce an AA tx env"
602        );
603        assert_eq!(
604            tx_env.fee_payer,
605            Some(None),
606            "invalid fee_payer_signature should remain unresolved"
607        );
608    }
609
610    #[tokio::test]
611    async fn test_signable_tx_request_preserves_tempo_fields() {
612        let signer = PrivateKeySigner::random();
613
614        let call = Call {
615            to: alloy_primitives::TxKind::Call(address!(
616                "0x1111111111111111111111111111111111111111"
617            )),
618            value: alloy_primitives::U256::from(1),
619            input: Bytes::from(vec![0xaa]),
620        };
621
622        let fee_token = address!("0x20c0000000000000000000000000000000000000");
623        let nonce_key = alloy_primitives::U256::from(42);
624
625        let req = TempoTransactionRequest {
626            inner: TransactionRequest {
627                nonce: Some(0),
628                gas: Some(100_000),
629                max_fee_per_gas: Some(1_000_000_000),
630                max_priority_fee_per_gas: Some(1_000_000),
631                chain_id: Some(4217),
632                ..Default::default()
633            },
634            calls: vec![call.clone()],
635            fee_token: Some(fee_token),
636            nonce_key: Some(nonce_key),
637            ..Default::default()
638        };
639
640        let envelope = SignableTxRequest::<TempoTxEnvelope>::try_build_and_sign(req, &signer)
641            .await
642            .expect("should build and sign");
643
644        match &envelope {
645            TempoTxEnvelope::AA(signed) => {
646                let tx = signed.tx();
647                assert_eq!(tx.fee_token, Some(fee_token), "fee_token must be preserved");
648                assert_eq!(tx.nonce_key, nonce_key, "nonce_key must be preserved");
649                assert_eq!(tx.calls, vec![call], "calls must be preserved");
650            }
651            other => panic!(
652                "Expected AA envelope for request with Tempo fields, got {:?}",
653                other.tx_type()
654            ),
655        }
656    }
657}