Skip to main content

soldb_ethdebug/
events.rs

1//! Decoding transaction logs against event ABIs.
2//!
3//! An [`EventRegistry`] is built from the event entries of one or more contract ABIs and
4//! keyed by topic hash, so a log can be matched and its indexed and non-indexed
5//! arguments decoded back into typed values.
6//!
7//! Log payloads come from the chain and are not trusted. Offsets and lengths read out of
8//! event data are validated and their arithmetic is checked: an unchecked offset wraps in
9//! release builds and turns into a reversed slice range, and an unchecked length turns
10//! into an unbounded allocation.
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15use serde_json::{json, Value};
16use soldb_core::{SoldbError, SoldbResult};
17
18use crate::abi::keccak256;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct EventParam {
22    #[serde(default)]
23    pub name: String,
24    #[serde(rename = "type")]
25    pub ty: String,
26    #[serde(default)]
27    pub indexed: bool,
28    #[serde(default, skip_serializing_if = "Vec::is_empty")]
29    pub components: Vec<EventParam>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct EventAbi {
34    pub name: String,
35    #[serde(default)]
36    pub inputs: Vec<EventParam>,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct DecodedEventArg {
41    pub name: String,
42    #[serde(rename = "type")]
43    pub ty: String,
44    pub value: Value,
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct DecodedEvent {
49    pub event: String,
50    pub signature: String,
51    pub contract_name: Option<String>,
52    pub args: Vec<DecodedEventArg>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct EventRegistryEntry {
57    pub contract_name: Option<String>,
58    pub topic: String,
59    pub signature: String,
60    pub event: EventAbi,
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct EventRegistry {
65    entries: BTreeMap<String, EventRegistryEntry>,
66}
67
68impl EventRegistry {
69    pub fn insert(&mut self, contract_name: Option<String>, event: EventAbi) -> SoldbResult<()> {
70        let signature = event_signature(&event);
71        let topic = event_topic(&event);
72        self.entries.insert(
73            topic.clone(),
74            EventRegistryEntry {
75                contract_name,
76                topic,
77                signature,
78                event,
79            },
80        );
81        Ok(())
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.entries.is_empty()
86    }
87
88    pub fn decode_log(&self, topics: &[String], data: &str) -> Option<DecodedEvent> {
89        let topic = topics.first()?.to_ascii_lowercase();
90        let entry = self.entries.get(&topic)?;
91        decode_event(entry, topics, data).ok()
92    }
93}
94
95pub fn parse_event_abis(input: &str) -> SoldbResult<Vec<EventAbi>> {
96    let value = serde_json::from_str::<Value>(input)
97        .map_err(|error| SoldbError::Message(format!("Invalid ABI JSON: {error}")))?;
98    let items = if let Some(items) = value.as_array() {
99        items
100    } else {
101        value.get("abi").and_then(Value::as_array).ok_or_else(|| {
102            SoldbError::Message("ABI JSON must be an array or contain an 'abi' array".to_owned())
103        })?
104    };
105
106    let mut events = Vec::new();
107    for item in items {
108        if item.get("type").and_then(Value::as_str) != Some("event") {
109            continue;
110        }
111        let Some(name) = item.get("name").and_then(Value::as_str) else {
112            continue;
113        };
114        let inputs = item
115            .get("inputs")
116            .and_then(Value::as_array)
117            .map(|inputs| {
118                inputs
119                    .iter()
120                    .filter_map(parse_event_param)
121                    .collect::<Vec<_>>()
122            })
123            .unwrap_or_default();
124        events.push(EventAbi {
125            name: name.to_owned(),
126            inputs,
127        });
128    }
129
130    Ok(events)
131}
132
133pub fn event_signature(event: &EventAbi) -> String {
134    let input_types = event
135        .inputs
136        .iter()
137        .map(canonical_event_type)
138        .collect::<Vec<_>>()
139        .join(",");
140    format!("{}({input_types})", event.name)
141}
142
143pub fn event_topic(event: &EventAbi) -> String {
144    let signature = event_signature(event);
145    format!("0x{}", bytes_to_hex(&keccak256(signature.as_bytes())))
146}
147
148fn parse_event_param(value: &Value) -> Option<EventParam> {
149    let components = value
150        .get("components")
151        .and_then(Value::as_array)
152        .map(|components| {
153            components
154                .iter()
155                .filter_map(parse_event_param)
156                .collect::<Vec<_>>()
157        })
158        .unwrap_or_default();
159
160    Some(EventParam {
161        name: value
162            .get("name")
163            .and_then(Value::as_str)
164            .unwrap_or_default()
165            .to_owned(),
166        ty: value.get("type")?.as_str()?.to_owned(),
167        indexed: value
168            .get("indexed")
169            .and_then(Value::as_bool)
170            .unwrap_or(false),
171        components,
172    })
173}
174
175fn decode_event(
176    entry: &EventRegistryEntry,
177    topics: &[String],
178    data: &str,
179) -> SoldbResult<DecodedEvent> {
180    let data_bytes = parse_hex_data(data)?;
181    let mut next_topic = 1;
182    let mut next_data_offset = 0;
183    let mut args = Vec::with_capacity(entry.event.inputs.len());
184
185    for input in &entry.event.inputs {
186        let value = if input.indexed {
187            let topic = topics.get(next_topic).ok_or_else(|| {
188                SoldbError::Message(format!("Missing indexed topic for {}", input.name))
189            })?;
190            next_topic += 1;
191            decode_indexed_topic(input, topic)?
192        } else {
193            let head_words = event_param_head_words(input)?;
194            let value = decode_param_at(input, &data_bytes, next_data_offset, 0)?;
195            next_data_offset += head_words * 32;
196            value
197        };
198        args.push(DecodedEventArg {
199            name: input.name.clone(),
200            ty: canonical_event_type(input),
201            value,
202        });
203    }
204
205    Ok(DecodedEvent {
206        event: entry.event.name.clone(),
207        signature: entry.signature.clone(),
208        contract_name: entry.contract_name.clone(),
209        args,
210    })
211}
212
213fn canonical_event_type(input: &EventParam) -> String {
214    if let Some(suffix) = input.ty.strip_prefix("tuple") {
215        let components = input
216            .components
217            .iter()
218            .map(canonical_event_type)
219            .collect::<Vec<_>>()
220            .join(",");
221        return format!("({components}){suffix}");
222    }
223    input.ty.clone()
224}
225
226fn decode_indexed_topic(input: &EventParam, topic: &str) -> SoldbResult<Value> {
227    if indexed_param_is_hashed(input) {
228        let topic = normalize_word(topic)?;
229        return Ok(json!(format!("0x{topic}")));
230    }
231    decode_static_word(&input.ty, topic)
232}
233
234fn indexed_param_is_hashed(input: &EventParam) -> bool {
235    input.ty == "string"
236        || input.ty == "bytes"
237        || parse_array_type(&input.ty).is_some()
238        || is_tuple_param(input)
239}
240
241fn decode_param_at(
242    input: &EventParam,
243    data: &[u8],
244    head_offset: usize,
245    base_offset: usize,
246) -> SoldbResult<Value> {
247    if event_param_is_dynamic(input)? {
248        let relative_offset = read_usize_word(data, head_offset, &input.name)?;
249        let offset = checked_offset(base_offset, relative_offset, &input.name)?;
250        return decode_dynamic_param(input, data, offset);
251    }
252    decode_static_param(input, data, head_offset)
253}
254
255fn decode_dynamic_param(input: &EventParam, data: &[u8], offset: usize) -> SoldbResult<Value> {
256    if input.ty == "string" {
257        let bytes = read_dynamic_bytes(data, offset, &input.name)?;
258        let value = String::from_utf8(bytes).map_err(|error| {
259            SoldbError::Message(format!(
260                "Invalid UTF-8 event string {}: {error}",
261                input.name
262            ))
263        })?;
264        return Ok(json!(value));
265    }
266    if input.ty == "bytes" {
267        let bytes = read_dynamic_bytes(data, offset, &input.name)?;
268        return Ok(json!(format!("0x{}", bytes_to_hex(&bytes))));
269    }
270
271    if let Some((base_type, fixed_len)) = parse_array_type(&input.ty) {
272        let base = array_element_param(input, base_type);
273        let (len, heads_offset, offsets_base) = if let Some(len) = fixed_len {
274            (len, offset, offset)
275        } else {
276            let len = read_usize_word(data, offset, &input.name)?;
277            let heads_offset = checked_offset(offset, 32, &input.name)?;
278            (len, heads_offset, heads_offset)
279        };
280        return decode_array_items(&base, len, data, heads_offset, offsets_base);
281    }
282
283    if is_tuple_param(input) {
284        return decode_tuple_components(&input.components, data, offset);
285    }
286
287    Err(SoldbError::Message(format!(
288        "Dynamic event ABI type '{}' is not supported",
289        input.ty
290    )))
291}
292
293fn decode_static_param(input: &EventParam, data: &[u8], offset: usize) -> SoldbResult<Value> {
294    if let Some((base_type, Some(len))) = parse_array_type(&input.ty) {
295        let base = array_element_param(input, base_type);
296        return decode_array_items(&base, len, data, offset, offset);
297    }
298
299    if is_tuple_param(input) {
300        return decode_tuple_components(&input.components, data, offset);
301    }
302
303    let word = read_word_hex(data, offset)?;
304    decode_static_word(&input.ty, &word)
305}
306
307fn decode_tuple_components(
308    components: &[EventParam],
309    data: &[u8],
310    tuple_offset: usize,
311) -> SoldbResult<Value> {
312    let mut offset = tuple_offset;
313    let mut values = Vec::with_capacity(components.len());
314    for component in components {
315        let head_words = event_param_head_words(component)?;
316        values.push(decode_param_at(component, data, offset, tuple_offset)?);
317        offset = advance_offset(offset, head_words, &component.name)?;
318    }
319    Ok(Value::Array(values))
320}
321
322fn decode_array_items(
323    base: &EventParam,
324    len: usize,
325    data: &[u8],
326    heads_offset: usize,
327    offsets_base: usize,
328) -> SoldbResult<Value> {
329    let base_head_words = event_param_head_words(base)?;
330    // `len` is read from the event payload for dynamic arrays, so it is untrusted. Every
331    // element occupies at least its head words inside `data`; reject a length that cannot
332    // possibly fit before reserving anything, otherwise a hostile log reserves gigabytes.
333    let element_head = base_head_words
334        .checked_mul(32)
335        .filter(|size| *size > 0)
336        .ok_or_else(|| {
337            SoldbError::Message(format!(
338                "Event array element '{}' has no decodable width",
339                base.ty
340            ))
341        })?;
342    let available = data.len().saturating_sub(heads_offset);
343    if element_head
344        .checked_mul(len)
345        .is_none_or(|required| required > available)
346    {
347        return Err(SoldbError::Message(format!(
348            "Event array length {len} exceeds the {available} bytes of remaining event data"
349        )));
350    }
351
352    let mut offset = heads_offset;
353    let mut values = Vec::with_capacity(len);
354    for _ in 0..len {
355        values.push(decode_param_at(base, data, offset, offsets_base)?);
356        offset = advance_offset(offset, base_head_words, &base.name)?;
357    }
358    Ok(Value::Array(values))
359}
360
361fn event_param_head_words(input: &EventParam) -> SoldbResult<usize> {
362    if event_param_is_dynamic(input)? {
363        return Ok(1);
364    }
365
366    if let Some((base_type, Some(len))) = parse_array_type(&input.ty) {
367        let base = array_element_param(input, base_type);
368        return Ok(event_param_head_words(&base)? * len);
369    }
370
371    if is_tuple_param(input) {
372        return input.components.iter().try_fold(0, |total, component| {
373            Ok(total + event_param_head_words(component)?)
374        });
375    }
376
377    Ok(1)
378}
379
380fn event_param_is_dynamic(input: &EventParam) -> SoldbResult<bool> {
381    if input.ty == "string" || input.ty == "bytes" {
382        return Ok(true);
383    }
384
385    if let Some((base_type, fixed_len)) = parse_array_type(&input.ty) {
386        if fixed_len.is_none() {
387            return Ok(true);
388        }
389        let base = array_element_param(input, base_type);
390        return event_param_is_dynamic(&base);
391    }
392
393    if is_tuple_param(input) {
394        return input
395            .components
396            .iter()
397            .try_fold(false, |dynamic, component| {
398                Ok(dynamic || event_param_is_dynamic(component)?)
399            });
400    }
401
402    Ok(false)
403}
404
405fn array_element_param(input: &EventParam, base_type: &str) -> EventParam {
406    EventParam {
407        name: input.name.clone(),
408        ty: base_type.to_owned(),
409        indexed: false,
410        components: input.components.clone(),
411    }
412}
413
414fn parse_array_type(arg_type: &str) -> Option<(&str, Option<usize>)> {
415    if !arg_type.ends_with(']') {
416        return None;
417    }
418    let open = arg_type.rfind('[')?;
419    let base_type = arg_type[..open].trim();
420    if base_type.is_empty() {
421        return None;
422    }
423
424    let length = &arg_type[open + 1..arg_type.len() - 1];
425    if length.is_empty() {
426        return Some((base_type, None));
427    }
428    Some((base_type, Some(length.parse::<usize>().ok()?)))
429}
430
431fn is_tuple_param(input: &EventParam) -> bool {
432    input.ty == "tuple" || input.ty.starts_with("tuple[")
433}
434
435fn read_dynamic_bytes(data: &[u8], offset: usize, label: &str) -> SoldbResult<Vec<u8>> {
436    let len = read_usize_word(data, offset, label)?;
437    // Both `offset` and `len` come from the payload; adding them unchecked wraps in release
438    // and produces a reversed range that panics on slicing.
439    let start = checked_offset(offset, 32, label)?;
440    let end = checked_offset(start, len, label)?;
441    data.get(start..end).map(<[u8]>::to_vec).ok_or_else(|| {
442        SoldbError::Message(format!(
443            "Event dynamic bytes for {label} exceed data length"
444        ))
445    })
446}
447
448/// Adds an untrusted ABI delta to an offset, reporting an error instead of overflowing.
449fn checked_offset(base: usize, delta: usize, label: &str) -> SoldbResult<usize> {
450    base.checked_add(delta).ok_or_else(|| {
451        SoldbError::Message(format!(
452            "Event offset for {label} overflows the address space"
453        ))
454    })
455}
456
457/// Advances a head cursor by `words` ABI words, reporting an error instead of overflowing.
458fn advance_offset(offset: usize, words: usize, label: &str) -> SoldbResult<usize> {
459    let advance = words.checked_mul(32).ok_or_else(|| {
460        SoldbError::Message(format!(
461            "Event head width for {label} overflows the address space"
462        ))
463    })?;
464    checked_offset(offset, advance, label)
465}
466
467fn read_usize_word(data: &[u8], offset: usize, label: &str) -> SoldbResult<usize> {
468    let word = read_word(data, offset)?;
469    if word[..24].iter().any(|byte| *byte != 0) {
470        return Err(SoldbError::Message(format!(
471            "Event offset or length for {label} does not fit usize"
472        )));
473    }
474    let low = u64::from_be_bytes(word[24..32].try_into().expect("8-byte slice"));
475    usize::try_from(low).map_err(|error| {
476        SoldbError::Message(format!(
477            "Event offset or length for {label} does not fit usize: {error}"
478        ))
479    })
480}
481
482fn read_word_hex(data: &[u8], offset: usize) -> SoldbResult<String> {
483    Ok(bytes_to_hex(read_word(data, offset)?))
484}
485
486fn read_word(data: &[u8], offset: usize) -> SoldbResult<&[u8]> {
487    let end = checked_offset(offset, 32, "ABI word")?;
488    data.get(offset..end)
489        .ok_or_else(|| SoldbError::Message("Event data is missing an ABI word".to_owned()))
490}
491
492fn parse_hex_data(data: &str) -> SoldbResult<Vec<u8>> {
493    let data = data.trim_start_matches("0x");
494    if data.is_empty() {
495        return Ok(Vec::new());
496    }
497    if !data.len().is_multiple_of(2) || !data.bytes().all(|byte| byte.is_ascii_hexdigit()) {
498        return Err(SoldbError::Message("Event data is not hex".to_owned()));
499    }
500
501    data.as_bytes()
502        .chunks(2)
503        .map(|chunk| {
504            let pair = std::str::from_utf8(chunk)
505                .map_err(|error| SoldbError::Message(format!("Invalid event data: {error}")))?;
506            u8::from_str_radix(pair, 16)
507                .map_err(|error| SoldbError::Message(format!("Invalid event data: {error}")))
508        })
509        .collect()
510}
511
512fn decode_static_word(arg_type: &str, word: &str) -> SoldbResult<Value> {
513    let word = normalize_word(word)?;
514    if arg_type == "address" {
515        return Ok(json!(format!("0x{}", &word[24..64])));
516    }
517    if arg_type == "bool" {
518        return Ok(json!(u128::from_str_radix(&word, 16).unwrap_or(0) != 0));
519    }
520    if arg_type.starts_with("uint") {
521        let value = u128::from_str_radix(&word, 16)
522            .map_err(|error| SoldbError::Message(format!("Invalid uint event word: {error}")))?;
523        if let Ok(value) = u64::try_from(value) {
524            return Ok(json!(value));
525        }
526        return Ok(json!(value.to_string()));
527    }
528    if arg_type == "bytes32" {
529        return Ok(json!(format!("0x{word}")));
530    }
531
532    Ok(json!(format!("0x{word}")))
533}
534
535fn normalize_word(word: &str) -> SoldbResult<String> {
536    let word = word.trim_start_matches("0x");
537    if word.len() > 64 || !word.bytes().all(|byte| byte.is_ascii_hexdigit()) {
538        return Err(SoldbError::Message(format!(
539            "Invalid ABI event word: {word}"
540        )));
541    }
542    Ok(format!("{word:0>64}").to_ascii_lowercase())
543}
544
545fn bytes_to_hex(bytes: &[u8]) -> String {
546    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
547}
548
549#[cfg(test)]
550mod tests {
551    use super::{event_signature, event_topic, parse_event_abis, EventRegistry};
552    use serde_json::json;
553
554    const BALANCE_UPDATED_ABI: &str = r#"[
555        {
556            "type": "event",
557            "name": "BalanceUpdated",
558            "inputs": [
559                {"name": "user", "type": "address", "indexed": true},
560                {"name": "newBalance", "type": "uint256", "indexed": false}
561            ]
562        },
563        {"type": "function", "name": "ignored", "inputs": []}
564    ]"#;
565
566    const COMPLEX_EVENT_ABI: &str = r#"[
567        {
568            "type": "event",
569            "name": "Complex",
570            "inputs": [
571                {"name": "sender", "type": "address", "indexed": true},
572                {"name": "message", "type": "string", "indexed": false},
573                {"name": "payload", "type": "bytes", "indexed": false},
574                {"name": "values", "type": "uint256[]", "indexed": false},
575                {"name": "labels", "type": "string[2]", "indexed": false},
576                {
577                    "name": "item",
578                    "type": "tuple",
579                    "indexed": false,
580                    "components": [
581                        {"name": "count", "type": "uint256"},
582                        {"name": "note", "type": "string"}
583                    ]
584                },
585                {
586                    "name": "nested",
587                    "type": "tuple",
588                    "indexed": false,
589                    "components": [
590                        {"name": "count", "type": "uint256"},
591                        {
592                            "name": "inner",
593                            "type": "tuple",
594                            "components": [
595                                {"name": "target", "type": "address"},
596                                {"name": "label", "type": "string"}
597                            ]
598                        }
599                    ]
600                },
601                {"name": "tag", "type": "string", "indexed": true}
602            ]
603        }
604    ]"#;
605
606    const BLOB_ABI: &str = r#"[
607        {
608            "type": "event",
609            "name": "Blob",
610            "inputs": [{"name": "payload", "type": "bytes", "indexed": false}]
611        }
612    ]"#;
613
614    const NUMBERS_ABI: &str = r#"[
615        {
616            "type": "event",
617            "name": "Numbers",
618            "inputs": [{"name": "values", "type": "uint256[]", "indexed": false}]
619        }
620    ]"#;
621
622    fn registry_for(abi: &str) -> (EventRegistry, String) {
623        let events = parse_event_abis(abi).expect("parse abi");
624        let topic = event_topic(&events[0]);
625        let mut registry = EventRegistry::default();
626        registry
627            .insert(None, events[0].clone())
628            .expect("insert event");
629        (registry, topic)
630    }
631
632    #[test]
633    fn rejects_dynamic_length_that_overflows_the_offset() {
634        let (registry, topic) = registry_for(BLOB_ABI);
635        // The head word points at offset 32, where the length word is `u64::MAX`. Adding
636        // that to the data start used to overflow: a panic in debug, and in release a
637        // wrapped end offset that produced a reversed slice range.
638        let data = format!("0x{:064x}{}{}", 32u64, "0".repeat(48), "f".repeat(16));
639
640        assert!(registry.decode_log(&[topic], &data).is_none());
641    }
642
643    #[test]
644    fn rejects_array_length_larger_than_the_event_data() {
645        let (registry, topic) = registry_for(NUMBERS_ABI);
646        // A one-element payload that claims to hold 2^40 elements. Reserving that many
647        // `Value`s up front is an immediate multi-terabyte allocation.
648        let data = format!("0x{:064x}{:064x}", 32u64, 1u64 << 40);
649
650        assert!(registry.decode_log(&[topic], &data).is_none());
651    }
652
653    #[test]
654    fn decodes_a_well_formed_dynamic_array() {
655        let (registry, topic) = registry_for(NUMBERS_ABI);
656        let data = format!("0x{:064x}{:064x}{:064x}{:064x}", 32u64, 2u64, 7u64, 8u64);
657
658        let decoded = registry
659            .decode_log(&[topic], &data)
660            .expect("decode dynamic array");
661
662        assert_eq!(decoded.args[0].value, json!([7, 8]));
663    }
664
665    #[test]
666    fn parses_event_abis_from_array() {
667        let events = parse_event_abis(BALANCE_UPDATED_ABI).expect("parse abi");
668
669        assert_eq!(events.len(), 1);
670        assert_eq!(
671            event_signature(&events[0]),
672            "BalanceUpdated(address,uint256)"
673        );
674        assert_eq!(event_topic(&events[0]).len(), 66);
675    }
676
677    #[test]
678    fn decodes_static_event_arguments() {
679        let event = parse_event_abis(BALANCE_UPDATED_ABI)
680            .expect("parse abi")
681            .remove(0);
682        let topic = event_topic(&event);
683        let mut registry = EventRegistry::default();
684        registry
685            .insert(Some("TestContract".to_owned()), event)
686            .expect("insert event");
687
688        let user = format!(
689            "0x{}f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
690            "0".repeat(24)
691        );
692        let data = format!("0x{:064x}", 99);
693        let decoded = registry
694            .decode_log(&[topic, user], &data)
695            .expect("decode event");
696
697        assert_eq!(decoded.contract_name.as_deref(), Some("TestContract"));
698        assert_eq!(decoded.signature, "BalanceUpdated(address,uint256)");
699        assert_eq!(
700            decoded.args[0].value,
701            "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
702        );
703        assert_eq!(decoded.args[1].value, 99);
704    }
705
706    #[test]
707    fn decodes_dynamic_arrays_and_tuple_event_arguments() {
708        let event = parse_event_abis(COMPLEX_EVENT_ABI)
709            .expect("parse abi")
710            .remove(0);
711        assert_eq!(
712            event_signature(&event),
713            "Complex(address,string,bytes,uint256[],string[2],(uint256,string),(uint256,(address,string)),string)"
714        );
715
716        let topic = event_topic(&event);
717        let mut registry = EventRegistry::default();
718        registry.insert(None, event).expect("insert event");
719
720        let sender = format!(
721            "0x{}f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
722            "0".repeat(24)
723        );
724        let indexed_tag_hash = format!("0x{}", "aa".repeat(32));
725        let data = concat!(
726            "0x",
727            "00000000000000000000000000000000000000000000000000000000000000c0",
728            "0000000000000000000000000000000000000000000000000000000000000100",
729            "0000000000000000000000000000000000000000000000000000000000000140",
730            "00000000000000000000000000000000000000000000000000000000000001a0",
731            "0000000000000000000000000000000000000000000000000000000000000260",
732            "00000000000000000000000000000000000000000000000000000000000002e0",
733            "0000000000000000000000000000000000000000000000000000000000000005",
734            "68656c6c6f000000000000000000000000000000000000000000000000000000",
735            "0000000000000000000000000000000000000000000000000000000000000002",
736            "0102000000000000000000000000000000000000000000000000000000000000",
737            "0000000000000000000000000000000000000000000000000000000000000002",
738            "0000000000000000000000000000000000000000000000000000000000000007",
739            "0000000000000000000000000000000000000000000000000000000000000008",
740            "0000000000000000000000000000000000000000000000000000000000000040",
741            "0000000000000000000000000000000000000000000000000000000000000080",
742            "0000000000000000000000000000000000000000000000000000000000000001",
743            "6100000000000000000000000000000000000000000000000000000000000000",
744            "0000000000000000000000000000000000000000000000000000000000000002",
745            "6262000000000000000000000000000000000000000000000000000000000000",
746            "0000000000000000000000000000000000000000000000000000000000000005",
747            "0000000000000000000000000000000000000000000000000000000000000040",
748            "0000000000000000000000000000000000000000000000000000000000000002",
749            "6869000000000000000000000000000000000000000000000000000000000000",
750            "0000000000000000000000000000000000000000000000000000000000000009",
751            "0000000000000000000000000000000000000000000000000000000000000040",
752            "0000000000000000000000000000000000000000000000000000000000000002",
753            "0000000000000000000000000000000000000000000000000000000000000040",
754            "0000000000000000000000000000000000000000000000000000000000000006",
755            "6e65737465640000000000000000000000000000000000000000000000000000"
756        );
757
758        let decoded = registry
759            .decode_log(&[topic, sender, indexed_tag_hash.clone()], data)
760            .expect("decode dynamic event");
761
762        assert_eq!(
763            decoded.args[0].value,
764            "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
765        );
766        assert_eq!(decoded.args[1].value, "hello");
767        assert_eq!(decoded.args[2].value, "0x0102");
768        assert_eq!(decoded.args[3].value, json!([7, 8]));
769        assert_eq!(decoded.args[4].value, json!(["a", "bb"]));
770        assert_eq!(decoded.args[5].value, json!([5, "hi"]));
771        assert_eq!(
772            decoded.args[6].value,
773            json!([9, ["0x0000000000000000000000000000000000000002", "nested"]])
774        );
775        assert_eq!(decoded.args[7].value, indexed_tag_hash);
776    }
777
778    #[test]
779    fn parses_event_abis_from_compiler_json_object() {
780        let wrapped = format!(r#"{{"abi": {BALANCE_UPDATED_ABI}}}"#);
781        let events = parse_event_abis(&wrapped).expect("parse wrapped abi");
782
783        assert_eq!(events.len(), 1);
784        assert_eq!(events[0].inputs[0].name, "user");
785    }
786}