From 7e39c85f9f51e6449a8b661f54df0ac213f18639 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Wed, 27 Mar 2024 00:57:16 +0400 Subject: [PATCH] feat: serde for consensus tx types (#361) --- Cargo.toml | 14 +-- crates/consensus/Cargo.toml | 6 + crates/consensus/src/signed.rs | 3 + crates/consensus/src/transaction/eip1559.rs | 10 +- crates/consensus/src/transaction/eip2930.rs | 7 ++ crates/consensus/src/transaction/eip4844.rs | 41 +++++++ crates/consensus/src/transaction/envelope.rs | 116 +++++++++++++++++++ crates/consensus/src/transaction/legacy.rs | 14 +++ crates/consensus/src/transaction/typed.rs | 6 + crates/eips/src/eip2930.rs | 1 - crates/serde/src/num.rs | 41 +++++++ 11 files changed, 250 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3c64d4e17ec..7302e6e360c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,10 +113,10 @@ tempfile = "3.10" # TODO: Remove eventually. [patch.crates-io] -alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" } -alloy-primitives = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" } -alloy-sol-types = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" } -alloy-json-abi = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" } -alloy-dyn-abi = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" } -syn-solidity = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" } -alloy-core = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" } +alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "ff33969" } +alloy-primitives = { git = "https://github.com/alloy-rs/core", rev = "ff33969" } +alloy-sol-types = { git = "https://github.com/alloy-rs/core", rev = "ff33969" } +alloy-json-abi = { git = "https://github.com/alloy-rs/core", rev = "ff33969" } +alloy-dyn-abi = { git = "https://github.com/alloy-rs/core", rev = "ff33969" } +syn-solidity = { git = "https://github.com/alloy-rs/core", rev = "ff33969" } +alloy-core = { git = "https://github.com/alloy-rs/core", rev = "ff33969" } diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index d44f4a605d5..09740eb5dc4 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -15,6 +15,7 @@ exclude.workspace = true alloy-primitives = { workspace = true, features = ["rlp"] } alloy-rlp.workspace = true alloy-eips.workspace = true +alloy-serde = { workspace = true, optional = true } sha2 = "0.10" @@ -25,13 +26,18 @@ c-kzg = { workspace = true, features = ["std", "serde"], optional = true } # arbitrary arbitrary = { workspace = true, features = ["derive"], optional = true } +# serde +serde = { workspace = true, features = ["derive"], optional = true } + [dev-dependencies] alloy-signer.workspace = true arbitrary = { workspace = true, features = ["derive"] } k256.workspace = true tokio = { workspace = true, features = ["macros"] } +serde_json.workspace = true [features] k256 = ["alloy-primitives/k256"] kzg = ["dep:c-kzg", "dep:thiserror", "alloy-eips/kzg"] arbitrary = ["dep:arbitrary", "alloy-eips/arbitrary"] +serde = ["dep:serde", "alloy-primitives/serde", "dep:alloy-serde", "alloy-eips/serde"] diff --git a/crates/consensus/src/signed.rs b/crates/consensus/src/signed.rs index 5fd4b3bb588..ce1f580c61b 100644 --- a/crates/consensus/src/signed.rs +++ b/crates/consensus/src/signed.rs @@ -3,8 +3,11 @@ use alloy_primitives::{Signature, B256}; /// A transaction with a signature and hash seal. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Signed { + #[cfg_attr(feature = "serde", serde(flatten))] tx: T, + #[cfg_attr(feature = "serde", serde(flatten))] signature: Sig, hash: B256, } diff --git a/crates/consensus/src/transaction/eip1559.rs b/crates/consensus/src/transaction/eip1559.rs index 1ab2f190e42..f3010c03ade 100644 --- a/crates/consensus/src/transaction/eip1559.rs +++ b/crates/consensus/src/transaction/eip1559.rs @@ -6,16 +6,21 @@ use std::mem; /// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)). #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct TxEip1559 { /// EIP-155: Simple replay attack protection - pub chain_id: u64, + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] + pub chain_id: ChainId, /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub nonce: u64, /// A scalar value equal to the maximum /// amount of gas that should be used in executing /// this transaction. This is paid up-front, before any /// computation is done and may not be increased /// later; formally Tg. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub gas_limit: u64, /// A scalar value equal to the maximum /// amount of gas that should be used in executing @@ -28,6 +33,7 @@ pub struct TxEip1559 { /// 340282366920938463463374607431768211455 /// /// This is also known as `GasFeeCap` + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))] pub max_fee_per_gas: u128, /// Max Priority fee that transaction is paying /// @@ -36,9 +42,11 @@ pub struct TxEip1559 { /// 340282366920938463463374607431768211455 /// /// This is also known as `GasTipCap` + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))] pub max_priority_fee_per_gas: u128, /// The 160-bit address of the message call’s recipient or, for a contract creation /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))] pub to: TxKind, /// A scalar value equal to the number of Wei to /// be transferred to the message call’s recipient or, diff --git a/crates/consensus/src/transaction/eip2930.rs b/crates/consensus/src/transaction/eip2930.rs index cf5701387bb..e0ef9b252ac 100644 --- a/crates/consensus/src/transaction/eip2930.rs +++ b/crates/consensus/src/transaction/eip2930.rs @@ -6,10 +6,14 @@ use std::mem; /// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)). #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct TxEip2930 { /// Added as EIP-pub 155: Simple replay attack protection + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub chain_id: ChainId, /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub nonce: u64, /// A scalar value equal to the number of /// Wei to be paid per unit of gas for all computation @@ -18,15 +22,18 @@ pub struct TxEip2930 { /// As ethereum circulation is around 120mil eth as of 2022 that is around /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: /// 340282366920938463463374607431768211455 + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))] pub gas_price: u128, /// A scalar value equal to the maximum /// amount of gas that should be used in executing /// this transaction. This is paid up-front, before any /// computation is done and may not be increased /// later; formally Tg. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub gas_limit: u64, /// The 160-bit address of the message call’s recipient or, for a contract creation /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))] pub to: TxKind, /// A scalar value equal to the number of Wei to /// be transferred to the message call’s recipient or, diff --git a/crates/consensus/src/transaction/eip4844.rs b/crates/consensus/src/transaction/eip4844.rs index 7582f515341..4019d1a27c7 100644 --- a/crates/consensus/src/transaction/eip4844.rs +++ b/crates/consensus/src/transaction/eip4844.rs @@ -56,6 +56,8 @@ pub enum BlobTransactionValidationError { /// or a transaction with a sidecar, which is used when submitting a transaction to the network and /// when receiving and sending transactions during the gossip stage. #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum TxEip4844Variant { /// A standalone transaction with blob hashes and max blob fee. TxEip4844(TxEip4844), @@ -63,6 +65,32 @@ pub enum TxEip4844Variant { TxEip4844WithSidecar(TxEip4844WithSidecar), } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for TxEip4844Variant { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct TxEip4844SerdeHelper { + #[serde(flatten)] + tx: TxEip4844, + #[serde(flatten)] + sidecar: Option, + } + + let tx = TxEip4844SerdeHelper::deserialize(deserializer)?; + + if let Some(sidecar) = tx.sidecar { + Ok(TxEip4844Variant::TxEip4844WithSidecar(TxEip4844WithSidecar::from_tx_and_sidecar( + tx.tx, sidecar, + ))) + } else { + Ok(TxEip4844Variant::TxEip4844(tx.tx)) + } + } +} + impl From for TxEip4844Variant { fn from(tx: TxEip4844WithSidecar) -> Self { TxEip4844Variant::TxEip4844WithSidecar(tx) @@ -286,16 +314,21 @@ impl SignableTransaction for TxEip4844Variant { /// /// A transaction with blob hashes and max blob fee. It does not have the Blob sidecar. #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct TxEip4844 { /// Added as EIP-pub 155: Simple replay attack protection + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub chain_id: ChainId, /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub nonce: u64, /// A scalar value equal to the maximum /// amount of gas that should be used in executing /// this transaction. This is paid up-front, before any /// computation is done and may not be increased /// later; formally Tg. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub gas_limit: u64, /// A scalar value equal to the maximum /// amount of gas that should be used in executing @@ -308,6 +341,7 @@ pub struct TxEip4844 { /// 340282366920938463463374607431768211455 /// /// This is also known as `GasFeeCap` + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))] pub max_fee_per_gas: u128, /// Max Priority fee that transaction is paying /// @@ -316,6 +350,7 @@ pub struct TxEip4844 { /// 340282366920938463463374607431768211455 /// /// This is also known as `GasTipCap` + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))] pub max_priority_fee_per_gas: u128, /// The 160-bit address of the message call’s recipient. pub to: Address, @@ -337,6 +372,7 @@ pub struct TxEip4844 { /// Max fee per data gas /// /// aka BlobFeeCap or blobGasFeeCap + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))] pub max_fee_per_blob_gas: u128, /// Input has two uses depending if transaction is Create or Call (if `to` field is None or @@ -719,10 +755,14 @@ impl Decodable for TxEip4844 { /// of a `PooledTransactions` response, and is also used as the format for sending raw transactions /// through the network (eth_sendRawTransaction/eth_sendTransaction). #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct TxEip4844WithSidecar { /// The actual transaction. + #[cfg_attr(feature = "serde", serde(flatten))] pub tx: TxEip4844, /// The sidecar. + #[cfg_attr(feature = "serde", serde(flatten))] pub sidecar: BlobTransactionSidecar, } @@ -914,6 +954,7 @@ impl Transaction for TxEip4844WithSidecar { /// This represents a set of blobs, and its corresponding commitments and proofs. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] #[repr(C)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BlobTransactionSidecar { /// The blob data. pub blobs: Vec, diff --git a/crates/consensus/src/transaction/envelope.rs b/crates/consensus/src/transaction/envelope.rs index ff61b209e1d..a7e6e743390 100644 --- a/crates/consensus/src/transaction/envelope.rs +++ b/crates/consensus/src/transaction/envelope.rs @@ -61,12 +61,17 @@ impl TryFrom for TxType { /// /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] pub enum TxEnvelope { /// An untagged [`TxLegacy`]. + #[cfg_attr(feature = "serde", serde(rename = "0x00", alias = "0x0"))] Legacy(Signed), /// A [`TxEip2930`] tagged with type 1. + #[cfg_attr(feature = "serde", serde(rename = "0x01", alias = "0x1"))] Eip2930(Signed), /// A [`TxEip1559`] tagged with type 2. + #[cfg_attr(feature = "serde", serde(rename = "0x02", alias = "0x2"))] Eip1559(Signed), /// A TxEip4844 tagged with type 3. /// An EIP-4844 transaction has two network representations: @@ -75,6 +80,7 @@ pub enum TxEnvelope { /// /// 2 - The transaction with a sidecar, which is the form used to /// send transactions to the network. + #[cfg_attr(feature = "serde", serde(rename = "0x03", alias = "0x3"))] Eip4844(Signed), } @@ -398,4 +404,114 @@ mod tests { assert_eq!(encoded, hex_data); assert_eq!(tx.encode_2718_len(), hex_data.len()); } + + #[cfg(feature = "serde")] + fn test_serde_roundtrip>(tx: T) + where + Signed: Into, + { + let signature = Signature::test_signature(); + let tx_envelope: TxEnvelope = tx.into_signed(signature).into(); + + let serialized = serde_json::to_string(&tx_envelope).unwrap(); + let deserialized: TxEnvelope = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(tx_envelope, deserialized); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_legacy() { + let tx = TxLegacy { + chain_id: Some(1), + nonce: 100, + gas_price: 3_000_000_000, + gas_limit: 50_000, + to: TxKind::Call(Address::default()), + value: U256::from(10e18), + input: Bytes::new(), + }; + test_serde_roundtrip(tx); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_eip1559() { + let tx = TxEip1559 { + chain_id: 1, + nonce: 100, + max_fee_per_gas: 50_000_000_000, + max_priority_fee_per_gas: 1_000_000_000_000, + gas_limit: 1_000_000, + to: TxKind::Create, + value: U256::from(10e18), + input: Bytes::new(), + access_list: AccessList(vec![AccessListItem { + address: Address::random(), + storage_keys: vec![B256::random()], + }]), + }; + test_serde_roundtrip(tx); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_eip2930() { + let tx = TxEip2930 { + chain_id: u64::MAX, + nonce: u64::MAX, + gas_price: u128::MAX, + gas_limit: u64::MAX, + to: TxKind::Call(Address::random()), + value: U256::MAX, + input: Bytes::new(), + access_list: Default::default(), + }; + test_serde_roundtrip(tx); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_eip4844() { + use crate::BlobTransactionSidecar; + + let tx = TxEip4844Variant::TxEip4844(TxEip4844 { + chain_id: 1, + nonce: 100, + max_fee_per_gas: 50_000_000_000, + max_priority_fee_per_gas: 1_000_000_000_000, + gas_limit: 1_000_000, + to: Address::random(), + value: U256::from(10e18), + input: Bytes::new(), + access_list: AccessList(vec![AccessListItem { + address: Address::random(), + storage_keys: vec![B256::random()], + }]), + blob_versioned_hashes: vec![B256::random()], + max_fee_per_blob_gas: 0, + }); + test_serde_roundtrip(tx); + + let tx = TxEip4844Variant::TxEip4844WithSidecar(TxEip4844WithSidecar { + tx: TxEip4844 { + chain_id: 1, + nonce: 100, + max_fee_per_gas: 50_000_000_000, + max_priority_fee_per_gas: 1_000_000_000_000, + gas_limit: 1_000_000, + to: Address::random(), + value: U256::from(10e18), + input: Bytes::new(), + access_list: AccessList(vec![AccessListItem { + address: Address::random(), + storage_keys: vec![B256::random()], + }]), + blob_versioned_hashes: vec![B256::random()], + max_fee_per_blob_gas: 0, + }, + sidecar: BlobTransactionSidecar { ..Default::default() }, + }); + test_serde_roundtrip(tx); + } } diff --git a/crates/consensus/src/transaction/legacy.rs b/crates/consensus/src/transaction/legacy.rs index 8a5025a0750..02f80bf97af 100644 --- a/crates/consensus/src/transaction/legacy.rs +++ b/crates/consensus/src/transaction/legacy.rs @@ -5,10 +5,21 @@ use std::mem; /// Legacy transaction. #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct TxLegacy { /// Added as EIP-155: Simple replay attack protection + #[cfg_attr( + feature = "serde", + serde( + default, + with = "alloy_serde::u64_hex_or_decimal_opt", + skip_serializing_if = "Option::is_none", + ) + )] pub chain_id: Option, /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub nonce: u64, /// A scalar value equal to the number of /// Wei to be paid per unit of gas for all computation @@ -17,15 +28,18 @@ pub struct TxLegacy { /// As ethereum circulation is around 120mil eth as of 2022 that is around /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: /// 340282366920938463463374607431768211455 + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))] pub gas_price: u128, /// A scalar value equal to the maximum /// amount of gas that should be used in executing /// this transaction. This is paid up-front, before any /// computation is done and may not be increased /// later; formally Tg. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))] pub gas_limit: u64, /// The 160-bit address of the message call’s recipient or, for a contract creation /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))] pub to: TxKind, /// A scalar value equal to the number of Wei to /// be transferred to the message call’s recipient or, diff --git a/crates/consensus/src/transaction/typed.rs b/crates/consensus/src/transaction/typed.rs index d428f7d643b..401362b247e 100644 --- a/crates/consensus/src/transaction/typed.rs +++ b/crates/consensus/src/transaction/typed.rs @@ -9,14 +9,20 @@ use alloy_primitives::TxKind; /// 3. EIP1559 [`TxEip1559`] /// 4. EIP4844 [`TxEip4844Variant`] #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] pub enum TypedTransaction { /// Legacy transaction + #[cfg_attr(feature = "serde", serde(rename = "0x00", alias = "0x0"))] Legacy(TxLegacy), /// EIP-2930 transaction + #[cfg_attr(feature = "serde", serde(rename = "0x01", alias = "0x1"))] Eip2930(TxEip2930), /// EIP-1559 transaction + #[cfg_attr(feature = "serde", serde(rename = "0x02", alias = "0x2"))] Eip1559(TxEip1559), /// EIP-4844 transaction + #[cfg_attr(feature = "serde", serde(rename = "0x03", alias = "0x3"))] Eip4844(TxEip4844Variant), } diff --git a/crates/eips/src/eip2930.rs b/crates/eips/src/eip2930.rs index 9366b758eee..d14e50d4015 100644 --- a/crates/eips/src/eip2930.rs +++ b/crates/eips/src/eip2930.rs @@ -48,7 +48,6 @@ impl AccessListItem { derive(proptest_derive::Arbitrary, arbitrary::Arbitrary) )] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct AccessList( #[cfg_attr( all(any(test, feature = "arbitrary"), feature = "std"), diff --git a/crates/serde/src/num.rs b/crates/serde/src/num.rs index 3740778965f..f08cf12033b 100644 --- a/crates/serde/src/num.rs +++ b/crates/serde/src/num.rs @@ -203,6 +203,26 @@ where NumberOrHexU256::deserialize(deserializer)?.try_into_u256() } +/// serde functions for handling primitive `u64` as [U64] +pub mod u128_hex_or_decimal { + use alloy_primitives::U128; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + /// Deserializes an `u64` accepting a hex quantity string with optional 0x prefix or + /// a number + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + U128::deserialize(deserializer).map(|val| val.to()) + } + + /// Serializes u64 as hex string + pub fn serialize(value: &u128, s: S) -> Result { + U128::from(*value).serialize(s) + } +} + #[cfg(test)] mod tests { use super::*; @@ -223,4 +243,25 @@ mod tests { let deserialized: Value = serde_json::from_str(&s).unwrap(); assert_eq!(val, deserialized); } + + #[test] + fn test_u128_hex_or_decimal() { + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + struct Value { + #[serde(with = "u128_hex_or_decimal")] + inner: u128, + } + + let val = Value { inner: 1000 }; + let s = serde_json::to_string(&val).unwrap(); + assert_eq!(s, "{\"inner\":\"0x3e8\"}"); + + let deserialized: Value = serde_json::from_str(&s).unwrap(); + assert_eq!(val, deserialized); + + let s = "{\"inner\":\"1000\"}".to_string(); + let deserialized: Value = serde_json::from_str(&s).unwrap(); + + assert_eq!(val, deserialized); + } }