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