diff --git a/Cargo.lock b/Cargo.lock index 0455feb2..ec0704b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1954,9 +1954,9 @@ dependencies = [ [[package]] name = "soroban-token-sdk" -version = "22.0.0-rc.3" +version = "22.0.0-rc.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb933a3dcf41d234f6d669b077eb755663773630c6899a1c8a30dddf950f52" +checksum = "2d9986d00de31e52f3d19b6e1a867403387226e1011987ff48f9c9a9e3379335" dependencies = [ "soroban-sdk", ] diff --git a/contracts/interchain-token-service/src/testdata/interchain_transfer_encode_decode.golden b/contracts/interchain-token-service/src/testdata/interchain_transfer_encode_decode.golden index 5095b4b3..7cb0ee91 100644 --- a/contracts/interchain-token-service/src/testdata/interchain_transfer_encode_decode.golden +++ b/contracts/interchain-token-service/src/testdata/interchain_transfer_encode_decode.golden @@ -3,4 +3,4 @@ "0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005636861696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000007fffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000144f4495243837681061c4743b74b3eedf548d56a500000000000000000000000000000000000000000000000000000000000000000000000000000000000000144f4495243837681061c4743b74b3eedf548d56a50000000000000000000000000000000000000000000000000000000000000000000000000000000000000002abcd000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005636861696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005636861696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000007fffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000144f4495243837681061c4743b74b3eedf548d56a500000000000000000000000000000000000000000000000000000000000000000000000000000000000000144f4495243837681061c4743b74b3eedf548d56a50000000000000000000000000000000000000000000000000000000000000000000000000000000000000002abcd000000000000000000000000000000000000000000000000000000000000" -] +] \ No newline at end of file diff --git a/packages/axelar-soroban-std/src/events.rs b/packages/axelar-soroban-std/src/events.rs new file mode 100644 index 00000000..62e931ce --- /dev/null +++ b/packages/axelar-soroban-std/src/events.rs @@ -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 + 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)) -> bool; + + fn standardized_fmt( + env: &Env, + event: &(soroban_sdk::Address, soroban_sdk::Vec, Val), + ) -> std::string::String; + } + + pub fn fmt_last_emitted_event(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(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)) -> 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)) -> 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::::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; + $( + 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(", ")) + } + } + }; + } +} + +#[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 + 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::(&env)); + } +} diff --git a/packages/axelar-soroban-std/src/lib.rs b/packages/axelar-soroban-std/src/lib.rs index f55e07de..ec4d6eb1 100644 --- a/packages/axelar-soroban-std/src/lib.rs +++ b/packages/axelar-soroban-std/src/lib.rs @@ -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; @@ -19,5 +20,7 @@ pub mod shared_interfaces; pub mod ttl; +pub mod events; + #[cfg(test)] mod testdata; diff --git a/packages/axelar-soroban-std/src/shared_interfaces.rs b/packages/axelar-soroban-std/src/shared_interfaces.rs index c1ae9f54..000a3079 100644 --- a/packages/axelar-soroban-std/src/shared_interfaces.rs +++ b/packages/axelar-soroban-std/src/shared_interfaces.rs @@ -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")] @@ -68,20 +71,14 @@ pub fn migrate( 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() @@ -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 { - (self.version.to_val(),) + fn data(&self) -> impl IntoVal + Debug { + (self.version.clone(),) } } -impl TryFromVal, Val)> for UpgradedEvent { - type Error = ConversionError; - - fn try_from_val( - env: &Env, - (_address, topics, data): &(Address, Vec, Val), - ) -> Result { - ensure!(topics.eq(&Self::topic().into_val(env)), ConversionError); - - let v: Vec = 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)); // submodule to encapsulate the disabled linting mod storage { @@ -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"); @@ -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::(&env); + goldie::assert!(event) } // Because migration happens on a contract loaded from WASM, code coverage analysis doesn't recognize diff --git a/packages/axelar-soroban-std/src/testdata/migrate_succeeds_if_owner_is_authenticated_and_called_after_upgrade.golden b/packages/axelar-soroban-std/src/testdata/migrate_succeeds_if_owner_is_authenticated_and_called_after_upgrade.golden index 284157b4..f71caf31 100644 --- a/packages/axelar-soroban-std/src/testdata/migrate_succeeds_if_owner_is_authenticated_and_called_after_upgrade.golden +++ b/packages/axelar-soroban-std/src/testdata/migrate_succeeds_if_owner_is_authenticated_and_called_after_upgrade.golden @@ -1 +1,3 @@ -Some(UpgradedEvent { version: String(0.1.0) }) \ No newline at end of file +contract: Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM) +topics: (Symbol(upgraded)) +data: (String(0.1.0)) \ No newline at end of file diff --git a/packages/axelar-soroban-std/src/testdata/test_format_last_emitted_event.golden b/packages/axelar-soroban-std/src/testdata/test_format_last_emitted_event.golden new file mode 100644 index 00000000..6ed8dd01 --- /dev/null +++ b/packages/axelar-soroban-std/src/testdata/test_format_last_emitted_event.golden @@ -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)) \ No newline at end of file