Skip to main content

thruput_sdk/
event.rs

1use crate::call::with_scratch;
2use crate::exn::*;
3use crate::internal::encode_scratch;
4use crate::re_throw::OrSyscallThrow;
5use crate::sys::{self, SyscallNo};
6
7// Re-export core event types from thruput-event
8pub use thruput_event::{EmittedEvent, Event, Topic, TopicSrc, TypedEvent};
9
10/// Trait for types that can be converted into an event topic.
11///
12/// `u32`, `u64`, `u128` and `U256` use zero-extended, 32-byte big-endian encoding.
13/// `i32`, `i64`, `i128` and `I256` use sign-extended, 32-byte big-endian encoding.
14/// `bool` uses the same representation for 0 or 1. Other primitive integer types
15/// are not supported as topics.
16/// Byte arrays of length 32, addresses and hashes borrow their bytes directly.
17/// Strings and byte slices are always XDR-encoded and SHA-256 hashed.
18#[diagnostic::on_unimplemented(
19    message = "`{Self}` cannot be an event topic",
20    // No list of accepted types: the impls are explicit, so rustc prints them
21    // itself. A hand-kept copy only drifts.
22    note = "either widen the field to a listed type, or drop `#[topic]` and let it ride in the event payload, where it stays readable"
23)]
24pub trait IntoTopic {
25    #[allow(clippy::wrong_self_convention)]
26    fn into_topic(&self) -> TopicSrc<'_>;
27}
28
29impl IntoTopic for [u8; 32] {
30    fn into_topic(&self) -> TopicSrc<'_> {
31        TopicSrc::Shared(self)
32    }
33}
34
35/// Maximum number of topics per event
36pub const EVENT_MAX_TOPICS: u32 = thruput_xdr::event::kMaxTopics;
37
38/// Maximum size of serialized event data (including the topics)
39pub const EVENT_MAX_DATA: u32 = thruput_xdr::event::kMaxEventLength;
40
41/// Convert any `WriteXDR` value into a topic by XDR-encoding and hashing.
42pub fn hash_to_topic<T: crate::WriteXDR>(val: &T) -> Topic {
43    with_scratch(|buf| {
44        encode_scratch(buf, val).unwrap_or_else(|e| encoding_event_throw(e));
45        crate::crypto::sha256(buf.as_slice()).into_inner()
46    })
47}
48
49// Widen before encoding to reuse U256/I256's zero/sign extension and byte order.
50macro_rules! impl_into_topic_direct {
51    ($wide:ty; $($ty:ty),*) => {
52        $(
53            impl IntoTopic for $ty {
54                fn into_topic(&self) -> TopicSrc<'_> {
55                    let value: $wide = (*self).into();
56                    TopicSrc::Owned(value.to_be_bytes())
57                }
58            }
59        )*
60    };
61}
62
63impl_into_topic_direct!(crate::U256; u128, u64, u32, bool);
64impl_into_topic_direct!(crate::I256; i128, i64, i32);
65
66impl IntoTopic for str {
67    fn into_topic(&self) -> TopicSrc<'_> {
68        TopicSrc::Owned(hash_to_topic(&self))
69    }
70}
71
72impl IntoTopic for [u8] {
73    fn into_topic(&self) -> TopicSrc<'_> {
74        TopicSrc::Owned(hash_to_topic(&self))
75    }
76}
77
78// The note above names `ContractAddress` and `SHA256Hash` as valid topics, but
79// they only reached an impl through auto-deref on the method call, so a
80// `T: IntoTopic` bound rejected them. Real impls make the documented set true
81// and let the derive state the bound.
82impl IntoTopic for crate::types::ContractAddress {
83    fn into_topic(&self) -> TopicSrc<'_> {
84        (**self).into_topic()
85    }
86}
87
88impl IntoTopic for crate::types::SHA256Hash {
89    fn into_topic(&self) -> TopicSrc<'_> {
90        (**self).into_topic()
91    }
92}
93
94impl IntoTopic for crate::U256 {
95    fn into_topic(&self) -> TopicSrc<'_> {
96        TopicSrc::Owned(self.to_be_bytes())
97    }
98}
99
100impl IntoTopic for crate::I256 {
101    fn into_topic(&self) -> TopicSrc<'_> {
102        TopicSrc::Owned(self.to_be_bytes())
103    }
104}
105
106impl<T: IntoTopic + ?Sized> IntoTopic for &T {
107    fn into_topic(&self) -> TopicSrc<'_> {
108        (**self).into_topic()
109    }
110}
111
112/// Emit an event.
113///
114/// Thruput currently supports up to three total topics per event. Derived
115/// events typically reserve topic 0 for the event signature, leaving up to two
116/// additional user-declared `#[topic]` fields.
117///
118/// Topic fields use their [`IntoTopic`] representation: supported scalar types
119/// are encoded directly, while strings and byte slices are hashed.
120pub fn log_event<E: Event>(event: &E) {
121    let n = E::num_topics();
122
123    if n > EVENT_MAX_TOPICS {
124        encoding_event_throw(format_args!(
125            "event declared {n} topics, but the maximum is {EVENT_MAX_TOPICS}"
126        ));
127    }
128
129    let topic = |i| {
130        if i < n {
131            let t = event.topic(i);
132            debug_assert!(
133                t.is_some(),
134                "event declared {n} topics, but topic({i}) returned None"
135            );
136            Some(t?.into())
137        } else {
138            None
139        }
140    };
141
142    let topic0: Option<Topic> = topic(0);
143    let topic1: Option<Topic> = topic(1);
144    let topic2: Option<Topic> = topic(2);
145
146    with_scratch(|buf| {
147        encode_scratch(buf, event).unwrap_or_else(|e| encoding_event_throw(e));
148        sys::uninit_LogEvent(
149            topic0.as_ref(),
150            topic1.as_ref(),
151            topic2.as_ref(),
152            buf.as_slice(),
153        )
154        .or_syscall_throw(SyscallNo::kLogEvent);
155    });
156}