1use alloy_consensus::{BlobTransactionSidecarVariant, EthereumTypedTransaction};
2use alloy_network::{
3 BuildResult, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder,
4 TransactionBuilder4844, TransactionBuilderError,
5};
6use alloy_primitives::{Address, ChainId, TxKind, U256};
7use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest};
8use alloy_serde::{OtherFields, WithOtherFields};
9use core::num::NonZeroU64;
10#[cfg(feature = "optimism")]
11use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit};
12#[cfg(feature = "optimism")]
13use op_revm::transaction::deposit::DepositTransactionParts;
14use serde::{Deserialize, Serialize};
15use tempo_alloy::rpc::TempoTransactionRequest;
16use tempo_primitives::{
17 SignatureType, TEMPO_TX_TYPE_ID, TempoTxType,
18 transaction::{Call, SignedKeyAuthorization, TempoSignedAuthorization},
19};
20
21#[cfg(feature = "optimism")]
22use super::optimism::get_deposit_tx_parts;
23use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx};
24use crate::FoundryNetwork;
25
26#[derive(Clone, Debug, PartialEq, Eq)]
36#[allow(clippy::large_enum_variant)]
37pub enum FoundryTransactionRequest {
38 Ethereum(TransactionRequest),
39 #[cfg(feature = "optimism")]
40 Op(WithOtherFields<TransactionRequest>),
41 Tempo(Box<TempoTransactionRequest>),
42}
43
44const TEMPO_REQUEST_FIELDS: &[&str] = &[
45 "feeToken",
46 "nonceKey",
47 "calls",
48 "keyType",
49 "keyData",
50 "keyId",
51 "aaAuthorizationList",
52 "keyAuthorization",
53 "validBefore",
54 "validAfter",
55 "feePayerSignature",
56];
57
58impl FoundryTransactionRequest {
59 #[inline]
62 pub fn new(inner: WithOtherFields<TransactionRequest>) -> serde_json::Result<Self> {
63 inner.try_into()
64 }
65
66 pub fn into_inner(self) -> TransactionRequest {
68 match self {
69 Self::Ethereum(tx) => tx,
70 #[cfg(feature = "optimism")]
71 Self::Op(tx) => tx.inner,
72 Self::Tempo(tx) => tx.inner,
73 }
74 }
75
76 #[cfg(feature = "optimism")]
83 pub fn get_deposit_tx_parts(&self) -> Result<DepositTransactionParts, Vec<&'static str>> {
84 match self {
85 Self::Op(tx) => get_deposit_tx_parts(&tx.other),
86 _ => Err(vec!["sourceHash", "mint", "isSystemTx"]),
89 }
90 }
91
92 pub fn preferred_type(&self) -> FoundryTxType {
95 match self {
96 Self::Ethereum(tx) => tx.preferred_type().into(),
97 #[cfg(feature = "optimism")]
98 Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => {
99 FoundryTxType::PostExec
100 }
101 #[cfg(feature = "optimism")]
102 Self::Op(_) => FoundryTxType::Deposit,
103 Self::Tempo(_) => FoundryTxType::Tempo,
104 }
105 }
106
107 pub fn complete_4844(&self) -> Result<(), Vec<&'static str>> {
113 match self.as_ref().complete_4844() {
114 Ok(()) => Ok(()),
115 Err(missing) => {
116 let filtered: Vec<_> =
117 missing.into_iter().filter(|&key| key != "sidecar").collect();
118 if filtered.is_empty() { Ok(()) } else { Err(filtered) }
119 }
120 }
121 }
122
123 #[cfg(feature = "optimism")]
126 pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> {
127 self.get_deposit_tx_parts().map(|_| ())
128 }
129
130 pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> {
133 match self {
134 Self::Tempo(tx) => tx.complete_type(TempoTxType::AA).map(|_| ()),
135 _ => Err(vec!["feeToken", "nonceKey"]),
137 }
138 }
139
140 pub fn missing_keys(&self) -> Result<FoundryTxType, (FoundryTxType, Vec<&'static str>)> {
147 let pref = self.preferred_type();
148 if let Err(missing) = match pref {
149 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
150 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
151 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
152 FoundryTxType::Eip4844 => self.complete_4844(),
153 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
154 #[cfg(feature = "optimism")]
155 FoundryTxType::Deposit => self.complete_deposit(),
156 #[cfg(feature = "optimism")]
157 FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]),
158 FoundryTxType::Tempo => self.complete_tempo(),
159 } {
160 Err((pref, missing))
161 } else {
162 Ok(pref)
163 }
164 }
165
166 pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
171 #[cfg(feature = "optimism")]
172 if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() {
173 return Ok(FoundryTypedTx::Deposit(TxDeposit {
175 from: self.from().unwrap_or_default(),
176 source_hash: deposit_tx_parts.source_hash,
177 to: self.kind().unwrap_or_default(),
178 mint: deposit_tx_parts.mint.unwrap_or_default(),
179 value: self.value().unwrap_or_default(),
180 gas_limit: self.gas_limit().unwrap_or_default(),
181 is_system_transaction: deposit_tx_parts.is_system_transaction,
182 input: self.input().cloned().unwrap_or_default(),
183 }));
184 }
185 if self.complete_tempo().is_ok()
186 && let Self::Tempo(tx_req) = self
187 {
188 Ok(FoundryTypedTx::Tempo(
190 tx_req.build_aa().map_err(|e| Self::Tempo(Box::new(e.into_value())))?,
191 ))
192 } else if self.as_ref().has_eip4844_fields() && self.blob_sidecar().is_none() {
193 self.into_inner()
196 .build_4844_without_sidecar()
197 .map_err(|e| Self::Ethereum(e.into_value()))
198 .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
199 } else {
200 let typed_tx = self.into_inner().build_typed_tx().map_err(Self::Ethereum)?;
202 Ok(match typed_tx {
204 EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
205 EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
206 EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
207 EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
208 EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
209 })
210 }
211 }
212}
213
214impl Default for FoundryTransactionRequest {
215 fn default() -> Self {
216 Self::Ethereum(TransactionRequest::default())
217 }
218}
219
220impl Serialize for FoundryTransactionRequest {
221 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
222 where
223 S: serde::Serializer,
224 {
225 match self {
226 Self::Ethereum(tx) => tx.serialize(serializer),
227 #[cfg(feature = "optimism")]
228 Self::Op(tx) => tx.serialize(serializer),
229 Self::Tempo(tx) => tx.serialize(serializer),
230 }
231 }
232}
233
234impl<'de> Deserialize<'de> for FoundryTransactionRequest {
235 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236 where
237 D: serde::Deserializer<'de>,
238 {
239 WithOtherFields::<TransactionRequest>::deserialize(deserializer)?
240 .try_into()
241 .map_err(serde::de::Error::custom)
242 }
243}
244
245impl AsRef<TransactionRequest> for FoundryTransactionRequest {
246 fn as_ref(&self) -> &TransactionRequest {
247 match self {
248 Self::Ethereum(tx) => tx,
249 #[cfg(feature = "optimism")]
250 Self::Op(tx) => tx,
251 Self::Tempo(tx) => tx.as_ref(),
252 }
253 }
254}
255
256impl AsMut<TransactionRequest> for FoundryTransactionRequest {
257 fn as_mut(&mut self) -> &mut TransactionRequest {
258 match self {
259 Self::Ethereum(tx) => tx,
260 #[cfg(feature = "optimism")]
261 Self::Op(tx) => tx,
262 Self::Tempo(tx) => tx.as_mut(),
263 }
264 }
265}
266
267impl TryFrom<WithOtherFields<TransactionRequest>> for FoundryTransactionRequest {
268 type Error = serde_json::Error;
269
270 fn try_from(tx: WithOtherFields<TransactionRequest>) -> Result<Self, Self::Error> {
271 #[derive(Deserialize)]
272 struct NonZeroQuantity(
273 #[serde(
274 with = "tempo_primitives::transaction::key_authorization::serde_nonzero_quantity_opt"
275 )]
276 Option<NonZeroU64>,
277 );
278
279 if tx.transaction_type == Some(TEMPO_TX_TYPE_ID)
280 || TEMPO_REQUEST_FIELDS.iter().any(|field| tx.other.contains_key(*field))
281 {
282 let mut tempo_tx_req: TempoTransactionRequest = tx.inner.into();
283 tempo_tx_req.fee_token =
284 tx.other.get_deserialized::<Option<Address>>("feeToken").transpose()?.flatten();
285 tempo_tx_req.nonce_key =
286 tx.other.get_deserialized::<Option<U256>>("nonceKey").transpose()?.flatten();
287 tempo_tx_req.calls =
288 tx.other.get_deserialized::<Vec<Call>>("calls").transpose()?.unwrap_or_default();
289 tempo_tx_req.key_type = tx
290 .other
291 .get_deserialized::<Option<SignatureType>>("keyType")
292 .transpose()?
293 .flatten();
294 tempo_tx_req.key_data =
295 tx.other.get_deserialized::<Option<_>>("keyData").transpose()?.flatten();
296 tempo_tx_req.key_id =
297 tx.other.get_deserialized::<Option<_>>("keyId").transpose()?.flatten();
298 tempo_tx_req.tempo_authorization_list = tx
299 .other
300 .get_deserialized::<Vec<TempoSignedAuthorization>>("aaAuthorizationList")
301 .transpose()?
302 .unwrap_or_default();
303 tempo_tx_req.key_authorization = tx
304 .other
305 .get_deserialized::<Option<SignedKeyAuthorization>>("keyAuthorization")
306 .transpose()?
307 .flatten();
308 tempo_tx_req.valid_before = tx
309 .other
310 .get_deserialized::<NonZeroQuantity>("validBefore")
311 .transpose()?
312 .and_then(|value| value.0);
313 tempo_tx_req.valid_after = tx
314 .other
315 .get_deserialized::<NonZeroQuantity>("validAfter")
316 .transpose()?
317 .and_then(|value| value.0);
318 tempo_tx_req.fee_payer_signature =
319 tx.other.get_deserialized::<Option<_>>("feePayerSignature").transpose()?.flatten();
320 return Ok(Self::Tempo(Box::new(tempo_tx_req)));
321 }
322 #[cfg(feature = "optimism")]
323 if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID)
324 || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID)
325 || get_deposit_tx_parts(&tx.other).is_ok()
326 {
327 return Ok(Self::Op(tx));
328 }
329 Ok(Self::Ethereum(tx.into_inner()))
330 }
331}
332
333impl From<FoundryTypedTx> for FoundryTransactionRequest {
334 fn from(tx: FoundryTypedTx) -> Self {
335 match tx {
336 FoundryTypedTx::Legacy(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
337 FoundryTypedTx::Eip2930(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
338 FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
339 FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
340 FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
341 #[cfg(feature = "optimism")]
342 FoundryTypedTx::Deposit(tx) => {
343 let other = OtherFields::from_iter([
344 ("sourceHash", serde_json::to_value(tx.source_hash).unwrap()),
345 ("mint", serde_json::to_value(U256::from(tx.mint)).unwrap()),
346 ("isSystemTx", serde_json::to_value(tx.is_system_transaction).unwrap()),
347 ]);
348 WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }
349 .try_into()
350 .expect("valid OP transaction request")
351 }
352 #[cfg(feature = "optimism")]
353 FoundryTypedTx::PostExec(tx) => WithOtherFields {
354 inner: Into::<TransactionRequest>::into(tx),
355 other: OtherFields::default(),
356 }
357 .try_into()
358 .expect("valid OP post-exec transaction request"),
359 FoundryTypedTx::Tempo(tx) => Self::Tempo(Box::new(tx.into())),
360 }
361 }
362}
363
364impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
365 fn from(tx: FoundryTxEnvelope) -> Self {
366 FoundryTypedTx::from(tx).into()
367 }
368}
369
370#[cfg(not(feature = "optimism"))]
371impl From<alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>> for FoundryTransactionRequest {
372 fn from(tx: alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>) -> Self {
373 tx.inner.into_inner().into()
374 }
375}
376
377impl TransactionBuilder for FoundryTransactionRequest {
379 fn chain_id(&self) -> Option<ChainId> {
380 self.as_ref().chain_id
381 }
382
383 fn set_chain_id(&mut self, chain_id: ChainId) {
384 self.as_mut().chain_id = Some(chain_id);
385 }
386
387 fn nonce(&self) -> Option<u64> {
388 self.as_ref().nonce
389 }
390
391 fn set_nonce(&mut self, nonce: u64) {
392 self.as_mut().nonce = Some(nonce);
393 }
394
395 fn take_nonce(&mut self) -> Option<u64> {
396 self.as_mut().nonce.take()
397 }
398
399 fn input(&self) -> Option<&alloy_primitives::Bytes> {
400 self.as_ref().input.input()
401 }
402
403 fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
404 self.as_mut().input.input = Some(input.into());
405 }
406
407 fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
408 &mut self,
409 input: T,
410 kind: TransactionInputKind,
411 ) {
412 let inner = self.as_mut();
413 match kind {
414 TransactionInputKind::Input => inner.input.input = Some(input.into()),
415 TransactionInputKind::Data => inner.input.data = Some(input.into()),
416 TransactionInputKind::Both => {
417 let bytes = input.into();
418 inner.input.input = Some(bytes.clone());
419 inner.input.data = Some(bytes);
420 }
421 }
422 }
423
424 fn from(&self) -> Option<Address> {
425 self.as_ref().from
426 }
427
428 fn set_from(&mut self, from: Address) {
429 self.as_mut().from = Some(from);
430 }
431
432 fn kind(&self) -> Option<TxKind> {
433 self.as_ref().to
434 }
435
436 fn clear_kind(&mut self) {
437 self.as_mut().to = None;
438 }
439
440 fn set_kind(&mut self, kind: TxKind) {
441 self.as_mut().to = Some(kind);
442 }
443
444 fn value(&self) -> Option<U256> {
445 self.as_ref().value
446 }
447
448 fn set_value(&mut self, value: U256) {
449 self.as_mut().value = Some(value);
450 }
451
452 fn gas_price(&self) -> Option<u128> {
453 self.as_ref().gas_price
454 }
455
456 fn set_gas_price(&mut self, gas_price: u128) {
457 self.as_mut().gas_price = Some(gas_price);
458 }
459
460 fn max_fee_per_gas(&self) -> Option<u128> {
461 self.as_ref().max_fee_per_gas
462 }
463
464 fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
465 self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
466 }
467
468 fn max_priority_fee_per_gas(&self) -> Option<u128> {
469 self.as_ref().max_priority_fee_per_gas
470 }
471
472 fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
473 self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
474 }
475
476 fn gas_limit(&self) -> Option<u64> {
477 self.as_ref().gas
478 }
479
480 fn set_gas_limit(&mut self, gas_limit: u64) {
481 self.as_mut().gas = Some(gas_limit);
482 }
483
484 fn access_list(&self) -> Option<&AccessList> {
485 self.as_ref().access_list.as_ref()
486 }
487
488 fn set_access_list(&mut self, access_list: AccessList) {
489 self.as_mut().access_list = Some(access_list);
490 }
491}
492
493impl NetworkTransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
494 fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
495 match ty {
496 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
497 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
498 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
499 FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
500 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
501 #[cfg(feature = "optimism")]
502 FoundryTxType::Deposit => self.complete_deposit(),
503 #[cfg(feature = "optimism")]
504 FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]),
505 FoundryTxType::Tempo => self.complete_tempo(),
506 }
507 }
508
509 fn can_submit(&self) -> bool {
510 self.from().is_some()
511 }
512
513 fn can_build(&self) -> bool {
514 if self.as_ref().can_build() || self.complete_tempo().is_ok() {
515 return true;
516 }
517 #[cfg(feature = "optimism")]
518 if self.complete_deposit().is_ok() {
519 return true;
520 }
521 false
522 }
523
524 fn output_tx_type(&self) -> FoundryTxType {
525 self.preferred_type()
526 }
527
528 fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
529 let pref = self.preferred_type();
530 match pref {
531 FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(),
532 FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(),
533 FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(),
534 FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(),
535 FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(),
536 #[cfg(feature = "optimism")]
537 FoundryTxType::Deposit => self.complete_deposit().ok(),
538 #[cfg(feature = "optimism")]
539 FoundryTxType::PostExec => self.complete_type(pref).ok(),
540 FoundryTxType::Tempo => self.complete_tempo().ok(),
541 }?;
542 Some(pref)
543 }
544
545 fn prep_for_submission(&mut self) {
548 let preferred_type = self.preferred_type();
549 let inner = self.as_mut();
550 inner.transaction_type = Some(preferred_type as u8);
551 inner.gas.is_none().then(|| inner.set_gas_limit(Default::default()));
552 let is_deposit = {
553 #[cfg(feature = "optimism")]
554 {
555 preferred_type == FoundryTxType::Deposit
556 }
557 #[cfg(not(feature = "optimism"))]
558 {
559 false
560 }
561 };
562 if !is_deposit && preferred_type != FoundryTxType::Tempo {
563 inner.trim_conflicting_keys();
564 inner.populate_blob_hashes();
565 }
566 if !is_deposit {
567 inner.nonce.is_none().then(|| inner.set_nonce(Default::default()));
568 }
569 if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) {
570 inner.gas_price.is_none().then(|| inner.set_gas_price(Default::default()));
571 }
572 if preferred_type == FoundryTxType::Eip2930 {
573 inner.access_list.is_none().then(|| inner.set_access_list(Default::default()));
574 }
575 if matches!(
576 preferred_type,
577 FoundryTxType::Eip1559
578 | FoundryTxType::Eip4844
579 | FoundryTxType::Eip7702
580 | FoundryTxType::Tempo
581 ) {
582 inner
583 .max_priority_fee_per_gas
584 .is_none()
585 .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
586 inner.max_fee_per_gas.is_none().then(|| inner.set_max_fee_per_gas(Default::default()));
587 }
588 if preferred_type == FoundryTxType::Eip4844 {
589 inner
590 .as_ref()
591 .max_fee_per_blob_gas()
592 .is_none()
593 .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
594 }
595 }
596
597 fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
598 if let Err((tx_type, missing)) = self.missing_keys() {
599 return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
600 .into_unbuilt(self));
601 }
602 Ok(self.build_typed_tx().expect("checked by missing_keys"))
603 }
604
605 async fn build<W: NetworkWallet<FoundryNetwork>>(
606 self,
607 wallet: &W,
608 ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
609 Ok(wallet.sign_request(self).await?)
610 }
611}
612
613impl TransactionBuilder4844 for FoundryTransactionRequest {
614 fn max_fee_per_blob_gas(&self) -> Option<u128> {
615 self.as_ref().max_fee_per_blob_gas()
616 }
617
618 fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
619 self.as_mut().set_max_fee_per_blob_gas(max_fee_per_blob_gas);
620 }
621
622 fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
623 self.as_ref().blob_sidecar()
624 }
625
626 fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
627 self.as_mut().set_blob_sidecar(sidecar);
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use alloy_primitives::{B256, Bytes, Signature};
634 use tempo_primitives::{
635 TempoSignature, TempoTransaction,
636 transaction::{Authorization, KeyAuthorization, PrimitiveSignature},
637 };
638
639 use super::*;
640
641 fn default_tx_req() -> TransactionRequest {
642 TransactionRequest::default()
643 .with_to(Address::random())
644 .with_nonce(1)
645 .with_value(U256::from(1000000))
646 .with_gas_limit(1000000)
647 .with_max_fee_per_gas(1000000)
648 .with_max_priority_fee_per_gas(1000000)
649 }
650
651 #[test]
652 fn test_routing_ethereum_default() {
653 let tx = default_tx_req();
654 let req: FoundryTransactionRequest = WithOtherFields::new(tx).try_into().unwrap();
655
656 assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
657 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
658 }
659
660 #[test]
661 fn test_routing_tempo_by_fee_token() {
662 let tx = default_tx_req();
663 let mut other = OtherFields::default();
664 other.insert("feeToken".to_string(), serde_json::to_value(Address::random()).unwrap());
665
666 let req: FoundryTransactionRequest =
667 WithOtherFields { inner: tx, other }.try_into().unwrap();
668
669 assert!(matches!(req, FoundryTransactionRequest::Tempo(_)));
670 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Tempo(_))));
671 }
672
673 #[test]
674 #[cfg(feature = "optimism")]
675 fn test_routing_op_by_deposit_fields() {
676 let tx = default_tx_req();
677 let mut other = OtherFields::default();
678 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
679 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
680 other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
681
682 let req: FoundryTransactionRequest =
683 WithOtherFields { inner: tx, other }.try_into().unwrap();
684
685 assert!(matches!(req, FoundryTransactionRequest::Op(_)));
686 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Deposit(_))));
687 }
688
689 #[test]
690 fn test_op_incomplete_routes_to_ethereum() {
691 let tx = default_tx_req();
692 let mut other = OtherFields::default();
693 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
695 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
696
697 let req: FoundryTransactionRequest =
698 WithOtherFields { inner: tx, other }.try_into().unwrap();
699
700 assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
701 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
702 }
703
704 #[test]
705 fn test_ethereum_with_unrelated_other_fields() {
706 let tx = default_tx_req();
707 let mut other = OtherFields::default();
708 other.insert("anotherField".to_string(), serde_json::to_value(123).unwrap());
709
710 let req: FoundryTransactionRequest =
711 WithOtherFields { inner: tx, other }.try_into().unwrap();
712
713 assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
714 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
715 }
716
717 #[test]
718 fn test_serialization_ethereum() {
719 let tx = default_tx_req();
720 let original: FoundryTransactionRequest = WithOtherFields::new(tx).try_into().unwrap();
721
722 let serialized = serde_json::to_string(&original).unwrap();
723 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
724
725 assert!(matches!(deserialized, FoundryTransactionRequest::Ethereum(_)));
726 }
727
728 #[test]
729 #[cfg(feature = "optimism")]
730 fn test_serialization_op() {
731 let tx = default_tx_req();
732 let mut other = OtherFields::default();
733 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
734 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
735 other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
736
737 let original: FoundryTransactionRequest =
738 WithOtherFields { inner: tx, other }.try_into().unwrap();
739
740 let serialized = serde_json::to_string(&original).unwrap();
741 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
742
743 assert!(matches!(deserialized, FoundryTransactionRequest::Op(_)));
744 }
745
746 #[test]
747 fn test_serialization_tempo() {
748 let tx = default_tx_req();
749 let mut other = OtherFields::default();
750 other.insert("feeToken".to_string(), serde_json::to_value(Address::ZERO).unwrap());
751 other.insert("nonceKey".to_string(), serde_json::to_value(U256::from(42)).unwrap());
752
753 let original: FoundryTransactionRequest =
754 WithOtherFields { inner: tx, other }.try_into().unwrap();
755
756 let serialized = serde_json::to_string(&original).unwrap();
757 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
758
759 assert!(matches!(deserialized, FoundryTransactionRequest::Tempo(_)));
760 }
761
762 #[test]
763 fn test_tempo_request_decodes_all_extension_fields() {
764 let signature = Signature::test_signature();
765 let expected = TempoTransactionRequest {
766 inner: TransactionRequest {
767 transaction_type: Some(TEMPO_TX_TYPE_ID),
768 ..default_tx_req()
769 },
770 fee_token: Some(Address::repeat_byte(0x11)),
771 nonce_key: Some(U256::from(42)),
772 calls: vec![Call {
773 to: TxKind::Call(Address::repeat_byte(0x22)),
774 value: U256::from(7),
775 input: Bytes::from_static(&[0xde, 0xad]),
776 }],
777 key_type: Some(SignatureType::WebAuthn),
778 key_data: Some(Bytes::from_static(&[0xbe, 0xef])),
779 key_id: Some(Address::repeat_byte(0x33)),
780 tempo_authorization_list: vec![TempoSignedAuthorization::new_unchecked(
781 Authorization {
782 chain_id: U256::from(4217),
783 address: Address::repeat_byte(0x44),
784 nonce: 3,
785 },
786 TempoSignature::default(),
787 )],
788 key_authorization: Some(
789 KeyAuthorization::unrestricted(
790 4217,
791 SignatureType::Secp256k1,
792 Address::repeat_byte(0x55),
793 )
794 .into_signed(PrimitiveSignature::Secp256k1(signature)),
795 ),
796 valid_before: NonZeroU64::new(100),
797 valid_after: NonZeroU64::new(10),
798 fee_payer_signature: Some(signature),
799 };
800 let request = serde_json::from_value::<WithOtherFields<TransactionRequest>>(
801 serde_json::to_value(&expected).unwrap(),
802 )
803 .unwrap();
804
805 let decoded = FoundryTransactionRequest::try_from(request).unwrap();
806
807 let FoundryTransactionRequest::Tempo(decoded) = decoded else { panic!() };
808 assert_eq!(*decoded, expected);
809 }
810
811 #[test]
812 fn test_malformed_tempo_field_is_rejected() {
813 let mut other = OtherFields::default();
814 other.insert("nonceKey".to_string(), serde_json::json!("not a quantity"));
815
816 let result =
817 FoundryTransactionRequest::new(WithOtherFields { inner: default_tx_req(), other });
818
819 assert!(result.is_err());
820 }
821
822 #[test]
823 fn test_tempo_validity_uses_rpc_quantities() {
824 let request = serde_json::from_value::<FoundryTransactionRequest>(serde_json::json!({
825 "type": "0x76",
826 "validBefore": "0x64",
827 "validAfter": "0xa",
828 }))
829 .unwrap();
830
831 let FoundryTransactionRequest::Tempo(request) = request else { panic!() };
832 assert_eq!(request.valid_before, NonZeroU64::new(100));
833 assert_eq!(request.valid_after, NonZeroU64::new(10));
834 }
835
836 #[test]
837 fn test_tempo_typed_request_roundtrip_preserves_fields() {
838 let tx = TempoTransaction {
839 chain_id: 4217,
840 nonce: 7,
841 fee_payer_signature: None,
842 valid_before: NonZeroU64::new(100),
843 valid_after: NonZeroU64::new(10),
844 gas_limit: 100_000,
845 max_fee_per_gas: 20,
846 max_priority_fee_per_gas: 2,
847 fee_token: Some(Address::random()),
848 access_list: Default::default(),
849 calls: vec![Call {
850 to: TxKind::Call(Address::random()),
851 value: U256::from(42),
852 input: vec![1, 2, 3].into(),
853 }],
854 tempo_authorization_list: Vec::new(),
855 nonce_key: U256::from(9),
856 key_authorization: None,
857 };
858
859 let request: FoundryTransactionRequest = FoundryTypedTx::Tempo(tx.clone()).into();
860 let rebuilt = request.build_unsigned().unwrap();
861
862 assert_eq!(rebuilt, FoundryTypedTx::Tempo(tx));
863 }
864
865 #[test]
866 #[cfg(feature = "optimism")]
867 fn test_deposit_typed_tx_roundtrip() {
868 let deposit_tx = TxDeposit {
869 from: Address::random(),
870 source_hash: B256::random(),
871 to: TxKind::Call(Address::random()),
872 mint: 1000u128,
873 value: U256::from(500),
874 gas_limit: 21000,
875 is_system_transaction: true,
876 input: Default::default(),
877 };
878
879 let req: FoundryTransactionRequest = FoundryTypedTx::Deposit(deposit_tx.clone()).into();
880
881 assert!(matches!(req, FoundryTransactionRequest::Op(_)));
882
883 let parts = req.get_deposit_tx_parts().expect("should parse deposit parts");
884 assert_eq!(parts.source_hash, deposit_tx.source_hash);
885 assert_eq!(parts.mint, Some(deposit_tx.mint));
886 assert_eq!(parts.is_system_transaction, deposit_tx.is_system_transaction);
887 }
888}