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::{Transaction, TransactionRequest, TransactionTrait};
7use core::num::NonZeroU64;
8use serde::{Deserialize, Serialize};
9use tempo_primitives::{
10 AASigned, SignatureType, TempoTransaction, TempoTxEnvelope,
11 transaction::{
12 Call, SignedKeyAuthorization, TempoSignedAuthorization, TempoTypedTransaction,
13 key_authorization::serde_nonzero_quantity_opt,
14 },
15};
16
17use crate::TempoNetwork;
18
19#[derive(
21 Clone,
22 Debug,
23 Default,
24 PartialEq,
25 Eq,
26 Hash,
27 Serialize,
28 Deserialize,
29 derive_more::Deref,
30 derive_more::DerefMut,
31)]
32#[serde(rename_all = "camelCase")]
33pub struct TempoTransactionRequest {
34 #[serde(flatten)]
36 #[deref]
37 #[deref_mut]
38 pub inner: TransactionRequest,
39
40 #[serde(default)]
42 pub fee_token: Option<Address>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub nonce_key: Option<U256>,
47
48 #[serde(default)]
50 pub calls: Vec<Call>,
51
52 #[serde(default)]
55 pub key_type: Option<SignatureType>,
56
57 #[serde(default)]
60 pub key_data: Option<Bytes>,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub key_id: Option<Address>,
69
70 #[serde(
72 default,
73 skip_serializing_if = "Vec::is_empty",
74 rename = "aaAuthorizationList"
75 )]
76 pub tempo_authorization_list: Vec<TempoSignedAuthorization>,
77
78 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub key_authorization: Option<SignedKeyAuthorization>,
82
83 #[serde(
88 default,
89 skip_serializing_if = "Option::is_none",
90 with = "serde_nonzero_quantity_opt"
91 )]
92 pub valid_before: Option<NonZeroU64>,
93
94 #[serde(
99 default,
100 skip_serializing_if = "Option::is_none",
101 with = "serde_nonzero_quantity_opt"
102 )]
103 pub valid_after: Option<NonZeroU64>,
104
105 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub fee_payer_signature: Option<alloy_primitives::Signature>,
109}
110
111impl TempoTransactionRequest {
112 pub fn set_fee_token(&mut self, fee_token: Address) {
114 self.fee_token = Some(fee_token);
115 }
116
117 pub fn with_fee_token(mut self, fee_token: Address) -> Self {
119 self.fee_token = Some(fee_token);
120 self
121 }
122
123 pub fn set_nonce_key(&mut self, nonce_key: U256) {
125 self.nonce_key = Some(nonce_key);
126 }
127
128 pub fn with_nonce_key(mut self, nonce_key: U256) -> Self {
130 self.nonce_key = Some(nonce_key);
131 self
132 }
133
134 pub fn set_calls(&mut self, calls: Vec<Call>) {
136 self.calls = calls;
137 }
138
139 pub fn with_calls(mut self, calls: Vec<Call>) -> Self {
141 self.calls = calls;
142 self
143 }
144
145 pub fn push_call(&mut self, call: Call) {
147 self.calls.push(call);
148 }
149
150 pub fn set_key_type(&mut self, key_type: SignatureType) {
152 self.key_type = Some(key_type);
153 }
154
155 pub fn with_key_type(mut self, key_type: SignatureType) -> Self {
157 self.key_type = Some(key_type);
158 self
159 }
160
161 pub fn set_key_data(&mut self, key_data: impl Into<Bytes>) {
163 self.key_data = Some(key_data.into());
164 }
165
166 pub fn with_key_data(mut self, key_data: impl Into<Bytes>) -> Self {
168 self.key_data = Some(key_data.into());
169 self
170 }
171
172 pub fn set_key_id(&mut self, key_id: Address) {
174 self.key_id = Some(key_id);
175 }
176
177 pub fn with_key_id(mut self, key_id: Address) -> Self {
179 self.key_id = Some(key_id);
180 self
181 }
182
183 pub fn set_key_authorization(&mut self, key_authorization: SignedKeyAuthorization) {
185 self.key_authorization = Some(key_authorization);
186 }
187
188 pub fn with_key_authorization(mut self, key_authorization: SignedKeyAuthorization) -> Self {
190 self.key_authorization = Some(key_authorization);
191 self
192 }
193
194 pub fn set_valid_before(&mut self, valid_before: NonZeroU64) {
198 self.valid_before = Some(valid_before);
199 }
200
201 pub fn with_valid_before(mut self, valid_before: NonZeroU64) -> Self {
203 self.valid_before = Some(valid_before);
204 self
205 }
206
207 pub fn set_valid_after(&mut self, valid_after: NonZeroU64) {
211 self.valid_after = Some(valid_after);
212 }
213
214 pub fn with_valid_after(mut self, valid_after: NonZeroU64) -> Self {
216 self.valid_after = Some(valid_after);
217 self
218 }
219
220 pub fn set_fee_payer_signature(&mut self, signature: alloy_primitives::Signature) {
222 self.fee_payer_signature = Some(signature);
223 }
224
225 pub fn with_fee_payer_signature(mut self, signature: alloy_primitives::Signature) -> Self {
227 self.fee_payer_signature = Some(signature);
228 self
229 }
230
231 pub fn build_aa(self) -> Result<TempoTransaction, ValueError<Self>> {
233 if self.calls.is_empty() && self.inner.to.is_none() {
234 return Err(ValueError::new(
235 self,
236 "Missing 'calls' or 'to' field for Tempo transaction.",
237 ));
238 }
239
240 let Some(nonce) = self.inner.nonce else {
241 return Err(ValueError::new(
242 self,
243 "Missing 'nonce' field for Tempo transaction.",
244 ));
245 };
246 let Some(gas_limit) = self.inner.gas else {
247 return Err(ValueError::new(
248 self,
249 "Missing 'gas_limit' field for Tempo transaction.",
250 ));
251 };
252 let Some(max_fee_per_gas) = self.inner.max_fee_per_gas else {
253 return Err(ValueError::new(
254 self,
255 "Missing 'max_fee_per_gas' field for Tempo transaction.",
256 ));
257 };
258 let Some(max_priority_fee_per_gas) = self.inner.max_priority_fee_per_gas else {
259 return Err(ValueError::new(
260 self,
261 "Missing 'max_priority_fee_per_gas' field for Tempo transaction.",
262 ));
263 };
264
265 let mut calls = self.calls;
266 if let Some(to) = self.inner.to {
267 calls.push(Call {
268 to,
269 value: self.inner.value.unwrap_or_default(),
270 input: self.inner.input.into_input().unwrap_or_default(),
271 });
272 }
273
274 Ok(TempoTransaction {
275 chain_id: self.inner.chain_id.unwrap_or(4217),
276 nonce,
277 fee_payer_signature: self.fee_payer_signature,
278 valid_before: self.valid_before,
279 valid_after: self.valid_after,
280 gas_limit,
281 max_fee_per_gas,
282 max_priority_fee_per_gas,
283 fee_token: self.fee_token,
284 access_list: self.inner.access_list.unwrap_or_default(),
285 calls,
286 tempo_authorization_list: self.tempo_authorization_list,
287 nonce_key: self.nonce_key.unwrap_or_default(),
288 key_authorization: self.key_authorization,
289 })
290 }
291}
292
293impl AsRef<TransactionRequest> for TempoTransactionRequest {
294 fn as_ref(&self) -> &TransactionRequest {
295 &self.inner
296 }
297}
298
299impl AsMut<TransactionRequest> for TempoTransactionRequest {
300 fn as_mut(&mut self) -> &mut TransactionRequest {
301 &mut self.inner
302 }
303}
304
305impl From<TransactionRequest> for TempoTransactionRequest {
306 fn from(value: TransactionRequest) -> Self {
307 Self {
308 inner: value,
309 fee_token: None,
310 ..Default::default()
311 }
312 }
313}
314
315impl From<TempoTransactionRequest> for TransactionRequest {
316 fn from(value: TempoTransactionRequest) -> Self {
317 value.inner
318 }
319}
320
321impl From<Transaction<TempoTxEnvelope>> for TempoTransactionRequest {
322 fn from(tx: Transaction<TempoTxEnvelope>) -> Self {
323 tx.inner.into_inner().into()
324 }
325}
326
327impl From<TempoTxEnvelope> for TempoTransactionRequest {
328 fn from(value: TempoTxEnvelope) -> Self {
329 match value {
330 TempoTxEnvelope::Legacy(tx) => tx.into(),
331 TempoTxEnvelope::Eip2930(tx) => tx.into(),
332 TempoTxEnvelope::Eip1559(tx) => tx.into(),
333 TempoTxEnvelope::Eip7702(tx) => tx.into(),
334 TempoTxEnvelope::AA(tx) => tx.into(),
335 }
336 }
337}
338
339pub trait FeeToken {
340 fn fee_token(&self) -> Option<Address>;
341}
342
343impl FeeToken for TempoTransaction {
344 fn fee_token(&self) -> Option<Address> {
345 self.fee_token
346 }
347}
348
349impl FeeToken for TxEip7702 {
350 fn fee_token(&self) -> Option<Address> {
351 None
352 }
353}
354
355impl FeeToken for TxEip1559 {
356 fn fee_token(&self) -> Option<Address> {
357 None
358 }
359}
360
361impl FeeToken for TxEip2930 {
362 fn fee_token(&self) -> Option<Address> {
363 None
364 }
365}
366
367impl FeeToken for TxLegacy {
368 fn fee_token(&self) -> Option<Address> {
369 None
370 }
371}
372
373impl<T: TransactionTrait + FeeToken> From<Signed<T>> for TempoTransactionRequest {
374 fn from(value: Signed<T>) -> Self {
375 Self {
376 fee_token: value.tx().fee_token(),
377 inner: TransactionRequest::from_transaction(value),
378 ..Default::default()
379 }
380 }
381}
382
383impl From<TempoTransaction> for TempoTransactionRequest {
384 fn from(tx: TempoTransaction) -> Self {
385 Self {
386 fee_token: tx.fee_token,
387 inner: TransactionRequest {
388 from: None,
389 to: None,
393 gas: Some(tx.gas_limit()),
394 gas_price: tx.gas_price(),
395 max_fee_per_gas: Some(tx.max_fee_per_gas()),
396 max_priority_fee_per_gas: tx.max_priority_fee_per_gas(),
397 value: None,
398 input: alloy_rpc_types_eth::TransactionInput::default(),
399 nonce: Some(tx.nonce()),
400 chain_id: tx.chain_id(),
401 access_list: tx.access_list().cloned(),
402 max_fee_per_blob_gas: None,
403 blob_versioned_hashes: None,
404 sidecar: None,
405 authorization_list: None,
406 transaction_type: Some(tx.ty()),
407 },
408 calls: tx.calls,
409 tempo_authorization_list: tx.tempo_authorization_list,
410 key_type: None,
411 key_data: None,
412 key_id: None,
413 nonce_key: Some(tx.nonce_key),
414 key_authorization: tx.key_authorization,
415 valid_before: tx.valid_before,
416 valid_after: tx.valid_after,
417 fee_payer_signature: tx.fee_payer_signature,
418 }
419 }
420}
421
422impl From<AASigned> for TempoTransactionRequest {
423 fn from(value: AASigned) -> Self {
424 value.into_parts().0.into()
425 }
426}
427
428impl From<TempoTypedTransaction> for TempoTransactionRequest {
429 fn from(value: TempoTypedTransaction) -> Self {
430 match value {
431 TempoTypedTransaction::Legacy(tx) => Self {
432 inner: tx.into(),
433 fee_token: None,
434 ..Default::default()
435 },
436 TempoTypedTransaction::Eip2930(tx) => Self {
437 inner: tx.into(),
438 fee_token: None,
439 ..Default::default()
440 },
441 TempoTypedTransaction::Eip1559(tx) => Self {
442 inner: tx.into(),
443 fee_token: None,
444 ..Default::default()
445 },
446 TempoTypedTransaction::Eip7702(tx) => Self {
447 inner: tx.into(),
448 fee_token: None,
449 ..Default::default()
450 },
451 TempoTypedTransaction::AA(tx) => tx.into(),
452 }
453 }
454}
455
456pub trait TempoCallBuilderExt {
458 fn fee_token(self, fee_token: Address) -> Self;
460
461 fn nonce_key(self, nonce_key: U256) -> Self;
463
464 fn valid_before(self, valid_before: NonZeroU64) -> Self;
466
467 fn valid_after(self, valid_after: NonZeroU64) -> Self;
469
470 fn key_id(self, key_id: Address) -> Self;
472
473 fn key_type(self, key_type: SignatureType) -> Self;
475
476 fn key_data(self, key_data: Bytes) -> Self;
478
479 fn key_authorization(self, key_authorization: SignedKeyAuthorization) -> Self;
481}
482
483impl<P: Provider<TempoNetwork>, D: CallDecoder> TempoCallBuilderExt
484 for CallBuilder<P, D, TempoNetwork>
485{
486 fn fee_token(self, fee_token: Address) -> Self {
487 self.map(|request| request.with_fee_token(fee_token))
488 }
489
490 fn nonce_key(self, nonce_key: U256) -> Self {
491 self.map(|request| request.with_nonce_key(nonce_key))
492 }
493
494 fn valid_before(self, valid_before: NonZeroU64) -> Self {
495 self.map(|request| request.with_valid_before(valid_before))
496 }
497
498 fn valid_after(self, valid_after: NonZeroU64) -> Self {
499 self.map(|request| request.with_valid_after(valid_after))
500 }
501
502 fn key_id(self, key_id: Address) -> Self {
503 self.map(|request| request.with_key_id(key_id))
504 }
505
506 fn key_type(self, key_type: SignatureType) -> Self {
507 self.map(|request| request.with_key_type(key_type))
508 }
509
510 fn key_data(self, key_data: Bytes) -> Self {
511 self.map(|request| request.with_key_data(key_data))
512 }
513
514 fn key_authorization(self, key_authorization: SignedKeyAuthorization) -> Self {
515 self.map(|request| request.with_key_authorization(key_authorization))
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use alloy_primitives::{Bytes, Signature, address};
523 use tempo_primitives::transaction::{
524 Call, KeyAuthorization, PrimitiveSignature, TEMPO_EXPIRING_NONCE_KEY,
525 };
526
527 fn nz(value: u64) -> NonZeroU64 {
528 NonZeroU64::new(value).expect("test timestamp must be non-zero")
529 }
530
531 #[test]
532 fn test_set_valid_before() {
533 let mut request = TempoTransactionRequest::default();
534 assert!(request.valid_before.is_none());
535
536 request.set_valid_before(nz(1234567890));
537 assert_eq!(request.valid_before, Some(nz(1234567890)));
538 }
539
540 #[test]
541 fn test_set_valid_after() {
542 let mut request = TempoTransactionRequest::default();
543 assert!(request.valid_after.is_none());
544
545 request.set_valid_after(nz(1234567800));
546 assert_eq!(request.valid_after, Some(nz(1234567800)));
547 }
548
549 #[test]
550 fn test_with_valid_before() {
551 let request = TempoTransactionRequest::default().with_valid_before(nz(1234567890));
552 assert_eq!(request.valid_before, Some(nz(1234567890)));
553 }
554
555 #[test]
556 fn test_with_valid_after() {
557 let request = TempoTransactionRequest::default().with_valid_after(nz(1234567800));
558 assert_eq!(request.valid_after, Some(nz(1234567800)));
559 }
560
561 #[test]
562 fn test_build_aa_with_validity_window() {
563 let request = TempoTransactionRequest::default()
564 .with_nonce_key(TEMPO_EXPIRING_NONCE_KEY)
565 .with_valid_before(nz(1234567890))
566 .with_valid_after(nz(1234567800));
567
568 let mut request = request;
570 request.inner.nonce = Some(0);
571 request.inner.gas = Some(21000);
572 request.inner.max_fee_per_gas = Some(1000000000);
573 request.inner.max_priority_fee_per_gas = Some(1000000);
574 request.inner.to = Some(address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into());
575
576 let tx = request.build_aa().expect("should build transaction");
577 assert_eq!(tx.valid_before, Some(nz(1234567890)));
578 assert_eq!(tx.valid_after, Some(nz(1234567800)));
579 assert_eq!(tx.nonce_key, TEMPO_EXPIRING_NONCE_KEY);
580 assert_eq!(tx.nonce, 0);
581 }
582
583 #[test]
584 fn test_deserialize_rejects_zero_validity_window_bounds() {
585 let err = serde_json::from_str::<TempoTransactionRequest>(r#"{"validBefore":"0x0"}"#)
586 .expect_err("zero valid_before must be rejected during deserialization");
587 assert!(err.to_string().contains("expected non-zero quantity"));
588
589 let err = serde_json::from_str::<TempoTransactionRequest>(r#"{"validAfter":"0x0"}"#)
590 .expect_err("zero valid_after must be rejected during deserialization");
591 assert!(err.to_string().contains("expected non-zero quantity"));
592 }
593
594 #[test]
595 fn test_from_tempo_transaction_preserves_validity_window() {
596 let tx = TempoTransaction {
597 chain_id: 1,
598 nonce: 0,
599 fee_payer_signature: None,
600 valid_before: Some(NonZeroU64::new(1234567890).unwrap()),
601 valid_after: Some(NonZeroU64::new(1234567800).unwrap()),
602 gas_limit: 21000,
603 max_fee_per_gas: 1000000000,
604 max_priority_fee_per_gas: 1000000,
605 fee_token: None,
606 access_list: Default::default(),
607 calls: vec![Call {
608 to: address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into(),
609 value: Default::default(),
610 input: Default::default(),
611 }],
612 tempo_authorization_list: vec![],
613 nonce_key: TEMPO_EXPIRING_NONCE_KEY,
614 key_authorization: None,
615 };
616
617 let request: TempoTransactionRequest = tx.into();
618 assert_eq!(request.valid_before, Some(nz(1234567890)));
619 assert_eq!(request.valid_after, Some(nz(1234567800)));
620 assert_eq!(request.nonce_key, Some(TEMPO_EXPIRING_NONCE_KEY));
621 }
622
623 #[test]
624 fn test_expiring_nonce_builder_chain() {
625 let request = TempoTransactionRequest::default()
626 .with_nonce_key(TEMPO_EXPIRING_NONCE_KEY)
627 .with_valid_before(nz(1234567890))
628 .with_valid_after(nz(1234567800))
629 .with_fee_token(address!("0x20c0000000000000000000000000000000000000"));
630
631 assert_eq!(request.nonce_key, Some(TEMPO_EXPIRING_NONCE_KEY));
632 assert_eq!(request.valid_before, Some(nz(1234567890)));
633 assert_eq!(request.valid_after, Some(nz(1234567800)));
634 assert_eq!(
635 request.fee_token,
636 Some(address!("0x20c0000000000000000000000000000000000000"))
637 );
638 }
639
640 #[test]
641 fn test_set_fee_payer_signature() {
642 let mut request = TempoTransactionRequest::default();
643 assert!(request.fee_payer_signature.is_none());
644
645 let sig = Signature::test_signature();
646 request.set_fee_payer_signature(sig);
647 assert!(request.fee_payer_signature.is_some());
648 }
649
650 #[test]
651 fn test_with_fee_payer_signature() {
652 let sig = Signature::test_signature();
653 let request = TempoTransactionRequest::default().with_fee_payer_signature(sig);
654 assert!(request.fee_payer_signature.is_some());
655 }
656
657 #[test]
658 fn test_build_aa_with_fee_payer_signature() {
659 let sig = Signature::test_signature();
660 let mut request = TempoTransactionRequest::default().with_fee_payer_signature(sig);
661
662 request.inner.nonce = Some(0);
663 request.inner.gas = Some(21000);
664 request.inner.max_fee_per_gas = Some(1000000000);
665 request.inner.max_priority_fee_per_gas = Some(1000000);
666 request.inner.to = Some(address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into());
667
668 let tx = request.build_aa().expect("should build transaction");
669 assert_eq!(tx.fee_payer_signature, Some(sig));
670 }
671
672 #[test]
673 fn test_from_tempo_transaction_preserves_fee_payer_signature() {
674 let sig = Signature::test_signature();
675 let tx = TempoTransaction {
676 chain_id: 1,
677 nonce: 0,
678 fee_payer_signature: Some(sig),
679 valid_before: None,
680 valid_after: None,
681 gas_limit: 21000,
682 max_fee_per_gas: 1000000000,
683 max_priority_fee_per_gas: 1000000,
684 fee_token: None,
685 access_list: Default::default(),
686 calls: vec![Call {
687 to: address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into(),
688 value: Default::default(),
689 input: Default::default(),
690 }],
691 tempo_authorization_list: vec![],
692 nonce_key: Default::default(),
693 key_authorization: None,
694 };
695
696 let request: TempoTransactionRequest = tx.into();
697 assert_eq!(request.fee_payer_signature, Some(sig));
698 }
699
700 #[test]
701 fn test_build_aa_preserves_key_authorization() {
702 let key_auth = KeyAuthorization::unrestricted(
703 4217,
704 SignatureType::Secp256k1,
705 address!("0x1111111111111111111111111111111111111111"),
706 )
707 .into_signed(PrimitiveSignature::default());
708
709 let mut request = TempoTransactionRequest {
710 key_authorization: Some(key_auth.clone()),
711 ..Default::default()
712 };
713 request.inner.nonce = Some(0);
714 request.inner.gas = Some(21000);
715 request.inner.max_fee_per_gas = Some(1000000000);
716 request.inner.max_priority_fee_per_gas = Some(1000000);
717 request.inner.to = Some(address!("0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D").into());
718
719 let tx = request.build_aa().expect("should build transaction");
720 assert_eq!(
721 tx.key_authorization,
722 Some(key_auth),
723 "build_aa must preserve key_authorization from the request"
724 );
725 }
726
727 #[test]
728 fn test_set_calls_and_push_call() {
729 let call = Call {
730 to: address!("0x1111111111111111111111111111111111111111").into(),
731 value: U256::ZERO,
732 input: Bytes::from(vec![0xaa]),
733 };
734
735 let mut request = TempoTransactionRequest::default();
736 request.set_calls(vec![call.clone()]);
737 request.push_call(call.clone());
738
739 assert_eq!(request.calls, vec![call.clone(), call]);
740 }
741
742 #[test]
743 fn test_keychain_builder_helpers() {
744 let key_auth = KeyAuthorization::unrestricted(
745 4217,
746 SignatureType::Secp256k1,
747 address!("0x1111111111111111111111111111111111111111"),
748 )
749 .into_signed(PrimitiveSignature::default());
750
751 let request = TempoTransactionRequest::default()
752 .with_key_id(address!("0x2222222222222222222222222222222222222222"))
753 .with_key_type(SignatureType::WebAuthn)
754 .with_key_data(Bytes::from_static(b"auth-data"))
755 .with_key_authorization(key_auth.clone());
756
757 assert_eq!(
758 request.key_id,
759 Some(address!("0x2222222222222222222222222222222222222222"))
760 );
761 assert_eq!(request.key_type, Some(SignatureType::WebAuthn));
762 assert_eq!(request.key_data, Some(Bytes::from_static(b"auth-data")));
763 assert_eq!(request.key_authorization, Some(key_auth));
764 }
765
766 #[test]
767 fn test_aa_roundtrip_preserves_count() {
768 let base = TempoTransaction {
769 chain_id: 4217,
770 nonce: 1,
771 gas_limit: 100_000,
772 max_fee_per_gas: 1_000_000_000,
773 max_priority_fee_per_gas: 1_000_000,
774 calls: vec![],
775 ..Default::default()
776 };
777
778 let call = vec![Call {
780 to: address!("0x1111111111111111111111111111111111111111").into(),
781 value: U256::ZERO,
782 input: Bytes::from(vec![0xaa]),
783 }];
784 let mut original = base.clone();
785 original.calls = call.clone();
786
787 let roundtrip = TempoTransactionRequest::from(original)
788 .build_aa()
789 .expect("build_aa should succeed");
790 assert_eq!(
791 roundtrip.calls, call,
792 "single-call AA must not gain extra calls on round-trip"
793 );
794
795 let batch = vec![
797 Call {
798 to: address!("0x1111111111111111111111111111111111111111").into(),
799 value: U256::ZERO,
800 input: Bytes::from(vec![0xaa]),
801 },
802 Call {
803 to: address!("0x2222222222222222222222222222222222222222").into(),
804 value: U256::ZERO,
805 input: Bytes::from(vec![0xbb]),
806 },
807 ];
808 let mut original = base;
809 original.calls = batch.clone();
810
811 let roundtrip = TempoTransactionRequest::from(original)
812 .build_aa()
813 .expect("build_aa should succeed");
814 assert_eq!(
815 roundtrip.calls, batch,
816 "multi-call AA must not gain phantom calls on round-trip"
817 );
818 }
819}