Skip to main content

truthlinked_sdk/
log.rs

1//! Event logging system for smart contracts.
2//!
3//! This module provides traits and utilities for emitting structured events
4//! from smart contracts. Events are indexed logs that can be queried off-chain.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use truthlinked_sdk::prelude::*;
10//!
11//! #[derive(Event)]
12//! #[event(name = "Transfer")]
13//! struct Transfer {
14//!     #[topic]
15//!     from: [u8; 32],
16//!     #[topic]
17//!     to: [u8; 32],
18//!     amount: u128,
19//! }
20//!
21//! fn transfer(from: [u8; 32], to: [u8; 32], amount: u128) -> Result<()> {
22//!     Transfer { from, to, amount }.emit()?;
23//!     Ok(())
24//! }
25//! ```
26
27extern crate alloc;
28
29use alloc::string::String;
30use alloc::vec::Vec;
31
32use crate::codec::{Codec32, Encoder};
33use crate::env;
34use crate::error::Result;
35use crate::hashing;
36
37/// Private re-exports for macro-generated code.
38pub mod __private {
39    pub use alloc::vec::Vec;
40}
41
42/// Emits a log event with the given topics and data.
43pub fn emit(topics: &[[u8; 32]], data: &[u8]) -> Result<()> {
44    let mut flat = Vec::with_capacity(topics.len() * 32);
45    for topic in topics {
46        flat.extend_from_slice(topic);
47    }
48    env::emit_log_bytes(&flat, data)
49}
50
51/// Converts raw bytes to a 32-byte topic (left-padded with zeros if shorter).
52pub fn topic_from_bytes(bytes: &[u8]) -> [u8; 32] {
53    let mut topic = [0u8; 32];
54    let copy_len = bytes.len().min(32);
55    topic[..copy_len].copy_from_slice(&bytes[..copy_len]);
56    topic
57}
58
59/// Computes the event signature hash for a given event name.
60pub fn event_signature(signature: &str) -> [u8; 32] {
61    hashing::hash32(signature.as_bytes())
62}
63
64/// Trait for types that can be converted to event topics.
65pub trait EventTopic {
66    /// Converts this value to a 32-byte topic.
67    fn to_topic(&self) -> [u8; 32];
68}
69
70/// Trait for types that can be encoded as event data.
71pub trait EventData {
72    /// Encodes this value into the event data encoder.
73    fn encode_event_data(&self, encoder: &mut Encoder);
74}
75
76/// Trait for event types that can be emitted.
77///
78/// Typically derived using `#[derive(Event)]`.
79pub trait Event {
80    /// Returns the event name (e.g., "Transfer").
81    fn event_name() -> &'static str;
82
83    /// Returns whether this event is anonymous (no signature topic).
84    fn is_anonymous() -> bool {
85        false
86    }
87
88    /// Returns the event signature hash.
89    fn event_signature() -> [u8; 32] {
90        event_signature(Self::event_name())
91    }
92
93    /// Returns the topics for this event instance.
94    fn event_topics(&self) -> Vec<[u8; 32]>;
95
96    /// Returns the encoded data for this event instance.
97    fn event_data(&self) -> Vec<u8>;
98
99    /// Emits this event.
100    fn emit(&self) -> Result<()>
101    where
102        Self: Sized,
103    {
104        emit_event(self)
105    }
106}
107
108/// Emits an event implementing the `Event` trait.
109pub fn emit_event<E: Event>(event: &E) -> Result<()> {
110    let topics = event.event_topics();
111    let data = event.event_data();
112    emit(&topics, &data)
113}
114
115/// Checks if the given topics match the event signature.
116pub fn event_topics_match<E: Event>(topics: &[[u8; 32]]) -> bool {
117    if E::is_anonymous() {
118        true
119    } else {
120        topics
121            .first()
122            .map(|t| *t == E::event_signature())
123            .unwrap_or(false)
124    }
125}
126
127/// Returns the indexed topics (excluding the signature topic).
128pub fn indexed_topics<'a, E: Event>(topics: &'a [[u8; 32]]) -> &'a [[u8; 32]] {
129    if E::is_anonymous() {
130        topics
131    } else {
132        topics.get(1..).unwrap_or(&[])
133    }
134}
135
136/// Decodes event data into a typed value.
137pub fn decode_event_data<T: crate::codec::BytesCodec>(data: &[u8]) -> Result<T> {
138    T::decode_bytes(data)
139}
140
141macro_rules! impl_topic_codec32 {
142    ($ty:ty) => {
143        impl EventTopic for $ty {
144            fn to_topic(&self) -> [u8; 32] {
145                self.encode_32()
146            }
147        }
148    };
149}
150
151impl_topic_codec32!(bool);
152impl_topic_codec32!(u8);
153impl_topic_codec32!(u16);
154impl_topic_codec32!(u32);
155impl_topic_codec32!(u64);
156impl_topic_codec32!(u128);
157impl_topic_codec32!(i8);
158impl_topic_codec32!(i16);
159impl_topic_codec32!(i32);
160impl_topic_codec32!(i64);
161impl_topic_codec32!(i128);
162
163impl<const N: usize> EventTopic for [u8; N] {
164    fn to_topic(&self) -> [u8; 32] {
165        if N == 32 {
166            let mut out = [0u8; 32];
167            out.copy_from_slice(self);
168            out
169        } else if N < 32 {
170            let mut out = [0u8; 32];
171            out[..N].copy_from_slice(self);
172            out
173        } else {
174            hashing::hash32(self)
175        }
176    }
177}
178
179impl EventTopic for [u8] {
180    fn to_topic(&self) -> [u8; 32] {
181        if self.len() <= 32 {
182            topic_from_bytes(self)
183        } else {
184            hashing::hash32(self)
185        }
186    }
187}
188
189impl EventTopic for Vec<u8> {
190    fn to_topic(&self) -> [u8; 32] {
191        self.as_slice().to_topic()
192    }
193}
194
195impl EventTopic for str {
196    fn to_topic(&self) -> [u8; 32] {
197        hashing::hash32(self.as_bytes())
198    }
199}
200
201impl EventTopic for String {
202    fn to_topic(&self) -> [u8; 32] {
203        self.as_str().to_topic()
204    }
205}
206
207impl<T: crate::codec::BytesCodec> EventData for T {
208    fn encode_event_data(&self, encoder: &mut Encoder) {
209        encoder.push_bytes(&self.encode_bytes());
210    }
211}
212
213impl EventData for [u8] {
214    fn encode_event_data(&self, encoder: &mut Encoder) {
215        encoder.push_bytes(self);
216    }
217}
218
219impl EventData for str {
220    fn encode_event_data(&self, encoder: &mut Encoder) {
221        encoder.push_bytes(self.as_bytes());
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[derive(crate::Event)]
230    #[event(name = "CounterUpdated")]
231    struct CounterUpdated {
232        #[topic]
233        caller: [u8; 32],
234        value: u64,
235        memo: alloc::string::String,
236    }
237
238    #[test]
239    fn derive_event_encodes_topics_and_data() {
240        let event = CounterUpdated {
241            caller: [9u8; 32],
242            value: 42,
243            memo: alloc::string::String::from("ok"),
244        };
245
246        let topics = event.event_topics();
247        assert_eq!(topics.len(), 2);
248        assert_eq!(topics[1], [9u8; 32]);
249
250        let data = event.event_data();
251        assert!(!data.is_empty());
252    }
253
254    fn encode_u64_topic(v: &u64) -> [u8; 32] {
255        let mut out = [0u8; 32];
256        out[..8].copy_from_slice(&v.to_le_bytes());
257        out
258    }
259
260    fn encode_bool_data(v: &bool, encoder: &mut crate::codec::Encoder) {
261        encoder.push_bool(*v);
262    }
263
264    #[derive(crate::Event)]
265    #[event(name = "OrderFilled", anonymous)]
266    struct OrderFilled {
267        #[event(topic, with_topic = "encode_u64_topic", type = "uint64")]
268        order_id: u64,
269        #[event(with_data = "encode_bool_data")]
270        settled: bool,
271        #[event(skip)]
272        _padding: u8,
273    }
274
275    #[test]
276    fn derive_event_supports_advanced_field_options() {
277        let event = OrderFilled {
278            order_id: 77,
279            settled: true,
280            _padding: 0,
281        };
282
283        assert!(OrderFilled::is_anonymous());
284        let topics = event.event_topics();
285        assert_eq!(topics.len(), 1);
286        assert_eq!(topics[0][..8], 77u64.to_le_bytes());
287        assert!(!event.event_data().is_empty());
288    }
289}