Skip to main content

graph_native/
abi.rs

1use crate::log::Log;
2use crate::types::{Address, BigInt};
3
4use alloy::dyn_abi::EventExt;
5use alloy::dyn_abi::DynSolValue;
6use alloy::json_abi::Event;
7use alloy::primitives::LogData;
8
9/// A decoded event parameter value.
10#[derive(Debug, Clone)]
11pub enum DecodedParam {
12    Address(Address),
13    Uint(BigInt),
14    Int(BigInt),
15    Bool(bool),
16    String(String),
17    Bytes(Vec<u8>),
18    Array(Vec<DecodedParam>),
19}
20
21impl DecodedParam {
22    pub fn to_address(&self) -> Address {
23        match self {
24            DecodedParam::Address(a) => a.clone(),
25            DecodedParam::Bytes(b) if b.len() >= 20 => {
26                let mut addr = [0u8; 20];
27                addr.copy_from_slice(&b[..20]);
28                Address(addr)
29            }
30            _ => Address::zero(),
31        }
32    }
33
34    pub fn to_big_int(&self) -> BigInt {
35        match self {
36            DecodedParam::Uint(v) | DecodedParam::Int(v) => v.clone(),
37            _ => BigInt::zero(),
38        }
39    }
40
41    pub fn to_bool(&self) -> bool {
42        match self {
43            DecodedParam::Bool(v) => *v,
44            _ => false,
45        }
46    }
47
48    pub fn to_string(&self) -> String {
49        match self {
50            DecodedParam::String(s) => s.clone(),
51            _ => String::new(),
52        }
53    }
54
55    pub fn to_bytes(&self) -> Vec<u8> {
56        match self {
57            DecodedParam::Bytes(b) => b.clone(),
58            _ => Vec::new(),
59        }
60    }
61
62    pub fn to_array(&self) -> Vec<DecodedParam> {
63        match self {
64            DecodedParam::Array(a) => a.clone(),
65            _ => Vec::new(),
66        }
67    }
68}
69
70/// Decode event parameters from a log entry using its JSON ABI definition.
71///
72/// The ABI string is JSON like:
73///   {"name":"Transfer","inputs":[{"type":"address","indexed":true},{"type":"uint256","indexed":false}]}
74///
75/// Uses alloy's EventExt::decode_log (same library as graph-node's WASM runtime)
76/// to decode indexed params from topics and non-indexed params from data.
77///
78/// Supports anonymous events via `"anonymous":true`.
79///
80/// Returns params in declaration order.
81pub fn decode_event_params(log: &Log, event_abi: &str) -> Vec<DecodedParam> {
82    // Parse the JSON ABI into an alloy Event.
83    // Alloy requires the "anonymous" field — inject default false if missing.
84    let abi_with_anon = if event_abi.contains("\"anonymous\"") {
85        event_abi.to_string()
86    } else {
87        // Insert "anonymous":false before the closing }
88        let mut s = event_abi.to_string();
89        if let Some(pos) = s.rfind('}') {
90            s.insert_str(pos, ",\"anonymous\":false");
91        }
92        s
93    };
94
95    let event: Event = match serde_json::from_str(&abi_with_anon) {
96        Ok(e) => e,
97        Err(e) => {
98            eprintln!(
99                "[graph-native] ERROR: failed to parse event ABI JSON: {e} — abi: {event_abi}"
100            );
101            return Vec::new();
102        }
103    };
104
105    // Convert our Log topics (Vec<Vec<u8>>) to alloy's B256 format
106    let topics: Vec<alloy::primitives::B256> = log
107        .topics
108        .iter()
109        .map(|t| {
110            let mut bytes = [0u8; 32];
111            let len = t.len().min(32);
112            bytes[32 - len..].copy_from_slice(&t[..len]);
113            alloy::primitives::B256::from(bytes)
114        })
115        .collect();
116
117    // Build alloy LogData from topics + data
118    let log_data = LogData::new_unchecked(topics, log.data.clone().into());
119
120    // Decode using alloy's EventExt (same path as graph-node's WASM runtime)
121    let decoded = match EventExt::decode_log(&event, &log_data) {
122        Ok(d) => d,
123        Err(e) => {
124            eprintln!("[graph-native] ERROR: alloy failed to decode log: {e} — abi: {event_abi}");
125            return Vec::new();
126        }
127    };
128
129    // Reassemble indexed + body in declaration order
130    let mut indexed_iter = decoded.indexed.into_iter();
131    let mut body_iter = decoded.body.into_iter();
132
133    let mut result = Vec::with_capacity(event.inputs.len());
134    for input in &event.inputs {
135        let value = if input.indexed {
136            indexed_iter.next()
137        } else {
138            body_iter.next()
139        };
140
141        match value {
142            Some(v) => result.push(dyn_sol_to_decoded(v)),
143            None => result.push(DecodedParam::Bytes(Vec::new())),
144        }
145    }
146
147    result
148}
149
150/// Decode ABI-encoded bytes using a Solidity type signature string.
151/// Mirrors graph-ts `ethereum.decode(signature, data)`.
152///
153/// The signature uses Solidity ABI types: "(uint8,uint32)", "address", etc.
154/// Returns None if decoding fails (matches graph-ts nullable return).
155pub fn abi_decode(signature: &str, data: &[u8]) -> Option<DecodedParam> {
156    use alloy::dyn_abi::DynSolType;
157    let sol_type = DynSolType::parse(signature).ok()?;
158    let decoded = sol_type.abi_decode(data).ok()?;
159    Some(dyn_sol_to_decoded(decoded))
160}
161
162/// Convert an alloy DynSolValue to our DecodedParam.
163fn dyn_sol_to_decoded(value: DynSolValue) -> DecodedParam {
164    match value {
165        DynSolValue::Address(a) => DecodedParam::Address(Address(a.0 .0)),
166        DynSolValue::Uint(v, _bits) => {
167            let bytes = v.to_be_bytes::<32>();
168            DecodedParam::Uint(BigInt::from_unsigned_be_bytes(&bytes))
169        }
170        DynSolValue::Int(v, _bits) => {
171            let bytes = v.to_be_bytes::<32>();
172            DecodedParam::Int(BigInt::from_be_bytes(&bytes))
173        }
174        DynSolValue::Bool(b) => DecodedParam::Bool(b),
175        DynSolValue::String(s) => DecodedParam::String(s),
176        DynSolValue::Bytes(b) => DecodedParam::Bytes(b),
177        DynSolValue::FixedBytes(b, size) => DecodedParam::Bytes(b.0[..size].to_vec()),
178        DynSolValue::Array(arr) | DynSolValue::Tuple(arr) | DynSolValue::FixedArray(arr) => {
179            DecodedParam::Array(arr.into_iter().map(dyn_sol_to_decoded).collect())
180        }
181        DynSolValue::Function(f) => DecodedParam::Bytes(f.0.to_vec()),
182        // CustomStruct (eip712) or future variants — not produced by standard ABI event decoding
183        other => {
184            eprintln!("Warning: unexpected DynSolValue variant in ABI decoding: {:?}", other);
185            DecodedParam::Bytes(Vec::new())
186        }
187    }
188}