Skip to content

Commit

Permalink
fix: serde for AnyTxType
Browse files Browse the repository at this point in the history
  • Loading branch information
klkvr committed Nov 4, 2024
1 parent d99f09c commit ede6173
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 17 deletions.
49 changes: 46 additions & 3 deletions crates/consensus/src/transaction/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,8 +554,12 @@ mod serde_from {
#[serde(untagged)]
pub(crate) enum MaybeTaggedTxEnvelope {
Tagged(TaggedTxEnvelope),
#[serde(with = "crate::transaction::signed_legacy_serde")]
Untagged(Signed<TxLegacy>),
Untagged {
#[serde(default, rename = "type", deserialize_with = "alloy_serde::reject_if_some")]
_ty: Option<()>,
#[serde(flatten, with = "crate::transaction::signed_legacy_serde")]
tx: Signed<TxLegacy>,
},
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
Expand All @@ -577,7 +581,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),
}
}
}
Expand Down Expand Up @@ -1159,4 +1163,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::<TxEnvelope>(data_with_wrong_type).is_err());
}
}
9 changes: 7 additions & 2 deletions crates/consensus/src/transaction/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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),
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/network/src/any/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub type AnyRpcHeader = alloy_rpc_types_eth::Header<alloy_consensus::AnyHeader>;
pub type AnyRpcBlock =
WithOtherFields<Block<WithOtherFields<Transaction<AnyTxEnvelope>>, AnyRpcHeader>>;

/// A catch-all transaction type for handling transactions on multiple networks.
pub type AnyRpcTransaction = WithOtherFields<Transaction<AnyTxEnvelope>>;

/// Types for a catch-all network.
///
/// `AnyNetwork`'s associated types allow for many different types of
Expand Down Expand Up @@ -65,7 +68,7 @@ impl Network for AnyNetwork {

type TransactionRequest = WithOtherFields<TransactionRequest>;

type TransactionResponse = WithOtherFields<Transaction<Self::TxEnvelope>>;
type TransactionResponse = AnyRpcTransaction;

type ReceiptResponse = AnyTransactionReceipt;

Expand Down
104 changes: 95 additions & 9 deletions crates/network/src/any/unknowns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -38,6 +38,24 @@ impl From<AnyTxType> for u8 {
}
}

impl serde::Serialize for AnyTxType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
U8::from(self.0).serialize(serializer)
}
}

impl<'de> serde::Deserialize<'de> for AnyTxType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
U8::deserialize(deserializer).map(|t| Self(t.to::<u8>()))
}
}

impl TryFrom<AnyTxType> for TxType {
type Error = Eip2718Error;

Expand Down Expand Up @@ -87,31 +105,41 @@ pub struct UnknownTypedTransaction {

impl alloy_consensus::Transaction for UnknownTypedTransaction {
fn chain_id(&self) -> Option<alloy_primitives::ChainId> {
self.fields.get_deserialized("chainId").and_then(Result::ok)
self.fields.get_deserialized::<U64>("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::<U64>("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::<U64>("gas").and_then(Result::ok).unwrap_or_default().to()
}

fn gas_price(&self) -> Option<u128> {
self.fields.get_deserialized("gasPrice").and_then(Result::ok)
self.fields.get_deserialized::<U128>("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::<U128>("maxFeePerGas")
.and_then(Result::ok)
.unwrap_or_default()
.to()
}

fn max_priority_fee_per_gas(&self) -> Option<u128> {
self.fields.get_deserialized("maxPriorityFeePerGas").and_then(Result::ok)
self.fields
.get_deserialized::<U128>("maxPriorityFeePerGas")
.and_then(Result::ok)
.map(|v| v.to())
}

fn max_fee_per_blob_gas(&self) -> Option<u128> {
self.fields.get_deserialized("maxFeePerBlobGas").and_then(Result::ok)
self.fields
.get_deserialized::<U128>("maxFeePerBlobGas")
.and_then(Result::ok)
.map(|v| v.to())
}

fn priority_fee_or_price(&self) -> u128 {
Expand Down Expand Up @@ -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);
}
}
4 changes: 2 additions & 2 deletions crates/network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions crates/serde/src/optional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,18 @@ where
{
Option::<T>::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<Option<T>, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>,
{
let value = Option::<T>::deserialize(deserializer)?;

if value.is_some() {
return Err(serde::de::Error::custom("unexpected value"));
}

Ok(value)
}

0 comments on commit ede6173

Please sign in to comment.