//! Event emission for contracts. //! //! Events (logs) are a way to emit data that can be indexed off-chain //! but is not stored on-chain. They're useful for: //! - Tracking token transfers //! - Recording state changes //! - Debugging and monitoring //! //! # Example //! //! ```rust,ignore //! use synor_sdk::events::{emit, Event, Topic}; //! //! // Define an event //! struct Transfer { //! from: Address, //! to: Address, //! amount: u64, //! } //! //! impl Event for Transfer { //! fn topics(&self) -> Vec { //! vec![ //! Topic::from(b"Transfer"), //! Topic::from(&self.from), //! Topic::from(&self.to), //! ] //! } //! //! fn data(&self) -> Vec { //! self.amount.to_le_bytes().to_vec() //! } //! } //! //! // Emit the event //! emit(&Transfer { from, to, amount }); //! ``` use alloc::vec::Vec; use crate::host; use crate::types::{Address, Hash256}; /// A topic (indexed field) in an event log. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Topic(pub [u8; 32]); impl Topic { /// Creates a topic from raw bytes. pub fn new(bytes: [u8; 32]) -> Self { Topic(bytes) } /// Creates a topic from a hash. pub fn from_hash(hash: Hash256) -> Self { Topic(hash.0) } /// Creates a topic from a string (hashed). pub fn from_name(name: &str) -> Self { Topic(host::blake3(name.as_bytes())) } /// Creates a topic from bytes (hashed if longer than 32 bytes). pub fn from_bytes(bytes: &[u8]) -> Self { if bytes.len() <= 32 { let mut arr = [0u8; 32]; arr[..bytes.len()].copy_from_slice(bytes); Topic(arr) } else { Topic(host::blake3(bytes)) } } /// Returns as bytes. pub fn as_bytes(&self) -> &[u8; 32] { &self.0 } } impl From<[u8; 32]> for Topic { fn from(bytes: [u8; 32]) -> Self { Topic(bytes) } } impl From for Topic { fn from(hash: Hash256) -> Self { Topic(hash.0) } } impl From<&Address> for Topic { fn from(addr: &Address) -> Self { // Use first 32 bytes of address let mut arr = [0u8; 32]; arr.copy_from_slice(&addr.0[..32]); Topic(arr) } } impl From<&str> for Topic { fn from(name: &str) -> Self { Topic::from_name(name) } } impl From for Topic { fn from(value: u64) -> Self { let mut arr = [0u8; 32]; arr[24..32].copy_from_slice(&value.to_be_bytes()); Topic(arr) } } impl From for Topic { fn from(value: u128) -> Self { let mut arr = [0u8; 32]; arr[16..32].copy_from_slice(&value.to_be_bytes()); Topic(arr) } } /// Trait for types that can be emitted as events. pub trait Event { /// Returns the indexed topics for this event. /// /// The first topic is typically the event signature (hash of event name). /// Maximum 4 topics are allowed. fn topics(&self) -> Vec; /// Returns the non-indexed data for this event. fn data(&self) -> Vec; } /// Emits an event. pub fn emit(event: &E) { let topics = event.topics(); let data = event.data(); let topic_bytes: Vec<[u8; 32]> = topics.iter().map(|t| t.0).collect(); host::emit_log(&topic_bytes, &data); } /// Emits a raw event with topics and data. pub fn emit_raw(topics: &[[u8; 32]], data: &[u8]) { host::emit_log(topics, data); } /// Helper macro for defining events. /// /// # Example /// /// ```rust,ignore /// synor_sdk::define_event! { /// /// Transfer event /// Transfer { /// #[indexed] from: Address, /// #[indexed] to: Address, /// amount: u64, /// } /// } /// ``` #[macro_export] macro_rules! define_event { ( $(#[$meta:meta])* $name:ident { $( $(#[indexed])? $field:ident: $ty:ty ),* $(,)? } ) => { $(#[$meta])* #[derive(Clone, Debug)] pub struct $name { $(pub $field: $ty,)* } impl $crate::events::Event for $name { fn topics(&self) -> alloc::vec::Vec<$crate::events::Topic> { let mut topics = alloc::vec::Vec::new(); topics.push($crate::events::Topic::from_name(stringify!($name))); // Add indexed fields topics } fn data(&self) -> alloc::vec::Vec { // Serialize non-indexed fields borsh::to_vec(self).unwrap_or_default() } } }; } // Common events /// Transfer event for token contracts. #[derive(Clone, Debug)] pub struct Transfer { pub from: Address, pub to: Address, pub amount: u64, } impl Event for Transfer { fn topics(&self) -> Vec { alloc::vec![ Topic::from_name("Transfer"), Topic::from(&self.from), Topic::from(&self.to), ] } fn data(&self) -> Vec { self.amount.to_le_bytes().to_vec() } } /// Approval event for token contracts. #[derive(Clone, Debug)] pub struct Approval { pub owner: Address, pub spender: Address, pub amount: u64, } impl Event for Approval { fn topics(&self) -> Vec { alloc::vec![ Topic::from_name("Approval"), Topic::from(&self.owner), Topic::from(&self.spender), ] } fn data(&self) -> Vec { self.amount.to_le_bytes().to_vec() } } /// Ownership transferred event. #[derive(Clone, Debug)] pub struct OwnershipTransferred { pub previous_owner: Address, pub new_owner: Address, } impl Event for OwnershipTransferred { fn topics(&self) -> Vec { alloc::vec![ Topic::from_name("OwnershipTransferred"), Topic::from(&self.previous_owner), Topic::from(&self.new_owner), ] } fn data(&self) -> Vec { Vec::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_topic_from_name() { let topic = Topic::from_name("Transfer"); assert_ne!(topic.0, [0u8; 32]); } #[test] fn test_topic_from_u64() { let topic = Topic::from(12345u64); // Value should be in last 8 bytes let value = u64::from_be_bytes(topic.0[24..32].try_into().unwrap()); assert_eq!(value, 12345); } #[test] fn test_transfer_event() { let event = Transfer { from: Address::zero(), to: Address::zero(), amount: 1000, }; let topics = event.topics(); assert_eq!(topics.len(), 3); let data = event.data(); assert_eq!(data.len(), 8); } }