Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(axelar-soroban-std): allow typed matching of emitted events #92

Merged
merged 17 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005636861696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000007fffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000144f4495243837681061c4743b74b3eedf548d56a500000000000000000000000000000000000000000000000000000000000000000000000000000000000000144f4495243837681061c4743b74b3eedf548d56a50000000000000000000000000000000000000000000000000000000000000000000000000000000000000002abcd000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005636861696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005636861696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000007fffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000144f4495243837681061c4743b74b3eedf548d56a500000000000000000000000000000000000000000000000000000000000000000000000000000000000000144f4495243837681061c4743b74b3eedf548d56a50000000000000000000000000000000000000000000000000000000000000000000000000000000000000002abcd000000000000000000000000000000000000000000000000000000000000"
]
]
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
150 changes: 150 additions & 0 deletions packages/axelar-soroban-std/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use core::fmt::Debug;
use soroban_sdk::{Env, IntoVal, Topics, Val};

#[cfg(any(test, feature = "testutils"))]
pub use testutils::*;

pub trait Event: Debug + PartialEq {
fn topics(&self) -> impl Topics + Debug;

fn data(&self) -> impl IntoVal<Env, Val> + Debug;

fn emit(&self, env: &Env) {
env.events().publish(self.topics(), self.data());
}
}

#[cfg(any(test, feature = "testutils"))]
mod testutils {
use crate::events::Event;
use soroban_sdk::testutils::Events;
use soroban_sdk::{Address, Env, Val, Vec};

pub trait EventTestutils: Event {
fn matches(&self, env: &Env, event: &(Address, Vec<Val>, Val)) -> bool;

fn standardized_fmt(
env: &Env,
event: &(soroban_sdk::Address, soroban_sdk::Vec<Val>, Val),
) -> std::string::String;
}

pub fn fmt_last_emitted_event<E>(env: &Env) -> std::string::String
where
E: EventTestutils,
{
let event = env.events().all().last().expect("no event found");
E::standardized_fmt(env, &event)
}

pub fn fmt_emitted_event_at_idx<E>(env: &Env, idx: u32) -> std::string::String
where
E: EventTestutils,
{
let event = env
.events()
.all()
.get(idx)
.expect("no event found at the given index");
E::standardized_fmt(env, &event)
}

#[macro_export]
macro_rules! impl_event_testutils {
($event_type:ty, ($($topic_type:ty),*), ($($data_type:ty),*)) => {
impl $crate::events::EventTestutils for $event_type {
fn matches(&self, env: &soroban_sdk::Env, event: &(soroban_sdk::Address, soroban_sdk::Vec<soroban_sdk::Val>, soroban_sdk::Val)) -> bool {
use soroban_sdk::IntoVal;

Self::standardized_fmt(env, event) == Self::standardized_fmt(env, &(event.0.clone(), self.topics().into_val(env), self.data().into_val(env)))
}

#[allow(unused_assignments)]
fn standardized_fmt(env: &soroban_sdk::Env, (contract_id, topics, data): &(soroban_sdk::Address, soroban_sdk::Vec<soroban_sdk::Val>, soroban_sdk::Val)) -> std::string::String {
use soroban_sdk::TryFromVal;

let mut topics_output = std::vec![];

let mut i = 0;
$(
let topic = topics.get(i).expect("the number of topics does not match this function's definition");
topics_output.push(std::format!("{:?}", <$topic_type>::try_from_val(env, &topic)
.expect("given topic value does not match the expected type")));

i += 1;
)*

let data = soroban_sdk::Vec::<soroban_sdk::Val>::try_from_val(env, data).expect("data should be defined as a vector-compatible type");

let mut data_output = std::vec![];

let mut i = 0;
$(
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
let data_entry = data.get(i).expect("the number of data entries does not match this function's definition");
data_output.push(std::format!("{:?}", <$data_type>::try_from_val(env, &data_entry)
.expect("given data value does not match the expected type")));

i += 1;
)*

std::format!("contract: {:?}\ntopics: ({})\ndata: ({})", contract_id, topics_output.join(", "), data_output.join(", "))
cgorenflo marked this conversation as resolved.
Show resolved Hide resolved
}
}
};
}
}

#[cfg(test)]
mod test {
use crate::events::{Event, EventTestutils};
use crate::{events, impl_event_testutils};
use core::fmt::Debug;
use soroban_sdk::testutils::Events;
use soroban_sdk::xdr::Int32;
use soroban_sdk::{contract, BytesN, Env, IntoVal, String, Symbol, Topics, Val};

#[derive(Debug, PartialEq, Eq)]
struct TestEvent {
topic1: Symbol,
topic2: String,
topic3: Int32,
data1: String,
data2: BytesN<32>,
}

impl Event for TestEvent {
fn topics(&self) -> impl Topics + Debug {
(self.topic1.clone(), self.topic2.clone(), self.topic3)
}

fn data(&self) -> impl IntoVal<Env, Val> + Debug {
(self.data1.clone(), self.data2.clone())
}
}

impl_event_testutils!(TestEvent, (Symbol, String, Int32), (String, BytesN<32>));

#[contract]
struct Contract;

#[test]
fn test_format_last_emitted_event() {
let env = Env::default();
let expected = TestEvent {
topic1: Symbol::new(&env, "topic1"),
topic2: String::from_str(&env, "topic2"),
topic3: 10,
data1: String::from_str(&env, "data1"),
data2: BytesN::from_array(&env, &[3; 32]),
};

let contract = env.register(Contract, ());
env.as_contract(&contract, || {
expected.emit(&env);
});

assert!(expected.matches(&env, &env.events().all().last().unwrap()));

goldie::assert!(events::fmt_last_emitted_event::<TestEvent>(&env));
}
}
7 changes: 5 additions & 2 deletions packages/axelar-soroban-std/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#![no_std]
#[cfg(test)]
extern crate std;

// required by goldie
#[cfg(any(test, feature = "testutils"))]
extern crate std;

#[cfg(any(test, feature = "testutils"))]
pub mod testutils;
Expand All @@ -19,5 +20,7 @@ pub mod shared_interfaces;

pub mod ttl;

pub mod events;

#[cfg(test)]
mod testdata;
64 changes: 23 additions & 41 deletions packages/axelar-soroban-std/src/shared_interfaces.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use crate::ensure;
use crate::events::Event;
#[cfg(any(test, feature = "testutils"))]
use crate::impl_event_testutils;
use core::fmt::Debug;
use soroban_sdk::{
contractclient, symbol_short, Address, BytesN, ConversionError, Env, FromVal, IntoVal, String,
Topics, TryFromVal, Val, Vec,
contractclient, symbol_short, Address, BytesN, Env, FromVal, IntoVal, String, Topics, Val,
};

#[contractclient(name = "OwnershipClient")]
Expand Down Expand Up @@ -68,20 +71,14 @@ pub fn migrate<T: UpgradableInterface>(
custom_migration();
complete_migration(env);

emit_event_upgraded(
env,
UpgradedEvent {
version: T::version(env),
},
);
UpgradedEvent {
version: T::version(env),
}
.emit(env);

Ok(())
}

fn emit_event_upgraded(env: &Env, event: UpgradedEvent) {
env.events().publish(UpgradedEvent::topic(), event.data());
}

fn start_migration(env: &Env) {
env.storage()
.instance()
Expand All @@ -105,35 +102,23 @@ fn complete_migration(env: &Env) {
.remove(&storage::DataKey::SharedInterfaces_Migrating);
}

#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub struct UpgradedEvent {
version: String,
}

impl UpgradedEvent {
pub fn topic() -> impl Topics {
impl Event for UpgradedEvent {
fn topics(&self) -> impl Topics + Debug {
(symbol_short!("upgraded"),)
}

pub fn data(&self) -> impl IntoVal<Env, Val> {
(self.version.to_val(),)
fn data(&self) -> impl IntoVal<Env, Val> + Debug {
(self.version.clone(),)
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl TryFromVal<Env, (Address, Vec<Val>, Val)> for UpgradedEvent {
type Error = ConversionError;

fn try_from_val(
env: &Env,
(_address, topics, data): &(Address, Vec<Val>, Val),
) -> Result<Self, Self::Error> {
ensure!(topics.eq(&Self::topic().into_val(env)), ConversionError);

let v: Vec<Val> = Vec::try_from_val(env, data)?;
String::try_from_val(env, &v.first().ok_or(ConversionError)?)
.map(|version| Self { version })
}
}
#[cfg(any(test, feature = "testutils"))]
impl_event_testutils!(UpgradedEvent, (soroban_sdk::Symbol), (String));
cgorenflo marked this conversation as resolved.
Show resolved Hide resolved

// submodule to encapsulate the disabled linting
mod storage {
Expand All @@ -160,12 +145,13 @@ pub enum MigrationError {
#[cfg(test)]
mod test {
use crate::shared_interfaces::{OwnershipClient, UpgradableClient, UpgradedEvent};
use crate::{assert_invoke_auth_err, assert_invoke_auth_ok, shared_interfaces, testdata};
use std::format;
use crate::{
assert_invoke_auth_err, assert_invoke_auth_ok, events, shared_interfaces, testdata,
};

use crate::testdata::contract::ContractClient;
use soroban_sdk::testutils::{Address as _, Events, MockAuth, MockAuthInvoke};
use soroban_sdk::{contracttype, Address, Env, String, TryFromVal};
use soroban_sdk::testutils::{Address as _, MockAuth, MockAuthInvoke};
use soroban_sdk::{contracttype, Address, Env, String};

const WASM: &[u8] = include_bytes!("testdata/contract.wasm");

Expand Down Expand Up @@ -347,12 +333,8 @@ mod test {
Some(String::from_str(&env, "migrated"))
);

let event = env
.events()
.all()
.iter()
.find_map(|event| UpgradedEvent::try_from_val(&env, &event).ok());
goldie::assert!(format!("{:?}", event))
let event = events::fmt_last_emitted_event::<UpgradedEvent>(&env);
goldie::assert!(event)
}

// Because migration happens on a contract loaded from WASM, code coverage analysis doesn't recognize
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
Some(UpgradedEvent { version: String(0.1.0) })
contract: Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM)
topics: (Symbol(upgraded))
data: (String(0.1.0))
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
contract: Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM)
topics: (Symbol(topic1), String(topic2), 10)
data: (String(data1), BytesN<32>(3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3))
Loading