Skip to main content

foundry_block_explorers/
serde_helpers.rs

1use crate::block_number::BlockNumber;
2use alloy_primitives::{U64, U256};
3use serde::{Deserialize, Deserializer};
4use std::str::FromStr;
5
6/// Helper type to parse numeric strings, `u64` and `U256`
7#[derive(Deserialize, Debug, Clone)]
8#[serde(untagged)]
9pub enum StringifiedNumeric {
10    String(String),
11    U256(U256),
12    Num(serde_json::Number),
13}
14
15impl TryFrom<StringifiedNumeric> for U256 {
16    type Error = String;
17
18    fn try_from(value: StringifiedNumeric) -> Result<Self, Self::Error> {
19        match value {
20            StringifiedNumeric::U256(n) => Ok(n),
21            StringifiedNumeric::Num(n) => {
22                Ok(U256::from_str(&n.to_string()).map_err(|err| err.to_string())?)
23            }
24            StringifiedNumeric::String(s) => {
25                if let Ok(val) = s.parse::<u128>() {
26                    Ok(U256::from(val))
27                } else if s.starts_with("0x") {
28                    // from_str_radix expects ONLY hex digits (0-9, A-F)
29                    // E.g from_str_radix("0xff", 16) will fail because 'x' is not a hex digit
30                    U256::from_str_radix(s.strip_prefix("0x").unwrap(), 16)
31                        .map_err(|err| err.to_string())
32                } else {
33                    U256::from_str(&s).map_err(|err| err.to_string())
34                }
35            }
36        }
37    }
38}
39
40impl TryFrom<StringifiedNumeric> for U64 {
41    type Error = String;
42
43    fn try_from(value: StringifiedNumeric) -> Result<Self, Self::Error> {
44        let value = U256::try_from(value)?;
45        Ok(value.wrapping_to::<U64>())
46    }
47}
48
49#[derive(Deserialize)]
50#[serde(untagged)]
51enum BoolOrU64 {
52    #[serde(deserialize_with = "deserialize_stringified_u64")]
53    U64(u64),
54    Bool(String),
55}
56
57/// Supports parsing either a u64 or a boolean (which will then be converted to u64)
58///
59/// Implemented to binary fields such as "OptimizationUsed" which are formatted either as 0/1 or
60/// "true/"false" by different block explorers (e.g. etherscan vs blockscout)
61pub fn deserialize_stringified_bool_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
62where
63    D: Deserializer<'de>,
64{
65    let num = BoolOrU64::deserialize(deserializer)?;
66    match num {
67        BoolOrU64::Bool(b) => {
68            let b = b.parse::<bool>().map_err(serde::de::Error::custom)?;
69            let u = if b { 1 } else { 0 };
70            Ok(u)
71        }
72        BoolOrU64::U64(u) => Ok(u),
73    }
74}
75
76/// Supports parsing u64
77///
78/// See <https://github.com/gakonst/ethers-rs/issues/1507>
79pub fn deserialize_stringified_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
80where
81    D: Deserializer<'de>,
82{
83    let num = StringifiedNumeric::deserialize(deserializer)?;
84    let num: U256 = num.try_into().map_err(serde::de::Error::custom)?;
85    num.try_into().map_err(serde::de::Error::custom)
86}
87
88/// Supports parsing numbers as strings
89///
90/// See <https://github.com/gakonst/ethers-rs/issues/1507>
91pub fn deserialize_stringified_numeric<'de, D>(deserializer: D) -> Result<U256, D::Error>
92where
93    D: Deserializer<'de>,
94{
95    let num = StringifiedNumeric::deserialize(deserializer)?;
96    num.try_into().map_err(serde::de::Error::custom)
97}
98
99/// Supports parsing numbers as strings
100///
101/// See <https://github.com/gakonst/ethers-rs/issues/1507>
102pub fn deserialize_stringified_numeric_opt<'de, D>(
103    deserializer: D,
104) -> Result<Option<U256>, D::Error>
105where
106    D: Deserializer<'de>,
107{
108    if let Some(num) = Option::<StringifiedNumeric>::deserialize(deserializer)? {
109        num.try_into().map(Some).map_err(serde::de::Error::custom)
110    } else {
111        Ok(None)
112    }
113}
114
115/// Supports parsing u64
116///
117/// See <https://github.com/gakonst/ethers-rs/issues/1507>
118pub fn deserialize_stringified_u64_opt<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
119where
120    D: Deserializer<'de>,
121{
122    if let Some(num) = Option::<StringifiedNumeric>::deserialize(deserializer)? {
123        let num: U256 = num.try_into().map_err(serde::de::Error::custom)?;
124        let num: u64 = num.try_into().map_err(serde::de::Error::custom)?;
125        Ok(Some(num))
126    } else {
127        Ok(None)
128    }
129}
130
131/// Helper type to parse numeric strings, `u64` and `U256`
132#[derive(Deserialize, Debug, Clone)]
133#[serde(untagged)]
134pub enum StringifiedBlockNumber {
135    Numeric(StringifiedNumeric),
136    BlockNumber(BlockNumber),
137}
138
139impl TryFrom<StringifiedBlockNumber> for BlockNumber {
140    type Error = String;
141
142    fn try_from(value: StringifiedBlockNumber) -> Result<Self, Self::Error> {
143        match value {
144            StringifiedBlockNumber::BlockNumber(b) => Ok(b),
145            StringifiedBlockNumber::Numeric(num) => match num {
146                StringifiedNumeric::String(s) => BlockNumber::from_str(&s),
147                other => {
148                    let u256 = U256::try_from(other)?;
149                    let n = u64::try_from(u256).map_err(|e| e.to_string())?;
150                    Ok(BlockNumber::Number(U64::from(n)))
151                }
152            },
153        }
154    }
155}
156
157/// Supports parsing block number as strings
158///
159/// See <https://github.com/gakonst/ethers-rs/issues/1507>
160pub fn deserialize_stringified_block_number<'de, D>(
161    deserializer: D,
162) -> Result<BlockNumber, D::Error>
163where
164    D: Deserializer<'de>,
165{
166    let num = StringifiedBlockNumber::deserialize(deserializer)?;
167    num.try_into().map_err(serde::de::Error::custom)
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use serde::{Deserialize, Serialize};
174
175    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
176    struct Txn {
177        #[serde(deserialize_with = "deserialize_stringified_block_number")]
178        bn: BlockNumber,
179    }
180
181    #[test]
182    fn deserializes_hex_with_0x_prefix() {
183        let json = r#"{ "bn": "0x1" }"#;
184        let tx: Txn = serde_json::from_str(json).unwrap();
185        assert_eq!(tx.bn, BlockNumber::Number(U64::from(1)));
186    }
187
188    #[test]
189    fn deserializes_decimal_string() {
190        let json = r#"{ "bn": "42" }"#;
191        let tx: Txn = serde_json::from_str(json).unwrap();
192        assert_eq!(tx.bn, BlockNumber::Number(U64::from(42)));
193    }
194
195    #[test]
196    fn deserializes_tag_latest() {
197        let json = r#"{ "bn": "latest" }"#;
198        let tx: Txn = serde_json::from_str(json).unwrap();
199        assert_eq!(tx.bn, BlockNumber::Latest);
200    }
201    #[test]
202    fn deserializes_large_hex_u64_max() {
203        let json = r#"{ "bn": "0xffffffffffffffff" }"#;
204        let tx: Txn = serde_json::from_str(json).unwrap();
205        assert_eq!(tx.bn, BlockNumber::Number(U64::MAX));
206    }
207
208    #[test]
209    fn roundtrip_serialized_hex_still_deserializes() {
210        let tx = Txn { bn: BlockNumber::Number(U64::from(42)) };
211        let s = serde_json::to_string(&tx).unwrap();
212        let de: Txn = serde_json::from_str(&s).unwrap();
213        assert_eq!(de, tx);
214    }
215}