sails-rs 1.0.1

Main abstractions for the Sails framework
Documentation
use crate::prelude::*;
use alloy_sol_types::{SolType, SolValue, abi::TokenSeq};

#[doc(hidden)]
#[cfg(target_arch = "wasm32")]
pub(crate) fn __emit_eth_event<TEvents>(event: TEvents) -> crate::errors::Result<()>
where
    TEvents: crate::EthEvent,
{
    with_optimized_encode(event, |payload| {
        gstd::msg::send_bytes(crate::solidity::ETH_EVENT_ADDR, payload, 0)?;
        Ok(())
    })
}

#[cfg(target_arch = "wasm32")]
fn with_optimized_encode<T, E: EthEvent>(event: E, f: impl FnOnce(&[u8]) -> T) -> T {
    use crate::utils::MaybeUninitBufferWriter;

    let topics = event.topics();
    let data = event.data();
    let size = 1 + topics.len() * 32 + data.len();

    gcore::stack_buffer::with_byte_buffer(size, |buffer| {
        let mut buffer_writer = MaybeUninitBufferWriter::new(buffer);

        // encode topics lenght as u8
        buffer_writer.write(&[topics.len() as u8]);
        for topic in topics {
            buffer_writer.write(topic.as_slice());
        }
        buffer_writer.write(data.as_slice());

        buffer_writer.with_buffer(f)
    })
}

pub type EthEventExpo = (
    &'static str, // Event name
    &'static str, // Event parameters types
    [u8; 32],     // Topic hash
);

/// Trait for encoding Ethereum events for the EVM.
///
/// This trait provides a uniform interface to convert an event into the topics and data payload
/// that are used to emit logs in the Ethereum Virtual Machine (EVM). The logs generated by the EVM
/// consist of:
///
/// - **Topics:** An array of 32-byte values. The first topic is always the keccak256 hash of the event
///   signature, while the remaining topics correspond to indexed fields. For dynamic types (as determined
///   by `<T as alloy_sol_types::SolType>::IS_DYNAMIC`), the ABI-encoded value is hashed before being stored.
///   For static types, the ABI-encoded value is left-padded with zeros to 32 bytes.
/// - **Data:** A byte array containing the ABI-encoded non-indexed fields of the event, encoded as a tuple.
///
/// This trait is intended to be used with the `#[sails_rs::event]` procedural macro, which automatically
/// implements the trait for your enum-based event definitions.
///
/// # Examples
///
/// Given an event definition:
///
/// ```rust,ignore
/// #[sails_rs::event]
/// #[derive(sails_rs::Encode, sails_rs::TypeInfo)]
/// #[codec(crate = sails_rs::scale_codec)]
/// #[type_info(crate = sails_rs::type_info)]
/// pub enum Events {
///     MyEvent {
///         #[indexed]
///         sender: uint128,
///         amount: uint128,
///         note: String,
///     },
/// }
/// ```
///
/// Calling the methods:
///
/// ```rust,ignore
/// let event = Events::MyEvent {
///     sender: 123,
///     amount: 1000,
///     note: "Hello, Ethereum".to_owned(),
/// };
///
/// let topics = event.topics();
/// let data = event.data();
/// ```
///
/// The first topic will be the hash of the event signature (e.g. `"MyEvent(uint128,uint128,String)"`),
/// and additional topics and the data payload will be computed based on the field attributes.
///
/// # Methods
///
/// - `topics()`: Returns a vector of 32-byte topics (`alloy_primitives::B256`) for the event.
/// - `data()`: Returns the ABI-encoded data payload (a `Vec<u8>`) for the non-indexed fields.
pub trait EthEvent {
    /// The signature(s) associated with the event.
    ///
    /// The signature is the event name and its parameter types, e.g. `MyEvent` and `(uint128,uint128,string)`.
    /// The signature is used as the first topic in the log.
    const SIGNATURES: &'static [EthEventExpo];

    /// Returns the topics associated with the event.
    ///
    /// The topics vector includes:
    ///
    /// 1. The keccak256 hash of the event signature as the first topic.
    /// 2. For each indexed field, a topic that is generated based on the field's ABI encoding:
    ///    - For dynamic types, the ABI-encoded value is hashed using keccak256.
    ///    - For static types, the ABI-encoded value is left-padded with zeros to 32 bytes.
    ///
    /// # Returns
    ///
    /// A vector of topics (`alloy_primitives::B256`) that represent the event.
    fn topics(&self) -> Vec<alloy_primitives::B256>;

    /// Returns the ABI-encoded data payload of the event.
    ///
    /// The non-indexed fields of the event are ABI-encoded together as a tuple. If there are no non-indexed
    /// fields, an empty vector is returned.
    ///
    /// # Returns
    ///
    /// A `Vec<u8>` containing the ABI-encoded data of the non-indexed fields.
    fn data(&self) -> Vec<u8>;

    /// Returns the topic hash for a given value.
    fn topic_hash<T: SolValue>(value: &T) -> alloy_primitives::B256 {
        if <T::SolType as SolType>::DYNAMIC {
            alloy_primitives::keccak256(SolValue::abi_encode(value))
        } else {
            let encoded = SolValue::abi_encode(value);
            // Assume ABI encoding gives us a byte vector no longer than 32 bytes.
            let mut topic = [0u8; 32];
            // Right-align (pad on the left) if needed.
            topic[(32 - encoded.len())..].copy_from_slice(&encoded);
            topic.into()
        }
    }

    /// Encodes a sequence of values into a single byte vector.
    #[inline]
    fn encode_sequence<T: SolValue>(value: &T) -> Vec<u8>
    where
        for<'a> <T::SolType as SolType>::Token<'a>: TokenSeq<'a>,
    {
        // this method always allocates `Vec<Word>` and transmutes it to `Vec<u8>`
        // there is no point in using `parity_scale_codec::Output`
        alloy_sol_types::SolValue::abi_encode_sequence(value)
    }
}

#[cfg(test)]
mod tests {
    #![allow(unused_assignments)]

    use super::*;
    use crate::{self as sails_rs, Encode, String, TypeInfo, event};

    #[allow(unused)]
    #[event]
    #[derive(Encode, TypeInfo)]
    enum Events {
        MyEvent1 {
            #[indexed]
            sender: u128,
            #[indexed]
            amount: u128,
            note: String,
        },
        MyEvent2(u128, u128, String),
        MyEvent3,
    }

    #[test]
    fn eth_event_sig() {
        const SIG_TOPIC: [u8; 32] = [
            148, 157, 201, 65, 144, 217, 114, 52, 67, 86, 206, 75, 197, 220, 61, 74, 138, 251, 52,
            61, 243, 110, 252, 93, 62, 91, 109, 51, 209, 107, 68, 200,
        ];
        let ev = Events::MyEvent1 {
            sender: 1,
            amount: 2,
            note: "hello".to_string(),
        };
        assert_eq!(SIG_TOPIC.as_slice(), ev.topics()[0].as_slice());
    }

    #[test]
    fn eth_event_topic_1() {
        const SENDER_TOPIC: [u8; 32] = [
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 1,
        ];
        let ev = Events::MyEvent1 {
            sender: 1,
            amount: 2,
            note: "hello".to_string(),
        };
        assert_eq!(SENDER_TOPIC.as_slice(), ev.topics()[1].as_slice());
    }

    #[test]
    fn eth_event_data() {
        const DATA: &[u8] = &[
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 5, 104, 101, 108, 108, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        ];
        let ev = Events::MyEvent1 {
            sender: 1,
            amount: 2,
            note: "hello".to_string(),
        };
        assert_eq!(DATA, ev.data().as_slice());
    }
}