diff --git a/crates/consensus/src/transaction/envelope.rs b/crates/consensus/src/transaction/envelope.rs index 75f416416aa..417cb552955 100644 --- a/crates/consensus/src/transaction/envelope.rs +++ b/crates/consensus/src/transaction/envelope.rs @@ -552,7 +552,12 @@ mod serde_from { #[serde(untagged)] pub(crate) enum MaybeTaggedTxEnvelope { Tagged(TaggedTxEnvelope), - Untagged(Signed), + Untagged { + #[serde(default, rename = "type", deserialize_with = "alloy_serde::reject_if_some")] + _ty: Option<()>, + #[serde(flatten)] + tx: Signed, + }, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -574,7 +579,7 @@ mod serde_from { fn from(value: MaybeTaggedTxEnvelope) -> Self { match value { MaybeTaggedTxEnvelope::Tagged(tagged) => tagged.into(), - MaybeTaggedTxEnvelope::Untagged(tx) => Self::Legacy(tx), + MaybeTaggedTxEnvelope::Untagged { tx, .. } => Self::Legacy(tx), } } } @@ -1159,4 +1164,43 @@ mod tests { assert!(tx.recover_signer().is_ok()); } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_untagged_legacy() { + let data = r#"{ + "hash": "0x97efb58d2b42df8d68ab5899ff42b16c7e0af35ed86ae4adb8acaad7e444220c", + "input": "0x", + "r": "0x5d71a4a548503f2916d10c6b1a1557a0e7352eb041acb2bac99d1ad6bb49fd45", + "s": "0x2627bf6d35be48b0e56c61733f63944c0ebcaa85cb4ed6bc7cba3161ba85e0e8", + "v": "0x1c", + "gas": "0x15f90", + "from": "0x2a65aca4d5fc5b5c859090a6c34d164135398226", + "to": "0x8fbeb4488a08d60979b5aa9e13dd00b2726320b2", + "value": "0xf606682badd7800", + "nonce": "0x11f398", + "gasPrice": "0x4a817c800" + }"#; + + let tx: TxEnvelope = serde_json::from_str(data).unwrap(); + + assert!(matches!(tx, TxEnvelope::Legacy(_))); + + let data_with_wrong_type = r#"{ + "hash": "0x97efb58d2b42df8d68ab5899ff42b16c7e0af35ed86ae4adb8acaad7e444220c", + "input": "0x", + "r": "0x5d71a4a548503f2916d10c6b1a1557a0e7352eb041acb2bac99d1ad6bb49fd45", + "s": "0x2627bf6d35be48b0e56c61733f63944c0ebcaa85cb4ed6bc7cba3161ba85e0e8", + "v": "0x1c", + "gas": "0x15f90", + "from": "0x2a65aca4d5fc5b5c859090a6c34d164135398226", + "to": "0x8fbeb4488a08d60979b5aa9e13dd00b2726320b2", + "value": "0xf606682badd7800", + "nonce": "0x11f398", + "gasPrice": "0x4a817c800", + "type": "0x12" + }"#; + + assert!(serde_json::from_str::(data_with_wrong_type).is_err()); + } } diff --git a/crates/consensus/src/transaction/typed.rs b/crates/consensus/src/transaction/typed.rs index c1b5ef38065..41d29224967 100644 --- a/crates/consensus/src/transaction/typed.rs +++ b/crates/consensus/src/transaction/typed.rs @@ -327,7 +327,12 @@ mod serde_from { #[serde(untagged)] pub(crate) enum MaybeTaggedTypedTransaction { Tagged(TaggedTypedTransaction), - Untagged(TxLegacy), + Untagged { + #[serde(default, rename = "type", deserialize_with = "alloy_serde::reject_if_some")] + _ty: Option<()>, + #[serde(flatten)] + tx: TxLegacy, + }, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -354,7 +359,7 @@ mod serde_from { fn from(value: MaybeTaggedTypedTransaction) -> Self { match value { MaybeTaggedTypedTransaction::Tagged(tagged) => tagged.into(), - MaybeTaggedTypedTransaction::Untagged(tx) => Self::Legacy(tx), + MaybeTaggedTypedTransaction::Untagged { tx, .. } => Self::Legacy(tx), } } } diff --git a/crates/network/src/any/mod.rs b/crates/network/src/any/mod.rs index 3e5891a122d..6dc5db6f645 100644 --- a/crates/network/src/any/mod.rs +++ b/crates/network/src/any/mod.rs @@ -19,6 +19,9 @@ pub type AnyRpcHeader = alloy_rpc_types_eth::Header; pub type AnyRpcBlock = WithOtherFields>, AnyRpcHeader>>; +/// A catch-all transaction type for handling transactions on multiple networks. +pub type AnyRpcTransaction = WithOtherFields>; + /// Types for a catch-all network. /// /// `AnyNetwork`'s associated types allow for many different types of @@ -65,7 +68,7 @@ impl Network for AnyNetwork { type TransactionRequest = WithOtherFields; - type TransactionResponse = WithOtherFields>; + type TransactionResponse = AnyRpcTransaction; type ReceiptResponse = AnyTransactionReceipt; diff --git a/crates/network/src/any/unknowns.rs b/crates/network/src/any/unknowns.rs index ccae02d7350..43993c83213 100644 --- a/crates/network/src/any/unknowns.rs +++ b/crates/network/src/any/unknowns.rs @@ -3,12 +3,12 @@ use std::sync::OnceLock; use alloy_consensus::TxType; use alloy_eips::{eip2718::Eip2718Error, eip7702::SignedAuthorization}; -use alloy_primitives::{Address, Bytes, TxKind, B256}; +use alloy_primitives::{Address, Bytes, TxKind, B256, U128, U64, U8}; use alloy_rpc_types_eth::AccessList; use alloy_serde::OtherFields; /// Transaction type for a catch-all network. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[doc(alias = "AnyTransactionType")] pub struct AnyTxType(pub u8); @@ -38,6 +38,24 @@ impl From for u8 { } } +impl serde::Serialize for AnyTxType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + U8::from(self.0).serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for AnyTxType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + U8::deserialize(deserializer).map(|t| Self(t.to::())) + } +} + impl TryFrom for TxType { type Error = Eip2718Error; @@ -87,31 +105,41 @@ pub struct UnknownTypedTransaction { impl alloy_consensus::Transaction for UnknownTypedTransaction { fn chain_id(&self) -> Option { - self.fields.get_deserialized("chainId").and_then(Result::ok) + self.fields.get_deserialized::("chainId").and_then(Result::ok).map(|v| v.to()) } fn nonce(&self) -> u64 { - self.fields.get_deserialized("nonce").and_then(Result::ok).unwrap_or_default() + self.fields.get_deserialized::("nonce").and_then(Result::ok).unwrap_or_default().to() } fn gas_limit(&self) -> u64 { - self.fields.get_deserialized("gasLimit").and_then(Result::ok).unwrap_or_default() + self.fields.get_deserialized::("gas").and_then(Result::ok).unwrap_or_default().to() } fn gas_price(&self) -> Option { - self.fields.get_deserialized("gasPrice").and_then(Result::ok) + self.fields.get_deserialized::("gasPrice").and_then(Result::ok).map(|v| v.to()) } fn max_fee_per_gas(&self) -> u128 { - self.fields.get_deserialized("maxFeePerGas").and_then(Result::ok).unwrap_or_default() + self.fields + .get_deserialized::("maxFeePerGas") + .and_then(Result::ok) + .unwrap_or_default() + .to() } fn max_priority_fee_per_gas(&self) -> Option { - self.fields.get_deserialized("maxPriorityFeePerGas").and_then(Result::ok) + self.fields + .get_deserialized::("maxPriorityFeePerGas") + .and_then(Result::ok) + .map(|v| v.to()) } fn max_fee_per_blob_gas(&self) -> Option { - self.fields.get_deserialized("maxFeePerBlobGas").and_then(Result::ok) + self.fields + .get_deserialized::("maxFeePerBlobGas") + .and_then(Result::ok) + .map(|v| v.to()) } fn priority_fee_or_price(&self) -> u128 { @@ -262,3 +290,61 @@ impl alloy_consensus::Transaction for UnknownTxEnvelope { self.inner.authorization_list() } } + +#[cfg(test)] +mod tests { + use alloy_consensus::Transaction; + + use crate::{AnyRpcTransaction, AnyTxEnvelope}; + + use super::*; + + #[test] + fn test_serde_anytype() { + let ty = AnyTxType(126); + assert_eq!(serde_json::to_string(&ty).unwrap(), "\"0x7e\""); + } + + #[test] + fn test_serde_op_deposit() { + let input = r#"{ + "blockHash": "0xef664d656f841b5ad6a2b527b963f1eb48b97d7889d742f6cbff6950388e24cd", + "blockNumber": "0x73a78fd", + "depositReceiptVersion": "0x1", + "from": "0x36bde71c97b33cc4729cf772ae268934f7ab70b2", + "gas": "0xc27a8", + "gasPrice": "0x521", + "hash": "0x0bf1845c5d7a82ec92365d5027f7310793d53004f3c86aa80965c67bf7e7dc80", + "input": "0xd764ad0b000100000000000000000000000000000000000000000000000000000001cf5400000000000000000000000099c9fc46f92e8a1c0dec1b1747d010903e884be100000000000000000000000042000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a12000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e40166a07a0000000000000000000000000994206dfe8de6ec6920ff4d779b0d950605fb53000000000000000000000000d533a949740bb3306d119cc777fa900ba034cd52000000000000000000000000ca74f404e0c7bfa35b13b511097df966d5a65597000000000000000000000000ca74f404e0c7bfa35b13b511097df966d5a65597000000000000000000000000000000000000000000000216614199391dbba2ba00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mint": "0x0", + "nonce": "0x74060", + "r": "0x0", + "s": "0x0", + "sourceHash": "0x074adb22f2e6ed9bdd31c52eefc1f050e5db56eb85056450bccd79a6649520b3", + "to": "0x4200000000000000000000000000000000000007", + "transactionIndex": "0x1", + "type": "0x7e", + "v": "0x0", + "value": "0x0" + }"#; + + let tx: AnyRpcTransaction = serde_json::from_str(input).unwrap(); + + let AnyTxEnvelope::Unknown(inner) = tx.inner.inner.clone() else { + panic!("expected unknown envelope"); + }; + + assert_eq!(inner.inner.ty, AnyTxType(126)); + assert!(inner.inner.fields.contains_key("input")); + assert!(inner.inner.fields.contains_key("mint")); + assert!(inner.inner.fields.contains_key("sourceHash")); + assert_eq!(inner.gas_limit(), 796584); + assert_eq!(inner.gas_price(), Some(1313)); + assert_eq!(inner.nonce(), 475232); + + let roundrip_tx: AnyRpcTransaction = + serde_json::from_str(&serde_json::to_string(&tx).unwrap()).unwrap(); + + assert_eq!(tx, roundrip_tx); + } +} diff --git a/crates/network/src/lib.rs b/crates/network/src/lib.rs index 0d6873971aa..9a5c09be81c 100644 --- a/crates/network/src/lib.rs +++ b/crates/network/src/lib.rs @@ -22,8 +22,8 @@ pub use ethereum::{Ethereum, EthereumWallet}; mod any; pub use any::{ - AnyHeader, AnyNetwork, AnyReceiptEnvelope, AnyRpcBlock, AnyRpcHeader, AnyTxEnvelope, AnyTxType, - AnyTypedTransaction, UnknownTxEnvelope, UnknownTypedTransaction, + AnyHeader, AnyNetwork, AnyReceiptEnvelope, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, + AnyTxEnvelope, AnyTxType, AnyTypedTransaction, UnknownTxEnvelope, UnknownTypedTransaction, }; pub use alloy_eips::eip2718; diff --git a/crates/serde/src/optional.rs b/crates/serde/src/optional.rs index 87362352022..d59292f8d75 100644 --- a/crates/serde/src/optional.rs +++ b/crates/serde/src/optional.rs @@ -11,3 +11,18 @@ where { Option::::deserialize(deserializer).map(Option::unwrap_or_default) } + +/// For use with serde's `deserialize_with` on a field that must be missing. +pub fn reject_if_some<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: Deserialize<'de>, + D: Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + + if value.is_some() { + return Err(serde::de::Error::custom("unexpected value")); + } + + Ok(value) +}