Skip to main content

ethrpc_rs/
decode.rs

1//! Decode helpers for Ethereum RPC results.
2//!
3//! Ethereum encodes integer "quantities" as hex strings (e.g. `"0x1b4"`), which
4//! serde won't parse into a number on its own. [`ValueExt`] adds methods to
5//! [`serde_json::Value`] that decode these, so a call result is decoded with
6//! ordinary method syntax and the `?` operator:
7//!
8//! ```no_run
9//! use ethrpc_rs::{Rpc, ValueExt};
10//! # async fn ex(rpc: &Rpc) -> Result<(), ethrpc_rs::Error> {
11//! let block = rpc.call("eth_blockNumber", vec![]).await?.to_u64()?;
12//! # let _ = block; Ok(()) }
13//! ```
14
15use num_bigint::BigInt;
16use num_traits::Num;
17use serde_json::Value;
18
19use crate::error::{Error, Result};
20
21/// Decoding helpers for Ethereum-encoded [`serde_json::Value`] results.
22///
23/// Implemented for [`serde_json::Value`], so any RPC call result can be decoded
24/// in place: `rpc.call(...).await?.to_u64()?`.
25pub trait ValueExt {
26    /// Decodes an Ethereum quantity as a `u64`. Accepts a hex/decimal JSON
27    /// string (e.g. `"0x1b4"`) or a JSON number.
28    fn to_u64(&self) -> Result<u64>;
29
30    /// Decodes an Ethereum quantity as a [`BigInt`]. Accepts a hex/decimal JSON
31    /// string or a JSON number.
32    fn to_big_int(&self) -> Result<BigInt>;
33
34    /// Returns the value as a string slice, erroring if it is not a JSON string.
35    fn to_str(&self) -> Result<&str>;
36}
37
38impl ValueExt for Value {
39    fn to_u64(&self) -> Result<u64> {
40        match self {
41            Value::String(s) => parse_uint_auto(s),
42            Value::Number(n) => n
43                .as_u64()
44                .ok_or_else(|| Error::Other(format!("value {n} is not a u64"))),
45            other => Err(Error::Other(format!("cannot decode {other} as u64"))),
46        }
47    }
48
49    fn to_big_int(&self) -> Result<BigInt> {
50        match self {
51            Value::String(s) => {
52                let (radix, digits) = split_radix(s);
53                BigInt::from_str_radix(digits, radix)
54                    .map_err(|_| Error::Other("invalid integer value".to_string()))
55            }
56            Value::Number(n) => {
57                if let Some(i) = n.as_i64() {
58                    Ok(BigInt::from(i))
59                } else if let Some(u) = n.as_u64() {
60                    Ok(BigInt::from(u))
61                } else {
62                    // Very large integers that don't fit i64/u64: fall back to
63                    // the decimal text representation.
64                    BigInt::from_str_radix(&n.to_string(), 10)
65                        .map_err(|_| Error::Other("invalid integer value".to_string()))
66                }
67            }
68            other => Err(Error::Other(format!("cannot decode {other} as integer"))),
69        }
70    }
71
72    fn to_str(&self) -> Result<&str> {
73        self.as_str()
74            .ok_or_else(|| Error::Other(format!("cannot decode {self} as string")))
75    }
76}
77
78/// Parses an unsigned integer from a string, auto-detecting the base from a
79/// `0x`/`0o`/`0b` prefix (or leading `0` for octal), mirroring Go's
80/// `strconv.ParseUint(s, 0, 64)`.
81fn parse_uint_auto(s: &str) -> Result<u64> {
82    let (radix, digits) = split_radix(s);
83    u64::from_str_radix(digits, radix).map_err(|e| Error::Other(e.to_string()))
84}
85
86/// Splits an optionally-prefixed integer literal into its radix and the
87/// remaining digits. Recognizes `0x`/`0X`, `0o`/`0O`, `0b`/`0B`, and a bare
88/// leading `0` (octal), matching Go's base-0 parsing.
89fn split_radix(s: &str) -> (u32, &str) {
90    let bytes = s.as_bytes();
91    if bytes.len() >= 2 && bytes[0] == b'0' {
92        match bytes[1] {
93            b'x' | b'X' => return (16, &s[2..]),
94            b'o' | b'O' => return (8, &s[2..]),
95            b'b' | b'B' => return (2, &s[2..]),
96            // Leading zero with more digits => octal (Go base-0 behavior).
97            b'0'..=b'7' => return (8, &s[1..]),
98            _ => {}
99        }
100    }
101    (10, s)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use num_bigint::BigInt;
108    use serde_json::json;
109
110    #[test]
111    fn u64_variants() {
112        assert_eq!(json!("0x1b4").to_u64().unwrap(), 436);
113        assert_eq!(json!("100").to_u64().unwrap(), 100);
114        assert_eq!(json!(42).to_u64().unwrap(), 42);
115        assert_eq!(json!("0x0").to_u64().unwrap(), 0);
116        assert!(json!("notanumber").to_u64().is_err());
117        assert!(json!({}).to_u64().is_err());
118    }
119
120    #[test]
121    fn big_int_variants() {
122        assert_eq!(json!("0x1b4").to_big_int().unwrap(), BigInt::from(436));
123        assert_eq!(json!("100").to_big_int().unwrap(), BigInt::from(100));
124        assert_eq!(json!(42).to_big_int().unwrap(), BigInt::from(42));
125        assert_eq!(
126            json!("0xDE0B6B3A7640000").to_big_int().unwrap(),
127            BigInt::from(1_000_000_000_000_000_000u64)
128        );
129        assert!(json!("notanumber").to_big_int().is_err());
130    }
131
132    #[test]
133    fn str_variants() {
134        assert_eq!(json!("hello").to_str().unwrap(), "hello");
135        assert_eq!(json!("").to_str().unwrap(), "");
136        assert_eq!(json!("0xdead").to_str().unwrap(), "0xdead");
137        assert!(json!(123).to_str().is_err());
138    }
139}