feat: capture unknown fields (#1423)
* feat(core): add OtherFields type * feat: capture unknown fields * chore: update CHANGELOG * test: update tests * docs: fix doc tests * fix: make docs compile on stable
This commit is contained in:
parent
f6eaa7e551
commit
f5793525f2
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
### Unreleased
|
### 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
|
- Methods like `set_to()` from `TypedTransaction` can be chained
|
||||||
- Use H64 for Block Nonce [#1396](https://github.com/gakonst/ethers-rs/pull/1396)
|
- Use H64 for Block Nonce [#1396](https://github.com/gakonst/ethers-rs/pull/1396)
|
||||||
- Add `as_*_mut` methods on `TypedTransaction`
|
- Add `as_*_mut` methods on `TypedTransaction`
|
||||||
|
|
|
@ -96,6 +96,11 @@ pub struct Block<TX> {
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "celo")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "celo")))]
|
||||||
#[serde(rename = "epochSnarkData", default)]
|
#[serde(rename = "epochSnarkData", default)]
|
||||||
pub epoch_snark_data: Option<EpochSnarkData>,
|
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`].
|
/// Error returned by [`Block::time`].
|
||||||
|
@ -203,6 +208,7 @@ impl Block<TxHash> {
|
||||||
mix_hash,
|
mix_hash,
|
||||||
nonce,
|
nonce,
|
||||||
base_fee_per_gas,
|
base_fee_per_gas,
|
||||||
|
other,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
Block {
|
Block {
|
||||||
|
@ -228,6 +234,7 @@ impl Block<TxHash> {
|
||||||
nonce,
|
nonce,
|
||||||
base_fee_per_gas,
|
base_fee_per_gas,
|
||||||
transactions,
|
transactions,
|
||||||
|
other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,6 +312,7 @@ impl From<Block<Transaction>> for Block<TxHash> {
|
||||||
mix_hash,
|
mix_hash,
|
||||||
nonce,
|
nonce,
|
||||||
base_fee_per_gas,
|
base_fee_per_gas,
|
||||||
|
other,
|
||||||
} = full;
|
} = full;
|
||||||
Block {
|
Block {
|
||||||
hash,
|
hash,
|
||||||
|
@ -329,6 +337,7 @@ impl From<Block<Transaction>> for Block<TxHash> {
|
||||||
nonce,
|
nonce,
|
||||||
base_fee_per_gas,
|
base_fee_per_gas,
|
||||||
transactions: transactions.iter().map(|tx| tx.hash).collect(),
|
transactions: transactions.iter().map(|tx| tx.hash).collect(),
|
||||||
|
other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,3 +63,6 @@ pub use proof::*;
|
||||||
|
|
||||||
mod fee;
|
mod fee;
|
||||||
pub use fee::*;
|
pub use fee::*;
|
||||||
|
|
||||||
|
mod other;
|
||||||
|
pub use other::OtherFields;
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -113,6 +113,11 @@ pub struct Transaction {
|
||||||
|
|
||||||
#[serde(rename = "chainId", default, skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "chainId", default, skip_serializing_if = "Option::is_none")]
|
||||||
pub chain_id: Option<U256>,
|
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 {
|
impl Transaction {
|
||||||
|
@ -550,6 +555,7 @@ mod tests {
|
||||||
16,
|
16,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
other: Default::default(),
|
||||||
};
|
};
|
||||||
println!("0x{}", hex::encode(&tx.rlp()));
|
println!("0x{}", hex::encode(&tx.rlp()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -593,6 +599,7 @@ mod tests {
|
||||||
16,
|
16,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
other: Default::default(),
|
||||||
};
|
};
|
||||||
println!("0x{}", hex::encode(&tx.rlp()));
|
println!("0x{}", hex::encode(&tx.rlp()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -626,7 +633,8 @@ mod tests {
|
||||||
chain_id: Some(U256::from(1)),
|
chain_id: Some(U256::from(1)),
|
||||||
access_list: None,
|
access_list: None,
|
||||||
max_fee_per_gas: None,
|
max_fee_per_gas: None,
|
||||||
max_priority_fee_per_gas: None
|
max_priority_fee_per_gas: None,
|
||||||
|
other: Default::default()
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tx.rlp(),
|
tx.rlp(),
|
||||||
|
@ -672,6 +680,7 @@ mod tests {
|
||||||
max_priority_fee_per_gas: Some(1500000000.into()),
|
max_priority_fee_per_gas: Some(1500000000.into()),
|
||||||
max_fee_per_gas: Some(1500000009.into()),
|
max_fee_per_gas: Some(1500000009.into()),
|
||||||
chain_id: Some(5.into()),
|
chain_id: Some(5.into()),
|
||||||
|
other: Default::default(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tx.rlp(),
|
tx.rlp(),
|
||||||
|
@ -717,6 +726,7 @@ mod tests {
|
||||||
max_priority_fee_per_gas: Some(1500000000.into()),
|
max_priority_fee_per_gas: Some(1500000000.into()),
|
||||||
max_fee_per_gas: Some(1500000009.into()),
|
max_fee_per_gas: Some(1500000009.into()),
|
||||||
chain_id: Some(5.into()),
|
chain_id: Some(5.into()),
|
||||||
|
other: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let rlp_bytes = hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap();
|
let rlp_bytes = hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap();
|
||||||
|
@ -762,6 +772,7 @@ mod tests {
|
||||||
max_priority_fee_per_gas: Some(1500000000.into()),
|
max_priority_fee_per_gas: Some(1500000000.into()),
|
||||||
max_fee_per_gas: Some(1500000009.into()),
|
max_fee_per_gas: Some(1500000009.into()),
|
||||||
chain_id: Some(5.into()),
|
chain_id: Some(5.into()),
|
||||||
|
other: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(tx.hash, tx.hash());
|
assert_eq!(tx.hash, tx.hash());
|
||||||
|
|
Loading…
Reference in New Issue