From 46bf17a5bd7403ccb4623dc2a18a1ac887bfbf9f Mon Sep 17 00:00:00 2001 From: evalir Date: Mon, 11 Dec 2023 15:18:47 -0400 Subject: [PATCH] feat: support for additional fields on TransactionReceipt and Block (#66) --- crates/rpc-types/src/eth/block.rs | 15 ++- crates/rpc-types/src/eth/mod.rs | 1 + crates/rpc-types/src/eth/other.rs | 117 ++++++++++++++++++ .../rpc-types/src/eth/transaction/receipt.rs | 5 +- 4 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 crates/rpc-types/src/eth/other.rs diff --git a/crates/rpc-types/src/eth/block.rs b/crates/rpc-types/src/eth/block.rs index 9808fd953f2..ce3c115ee43 100644 --- a/crates/rpc-types/src/eth/block.rs +++ b/crates/rpc-types/src/eth/block.rs @@ -1,5 +1,5 @@ //! Contains types that represent ethereum types when used in RPC -use crate::{Transaction, Withdrawal}; +use crate::{other::OtherFields, Transaction, Withdrawal}; use alloy_primitives::{ ruint::ParseError, Address, BlockHash, BlockNumber, Bloom, Bytes, B256, B64, U256, U64, }; @@ -117,24 +117,27 @@ pub enum BlockError { #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Block { - /// Header of the block + /// Header of the block. #[serde(flatten)] pub header: Header, /// Total difficulty, this field is None only if representing /// an Uncle block. #[serde(skip_serializing_if = "Option::is_none")] pub total_difficulty: Option, - /// Uncles' hashes + /// Uncles' hashes. pub uncles: Vec, - /// Transactions + /// Transactions. #[serde(skip_serializing_if = "BlockTransactions::is_uncle")] #[serde(default = "BlockTransactions::uncle")] pub transactions: BlockTransactions, /// Integer the size of this block in bytes. pub size: Option, - /// Withdrawals in the block + /// Withdrawals in the block. #[serde(default, skip_serializing_if = "Option::is_none")] pub withdrawals: Option>, + /// Support for arbitrary additional fields. + #[serde(flatten)] + pub other: OtherFields, } impl Block { @@ -861,6 +864,7 @@ mod tests { transactions: BlockTransactions::Hashes(vec![B256::with_last_byte(18)]), size: Some(U256::from(19)), withdrawals: Some(vec![]), + other: Default::default(), }; let serialized = serde_json::to_string(&block).unwrap(); assert_eq!( @@ -902,6 +906,7 @@ mod tests { transactions: BlockTransactions::Hashes(vec![B256::with_last_byte(18)]), size: Some(U256::from(19)), withdrawals: None, + other: Default::default(), }; let serialized = serde_json::to_string(&block).unwrap(); assert_eq!( diff --git a/crates/rpc-types/src/eth/mod.rs b/crates/rpc-types/src/eth/mod.rs index 569dde4ca41..9da97f85a79 100644 --- a/crates/rpc-types/src/eth/mod.rs +++ b/crates/rpc-types/src/eth/mod.rs @@ -6,6 +6,7 @@ mod call; mod fee; mod filter; mod log; +pub mod other; pub mod pubsub; pub mod raw_log; pub mod state; diff --git a/crates/rpc-types/src/eth/other.rs b/crates/rpc-types/src/eth/other.rs new file mode 100644 index 00000000000..e6efe12dc53 --- /dev/null +++ b/crates/rpc-types/src/eth/other.rs @@ -0,0 +1,117 @@ +//! Support for capturing other fields +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Map; +use std::{ + collections::BTreeMap, + ops::{Deref, DerefMut}, +}; + +/// A type that is supposed to capture additional fields that are not native to ethereum but included in ethereum adjacent networks, for example fields the [optimism `eth_getTransactionByHash` request](https://docs.alchemy.com/alchemy/apis/optimism/eth-gettransactionbyhash) returns additional fields that this type will capture +/// +/// This type is supposed to be used with [`#[serde(flatten)`](https://serde.rs/field-attrs.html#flatten) +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)] +#[serde(transparent)] +pub struct OtherFields { + /// Contains all unknown fields + inner: BTreeMap, +} + +// === impl OtherFields === + +impl OtherFields { + /// Returns the deserialized value of the field, if it exists. + /// Deserializes the value with the given closure + pub fn get_with(&self, key: impl AsRef, with: F) -> Option + where + F: FnOnce(serde_json::Value) -> V, + { + self.inner.get(key.as_ref()).cloned().map(with) + } + + /// Returns the deserialized value of the field, if it exists + pub fn get_deserialized( + &self, + key: impl AsRef, + ) -> Option> { + self.inner.get(key.as_ref()).cloned().map(serde_json::from_value) + } + + /// Removes the deserialized value of the field, if it exists + /// + /// **Note:** this will also remove the value if deserializing it resulted in an error + pub fn remove_deserialized( + &mut self, + key: impl AsRef, + ) -> Option> { + self.inner.remove(key.as_ref()).map(serde_json::from_value) + } + + /// Removes the deserialized value of the field, if it exists. + /// Deserializes the value with the given closure + /// + /// **Note:** this will also remove the value if deserializing it resulted in an error + pub fn remove_with(&mut self, key: impl AsRef, with: F) -> Option + where + F: FnOnce(serde_json::Value) -> V, + { + self.inner.remove(key.as_ref()).map(with) + } + + /// Removes the deserialized value of the field, if it exists and also returns the key + /// + /// **Note:** this will also remove the value if deserializing it resulted in an error + pub fn remove_entry_deserialized( + &mut self, + key: impl AsRef, + ) -> Option<(String, serde_json::Result)> { + self.inner + .remove_entry(key.as_ref()) + .map(|(key, value)| (key, serde_json::from_value(value))) + } + + /// Deserialized this type into another container type + pub fn deserialize_into(self) -> serde_json::Result { + let mut map = Map::with_capacity(self.inner.len()); + map.extend(self); + serde_json::from_value(serde_json::Value::Object(map)) + } +} + +impl Deref for OtherFields { + type Target = BTreeMap; + + #[inline] + fn deref(&self) -> &BTreeMap { + self.as_ref() + } +} + +impl DerefMut for OtherFields { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl AsRef> for OtherFields { + fn as_ref(&self) -> &BTreeMap { + &self.inner + } +} + +impl IntoIterator for OtherFields { + type Item = (String, serde_json::Value); + type IntoIter = std::collections::btree_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + +impl<'a> IntoIterator for &'a OtherFields { + type Item = (&'a String, &'a serde_json::Value); + type IntoIter = std::collections::btree_map::Iter<'a, String, serde_json::Value>; + + fn into_iter(self) -> Self::IntoIter { + self.as_ref().iter() + } +} diff --git a/crates/rpc-types/src/eth/transaction/receipt.rs b/crates/rpc-types/src/eth/transaction/receipt.rs index 7fae30855eb..557e19316df 100644 --- a/crates/rpc-types/src/eth/transaction/receipt.rs +++ b/crates/rpc-types/src/eth/transaction/receipt.rs @@ -1,4 +1,4 @@ -use crate::Log; +use crate::{other::OtherFields, Log}; use alloy_primitives::{Address, Bloom, B256, U128, U256, U64, U8}; use serde::{Deserialize, Serialize}; @@ -51,4 +51,7 @@ pub struct TransactionReceipt { /// EIP-2718 Transaction type, Some(1) for AccessList transaction, None for Legacy #[serde(rename = "type")] pub transaction_type: U8, + /// Support for arbitrary additional fields. + #[serde(flatten)] + pub other: OtherFields, }