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