foundry_primitives/transaction/
request.rs1use 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};
9#[cfg(feature = "optimism")]
10use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit};
11#[cfg(feature = "optimism")]
12use op_revm::transaction::deposit::DepositTransactionParts;
13use serde::{Deserialize, Serialize};
14use tempo_alloy::rpc::TempoTransactionRequest;
15use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTxType};
16
17#[cfg(feature = "optimism")]
18use super::optimism::get_deposit_tx_parts;
19use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx};
20use crate::FoundryNetwork;
21
22#[derive(Clone, Debug, PartialEq, Eq)]
33#[allow(clippy::large_enum_variant)]
34pub enum FoundryTransactionRequest {
35 Ethereum(TransactionRequest),
36 #[cfg(feature = "optimism")]
37 Op(WithOtherFields<TransactionRequest>),
38 Tempo(Box<TempoTransactionRequest>),
39}
40
41impl FoundryTransactionRequest {
42 #[inline]
45 pub fn new(inner: WithOtherFields<TransactionRequest>) -> Self {
46 inner.into()
47 }
48
49 pub fn into_inner(self) -> TransactionRequest {
51 match self {
52 Self::Ethereum(tx) => tx,
53 #[cfg(feature = "optimism")]
54 Self::Op(tx) => tx.inner,
55 Self::Tempo(tx) => tx.inner,
56 }
57 }
58
59 #[cfg(feature = "optimism")]
66 pub fn get_deposit_tx_parts(&self) -> Result<DepositTransactionParts, Vec<&'static str>> {
67 match self {
68 Self::Op(tx) => get_deposit_tx_parts(&tx.other),
69 _ => Err(vec!["sourceHash", "mint", "isSystemTx"]),
72 }
73 }
74
75 pub fn preferred_type(&self) -> FoundryTxType {
78 match self {
79 Self::Ethereum(tx) => tx.preferred_type().into(),
80 #[cfg(feature = "optimism")]
81 Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => {
82 FoundryTxType::PostExec
83 }
84 #[cfg(feature = "optimism")]
85 Self::Op(_) => FoundryTxType::Deposit,
86 Self::Tempo(_) => FoundryTxType::Tempo,
87 }
88 }
89
90 pub fn complete_4844(&self) -> Result<(), Vec<&'static str>> {
96 match self.as_ref().complete_4844() {
97 Ok(()) => Ok(()),
98 Err(missing) => {
99 let filtered: Vec<_> =
100 missing.into_iter().filter(|&key| key != "sidecar").collect();
101 if filtered.is_empty() { Ok(()) } else { Err(filtered) }
102 }
103 }
104 }
105
106 #[cfg(feature = "optimism")]
109 pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> {
110 self.get_deposit_tx_parts().map(|_| ())
111 }
112
113 pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> {
116 match self {
117 Self::Tempo(tx) => tx.complete_type(TempoTxType::AA).map(|_| ()),
118 _ => Err(vec!["feeToken", "nonceKey"]),
120 }
121 }
122
123 pub fn missing_keys(&self) -> Result<FoundryTxType, (FoundryTxType, Vec<&'static str>)> {
130 let pref = self.preferred_type();
131 if let Err(missing) = match pref {
132 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
133 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
134 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
135 FoundryTxType::Eip4844 => self.complete_4844(),
136 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
137 #[cfg(feature = "optimism")]
138 FoundryTxType::Deposit => self.complete_deposit(),
139 #[cfg(feature = "optimism")]
140 FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]),
141 FoundryTxType::Tempo => self.complete_tempo(),
142 } {
143 Err((pref, missing))
144 } else {
145 Ok(pref)
146 }
147 }
148
149 pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
154 #[cfg(feature = "optimism")]
155 if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() {
156 return Ok(FoundryTypedTx::Deposit(TxDeposit {
158 from: self.from().unwrap_or_default(),
159 source_hash: deposit_tx_parts.source_hash,
160 to: self.kind().unwrap_or_default(),
161 mint: deposit_tx_parts.mint.unwrap_or_default(),
162 value: self.value().unwrap_or_default(),
163 gas_limit: self.gas_limit().unwrap_or_default(),
164 is_system_transaction: deposit_tx_parts.is_system_transaction,
165 input: self.input().cloned().unwrap_or_default(),
166 }));
167 }
168 if self.complete_tempo().is_ok()
169 && let Self::Tempo(tx_req) = self
170 {
171 Ok(FoundryTypedTx::Tempo(
173 tx_req.build_aa().map_err(|e| Self::Tempo(Box::new(e.into_value())))?,
174 ))
175 } else if self.as_ref().has_eip4844_fields() && self.blob_sidecar().is_none() {
176 self.into_inner()
179 .build_4844_without_sidecar()
180 .map_err(|e| Self::Ethereum(e.into_value()))
181 .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
182 } else {
183 let typed_tx = self.into_inner().build_typed_tx().map_err(Self::Ethereum)?;
185 Ok(match typed_tx {
187 EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
188 EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
189 EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
190 EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
191 EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
192 })
193 }
194 }
195}
196
197impl Default for FoundryTransactionRequest {
198 fn default() -> Self {
199 Self::Ethereum(TransactionRequest::default())
200 }
201}
202
203impl Serialize for FoundryTransactionRequest {
204 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
205 where
206 S: serde::Serializer,
207 {
208 match self {
209 Self::Ethereum(tx) => tx.serialize(serializer),
210 #[cfg(feature = "optimism")]
211 Self::Op(tx) => tx.serialize(serializer),
212 Self::Tempo(tx) => tx.serialize(serializer),
213 }
214 }
215}
216
217impl<'de> Deserialize<'de> for FoundryTransactionRequest {
218 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
219 where
220 D: serde::Deserializer<'de>,
221 {
222 WithOtherFields::<TransactionRequest>::deserialize(deserializer).map(Into::<Self>::into)
223 }
224}
225
226impl AsRef<TransactionRequest> for FoundryTransactionRequest {
227 fn as_ref(&self) -> &TransactionRequest {
228 match self {
229 Self::Ethereum(tx) => tx,
230 #[cfg(feature = "optimism")]
231 Self::Op(tx) => tx,
232 Self::Tempo(tx) => tx.as_ref(),
233 }
234 }
235}
236
237impl AsMut<TransactionRequest> for FoundryTransactionRequest {
238 fn as_mut(&mut self) -> &mut TransactionRequest {
239 match self {
240 Self::Ethereum(tx) => tx,
241 #[cfg(feature = "optimism")]
242 Self::Op(tx) => tx,
243 Self::Tempo(tx) => tx.as_mut(),
244 }
245 }
246}
247
248impl From<WithOtherFields<TransactionRequest>> for FoundryTransactionRequest {
249 fn from(tx: WithOtherFields<TransactionRequest>) -> Self {
250 if tx.transaction_type == Some(TEMPO_TX_TYPE_ID)
251 || tx.other.contains_key("feeToken")
252 || tx.other.contains_key("nonceKey")
253 {
254 let mut tempo_tx_req: TempoTransactionRequest = tx.inner.into();
255 if let Some(fee_token) =
256 tx.other.get_deserialized::<Address>("feeToken").transpose().ok().flatten()
257 {
258 tempo_tx_req.fee_token = Some(fee_token);
259 }
260 if let Some(nonce_key) =
261 tx.other.get_deserialized::<U256>("nonceKey").transpose().ok().flatten()
262 {
263 tempo_tx_req.set_nonce_key(nonce_key);
264 }
265 return Self::Tempo(Box::new(tempo_tx_req));
266 }
267 #[cfg(feature = "optimism")]
268 if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID)
269 || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID)
270 || get_deposit_tx_parts(&tx.other).is_ok()
271 {
272 return Self::Op(tx);
273 }
274 Self::Ethereum(tx.into_inner())
275 }
276}
277
278impl From<FoundryTypedTx> for FoundryTransactionRequest {
279 fn from(tx: FoundryTypedTx) -> Self {
280 match tx {
281 FoundryTypedTx::Legacy(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
282 FoundryTypedTx::Eip2930(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
283 FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
284 FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
285 FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
286 #[cfg(feature = "optimism")]
287 FoundryTypedTx::Deposit(tx) => {
288 let other = OtherFields::from_iter([
289 ("sourceHash", tx.source_hash.to_string().into()),
290 ("mint", tx.mint.to_string().into()),
291 ("isSystemTx", tx.is_system_transaction.to_string().into()),
292 ]);
293 WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }.into()
294 }
295 #[cfg(feature = "optimism")]
296 FoundryTypedTx::PostExec(tx) => WithOtherFields {
297 inner: Into::<TransactionRequest>::into(tx),
298 other: OtherFields::default(),
299 }
300 .into(),
301 FoundryTypedTx::Tempo(tx) => {
302 let mut other = OtherFields::default();
303 if let Some(fee_token) = tx.fee_token {
304 other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
305 }
306 other.insert("nonceKey".to_string(), serde_json::to_value(tx.nonce_key).unwrap());
307 let first_call = tx.calls.first();
308 let mut inner = TransactionRequest::default()
309 .with_chain_id(tx.chain_id)
310 .with_nonce(tx.nonce)
311 .with_gas_limit(tx.gas_limit)
312 .with_max_fee_per_gas(tx.max_fee_per_gas)
313 .with_max_priority_fee_per_gas(tx.max_priority_fee_per_gas)
314 .with_kind(first_call.map(|c| c.to).unwrap_or_default())
315 .with_value(first_call.map(|c| c.value).unwrap_or_default())
316 .with_input(first_call.map(|c| c.input.clone()).unwrap_or_default())
317 .with_access_list(tx.access_list);
318 inner.transaction_type = Some(TEMPO_TX_TYPE_ID);
319 WithOtherFields { inner, other }.into()
320 }
321 }
322 }
323}
324
325impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
326 fn from(tx: FoundryTxEnvelope) -> Self {
327 FoundryTypedTx::from(tx).into()
328 }
329}
330
331#[cfg(not(feature = "optimism"))]
332impl From<alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>> for FoundryTransactionRequest {
333 fn from(tx: alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>) -> Self {
334 tx.inner.into_inner().into()
335 }
336}
337
338impl TransactionBuilder for FoundryTransactionRequest {
340 fn chain_id(&self) -> Option<ChainId> {
341 self.as_ref().chain_id
342 }
343
344 fn set_chain_id(&mut self, chain_id: ChainId) {
345 self.as_mut().chain_id = Some(chain_id);
346 }
347
348 fn nonce(&self) -> Option<u64> {
349 self.as_ref().nonce
350 }
351
352 fn set_nonce(&mut self, nonce: u64) {
353 self.as_mut().nonce = Some(nonce);
354 }
355
356 fn take_nonce(&mut self) -> Option<u64> {
357 self.as_mut().nonce.take()
358 }
359
360 fn input(&self) -> Option<&alloy_primitives::Bytes> {
361 self.as_ref().input.input()
362 }
363
364 fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
365 self.as_mut().input.input = Some(input.into());
366 }
367
368 fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
369 &mut self,
370 input: T,
371 kind: TransactionInputKind,
372 ) {
373 let inner = self.as_mut();
374 match kind {
375 TransactionInputKind::Input => inner.input.input = Some(input.into()),
376 TransactionInputKind::Data => inner.input.data = Some(input.into()),
377 TransactionInputKind::Both => {
378 let bytes = input.into();
379 inner.input.input = Some(bytes.clone());
380 inner.input.data = Some(bytes);
381 }
382 }
383 }
384
385 fn from(&self) -> Option<Address> {
386 self.as_ref().from
387 }
388
389 fn set_from(&mut self, from: Address) {
390 self.as_mut().from = Some(from);
391 }
392
393 fn kind(&self) -> Option<TxKind> {
394 self.as_ref().to
395 }
396
397 fn clear_kind(&mut self) {
398 self.as_mut().to = None;
399 }
400
401 fn set_kind(&mut self, kind: TxKind) {
402 self.as_mut().to = Some(kind);
403 }
404
405 fn value(&self) -> Option<U256> {
406 self.as_ref().value
407 }
408
409 fn set_value(&mut self, value: U256) {
410 self.as_mut().value = Some(value);
411 }
412
413 fn gas_price(&self) -> Option<u128> {
414 self.as_ref().gas_price
415 }
416
417 fn set_gas_price(&mut self, gas_price: u128) {
418 self.as_mut().gas_price = Some(gas_price);
419 }
420
421 fn max_fee_per_gas(&self) -> Option<u128> {
422 self.as_ref().max_fee_per_gas
423 }
424
425 fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
426 self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
427 }
428
429 fn max_priority_fee_per_gas(&self) -> Option<u128> {
430 self.as_ref().max_priority_fee_per_gas
431 }
432
433 fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
434 self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
435 }
436
437 fn gas_limit(&self) -> Option<u64> {
438 self.as_ref().gas
439 }
440
441 fn set_gas_limit(&mut self, gas_limit: u64) {
442 self.as_mut().gas = Some(gas_limit);
443 }
444
445 fn access_list(&self) -> Option<&AccessList> {
446 self.as_ref().access_list.as_ref()
447 }
448
449 fn set_access_list(&mut self, access_list: AccessList) {
450 self.as_mut().access_list = Some(access_list);
451 }
452}
453
454impl NetworkTransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
455 fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
456 match ty {
457 FoundryTxType::Legacy => self.as_ref().complete_legacy(),
458 FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
459 FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
460 FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
461 FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
462 #[cfg(feature = "optimism")]
463 FoundryTxType::Deposit => self.complete_deposit(),
464 #[cfg(feature = "optimism")]
465 FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]),
466 FoundryTxType::Tempo => self.complete_tempo(),
467 }
468 }
469
470 fn can_submit(&self) -> bool {
471 self.from().is_some()
472 }
473
474 fn can_build(&self) -> bool {
475 if self.as_ref().can_build() || self.complete_tempo().is_ok() {
476 return true;
477 }
478 #[cfg(feature = "optimism")]
479 if self.complete_deposit().is_ok() {
480 return true;
481 }
482 false
483 }
484
485 fn output_tx_type(&self) -> FoundryTxType {
486 self.preferred_type()
487 }
488
489 fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
490 let pref = self.preferred_type();
491 match pref {
492 FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(),
493 FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(),
494 FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(),
495 FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(),
496 FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(),
497 #[cfg(feature = "optimism")]
498 FoundryTxType::Deposit => self.complete_deposit().ok(),
499 #[cfg(feature = "optimism")]
500 FoundryTxType::PostExec => self.complete_type(pref).ok(),
501 FoundryTxType::Tempo => self.complete_tempo().ok(),
502 }?;
503 Some(pref)
504 }
505
506 fn prep_for_submission(&mut self) {
509 let preferred_type = self.preferred_type();
510 let inner = self.as_mut();
511 inner.transaction_type = Some(preferred_type as u8);
512 inner.gas.is_none().then(|| inner.set_gas_limit(Default::default()));
513 let is_deposit = {
514 #[cfg(feature = "optimism")]
515 {
516 preferred_type == FoundryTxType::Deposit
517 }
518 #[cfg(not(feature = "optimism"))]
519 {
520 false
521 }
522 };
523 if !is_deposit && preferred_type != FoundryTxType::Tempo {
524 inner.trim_conflicting_keys();
525 inner.populate_blob_hashes();
526 }
527 if !is_deposit {
528 inner.nonce.is_none().then(|| inner.set_nonce(Default::default()));
529 }
530 if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) {
531 inner.gas_price.is_none().then(|| inner.set_gas_price(Default::default()));
532 }
533 if preferred_type == FoundryTxType::Eip2930 {
534 inner.access_list.is_none().then(|| inner.set_access_list(Default::default()));
535 }
536 if matches!(
537 preferred_type,
538 FoundryTxType::Eip1559
539 | FoundryTxType::Eip4844
540 | FoundryTxType::Eip7702
541 | FoundryTxType::Tempo
542 ) {
543 inner
544 .max_priority_fee_per_gas
545 .is_none()
546 .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
547 inner.max_fee_per_gas.is_none().then(|| inner.set_max_fee_per_gas(Default::default()));
548 }
549 if preferred_type == FoundryTxType::Eip4844 {
550 inner
551 .as_ref()
552 .max_fee_per_blob_gas()
553 .is_none()
554 .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
555 }
556 }
557
558 fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
559 if let Err((tx_type, missing)) = self.missing_keys() {
560 return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
561 .into_unbuilt(self));
562 }
563 Ok(self.build_typed_tx().expect("checked by missing_keys"))
564 }
565
566 async fn build<W: NetworkWallet<FoundryNetwork>>(
567 self,
568 wallet: &W,
569 ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
570 Ok(wallet.sign_request(self).await?)
571 }
572}
573
574impl TransactionBuilder4844 for FoundryTransactionRequest {
575 fn max_fee_per_blob_gas(&self) -> Option<u128> {
576 self.as_ref().max_fee_per_blob_gas()
577 }
578
579 fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
580 self.as_mut().set_max_fee_per_blob_gas(max_fee_per_blob_gas);
581 }
582
583 fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
584 self.as_ref().blob_sidecar()
585 }
586
587 fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
588 self.as_mut().set_blob_sidecar(sidecar);
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use alloy_primitives::B256;
595
596 use super::*;
597
598 fn default_tx_req() -> TransactionRequest {
599 TransactionRequest::default()
600 .with_to(Address::random())
601 .with_nonce(1)
602 .with_value(U256::from(1000000))
603 .with_gas_limit(1000000)
604 .with_max_fee_per_gas(1000000)
605 .with_max_priority_fee_per_gas(1000000)
606 }
607
608 #[test]
609 fn test_routing_ethereum_default() {
610 let tx = default_tx_req();
611 let req: FoundryTransactionRequest = WithOtherFields::new(tx).into();
612
613 assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
614 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
615 }
616
617 #[test]
618 fn test_routing_tempo_by_fee_token() {
619 let tx = default_tx_req();
620 let mut other = OtherFields::default();
621 other.insert("feeToken".to_string(), serde_json::to_value(Address::random()).unwrap());
622
623 let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
624
625 assert!(matches!(req, FoundryTransactionRequest::Tempo(_)));
626 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Tempo(_))));
627 }
628
629 #[test]
630 #[cfg(feature = "optimism")]
631 fn test_routing_op_by_deposit_fields() {
632 let tx = default_tx_req();
633 let mut other = OtherFields::default();
634 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
635 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
636 other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
637
638 let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
639
640 assert!(matches!(req, FoundryTransactionRequest::Op(_)));
641 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Deposit(_))));
642 }
643
644 #[test]
645 fn test_op_incomplete_routes_to_ethereum() {
646 let tx = default_tx_req();
647 let mut other = OtherFields::default();
648 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
650 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
651
652 let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
653
654 assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
655 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
656 }
657
658 #[test]
659 fn test_ethereum_with_unrelated_other_fields() {
660 let tx = default_tx_req();
661 let mut other = OtherFields::default();
662 other.insert("anotherField".to_string(), serde_json::to_value(123).unwrap());
663
664 let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
665
666 assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
667 assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
668 }
669
670 #[test]
671 fn test_serialization_ethereum() {
672 let tx = default_tx_req();
673 let original: FoundryTransactionRequest = WithOtherFields::new(tx).into();
674
675 let serialized = serde_json::to_string(&original).unwrap();
676 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
677
678 assert!(matches!(deserialized, FoundryTransactionRequest::Ethereum(_)));
679 }
680
681 #[test]
682 #[cfg(feature = "optimism")]
683 fn test_serialization_op() {
684 let tx = default_tx_req();
685 let mut other = OtherFields::default();
686 other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
687 other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
688 other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
689
690 let original: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
691
692 let serialized = serde_json::to_string(&original).unwrap();
693 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
694
695 assert!(matches!(deserialized, FoundryTransactionRequest::Op(_)));
696 }
697
698 #[test]
699 fn test_serialization_tempo() {
700 let tx = default_tx_req();
701 let mut other = OtherFields::default();
702 other.insert("feeToken".to_string(), serde_json::to_value(Address::ZERO).unwrap());
703 other.insert("nonceKey".to_string(), serde_json::to_value(U256::from(42)).unwrap());
704
705 let original: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
706
707 let serialized = serde_json::to_string(&original).unwrap();
708 let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
709
710 assert!(matches!(deserialized, FoundryTransactionRequest::Tempo(_)));
711 }
712}