Skip to main content

tiders_evm_decode/
abi.rs

1use alloy_json_abi::JsonAbi;
2use anyhow::{Context, Result};
3
4/// Parsed event info extracted from a JSON ABI.
5#[derive(Debug, Clone)]
6pub struct EvmAbiEvent {
7    /// Event name (e.g. "Swap").
8    pub name: String,
9    /// Event name in snake_case (e.g. "swap").
10    pub name_snake_case: String,
11    /// Human-readable signature with outer param names but unnamed tuple components
12    /// (e.g. `"Swap(address indexed sender, (int256,int256) data)"`).
13    /// Can be passed directly to [`crate::decode_events`] and [`crate::signature_to_topic0`].
14    pub signature: String,
15    /// Canonical selector signature without names
16    /// (e.g. "Swap(address,address,int256,int256,uint160,uint128,int24)").
17    pub selector_signature: String,
18    /// topic0 as 0x-prefixed hex string.
19    pub topic0: String,
20    /// Full JSON ABI fragment for this event, preserving all component names.
21    pub abi_json: String,
22}
23
24/// Parsed function info extracted from a JSON ABI.
25#[derive(Debug, Clone)]
26pub struct EvmAbiFunction {
27    /// Function name (e.g. "swap").
28    pub name: String,
29    /// Function name in snake_case (e.g. "swap").
30    pub name_snake_case: String,
31    /// Human-readable signature with names
32    /// (e.g. "swap(address recipient, bool zeroForOne, int256 amountSpecified, ...)").
33    pub signature: String,
34    /// Canonical selector signature without names
35    /// (e.g. "swap(address,bool,int256,uint160,bytes)").
36    pub selector_signature: String,
37    /// 4-byte selector as 0x-prefixed hex string.
38    pub selector: String,
39    /// Full JSON ABI fragment for this function, preserving all parameter names.
40    pub abi_json: String,
41}
42
43/// Converts a camelCase or PascalCase name to snake_case.
44fn to_snake_case(name: &str) -> String {
45    let mut out = String::with_capacity(name.len() + 4);
46    for (i, ch) in name.char_indices() {
47        if ch.is_uppercase() && i != 0 {
48            out.push('_');
49        }
50        out.extend(ch.to_lowercase());
51    }
52    out
53}
54
55/// Converts a param's type and components to a string without inner names,
56/// e.g. `tuple(int256,uint256)` → `(int256,uint256)`, `tuple[]` → `(int256,uint256)[]`.
57fn param_type_str(ty: &str, components: &[alloy_json_abi::Param]) -> String {
58    if !components.is_empty() && ty.starts_with("tuple") {
59        let suffix = &ty["tuple".len()..]; // "" | "[]" | "[3]" | …
60        let inner = components
61            .iter()
62            .map(|c| param_type_str(&c.ty, &c.components))
63            .collect::<Vec<_>>()
64            .join(",");
65        format!("tuple({inner}){suffix}")
66    } else {
67        ty.to_string()
68    }
69}
70
71/// Builds a human-readable event signature: outer param names and `indexed` flags kept,
72/// inner tuple component names stripped.
73///
74/// Example: `Swap(address indexed sender, tuple(int256,int256) delta)`
75fn event_decode_signature(event: &alloy_json_abi::Event) -> String {
76    let params = event
77        .inputs
78        .iter()
79        .map(|input| {
80            let ty = param_type_str(&input.ty, &input.components);
81            match (input.indexed, input.name.is_empty()) {
82                (true, false) => format!("{ty} indexed {}", input.name),
83                (true, true) => format!("{ty} indexed"),
84                (false, false) => format!("{ty} {}", input.name),
85                (false, true) => ty,
86            }
87        })
88        .collect::<Vec<_>>()
89        .join(", ");
90    format!("{}({params})", event.name)
91}
92
93/// Builds a human-readable function signature: outer param names kept, inner tuple names stripped.
94///
95/// Example: `swap(address recipient, tuple(int256,uint256) params)(bool success)`
96fn func_decode_signature(func: &alloy_json_abi::Function) -> String {
97    let fmt_params = |params: &[alloy_json_abi::Param]| {
98        params
99            .iter()
100            .map(|p| {
101                let ty = param_type_str(&p.ty, &p.components);
102                if p.name.is_empty() {
103                    ty
104                } else {
105                    format!("{ty} {}", p.name)
106                }
107            })
108            .collect::<Vec<_>>()
109            .join(", ")
110    };
111    format!(
112        "{}({})({})",
113        func.name,
114        fmt_params(&func.inputs),
115        fmt_params(&func.outputs)
116    )
117}
118
119/// Parse a JSON ABI string and extract all events.
120pub fn abi_events(json_str: &str) -> Result<Vec<EvmAbiEvent>> {
121    let abi: JsonAbi = serde_json::from_str(json_str).context("parse JSON ABI")?;
122    let mut events = Vec::new();
123    for event in abi.events() {
124        let selector = event.selector();
125        events.push(EvmAbiEvent {
126            name_snake_case: to_snake_case(&event.name),
127            name: event.name.clone(),
128            signature: event_decode_signature(event),
129            selector_signature: event.signature(),
130            topic0: format!("0x{}", faster_hex::hex_string(selector.as_slice())),
131            abi_json: serde_json::to_string(event)
132                .context("alloy_json_abi::Event serialization")?,
133        });
134    }
135    Ok(events)
136}
137
138/// Parse a JSON ABI string and extract all functions.
139pub fn abi_functions(json_str: &str) -> Result<Vec<EvmAbiFunction>> {
140    let abi: JsonAbi = serde_json::from_str(json_str).context("parse JSON ABI")?;
141    let mut functions = Vec::new();
142    for func in abi.functions() {
143        let selector = func.selector();
144        functions.push(EvmAbiFunction {
145            name_snake_case: to_snake_case(&func.name),
146            name: func.name.clone(),
147            signature: func_decode_signature(func),
148            selector_signature: func.signature(),
149            selector: format!("0x{}", faster_hex::hex_string(selector.as_slice())),
150            abi_json: serde_json::to_string(func)
151                .context("alloy_json_abi::Function serialization")?,
152        });
153    }
154    Ok(functions)
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::signature_to_topic0;
161
162    #[test]
163    fn test_abi_events() {
164        let abi_json = r#"[
165            {
166                "anonymous": false,
167                "inputs": [
168                    {"indexed": true, "internalType": "address", "name": "sender", "type": "address"},
169                    {"indexed": true, "internalType": "address", "name": "recipient", "type": "address"},
170                    {"indexed": false, "internalType": "int256", "name": "amount0", "type": "int256"},
171                    {"indexed": false, "internalType": "int256", "name": "amount1", "type": "int256"},
172                    {"indexed": false, "internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"},
173                    {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"},
174                    {"indexed": false, "internalType": "int24", "name": "tick", "type": "int24"}
175                ],
176                "name": "Swap",
177                "type": "event"
178            },
179            {
180                "inputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}],
181                "name": "initialize",
182                "outputs": [],
183                "stateMutability": "nonpayable",
184                "type": "function"
185            }
186        ]"#;
187
188        let events = abi_events(abi_json);
189        assert!(events.is_ok());
190        let events = events.unwrap_or_default();
191        assert_eq!(events.len(), 1);
192        assert_eq!(events[0].name, "Swap");
193        assert_eq!(
194            events[0].signature,
195            "Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)"
196        );
197        assert_eq!(
198            events[0].selector_signature,
199            "Swap(address,address,int256,int256,uint160,uint128,int24)"
200        );
201        assert!(events[0].topic0.starts_with("0x"));
202        // Verify the signature works with decode (parse round-trip)
203        let topic0_from_sig = signature_to_topic0(&events[0].signature);
204        assert!(topic0_from_sig.is_ok());
205    }
206
207    #[test]
208    fn test_abi_functions() {
209        let abi_json = r#"[
210            {
211                "inputs": [
212                    {"internalType": "address", "name": "recipient", "type": "address"},
213                    {"internalType": "bool", "name": "zeroForOne", "type": "bool"},
214                    {"internalType": "int256", "name": "amountSpecified", "type": "int256"},
215                    {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"},
216                    {"internalType": "bytes", "name": "data", "type": "bytes"}
217                ],
218                "name": "swap",
219                "outputs": [
220                    {"internalType": "int256", "name": "amount0", "type": "int256"},
221                    {"internalType": "int256", "name": "amount1", "type": "int256"}
222                ],
223                "stateMutability": "nonpayable",
224                "type": "function"
225            },
226            {
227                "anonymous": false,
228                "inputs": [
229                    {"indexed": true, "internalType": "address", "name": "sender", "type": "address"}
230                ],
231                "name": "Swap",
232                "type": "event"
233            }
234        ]"#;
235
236        let functions = abi_functions(abi_json);
237        assert!(functions.is_ok());
238        let functions = functions.unwrap_or_default();
239        assert_eq!(functions.len(), 1);
240        assert_eq!(functions[0].name, "swap");
241        assert_eq!(
242            functions[0].selector_signature,
243            "swap(address,bool,int256,uint160,bytes)"
244        );
245        assert!(functions[0].selector.starts_with("0x"));
246        assert_eq!(functions[0].selector.len(), 10); // "0x" + 8 hex chars
247    }
248
249    #[test]
250    fn test_abi_events_empty_abi() {
251        let events = abi_events("[]");
252        assert!(events.is_ok());
253        assert!(events.unwrap_or_default().is_empty());
254    }
255}