Skip to main content

thruput_event/
emitted_event.rs

1use crate::{Event, Topic};
2use thruput_xdr::event::TxEmittedEvent;
3
4/// Size of each topic in serialized event data
5pub const TOPIC_SIZE: usize = 32;
6
7/// A view over a raw `TxEmittedEvent` providing typed access to
8/// topics and the XDR-encoded payload.
9pub struct EmittedEvent<'a>(&'a TxEmittedEvent);
10
11impl<'a> EmittedEvent<'a> {
12    /// Byte length of the topics region, or `None` if the event is malformed:
13    /// either it claims more topics than the protocol allows, or the claimed
14    /// topics don't fit in `serialized_event`. The cap check also keeps the
15    /// `num_topics * TOPIC_SIZE` multiplication from overflowing `usize` on
16    /// 32-bit guest targets.
17    fn topics_byte_len(&self) -> Option<usize> {
18        let event = self.0;
19        if event.num_topics > thruput_xdr::event::kMaxTopics {
20            return None;
21        }
22        let extent = TOPIC_SIZE * event.num_topics as usize;
23        if extent > event.serialized_event.len() {
24            return None;
25        }
26        Some(extent)
27    }
28
29    /// The XDR-encoded payload (everything after the topics region), or `None`
30    /// if the event is malformed (claims more topics than the protocol allows,
31    /// or the claimed topics don't fit in `serialized_event`).
32    ///
33    /// Returning `None` rather than silently clamping to an empty slice keeps a
34    /// corrupt event from decoding as a valid zero-field payload, and matches
35    /// [`topic`](Self::topic)'s out-of-bounds behavior.
36    pub fn payload(&self) -> Option<&'a [u8]> {
37        let extent = self.topics_byte_len()?;
38        Some(&self.0.serialized_event[extent..])
39    }
40
41    pub fn topic(&self, index: u32) -> Option<&'a Topic> {
42        let event = self.0;
43
44        // A malformed event has no valid topics (consistent with `payload`).
45        // This also bounds `num_topics` to the cap, so the offset arithmetic
46        // below cannot overflow.
47        self.topics_byte_len()?;
48
49        if index >= event.num_topics {
50            return None;
51        }
52
53        let topic_offset = TOPIC_SIZE * index as usize;
54        let topic_extent = topic_offset + TOPIC_SIZE;
55
56        let topic_bytes = &event.serialized_event[topic_offset..topic_extent];
57
58        let topic: &[u8; TOPIC_SIZE] = topic_bytes.try_into().unwrap();
59
60        Some(topic)
61    }
62
63    pub fn num_topics(&self) -> u32 {
64        self.0.num_topics
65    }
66
67    /// Access the underlying raw event.
68    pub fn raw(&self) -> &'a TxEmittedEvent {
69        self.0
70    }
71}
72
73impl<'a> From<&'a TxEmittedEvent> for EmittedEvent<'a> {
74    fn from(event: &'a TxEmittedEvent) -> Self {
75        Self(event)
76    }
77}
78
79/// Why constructing a `TypedEvent` from a raw event failed. Internal:
80/// surfaced only through [`TypedEvent::new`]'s panic message (the `Display`
81/// texts below are that message and must not change) or discarded by
82/// [`TypedEvent::try_new`].
83enum BuildError {
84    /// The event's claimed topics don't fit its body.
85    Malformed { num_topics: u32, body_len: usize },
86    /// Topic 0 doesn't match `E`'s static discriminant.
87    SignatureMismatch {
88        actual: Option<Topic>,
89        expected: &'static Topic,
90        type_name: &'static str,
91    },
92    /// The event's topic count doesn't match `E`'s declared static count.
93    TopicCountMismatch {
94        actual: u32,
95        expected: u32,
96        type_name: &'static str,
97    },
98    /// The payload bytes don't decode as `E::Payload`.
99    Decode(xdrs::ReadError),
100}
101
102impl core::fmt::Display for BuildError {
103    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104        match self {
105            BuildError::Malformed {
106                num_topics,
107                body_len,
108            } => write!(
109                f,
110                "TypedEvent::new: malformed event — {} topics do not fit a \
111                 {}-byte body",
112                num_topics, body_len,
113            ),
114            BuildError::SignatureMismatch {
115                actual,
116                expected,
117                type_name,
118            } => write!(
119                f,
120                "TypedEvent::new: event type mismatch — topic 0 {:?} does \
121                 not match the expected discriminant {:?} for {}",
122                actual, expected, type_name,
123            ),
124            BuildError::TopicCountMismatch {
125                actual,
126                expected,
127                type_name,
128            } => write!(
129                f,
130                "TypedEvent::new: topic count mismatch — event has {} \
131                 topics but {} declares {}",
132                actual, type_name, expected,
133            ),
134            BuildError::Decode(e) => {
135                write!(f, "failed to decode event payload: {}", e)
136            }
137        }
138    }
139}
140
141/// A typed view over an emitted event, combining topic access with
142/// a pre-decoded payload.
143pub struct TypedEvent<'a, E: Event> {
144    emitted: EmittedEvent<'a>,
145    payload: E::Payload<'a>,
146}
147
148impl<'a, E: Event> TypedEvent<'a, E> {
149    /// Decode a raw event into a typed view, or say why it isn't an `E`.
150    /// Shared engine behind [`new`](Self::new) (panics with the error's
151    /// `Display` text) and [`try_new`](Self::try_new) (discards it).
152    fn build(raw: &'a TxEmittedEvent) -> Result<Self, BuildError> {
153        let emitted = EmittedEvent::from(raw);
154        // Reject a malformed event up front, before the signature/payload
155        // checks (which would otherwise report a misleading "type mismatch"
156        // or decode a silently-empty payload).
157        let payload_bytes =
158            emitted.payload().ok_or(BuildError::Malformed {
159                num_topics: raw.num_topics,
160                body_len: raw.serialized_event.len(),
161            })?;
162        if let Some(expected) = E::signature() {
163            match emitted.topic(0) {
164                Some(actual) if actual == expected => {}
165                other => {
166                    return Err(BuildError::SignatureMismatch {
167                        actual: other.copied(),
168                        expected,
169                        type_name: core::any::type_name::<E>(),
170                    });
171                }
172            }
173        }
174        // After the signature check, so a different-type event still reports
175        // "type mismatch" rather than a count difference.
176        if raw.num_topics != E::num_topics() {
177            return Err(BuildError::TopicCountMismatch {
178                actual: raw.num_topics,
179                expected: E::num_topics(),
180                type_name: core::any::type_name::<E>(),
181            });
182        }
183        let payload =
184            E::decode_payload(payload_bytes).map_err(BuildError::Decode)?;
185        Ok(Self { emitted, payload })
186    }
187
188    /// Decode a raw event into a typed view.
189    ///
190    /// When `E` declares a static topic-0 discriminant (via
191    /// [`Event::signature`]), the raw event's topic 0 is verified against it
192    /// first, so a payload that merely *shares a shape* with `E` cannot be
193    /// silently mistyped as `E`. Panics on a discriminant mismatch, a topic
194    /// count differing from [`Event::num_topics`], or payload decode
195    /// failure.
196    #[track_caller]
197    pub fn new(raw: &'a TxEmittedEvent) -> Self {
198        match Self::build(raw) {
199            Ok(typed) => typed,
200            Err(e) => panic!("{}", e),
201        }
202    }
203
204    /// Non-panicking twin of [`new`](Self::new): `Some` only if the event is
205    /// well-formed, its topic 0 matches `E`'s declared discriminant (when
206    /// one exists), its topic count matches [`Event::num_topics`], and its
207    /// payload decodes as `E::Payload`. The probe for scanning event streams
208    /// that contain foreign events.
209    pub fn try_new(raw: &'a TxEmittedEvent) -> Option<Self> {
210        Self::build(raw).ok()
211    }
212
213    /// The decoded payload.
214    pub fn payload(&self) -> &E::Payload<'a> {
215        &self.payload
216    }
217
218    /// Get topic at the given index.
219    pub fn topic(&self, index: u32) -> Option<&'a Topic> {
220        self.emitted.topic(index)
221    }
222
223    /// Number of topics.
224    pub fn num_topics(&self) -> u32 {
225        self.emitted.num_topics()
226    }
227
228    /// Access the underlying untyped emitted event.
229    pub fn raw(&self) -> &EmittedEvent<'a> {
230        &self.emitted
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use thruput_xdr::event::TxEmittedEvent;
238    use thruput_xdr::types::ContractAddress;
239
240    // `&'static [u8]` so this builds whether or not BytesM's `alloc` backing is
241    // enabled for the standalone test build.
242    fn raw_event(num_topics: u32, body: &'static [u8]) -> TxEmittedEvent {
243        TxEmittedEvent {
244            contract_address: ContractAddress::default(),
245            num_topics,
246            serialized_event: body.try_into().unwrap(),
247        }
248    }
249
250    #[test]
251    fn payload_is_none_when_topics_overrun_buffer() {
252        // Claims 3 topics (96 bytes) but the body is only 40.
253        static BODY: [u8; 40] = [0u8; 40];
254        let raw = raw_event(3, &BODY);
255        let ev = EmittedEvent::from(&raw);
256        assert_eq!(ev.payload(), None);
257        // topic() agrees — no silent disagreement between the two accessors.
258        assert_eq!(ev.topic(0), None);
259    }
260
261    #[test]
262    fn payload_is_none_when_num_topics_exceeds_cap() {
263        // 4 > kMaxTopics (3): malformed regardless of body length.
264        static BODY: [u8; 200] = [0u8; 200];
265        let raw = raw_event(4, &BODY);
266        let ev = EmittedEvent::from(&raw);
267        assert_eq!(ev.payload(), None);
268    }
269
270    #[test]
271    fn payload_is_some_for_well_formed_event() {
272        // One topic (32 bytes) followed by a 4-byte payload.
273        static BODY: [u8; 36] = {
274            let mut b = [0u8; 36];
275            b[32] = 1;
276            b[33] = 2;
277            b[34] = 3;
278            b[35] = 4;
279            b
280        };
281        let raw = raw_event(1, &BODY);
282        let ev = EmittedEvent::from(&raw);
283        assert_eq!(ev.payload(), Some(&[1u8, 2, 3, 4][..]));
284        assert!(ev.topic(0).is_some());
285        assert_eq!(ev.topic(1), None);
286    }
287
288    const SIG: Topic = [0xAB; 32];
289
290    /// Minimal Event impl with a static signature and a 4-byte big-endian
291    /// u32 payload. Decode is hand-rolled so these no_std tests don't need
292    /// cxs encode (which requires alloc).
293    struct SigEvent;
294
295    impl xdrs::WriteXDR for SigEvent {
296        fn write_xdr<F, W: xdrs::Writer<F>>(
297            &self,
298            _w: &mut W,
299        ) -> xdrs::WriteResult<()> {
300            Ok(())
301        }
302
303        unsafe fn write_xdr_unchecked<F, W: xdrs::WriterUnchecked<F>>(
304            &self,
305            _w: &mut W,
306        ) {
307        }
308    }
309
310    impl Event for SigEvent {
311        type Payload<'a> = u32;
312
313        fn num_topics() -> u32 {
314            1
315        }
316
317        fn signature() -> Option<&'static Topic> {
318            Some(&SIG)
319        }
320
321        fn decode_payload(bytes: &[u8]) -> Result<u32, xdrs::ReadError> {
322            match <[u8; 4]>::try_from(bytes) {
323                Ok(b) => Ok(u32::from_be_bytes(b)),
324                Err(_) => Err(xdrs::ReadError::source_empty(4, bytes.len())),
325            }
326        }
327    }
328
329    /// Event whose payload decode accepts any bytes — isolates the
330    /// topic-count check: without it, a wrong-count event with a matching
331    /// signature would still decode successfully and match.
332    struct LenientEvent;
333
334    impl xdrs::WriteXDR for LenientEvent {
335        fn write_xdr<F, W: xdrs::Writer<F>>(
336            &self,
337            _w: &mut W,
338        ) -> xdrs::WriteResult<()> {
339            Ok(())
340        }
341
342        unsafe fn write_xdr_unchecked<F, W: xdrs::WriterUnchecked<F>>(
343            &self,
344            _w: &mut W,
345        ) {
346        }
347    }
348
349    impl Event for LenientEvent {
350        type Payload<'a> = usize;
351
352        fn num_topics() -> u32 {
353            1
354        }
355
356        fn signature() -> Option<&'static Topic> {
357            Some(&SIG)
358        }
359
360        fn decode_payload(bytes: &[u8]) -> Result<usize, xdrs::ReadError> {
361            Ok(bytes.len())
362        }
363    }
364
365    #[test]
366    fn try_new_decodes_matching_event() {
367        // Topic 0 = SIG (32 × 0xAB), payload = 7u32 big-endian.
368        static BODY: [u8; 36] = {
369            let mut b = [0xABu8; 36];
370            b[32] = 0;
371            b[33] = 0;
372            b[34] = 0;
373            b[35] = 7;
374            b
375        };
376        let raw = raw_event(1, &BODY);
377        let typed =
378            TypedEvent::<SigEvent>::try_new(&raw).expect("should decode");
379        assert_eq!(*typed.payload(), 7);
380        assert_eq!(typed.topic(0), Some(&SIG));
381    }
382
383    #[test]
384    fn try_new_rejects_signature_mismatch() {
385        // Topic 0 is 32 × 0xCD ≠ SIG; payload bytes are valid u32.
386        static BODY: [u8; 36] = [0xCD; 36];
387        let raw = raw_event(1, &BODY);
388        assert!(TypedEvent::<SigEvent>::try_new(&raw).is_none());
389    }
390
391    #[test]
392    fn try_new_rejects_malformed_event() {
393        // Claims 3 topics (96 bytes) but the body is only 40.
394        static BODY: [u8; 40] = [0u8; 40];
395        let raw = raw_event(3, &BODY);
396        assert!(TypedEvent::<SigEvent>::try_new(&raw).is_none());
397    }
398
399    #[test]
400    fn try_new_rejects_undecodable_payload() {
401        // Topic 0 = SIG but only 3 payload bytes — decode_payload fails.
402        static BODY: [u8; 35] = [0xAB; 35];
403        let raw = raw_event(1, &BODY);
404        assert!(TypedEvent::<SigEvent>::try_new(&raw).is_none());
405    }
406
407    #[test]
408    #[should_panic(expected = "event type mismatch")]
409    fn new_still_panics_on_signature_mismatch() {
410        static BODY: [u8; 36] = [0xCD; 36];
411        let raw = raw_event(1, &BODY);
412        let _ = TypedEvent::<SigEvent>::new(&raw);
413    }
414
415    #[test]
416    fn try_new_rejects_topic_count_mismatch() {
417        // Two topics (both 0xAB, so topic 0 = SIG) + 4 payload bytes, but
418        // LenientEvent declares exactly 1 topic. Its payload decode accepts
419        // anything, so only the count check can reject this.
420        static BODY: [u8; 68] = [0xAB; 68];
421        let raw = raw_event(2, &BODY);
422        assert!(TypedEvent::<LenientEvent>::try_new(&raw).is_none());
423
424        // Sanity: with the declared count the same body prefix matches.
425        static OK_BODY: [u8; 36] = [0xAB; 36];
426        let ok = raw_event(1, &OK_BODY);
427        assert!(TypedEvent::<LenientEvent>::try_new(&ok).is_some());
428    }
429
430    #[test]
431    #[should_panic(expected = "topic count mismatch")]
432    fn new_panics_on_topic_count_mismatch() {
433        static BODY: [u8; 68] = [0xAB; 68];
434        let raw = raw_event(2, &BODY);
435        let _ = TypedEvent::<LenientEvent>::new(&raw);
436    }
437}