use crate::{Event, Topic};
use thruput_xdr::event::TxEmittedEvent;
pub const TOPIC_SIZE: usize = 32;
pub struct EmittedEvent<'a>(&'a TxEmittedEvent);
impl<'a> EmittedEvent<'a> {
fn topics_byte_len(&self) -> Option<usize> {
let event = self.0;
if event.num_topics > thruput_xdr::event::kMaxTopics {
return None;
}
let extent = TOPIC_SIZE * event.num_topics as usize;
if extent > event.serialized_event.len() {
return None;
}
Some(extent)
}
pub fn payload(&self) -> Option<&'a [u8]> {
let extent = self.topics_byte_len()?;
Some(&self.0.serialized_event[extent..])
}
pub fn topic(&self, index: u32) -> Option<&'a Topic> {
let event = self.0;
self.topics_byte_len()?;
if index >= event.num_topics {
return None;
}
let topic_offset = TOPIC_SIZE * index as usize;
let topic_extent = topic_offset + TOPIC_SIZE;
let topic_bytes = &event.serialized_event[topic_offset..topic_extent];
let topic: &[u8; TOPIC_SIZE] = topic_bytes.try_into().unwrap();
Some(topic)
}
pub fn num_topics(&self) -> u32 {
self.0.num_topics
}
pub fn raw(&self) -> &'a TxEmittedEvent {
self.0
}
}
impl<'a> From<&'a TxEmittedEvent> for EmittedEvent<'a> {
fn from(event: &'a TxEmittedEvent) -> Self {
Self(event)
}
}
enum BuildError {
Malformed { num_topics: u32, body_len: usize },
SignatureMismatch {
actual: Option<Topic>,
expected: &'static Topic,
type_name: &'static str,
},
TopicCountMismatch {
actual: u32,
expected: u32,
type_name: &'static str,
},
Decode(xdrs::ReadError),
}
impl core::fmt::Display for BuildError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
BuildError::Malformed {
num_topics,
body_len,
} => write!(
f,
"TypedEvent::new: malformed event — {} topics do not fit a \
{}-byte body",
num_topics, body_len,
),
BuildError::SignatureMismatch {
actual,
expected,
type_name,
} => write!(
f,
"TypedEvent::new: event type mismatch — topic 0 {:?} does \
not match the expected discriminant {:?} for {}",
actual, expected, type_name,
),
BuildError::TopicCountMismatch {
actual,
expected,
type_name,
} => write!(
f,
"TypedEvent::new: topic count mismatch — event has {} \
topics but {} declares {}",
actual, type_name, expected,
),
BuildError::Decode(e) => {
write!(f, "failed to decode event payload: {}", e)
}
}
}
}
pub struct TypedEvent<'a, E: Event> {
emitted: EmittedEvent<'a>,
payload: E::Payload<'a>,
}
impl<'a, E: Event> TypedEvent<'a, E> {
fn build(raw: &'a TxEmittedEvent) -> Result<Self, BuildError> {
let emitted = EmittedEvent::from(raw);
let payload_bytes =
emitted.payload().ok_or(BuildError::Malformed {
num_topics: raw.num_topics,
body_len: raw.serialized_event.len(),
})?;
if let Some(expected) = E::signature() {
match emitted.topic(0) {
Some(actual) if actual == expected => {}
other => {
return Err(BuildError::SignatureMismatch {
actual: other.copied(),
expected,
type_name: core::any::type_name::<E>(),
});
}
}
}
if raw.num_topics != E::num_topics() {
return Err(BuildError::TopicCountMismatch {
actual: raw.num_topics,
expected: E::num_topics(),
type_name: core::any::type_name::<E>(),
});
}
let payload =
E::decode_payload(payload_bytes).map_err(BuildError::Decode)?;
Ok(Self { emitted, payload })
}
#[track_caller]
pub fn new(raw: &'a TxEmittedEvent) -> Self {
match Self::build(raw) {
Ok(typed) => typed,
Err(e) => panic!("{}", e),
}
}
pub fn try_new(raw: &'a TxEmittedEvent) -> Option<Self> {
Self::build(raw).ok()
}
pub fn payload(&self) -> &E::Payload<'a> {
&self.payload
}
pub fn topic(&self, index: u32) -> Option<&'a Topic> {
self.emitted.topic(index)
}
pub fn num_topics(&self) -> u32 {
self.emitted.num_topics()
}
pub fn raw(&self) -> &EmittedEvent<'a> {
&self.emitted
}
}
#[cfg(test)]
mod tests {
use super::*;
use thruput_xdr::event::TxEmittedEvent;
use thruput_xdr::types::ContractAddress;
fn raw_event(num_topics: u32, body: &'static [u8]) -> TxEmittedEvent {
TxEmittedEvent {
contract_address: ContractAddress::default(),
num_topics,
serialized_event: body.try_into().unwrap(),
}
}
#[test]
fn payload_is_none_when_topics_overrun_buffer() {
static BODY: [u8; 40] = [0u8; 40];
let raw = raw_event(3, &BODY);
let ev = EmittedEvent::from(&raw);
assert_eq!(ev.payload(), None);
assert_eq!(ev.topic(0), None);
}
#[test]
fn payload_is_none_when_num_topics_exceeds_cap() {
static BODY: [u8; 200] = [0u8; 200];
let raw = raw_event(4, &BODY);
let ev = EmittedEvent::from(&raw);
assert_eq!(ev.payload(), None);
}
#[test]
fn payload_is_some_for_well_formed_event() {
static BODY: [u8; 36] = {
let mut b = [0u8; 36];
b[32] = 1;
b[33] = 2;
b[34] = 3;
b[35] = 4;
b
};
let raw = raw_event(1, &BODY);
let ev = EmittedEvent::from(&raw);
assert_eq!(ev.payload(), Some(&[1u8, 2, 3, 4][..]));
assert!(ev.topic(0).is_some());
assert_eq!(ev.topic(1), None);
}
const SIG: Topic = [0xAB; 32];
struct SigEvent;
impl xdrs::WriteXDR for SigEvent {
fn write_xdr<F, W: xdrs::Writer<F>>(
&self,
_w: &mut W,
) -> xdrs::WriteResult<()> {
Ok(())
}
unsafe fn write_xdr_unchecked<F, W: xdrs::WriterUnchecked<F>>(
&self,
_w: &mut W,
) {
}
}
impl Event for SigEvent {
type Payload<'a> = u32;
fn num_topics() -> u32 {
1
}
fn signature() -> Option<&'static Topic> {
Some(&SIG)
}
fn decode_payload(bytes: &[u8]) -> Result<u32, xdrs::ReadError> {
match <[u8; 4]>::try_from(bytes) {
Ok(b) => Ok(u32::from_be_bytes(b)),
Err(_) => Err(xdrs::ReadError::source_empty(4, bytes.len())),
}
}
}
struct LenientEvent;
impl xdrs::WriteXDR for LenientEvent {
fn write_xdr<F, W: xdrs::Writer<F>>(
&self,
_w: &mut W,
) -> xdrs::WriteResult<()> {
Ok(())
}
unsafe fn write_xdr_unchecked<F, W: xdrs::WriterUnchecked<F>>(
&self,
_w: &mut W,
) {
}
}
impl Event for LenientEvent {
type Payload<'a> = usize;
fn num_topics() -> u32 {
1
}
fn signature() -> Option<&'static Topic> {
Some(&SIG)
}
fn decode_payload(bytes: &[u8]) -> Result<usize, xdrs::ReadError> {
Ok(bytes.len())
}
}
#[test]
fn try_new_decodes_matching_event() {
static BODY: [u8; 36] = {
let mut b = [0xABu8; 36];
b[32] = 0;
b[33] = 0;
b[34] = 0;
b[35] = 7;
b
};
let raw = raw_event(1, &BODY);
let typed =
TypedEvent::<SigEvent>::try_new(&raw).expect("should decode");
assert_eq!(*typed.payload(), 7);
assert_eq!(typed.topic(0), Some(&SIG));
}
#[test]
fn try_new_rejects_signature_mismatch() {
static BODY: [u8; 36] = [0xCD; 36];
let raw = raw_event(1, &BODY);
assert!(TypedEvent::<SigEvent>::try_new(&raw).is_none());
}
#[test]
fn try_new_rejects_malformed_event() {
static BODY: [u8; 40] = [0u8; 40];
let raw = raw_event(3, &BODY);
assert!(TypedEvent::<SigEvent>::try_new(&raw).is_none());
}
#[test]
fn try_new_rejects_undecodable_payload() {
static BODY: [u8; 35] = [0xAB; 35];
let raw = raw_event(1, &BODY);
assert!(TypedEvent::<SigEvent>::try_new(&raw).is_none());
}
#[test]
#[should_panic(expected = "event type mismatch")]
fn new_still_panics_on_signature_mismatch() {
static BODY: [u8; 36] = [0xCD; 36];
let raw = raw_event(1, &BODY);
let _ = TypedEvent::<SigEvent>::new(&raw);
}
#[test]
fn try_new_rejects_topic_count_mismatch() {
static BODY: [u8; 68] = [0xAB; 68];
let raw = raw_event(2, &BODY);
assert!(TypedEvent::<LenientEvent>::try_new(&raw).is_none());
static OK_BODY: [u8; 36] = [0xAB; 36];
let ok = raw_event(1, &OK_BODY);
assert!(TypedEvent::<LenientEvent>::try_new(&ok).is_some());
}
#[test]
#[should_panic(expected = "topic count mismatch")]
fn new_panics_on_topic_count_mismatch() {
static BODY: [u8; 68] = [0xAB; 68];
let raw = raw_event(2, &BODY);
let _ = TypedEvent::<LenientEvent>::new(&raw);
}
}