alloy_dyn_abi/dynamic/
event.rs

1use crate::{DynSolType, DynSolValue, Error, Result};
2use alloc::vec::Vec;
3use alloy_primitives::{IntoLogData, Log, LogData, B256};
4
5/// A dynamic ABI event.
6///
7/// This is a representation of a Solidity event, which can be used to decode
8/// logs.
9#[derive(Clone, Debug, PartialEq)]
10pub struct DynSolEvent {
11    /// The event signature hash, if any.
12    pub(crate) topic_0: Option<B256>,
13    /// The indexed types.
14    pub(crate) indexed: Vec<DynSolType>,
15    /// The un-indexed types.
16    pub(crate) body: DynSolType,
17}
18
19impl DynSolEvent {
20    /// Creates a new event, without length-checking the indexed, or ensuring
21    /// the body is a tuple. This allows creation of invalid events.
22    pub const fn new_unchecked(
23        topic_0: Option<B256>,
24        indexed: Vec<DynSolType>,
25        body: DynSolType,
26    ) -> Self {
27        Self { topic_0, indexed, body }
28    }
29
30    /// Creates a new event.
31    ///
32    /// Checks that the indexed length is less than or equal to 4, and that the
33    /// body is a tuple.
34    pub fn new(topic_0: Option<B256>, indexed: Vec<DynSolType>, body: DynSolType) -> Option<Self> {
35        let topics = indexed.len() + topic_0.is_some() as usize;
36        if topics > 4 || body.as_tuple().is_none() {
37            return None;
38        }
39        Some(Self::new_unchecked(topic_0, indexed, body))
40    }
41
42    /// True if anonymous.
43    pub const fn is_anonymous(&self) -> bool {
44        self.topic_0.is_none()
45    }
46
47    /// Decode the event from the given log info.
48    pub fn decode_log_parts<I>(
49        &self,
50        topics: I,
51        data: &[u8],
52        validate: bool,
53    ) -> Result<DecodedEvent>
54    where
55        I: IntoIterator<Item = B256>,
56    {
57        let mut topics = topics.into_iter();
58        let num_topics = self.indexed.len() + !self.is_anonymous() as usize;
59        if validate {
60            match topics.size_hint() {
61                (n, Some(m)) if n == m && n != num_topics => {
62                    return Err(Error::TopicLengthMismatch { expected: num_topics, actual: n })
63                }
64                _ => {}
65            }
66        }
67
68        // skip event hash if not anonymous
69        if !self.is_anonymous() {
70            let t = topics.next();
71            // Validate only if requested
72            if validate {
73                match t {
74                    Some(sig) => {
75                        let expected = self.topic_0.expect("not anonymous");
76                        if sig != expected {
77                            return Err(Error::EventSignatureMismatch { expected, actual: sig });
78                        }
79                    }
80                    None => {
81                        return Err(Error::TopicLengthMismatch { expected: num_topics, actual: 0 })
82                    }
83                }
84            }
85        }
86
87        let indexed = self
88            .indexed
89            .iter()
90            .zip(topics.by_ref().take(self.indexed.len()))
91            .map(|(ty, topic)| {
92                let value = ty.decode_event_topic(topic);
93                Ok(value)
94            })
95            .collect::<Result<_>>()?;
96
97        let body = self.body.abi_decode_sequence(data)?.into_fixed_seq().expect("body is a tuple");
98
99        if validate {
100            let remaining = topics.count();
101            if remaining > 0 {
102                return Err(Error::TopicLengthMismatch {
103                    expected: num_topics,
104                    actual: num_topics + remaining,
105                });
106            }
107        }
108
109        Ok(DecodedEvent { selector: self.topic_0, indexed, body })
110    }
111
112    /// Decode the event from the given log info.
113    pub fn decode_log_data(&self, log: &LogData, validate: bool) -> Result<DecodedEvent> {
114        self.decode_log_parts(log.topics().iter().copied(), &log.data, validate)
115    }
116
117    /// Get the selector for this event, if any.
118    pub const fn topic_0(&self) -> Option<B256> {
119        self.topic_0
120    }
121
122    /// Get the indexed types.
123    pub fn indexed(&self) -> &[DynSolType] {
124        &self.indexed
125    }
126
127    /// Get the un-indexed types.
128    pub fn body(&self) -> &[DynSolType] {
129        self.body.as_tuple().expect("body is a tuple")
130    }
131}
132
133/// A decoded dynamic ABI event.
134#[derive(Clone, Debug, PartialEq)]
135pub struct DecodedEvent {
136    /// The hashes event_signature (if any)
137    #[doc(alias = "topic_0")]
138    pub selector: Option<B256>,
139    /// The indexed values, in order.
140    pub indexed: Vec<DynSolValue>,
141    /// The un-indexed values, in order.
142    pub body: Vec<DynSolValue>,
143}
144
145impl DecodedEvent {
146    /// True if anonymous. False if not.
147    pub const fn is_anonymous(&self) -> bool {
148        self.selector.is_none()
149    }
150
151    /// Re-encode the event into a [`LogData`]
152    pub fn encode_log_data(&self) -> LogData {
153        debug_assert!(
154            self.indexed.len() + !self.is_anonymous() as usize <= 4,
155            "too many indexed values"
156        );
157
158        LogData::new_unchecked(
159            self.selector
160                .iter()
161                .copied()
162                .chain(self.indexed.iter().flat_map(DynSolValue::as_word))
163                .collect(),
164            DynSolValue::encode_seq(&self.body).into(),
165        )
166    }
167
168    /// Transform a [`Log`] containing this event into a [`Log`] containing
169    /// [`LogData`].
170    pub fn encode_log(log: Log<Self>) -> Log<LogData> {
171        Log { address: log.address, data: log.data.encode_log_data() }
172    }
173}
174
175impl IntoLogData for DecodedEvent {
176    fn to_log_data(&self) -> LogData {
177        self.encode_log_data()
178    }
179
180    fn into_log_data(self) -> LogData {
181        self.encode_log_data()
182    }
183}
184
185#[cfg(test)]
186mod test {
187    use super::*;
188    use alloy_primitives::{address, b256, bytes, U256};
189
190    #[test]
191    fn it_decodes_a_simple_log() {
192        let log = LogData::new_unchecked(vec![], U256::ZERO.to_be_bytes_vec().into());
193        let event = DynSolEvent {
194            topic_0: None,
195            indexed: vec![],
196            body: DynSolType::Tuple(vec![DynSolType::Uint(256)]),
197        };
198        event.decode_log_data(&log, true).unwrap();
199    }
200
201    #[test]
202    fn it_decodes_logs_with_indexed_params() {
203        let t0 = b256!("0xcf74b4e62f836eeedcd6f92120ffb5afea90e6fa490d36f8b81075e2a7de0cf7");
204        let log: LogData = LogData::new_unchecked(
205            vec![t0, b256!("0x0000000000000000000000000000000000000000000000000000000000012321")],
206            bytes!(
207                "
208			    0000000000000000000000000000000000000000000000000000000000012345
209			    0000000000000000000000000000000000000000000000000000000000054321
210			    "
211            ),
212        );
213        let event = DynSolEvent {
214            topic_0: Some(t0),
215            indexed: vec![DynSolType::Address],
216            body: DynSolType::Tuple(vec![DynSolType::Tuple(vec![
217                DynSolType::Address,
218                DynSolType::Address,
219            ])]),
220        };
221
222        let decoded = event.decode_log_data(&log, true).unwrap();
223        assert_eq!(
224            decoded.indexed,
225            vec![DynSolValue::Address(address!("0x0000000000000000000000000000000000012321"))]
226        );
227
228        let encoded = decoded.encode_log_data();
229        assert_eq!(encoded, log);
230    }
231}