Skip to main content

ethrpc_rs/
abi.rs

1//! Minimal Ethereum contract-call helpers (behind the default `abi` feature).
2//!
3//! Covers the common ABI types — `address`, `uint<M>`, `int<M>`, `bool`,
4//! `bytes<N>`, dynamic `bytes`/`string`, and arrays of those — which is enough
5//! for ERC-20/721 reads and most `view` calls. It is a deliberately small codec,
6//! not a full ABI implementation: tuples/structs and nested-array corner cases
7//! are out of scope.
8//!
9//! ```no_run
10//! use ethrpc_rs::{Rpc, abi::{eth_call_abi, ParamType, Token}};
11//! use num_bigint::BigInt;
12//!
13//! # async fn ex() -> Result<(), ethrpc_rs::Error> {
14//! let rpc = Rpc::new("https://cloudflare-eth.com");
15//! // balanceOf(address) -> uint256
16//! let out = eth_call_abi(
17//!     &rpc,
18//!     "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT
19//!     "balanceOf(address)",
20//!     &[Token::address("0x28C6c06298d514Db089934071355E5743bf21d60")?],
21//!     &[ParamType::Uint(256)],
22//! )
23//! .await?;
24//! let balance: &BigInt = out[0].as_uint().unwrap();
25//! println!("balance: {balance}");
26//! # Ok(()) }
27//! ```
28
29use num_bigint::{BigInt, Sign};
30use serde_json::{json, Map, Value};
31
32use crate::decode::ValueExt;
33use crate::error::{Error, Result};
34use crate::rpc::Handler;
35
36/// A decoded/encodable ABI value. The variant, not a separate type string,
37/// carries what is needed to encode it.
38#[derive(Debug, Clone, PartialEq)]
39pub enum Token {
40    /// A 20-byte `address`.
41    Address([u8; 20]),
42    /// An unsigned integer (`uint<M>`); always encoded in 32 bytes.
43    Uint(BigInt),
44    /// A signed integer (`int<M>`); two's-complement in 32 bytes.
45    Int(BigInt),
46    /// A `bool`.
47    Bool(bool),
48    /// A fixed byte array `bytes<N>` (`N <= 32`), left-aligned in its word.
49    FixedBytes(Vec<u8>),
50    /// Dynamic `bytes`.
51    Bytes(Vec<u8>),
52    /// Dynamic `string` (UTF-8).
53    String(String),
54    /// A homogeneous array `T[]`.
55    Array(Vec<Token>),
56}
57
58impl Token {
59    /// Builds an [`Token::Address`] from a `0x`-prefixed (or bare) 40-hex-char
60    /// string.
61    pub fn address(s: &str) -> Result<Token> {
62        let bytes = from_hex(strip_0x(s))?;
63        if bytes.len() != 20 {
64            return Err(Error::Other(format!(
65                "address must be 20 bytes, got {}",
66                bytes.len()
67            )));
68        }
69        let mut a = [0u8; 20];
70        a.copy_from_slice(&bytes);
71        Ok(Token::Address(a))
72    }
73
74    /// Convenience constructor for a `uint256` from any integer.
75    pub fn uint(v: impl Into<BigInt>) -> Token {
76        Token::Uint(v.into())
77    }
78
79    /// Returns the integer value for [`Token::Uint`]/[`Token::Int`].
80    pub fn as_uint(&self) -> Option<&BigInt> {
81        match self {
82            Token::Uint(v) | Token::Int(v) => Some(v),
83            _ => None,
84        }
85    }
86
87    /// Returns the 20-byte address for [`Token::Address`].
88    pub fn as_address(&self) -> Option<[u8; 20]> {
89        match self {
90            Token::Address(a) => Some(*a),
91            _ => None,
92        }
93    }
94
95    /// Returns the address as a lowercase `0x`-prefixed hex string.
96    pub fn as_address_hex(&self) -> Option<String> {
97        self.as_address().map(|a| format!("0x{}", to_hex(&a)))
98    }
99
100    /// Returns the boolean value for [`Token::Bool`].
101    pub fn as_bool(&self) -> Option<bool> {
102        match self {
103            Token::Bool(b) => Some(*b),
104            _ => None,
105        }
106    }
107
108    /// Returns the bytes for [`Token::Bytes`]/[`Token::FixedBytes`].
109    pub fn as_bytes(&self) -> Option<&[u8]> {
110        match self {
111            Token::Bytes(b) | Token::FixedBytes(b) => Some(b),
112            _ => None,
113        }
114    }
115
116    /// Returns the string for [`Token::String`].
117    pub fn as_string(&self) -> Option<&str> {
118        match self {
119            Token::String(s) => Some(s),
120            _ => None,
121        }
122    }
123
124    /// Returns the elements for [`Token::Array`].
125    pub fn as_array(&self) -> Option<&[Token]> {
126        match self {
127            Token::Array(v) => Some(v),
128            _ => None,
129        }
130    }
131
132    fn is_dynamic(&self) -> bool {
133        matches!(self, Token::Bytes(_) | Token::String(_) | Token::Array(_))
134    }
135}
136
137/// The ABI type of a value to decode. Needed for decoding a call's return data,
138/// since the raw bytes carry no type information.
139#[derive(Debug, Clone, PartialEq)]
140pub enum ParamType {
141    /// `address`
142    Address,
143    /// `uint<M>` (the bit width is informational; decoding always reads 32 bytes)
144    Uint(usize),
145    /// `int<M>`
146    Int(usize),
147    /// `bool`
148    Bool,
149    /// `bytes<N>`
150    FixedBytes(usize),
151    /// dynamic `bytes`
152    Bytes,
153    /// `string`
154    String,
155    /// `T[]`
156    Array(Box<ParamType>),
157}
158
159impl ParamType {
160    fn is_dynamic(&self) -> bool {
161        matches!(
162            self,
163            ParamType::Bytes | ParamType::String | ParamType::Array(_)
164        )
165    }
166}
167
168/// Computes the 4-byte function selector `keccak256(signature)[..4]`.
169///
170/// `signature` must be the canonical form, e.g. `"transfer(address,uint256)"`.
171pub fn function_selector(signature: &str) -> [u8; 4] {
172    let h = purecrypto::hash::keccak256(signature.as_bytes());
173    [h[0], h[1], h[2], h[3]]
174}
175
176/// Encodes a full calldata payload: the 4-byte selector followed by the
177/// ABI-encoded `args`.
178pub fn encode_call(signature: &str, args: &[Token]) -> Vec<u8> {
179    let mut out = function_selector(signature).to_vec();
180    out.extend(encode(args));
181    out
182}
183
184/// ABI-encodes a list of tokens as a head/tail tuple.
185pub fn encode(tokens: &[Token]) -> Vec<u8> {
186    let head_len: usize = tokens.len() * 32;
187    let mut head = Vec::with_capacity(head_len);
188    let mut tail = Vec::new();
189    for t in tokens {
190        if t.is_dynamic() {
191            head.extend_from_slice(&word_usize(head_len + tail.len()));
192            tail.extend(encode_value(t));
193        } else {
194            head.extend(encode_value(t));
195        }
196    }
197    head.extend(tail);
198    head
199}
200
201/// Encodes a single token (static value inline, or dynamic payload for the tail).
202fn encode_value(t: &Token) -> Vec<u8> {
203    match t {
204        Token::Address(a) => {
205            let mut w = [0u8; 32];
206            w[12..].copy_from_slice(a);
207            w.to_vec()
208        }
209        Token::Uint(v) => encode_uint(v),
210        Token::Int(v) => encode_int(v),
211        Token::Bool(b) => {
212            let mut w = [0u8; 32];
213            w[31] = *b as u8;
214            w.to_vec()
215        }
216        Token::FixedBytes(b) => {
217            let mut w = [0u8; 32];
218            w[..b.len()].copy_from_slice(b);
219            w.to_vec()
220        }
221        Token::Bytes(b) => encode_dynamic_bytes(b),
222        Token::String(s) => encode_dynamic_bytes(s.as_bytes()),
223        Token::Array(elems) => {
224            let mut out = word_usize(elems.len()).to_vec();
225            out.extend(encode(elems));
226            out
227        }
228    }
229}
230
231fn encode_dynamic_bytes(b: &[u8]) -> Vec<u8> {
232    let mut out = word_usize(b.len()).to_vec();
233    out.extend_from_slice(b);
234    // Right-pad the payload up to a 32-byte boundary.
235    let padded = b.len().div_ceil(32) * 32;
236    out.resize(32 + padded, 0);
237    out
238}
239
240fn encode_uint(v: &BigInt) -> Vec<u8> {
241    let (sign, mag) = v.to_bytes_be();
242    if sign == Sign::Minus {
243        // Callers should use Token::Int for negatives; treat as two's complement.
244        return encode_int(v);
245    }
246    let mut w = [0u8; 32];
247    let start = 32usize.saturating_sub(mag.len());
248    w[start..].copy_from_slice(&mag[mag.len().saturating_sub(32)..]);
249    w.to_vec()
250}
251
252fn encode_int(v: &BigInt) -> Vec<u8> {
253    let encoded = if v.sign() == Sign::Minus {
254        // two's complement over 256 bits
255        let modulus = BigInt::from(1) << 256;
256        modulus + v
257    } else {
258        v.clone()
259    };
260    let (_, mag) = encoded.to_bytes_be();
261    let mut w = [0u8; 32];
262    let take = mag.len().min(32);
263    w[32 - take..].copy_from_slice(&mag[mag.len() - take..]);
264    w.to_vec()
265}
266
267/// ABI-decodes `data` according to `types`.
268pub fn decode(types: &[ParamType], data: &[u8]) -> Result<Vec<Token>> {
269    decode_tuple(types, data, 0)
270}
271
272fn decode_tuple(types: &[ParamType], data: &[u8], base: usize) -> Result<Vec<Token>> {
273    let mut out = Vec::with_capacity(types.len());
274    for (i, ty) in types.iter().enumerate() {
275        let head_pos = base + i * 32;
276        if ty.is_dynamic() {
277            let off = read_usize(data, head_pos)?;
278            out.push(decode_value(ty, data, base + off)?);
279        } else {
280            out.push(decode_static(ty, data, head_pos)?);
281        }
282    }
283    Ok(out)
284}
285
286fn decode_static(ty: &ParamType, data: &[u8], pos: usize) -> Result<Token> {
287    let w = word(data, pos)?;
288    Ok(match ty {
289        ParamType::Address => {
290            let mut a = [0u8; 20];
291            a.copy_from_slice(&w[12..32]);
292            Token::Address(a)
293        }
294        ParamType::Uint(_) => Token::Uint(BigInt::from_bytes_be(Sign::Plus, w)),
295        ParamType::Int(_) => {
296            let mut v = BigInt::from_bytes_be(Sign::Plus, w);
297            if w[0] & 0x80 != 0 {
298                v -= BigInt::from(1) << 256;
299            }
300            Token::Int(v)
301        }
302        ParamType::Bool => Token::Bool(w.iter().any(|&b| b != 0)),
303        ParamType::FixedBytes(n) => Token::FixedBytes(w[..(*n).min(32)].to_vec()),
304        _ => return Err(Error::Other("decode_static called on dynamic type".into())),
305    })
306}
307
308fn decode_value(ty: &ParamType, data: &[u8], pos: usize) -> Result<Token> {
309    match ty {
310        ParamType::Bytes => {
311            let len = read_usize(data, pos)?;
312            let start = pos + 32;
313            let end = start
314                .checked_add(len)
315                .ok_or_else(|| Error::Other("abi length overflow".into()))?;
316            slice(data, start, end).map(|s| Token::Bytes(s.to_vec()))
317        }
318        ParamType::String => {
319            let len = read_usize(data, pos)?;
320            let start = pos + 32;
321            let end = start
322                .checked_add(len)
323                .ok_or_else(|| Error::Other("abi length overflow".into()))?;
324            let s = slice(data, start, end)?;
325            Ok(Token::String(String::from_utf8(s.to_vec()).map_err(
326                |_| Error::Other("invalid utf-8 in string".into()),
327            )?))
328        }
329        ParamType::Array(inner) => {
330            let len = read_usize(data, pos)?;
331            let elem_base = pos + 32;
332            let types = vec![(**inner).clone(); len];
333            Ok(Token::Array(decode_tuple(&types, data, elem_base)?))
334        }
335        _ => decode_static(ty, data, pos),
336    }
337}
338
339/// Performs an `eth_call` against `to` with ABI-encoded `args` and decodes the
340/// return value per `returns`. `signature` is the canonical function signature
341/// used to compute the selector, e.g. `"balanceOf(address)"`.
342pub async fn eth_call_abi<H: Handler + ?Sized>(
343    handler: &H,
344    to: &str,
345    signature: &str,
346    args: &[Token],
347    returns: &[ParamType],
348) -> Result<Vec<Token>> {
349    let calldata = encode_call(signature, args);
350    let mut tx = Map::new();
351    tx.insert("to".to_string(), json!(to));
352    tx.insert(
353        "data".to_string(),
354        json!(format!("0x{}", to_hex(&calldata))),
355    );
356
357    let ret = handler
358        .call("eth_call", vec![Value::Object(tx), json!("latest")])
359        .await?;
360    let hex = ret.to_str()?;
361    let bytes = from_hex(strip_0x(hex))?;
362    decode(returns, &bytes)
363}
364
365// ---- word / hex helpers ----
366
367fn word_usize(v: usize) -> [u8; 32] {
368    let mut w = [0u8; 32];
369    w[24..].copy_from_slice(&(v as u64).to_be_bytes());
370    w
371}
372
373fn word(data: &[u8], pos: usize) -> Result<&[u8]> {
374    slice(data, pos, pos + 32)
375}
376
377fn read_usize(data: &[u8], pos: usize) -> Result<usize> {
378    let w = word(data, pos)?;
379    // Guard against values that don't fit in usize (the top 24 bytes must be 0).
380    if w[..24].iter().any(|&b| b != 0) {
381        return Err(Error::Other("abi offset/length too large".into()));
382    }
383    let mut buf = [0u8; 8];
384    buf.copy_from_slice(&w[24..32]);
385    Ok(u64::from_be_bytes(buf) as usize)
386}
387
388fn slice(data: &[u8], start: usize, end: usize) -> Result<&[u8]> {
389    data.get(start..end)
390        .ok_or_else(|| Error::Other("abi data truncated".into()))
391}
392
393fn strip_0x(s: &str) -> &str {
394    s.strip_prefix("0x")
395        .or_else(|| s.strip_prefix("0X"))
396        .unwrap_or(s)
397}
398
399fn to_hex(bytes: &[u8]) -> String {
400    let mut s = String::with_capacity(bytes.len() * 2);
401    for b in bytes {
402        s.push_str(&format!("{b:02x}"));
403    }
404    s
405}
406
407fn from_hex(s: &str) -> Result<Vec<u8>> {
408    if !s.len().is_multiple_of(2) {
409        return Err(Error::Other("odd-length hex string".into()));
410    }
411    (0..s.len())
412        .step_by(2)
413        .map(|i| {
414            u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| Error::Other("invalid hex".into()))
415        })
416        .collect()
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    fn hex(s: &str) -> Vec<u8> {
424        from_hex(strip_0x(s)).unwrap()
425    }
426
427    #[test]
428    fn selectors() {
429        assert_eq!(to_hex(&function_selector("balanceOf(address)")), "70a08231");
430        assert_eq!(
431            to_hex(&function_selector("transfer(address,uint256)")),
432            "a9059cbb"
433        );
434    }
435
436    #[test]
437    fn encode_transfer_calldata() {
438        // transfer(0x00..0064, 1) — canonical ABI encoding.
439        let to = Token::address("0x0000000000000000000000000000000000000064").unwrap();
440        let calldata = encode_call("transfer(address,uint256)", &[to, Token::uint(1u64)]);
441        let expected = hex("a9059cbb\
442             0000000000000000000000000000000000000000000000000000000000000064\
443             0000000000000000000000000000000000000000000000000000000000000001");
444        assert_eq!(calldata, expected);
445    }
446
447    #[test]
448    fn roundtrip_static() {
449        let tokens = vec![
450            Token::address("0x00000000000000000000000000000000000000ff").unwrap(),
451            Token::uint(12345u64),
452            Token::Bool(true),
453        ];
454        let enc = encode(&tokens);
455        let dec = decode(
456            &[ParamType::Address, ParamType::Uint(256), ParamType::Bool],
457            &enc,
458        )
459        .unwrap();
460        assert_eq!(dec, tokens);
461    }
462
463    #[test]
464    fn roundtrip_dynamic_string_and_bytes() {
465        let tokens = vec![
466            Token::String("hello world, this is longer than 32 bytes!!".to_string()),
467            Token::Uint(BigInt::from(7)),
468            Token::Bytes(vec![1, 2, 3, 4, 5]),
469        ];
470        let enc = encode(&tokens);
471        let dec = decode(
472            &[ParamType::String, ParamType::Uint(256), ParamType::Bytes],
473            &enc,
474        )
475        .unwrap();
476        assert_eq!(dec, tokens);
477    }
478
479    #[test]
480    fn roundtrip_array_of_addresses() {
481        let arr = Token::Array(vec![
482            Token::address("0x0000000000000000000000000000000000000001").unwrap(),
483            Token::address("0x0000000000000000000000000000000000000002").unwrap(),
484        ]);
485        let enc = encode(std::slice::from_ref(&arr));
486        let dec = decode(&[ParamType::Array(Box::new(ParamType::Address))], &enc).unwrap();
487        assert_eq!(dec, vec![arr]);
488    }
489
490    #[test]
491    fn decode_negative_int() {
492        // int256(-1) is all 0xff.
493        let data = hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
494        let dec = decode(&[ParamType::Int(256)], &data).unwrap();
495        assert_eq!(dec[0].as_uint().unwrap(), &BigInt::from(-1));
496        // And it round-trips through encoding.
497        assert_eq!(encode(&[Token::Int(BigInt::from(-1))]), data);
498    }
499
500    #[test]
501    fn decode_uint_string_result() {
502        // Simulate an ERC-20 `symbol()` returning "USDC".
503        let tokens = vec![Token::String("USDC".to_string())];
504        let enc = encode(&tokens);
505        let dec = decode(&[ParamType::String], &enc).unwrap();
506        assert_eq!(dec[0].as_string(), Some("USDC"));
507    }
508}