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