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 names and indexed markers
12    /// (e.g. "Swap(address indexed sender, address indexed recipient, int256 amount0, ...)").
13    /// Can be passed directly to [`crate::decode_events`].
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}
21
22/// Parsed function info extracted from a JSON ABI.
23#[derive(Debug, Clone)]
24pub struct EvmAbiFunction {
25    /// Function name (e.g. "swap").
26    pub name: String,
27    /// Function name in snake_case (e.g. "swap").
28    pub name_snake_case: String,
29    /// Human-readable signature with names
30    /// (e.g. "swap(address recipient, bool zeroForOne, int256 amountSpecified, ...)").
31    pub signature: String,
32    /// Canonical selector signature without names
33    /// (e.g. "swap(address,bool,int256,uint160,bytes)").
34    pub selector_signature: String,
35    /// 4-byte selector as 0x-prefixed hex string.
36    pub selector: String,
37}
38
39/// Converts a camelCase or PascalCase name to snake_case.
40fn to_snake_case(name: &str) -> String {
41    let mut out = String::with_capacity(name.len() + 4);
42    for (i, ch) in name.char_indices() {
43        if ch.is_uppercase() && i != 0 {
44            out.push('_');
45        }
46        out.extend(ch.to_lowercase());
47    }
48    out
49}
50
51/// Formats an event's full signature without the "event " prefix.
52/// Produces e.g. "Swap(address indexed sender, address indexed recipient, int256 amount0)"
53fn event_decode_signature(event: &alloy_json_abi::Event) -> String {
54    let full = event.full_signature();
55    // full_signature() returns "event Name(...)", strip the "event " prefix
56    full.strip_prefix("event ").unwrap_or(&full).to_string()
57}
58
59/// Parse a JSON ABI string and extract all events.
60pub fn abi_events(json_str: &str) -> Result<Vec<EvmAbiEvent>> {
61    let abi: JsonAbi = serde_json::from_str(json_str).context("parse JSON ABI")?;
62    let mut events = Vec::new();
63    for event in abi.events() {
64        let selector = event.selector();
65        events.push(EvmAbiEvent {
66            name_snake_case: to_snake_case(&event.name),
67            name: event.name.clone(),
68            signature: event_decode_signature(event),
69            selector_signature: event.signature(),
70            topic0: format!("0x{}", faster_hex::hex_string(selector.as_slice())),
71        });
72    }
73    Ok(events)
74}
75
76/// Parse a JSON ABI string and extract all functions.
77pub fn abi_functions(json_str: &str) -> Result<Vec<EvmAbiFunction>> {
78    let abi: JsonAbi = serde_json::from_str(json_str).context("parse JSON ABI")?;
79    let mut functions = Vec::new();
80    for func in abi.functions() {
81        let selector = func.selector();
82        functions.push(EvmAbiFunction {
83            name_snake_case: to_snake_case(&func.name),
84            name: func.name.clone(),
85            signature: func.full_signature(),
86            selector_signature: func.signature(),
87            selector: format!("0x{}", faster_hex::hex_string(selector.as_slice())),
88        });
89    }
90    Ok(functions)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::signature_to_topic0;
97
98    #[test]
99    fn test_abi_events() {
100        let abi_json = r#"[
101            {
102                "anonymous": false,
103                "inputs": [
104                    {"indexed": true, "internalType": "address", "name": "sender", "type": "address"},
105                    {"indexed": true, "internalType": "address", "name": "recipient", "type": "address"},
106                    {"indexed": false, "internalType": "int256", "name": "amount0", "type": "int256"},
107                    {"indexed": false, "internalType": "int256", "name": "amount1", "type": "int256"},
108                    {"indexed": false, "internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"},
109                    {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"},
110                    {"indexed": false, "internalType": "int24", "name": "tick", "type": "int24"}
111                ],
112                "name": "Swap",
113                "type": "event"
114            },
115            {
116                "inputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}],
117                "name": "initialize",
118                "outputs": [],
119                "stateMutability": "nonpayable",
120                "type": "function"
121            }
122        ]"#;
123
124        let events = abi_events(abi_json);
125        assert!(events.is_ok());
126        let events = events.unwrap_or_default();
127        assert_eq!(events.len(), 1);
128        assert_eq!(events[0].name, "Swap");
129        assert_eq!(
130            events[0].signature,
131            "Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)"
132        );
133        assert_eq!(
134            events[0].selector_signature,
135            "Swap(address,address,int256,int256,uint160,uint128,int24)"
136        );
137        assert!(events[0].topic0.starts_with("0x"));
138        // Verify the signature works with decode (parse round-trip)
139        let topic0_from_sig = signature_to_topic0(&events[0].signature);
140        assert!(topic0_from_sig.is_ok());
141    }
142
143    #[test]
144    fn test_abi_functions() {
145        let abi_json = r#"[
146            {
147                "inputs": [
148                    {"internalType": "address", "name": "recipient", "type": "address"},
149                    {"internalType": "bool", "name": "zeroForOne", "type": "bool"},
150                    {"internalType": "int256", "name": "amountSpecified", "type": "int256"},
151                    {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"},
152                    {"internalType": "bytes", "name": "data", "type": "bytes"}
153                ],
154                "name": "swap",
155                "outputs": [
156                    {"internalType": "int256", "name": "amount0", "type": "int256"},
157                    {"internalType": "int256", "name": "amount1", "type": "int256"}
158                ],
159                "stateMutability": "nonpayable",
160                "type": "function"
161            },
162            {
163                "anonymous": false,
164                "inputs": [
165                    {"indexed": true, "internalType": "address", "name": "sender", "type": "address"}
166                ],
167                "name": "Swap",
168                "type": "event"
169            }
170        ]"#;
171
172        let functions = abi_functions(abi_json);
173        assert!(functions.is_ok());
174        let functions = functions.unwrap_or_default();
175        assert_eq!(functions.len(), 1);
176        assert_eq!(functions[0].name, "swap");
177        assert_eq!(
178            functions[0].selector_signature,
179            "swap(address,bool,int256,uint160,bytes)"
180        );
181        assert!(functions[0].selector.starts_with("0x"));
182        assert_eq!(functions[0].selector.len(), 10); // "0x" + 8 hex chars
183    }
184
185    #[test]
186    fn test_abi_events_empty_abi() {
187        let events = abi_events("[]");
188        assert!(events.is_ok());
189        assert!(events.unwrap_or_default().is_empty());
190    }
191}