Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: capture unknown fields #1423

Merged
merged 6 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Unreleased

- capture unknown fields in `Block` and `Transaction` type via new `OtherFields` type [#1423](https://github.com/gakonst/ethers-rs/pull/1423)
- Methods like `set_to()` from `TypedTransaction` can be chained
- Use H64 for Block Nonce [#1396](https://github.com/gakonst/ethers-rs/pull/1396)
- Add `as_*_mut` methods on `TypedTransaction`
Expand Down
9 changes: 9 additions & 0 deletions ethers-core/src/types/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ pub struct Block<TX> {
#[cfg_attr(docsrs, doc(cfg(feature = "celo")))]
#[serde(rename = "epochSnarkData", default)]
pub epoch_snark_data: Option<EpochSnarkData>,

/// Captures unknown fields such as additional fields used by L2s
#[cfg(not(feature = "celo"))]
#[serde(flatten)]
pub other: crate::types::OtherFields,
}

/// Error returned by [`Block::time`].
Expand Down Expand Up @@ -203,6 +208,7 @@ impl Block<TxHash> {
mix_hash,
nonce,
base_fee_per_gas,
other,
..
} = self;
Block {
Expand All @@ -228,6 +234,7 @@ impl Block<TxHash> {
nonce,
base_fee_per_gas,
transactions,
other,
}
}

Expand Down Expand Up @@ -305,6 +312,7 @@ impl From<Block<Transaction>> for Block<TxHash> {
mix_hash,
nonce,
base_fee_per_gas,
other,
} = full;
Block {
hash,
Expand All @@ -329,6 +337,7 @@ impl From<Block<Transaction>> for Block<TxHash> {
nonce,
base_fee_per_gas,
transactions: transactions.iter().map(|tx| tx.hash).collect(),
other,
}
}

Expand Down
3 changes: 3 additions & 0 deletions ethers-core/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ pub use proof::*;

mod fee;
pub use fee::*;

mod other;
pub use other::OtherFields;
163 changes: 163 additions & 0 deletions ethers-core/src/types/other.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Support for capturing other fields
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Map;
use std::{collections::BTreeMap, ops::Deref};

/// 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<String, serde_json::Value>,
}

// === impl OtherFields ===

impl OtherFields {
/// Returns the deserialized value of the field, if it exists.
/// Deserializes the value with the given closure
///
/// ```
/// # use ethers_core::types::{OtherFields, U64};
/// fn d(other: OtherFields) {
/// let l1_block_number = other.get_with("l1BlockNumber", |value| serde_json::from_value::<U64>(value)).unwrap().unwrap();
/// # }
/// ```
pub fn get_with<F, V>(&self, key: impl AsRef<str>, with: F) -> Option<V>
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
///
/// ```
/// # use ethers_core::types::{OtherFields, U64};
/// fn d(other: OtherFields) {
/// let l1_block_number: U64 = other.get_deserialized("l1BlockNumber").unwrap().unwrap();
/// # }
/// ```
pub fn get_deserialized<V: DeserializeOwned>(
&self,
key: impl AsRef<str>,
) -> Option<serde_json::Result<V>> {
self.inner.get(key.as_ref()).cloned().map(serde_json::from_value)
}

/// Removes the deserialized value of the field, if it exists
///
/// ```
/// # use ethers_core::types::{OtherFields, U64};
/// fn d(mut other: OtherFields) {
/// let l1_block_number: U64 = other.remove_deserialized("l1BlockNumber").unwrap().unwrap();
/// assert!(!other.contains_key("l1BlockNumber"));
/// # }
/// ```
///
/// **Note:** this will also remove the value if deserializing it resulted in an error
pub fn remove_deserialized<V: DeserializeOwned>(
&mut self,
key: impl AsRef<str>,
) -> Option<serde_json::Result<V>> {
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
///
/// ```
/// # use ethers_core::types::{OtherFields, U64};
/// fn d(mut other: OtherFields) {
/// let l1_block_number: U64 = other.remove_with("l1BlockNumber", |value| serde_json::from_value(value)).unwrap().unwrap();
/// # }
/// ```
/// **Note:** this will also remove the value if deserializing it resulted in an error
pub fn remove_with<F, V>(&mut self, key: impl AsRef<str>, with: F) -> Option<V>
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
///
/// ```
/// # use ethers_core::types::{OtherFields, U64};
/// fn d(mut other: OtherFields) {
/// let (key, l1_block_number_result) : (_, serde_json::Result<U64>) = other.remove_entry_deserialized("l1BlockNumber").unwrap();
/// let l1_block_number = l1_block_number_result.unwrap();
/// assert!(!other.contains_key("l1BlockNumber"));
/// # }
/// ```
///
/// **Note:** this will also remove the value if deserializing it resulted in an error
pub fn remove_entry_deserialized<V: DeserializeOwned>(
&mut self,
key: impl AsRef<str>,
) -> Option<(String, serde_json::Result<V>)> {
self.inner
.remove_entry(key.as_ref())
.map(|(key, value)| (key, serde_json::from_value(value)))
}

/// Deserialized this type into another container type
///
/// ```
/// use ethers_core::types::{Address, OtherFields, U64};
/// use serde::Deserialize;
/// # fn d(mut other: OtherFields) {
///
/// /// Additional Optimism transaction fields
/// #[derive(Deserialize)]
/// #[serde(rename_all = "camelCase")]
/// struct OptimismExtraFields {
/// pub l1_tx_origin : Option<Address>,
/// pub l1_timestamp : U64,
/// pub l1_block_number : U64,
/// }
///
/// let optimism: OptimismExtraFields = other.deserialize_into().unwrap();
/// # }
/// ```
pub fn deserialize_into<T: DeserializeOwned>(self) -> serde_json::Result<T> {
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<String, serde_json::Value>;

#[inline]
fn deref(&self) -> &BTreeMap<String, serde_json::Value> {
self.as_ref()
}
}

impl AsRef<BTreeMap<String, serde_json::Value>> for OtherFields {
fn as_ref(&self) -> &BTreeMap<String, serde_json::Value> {
&self.inner
}
}

impl IntoIterator for OtherFields {
type Item = (String, serde_json::Value);
type IntoIter = std::collections::btree_map::IntoIter<String, serde_json::Value>;

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()
}
}
13 changes: 12 additions & 1 deletion ethers-core/src/types/transaction/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ pub struct Transaction {

#[serde(rename = "chainId", default, skip_serializing_if = "Option::is_none")]
pub chain_id: Option<U256>,

/// Captures unknown fields such as additional fields used by L2s
#[cfg(not(feature = "celo"))]
#[serde(flatten)]
pub other: crate::types::OtherFields,
}

impl Transaction {
Expand Down Expand Up @@ -550,6 +555,7 @@ mod tests {
16,
)
.unwrap(),
other: Default::default(),
};
println!("0x{}", hex::encode(&tx.rlp()));
assert_eq!(
Expand Down Expand Up @@ -593,6 +599,7 @@ mod tests {
16,
)
.unwrap(),
other: Default::default(),
};
println!("0x{}", hex::encode(&tx.rlp()));
assert_eq!(
Expand Down Expand Up @@ -626,7 +633,8 @@ mod tests {
chain_id: Some(U256::from(1)),
access_list: None,
max_fee_per_gas: None,
max_priority_fee_per_gas: None
max_priority_fee_per_gas: None,
other: Default::default()
};
assert_eq!(
tx.rlp(),
Expand Down Expand Up @@ -672,6 +680,7 @@ mod tests {
max_priority_fee_per_gas: Some(1500000000.into()),
max_fee_per_gas: Some(1500000009.into()),
chain_id: Some(5.into()),
other: Default::default(),
};
assert_eq!(
tx.rlp(),
Expand Down Expand Up @@ -717,6 +726,7 @@ mod tests {
max_priority_fee_per_gas: Some(1500000000.into()),
max_fee_per_gas: Some(1500000009.into()),
chain_id: Some(5.into()),
other: Default::default(),
};

let rlp_bytes = hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap();
Expand Down Expand Up @@ -762,6 +772,7 @@ mod tests {
max_priority_fee_per_gas: Some(1500000000.into()),
max_fee_per_gas: Some(1500000009.into()),
chain_id: Some(5.into()),
other: Default::default(),
};

assert_eq!(tx.hash, tx.hash());
Expand Down