soltrace 0.1.0

Structured, Borsh-serialized Anchor events as a drop-in replacement for msg! in Solana programs.
Documentation
//! The Anchor event struct emitted by every `sol_log!` call.

use anchor_lang::prelude::*;

use crate::LogLevel;

/// An Anchor event emitted by the `sol_log!` macro family.
///
/// Each macro invocation emits exactly one `SolTraceEvent` via Anchor's
/// `emit!` infrastructure. On-chain this produces a `Program data:` log line
/// in the transaction metadata, encoded as base64.
///
/// ## Wire format
///
/// The full event is Borsh-serialized by Anchor with an 8-byte discriminator
/// prefix derived from the type name. Fields follow in declaration order:
///
/// ```text
/// [discriminator: 8 bytes]
/// [level: 1 byte (u8 repr)]
/// [event_name length: 4 bytes LE u32]
/// [event_name: n bytes UTF-8]
/// [payload length: 4 bytes LE u32]
/// [payload: m bytes (caller-defined Borsh struct)]
/// ```
///
/// See <https://borsh.io/> for the full Borsh specification.
#[event]
pub struct SolTraceEvent {
    /// Severity of this event. Encoded as a single `u8` on the wire.
    pub level: LogLevel,

    /// Caller-supplied name identifying the logical event type,
    /// e.g. `"swap_executed"` or `"position_opened"`.
    ///
    /// Keep names short: every extra byte costs compute units and is paid for
    /// on every invocation.
    pub event_name: String,

    /// Borsh-serialized bytes of the caller-supplied payload struct.
    ///
    /// To decode off-chain:
    /// ```typescript
    /// import { deserializeUnchecked } from "borsh";
    /// const decoded = deserializeUnchecked(schema, MyStruct, Buffer.from(payload));
    /// ```
    ///
    /// The concrete Rust type must match what was passed to `sol_log!` at the
    /// call site. There is no type tag embedded — the caller is responsible for
    /// knowing which event name maps to which struct.
    pub payload: Vec<u8>,
}

impl SolTraceEvent {
    /// Constructs a new event.
    ///
    /// Prefer using the `sol_log!` family of macros rather than calling this
    /// directly — they handle `BorshSerialize`, call this constructor, and
    /// pass the result to `emit!`.
    ///
    /// The `payload` argument should be the output of
    /// `borsh::BorshSerialize::try_to_vec(&your_struct)`. Passing arbitrary
    /// bytes is valid but will confuse any off-chain decoder that expects a
    /// specific struct layout.
    pub fn new(level: LogLevel, event_name: impl Into<String>, payload: Vec<u8>) -> Self {
        Self {
            level,
            event_name: event_name.into(),
            payload,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_sets_all_fields() {
        let event = SolTraceEvent::new(LogLevel::Info, "test_event", vec![1, 2, 3]);
        assert_eq!(event.level, LogLevel::Info);
        assert_eq!(event.event_name, "test_event");
        assert_eq!(event.payload, vec![1, 2, 3]);
    }

    #[test]
    fn new_accepts_string_and_str() {
        let from_str = SolTraceEvent::new(LogLevel::Warn, "from_str", vec![]);
        let from_string = SolTraceEvent::new(LogLevel::Warn, "from_str".to_string(), vec![]);
        assert_eq!(from_str.event_name, from_string.event_name);
    }

    #[test]
    fn serialization_error_sentinel() {
        let event = SolTraceEvent::new(LogLevel::Error, "__soltrace_serialization_error", vec![]);
        assert!(event.payload.is_empty());
        assert_eq!(event.event_name, "__soltrace_serialization_error");
    }
}