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: simplify event definition via IntoEvent derive macro #136

Merged
merged 5 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rust-version = "1.81.0"
[workspace.dependencies]
soroban-sdk = { version = "22.0.2" }
soroban-token-sdk = { version = "22.0.2" }
proc-macro2 = "1.0"
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
cfg-if = { version = "1.0" }
stellar-axelar-std = { version = "^0.1.0", path = "packages/axelar-std", features = ["derive"] }
stellar-axelar-std-derive = { version = "^0.1.0", path = "packages/axelar-std-derive" }
Expand Down
1 change: 1 addition & 0 deletions contracts/axelar-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ rand = { version = "0.8.5", optional = true }
rand_chacha = "0.3"
soroban-sdk = { workspace = true }
stellar-axelar-std = { workspace = true }
stellar-axelar-std-derive = { workspace = true }
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved

[dev-dependencies]
ed25519-dalek = { version = "^2.1" }
Expand Down
81 changes: 9 additions & 72 deletions contracts/axelar-gateway/src/event.rs
Original file line number Diff line number Diff line change
@@ -1,97 +1,34 @@
use core::fmt::Debug;

use cfg_if::cfg_if;
use soroban_sdk::{Address, Bytes, BytesN, Env, IntoVal, String, Symbol, Topics, Val, Vec};
use soroban_sdk::{Address, Bytes, BytesN, String};
#[cfg(any(test, feature = "testutils"))]
milapsheth marked this conversation as resolved.
Show resolved Hide resolved
use stellar_axelar_std::events::Event;
use stellar_axelar_std::IntoEvent;

use crate::types::Message;

#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, IntoEvent)]
pub struct ContractCalledEvent {
pub caller: Address,
pub destination_chain: String,
pub destination_address: String,
pub payload: Bytes,
pub payload_hash: BytesN<32>,
#[data]
pub payload: Bytes,
}

#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, IntoEvent)]
pub struct MessageApprovedEvent {
pub message: Message,
}

#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, IntoEvent)]
pub struct MessageExecutedEvent {
pub message: Message,
}

#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, IntoEvent)]
pub struct SignersRotatedEvent {
pub epoch: u64,
pub signers_hash: BytesN<32>,
}

impl Event for ContractCalledEvent {
fn topics(&self, env: &Env) -> impl Topics + Debug {
(
Symbol::new(env, "contract_called"),
self.caller.to_val(),
self.destination_chain.to_val(),
self.destination_address.to_val(),
self.payload_hash.to_val(),
)
}

fn data(&self, _env: &Env) -> impl IntoVal<Env, Val> + Debug {
(self.payload.to_val(),)
}
}

impl Event for MessageApprovedEvent {
fn topics(&self, env: &Env) -> impl Topics + Debug {
(Symbol::new(env, "message_approved"), self.message.clone())
}

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
}
}

impl Event for MessageExecutedEvent {
fn topics(&self, env: &Env) -> impl Topics + Debug {
(Symbol::new(env, "message_executed"), self.message.clone())
}

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
}
}

impl Event for SignersRotatedEvent {
fn topics(&self, env: &Env) -> impl Topics + Debug {
(
Symbol::new(env, "signers_rotated"),
self.epoch,
self.signers_hash.to_val(),
)
}

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
}
}

cfg_if! {
if #[cfg(any(test, feature = "testutils"))] {
use stellar_axelar_std::impl_event_testutils;

impl_event_testutils!(
ContractCalledEvent,
(Symbol, Address, String, String, BytesN<32>),
(Bytes)
);
impl_event_testutils!(MessageApprovedEvent, (Symbol, Message), ());
impl_event_testutils!(MessageExecutedEvent, (Symbol, Message), ());
impl_event_testutils!(SignersRotatedEvent, (Symbol, u64, BytesN<32>), ());
}
}
7 changes: 6 additions & 1 deletion packages/axelar-std-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ publish = true
[lib]
proc-macro = true

[features]
testutils = []

[dependencies]
heck = "0.5.0"
proc-macro2 = "1.0"
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
quote = "1.0"
syn = { version = "2.0", features = ["full"] }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
stellar-axelar-std = { workspace = true }
stellar-axelar-std = { workspace = true, features = ["testutils"] }

[lints]
workspace = true
221 changes: 220 additions & 1 deletion packages/axelar-std-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use heck::ToSnakeCase;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, DeriveInput, Error, Ident, Token, Type};
use syn::{parse_macro_input, DeriveInput, Error, Ident, LitStr, Token, Type};

/// Implements the Operatable interface for a Soroban contract.
///
Expand Down Expand Up @@ -219,3 +220,221 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
}
}.into()
}

/// Implements the Event and EventTestUtils traits for a Soroban contract event.
///
/// Fields without a `#[data]` attribute are used as topics, while fields with `#[data]` are used as event data.
/// The event name can be specified with `#[event_name(...)]` or will default to the struct name in snake_case (minus "Event" suffix).
///
/// # Example
/// ```rust
/// # mod test {
/// use core::fmt::Debug;
/// use stellar_axelar_std::events::Event;
/// use stellar_axelar_std_derive::IntoEvent;
/// use soroban_sdk::{Address, contract, contractimpl, Env, String};
///
/// #[derive(Debug, PartialEq, IntoEvent)]
/// pub struct ApproveEvent {
/// pub owner: Address,
/// pub spender: Address,
/// #[data]
/// pub amount: String,
/// }
///
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
/// #[derive(Debug, PartialEq, IntoEvent)]
/// #[event_name("transfer")]
/// pub struct TransferEvent {
/// pub from: Address,
/// pub to: Address,
/// #[data]
/// pub amount: String,
/// }
///
/// #[contract]
/// pub struct TokenContract;
///
/// #[contractimpl]
/// impl TokenContract {
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
/// pub fn approve(env: &Env, spender: Address, amount: String) {
/// // ... approval logic ...
///
/// let event = ApproveEvent {
/// owner: env.current_contract_address(),
/// spender,
/// amount,
/// };
/// // Generates event with:
/// // - Topics: ["approve", contract_address, spender]
/// // - Data: [amount]
/// env.events().publish(event.topics(env), event.data(env));
/// }
///
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
/// pub fn transfer(env: &Env, to: Address, amount: String) {
/// // ... transfer logic ...
///
/// let event = TransferEvent {
/// from: env.current_contract_address(),
/// to,
/// amount,
/// };
///
/// // Generates event with:
/// // - Topics: ["transfer", contract_address, to]
/// // - Data: [amount]
///
/// env.events().publish(event.topics(env), event.data(env));
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
/// }
/// }
/// }
/// ```
#[proc_macro_derive(IntoEvent, attributes(data, event_name))]
pub fn derive_into_event(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

let event_impl = derive_event_impl(&input);
#[cfg(any(test, feature = "testutils"))]
let test_impl = derive_test_impl(&input);

#[cfg(not(any(test, feature = "testutils")))]
let test_impl = quote! {};

quote! {
#event_impl
#test_impl
}
.into()
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
}

fn derive_event_impl(input: &DeriveInput) -> proc_macro2::TokenStream {
let name = &input.ident;
let event_name = event_name_snake_case(input);
let ((topic_idents, _), (data_idents, _)) = event_struct_fields(input);

quote! {
impl stellar_axelar_std::events::Event for #name {
fn topics(&self, env: &soroban_sdk::Env) -> impl soroban_sdk::Topics + core::fmt::Debug {
(
soroban_sdk::Symbol::new(env, #event_name),
#(soroban_sdk::IntoVal::<soroban_sdk::Env, soroban_sdk::Val>::into_val(&self.#topic_idents, env),)*
)
}

fn data(&self, env: &soroban_sdk::Env) -> impl soroban_sdk::IntoVal<soroban_sdk::Env, soroban_sdk::Val> + core::fmt::Debug {
let vec: soroban_sdk::Vec<soroban_sdk::Val> = soroban_sdk::vec![env, #(soroban_sdk::IntoVal::<_, soroban_sdk::Val>::into_val(&self.#data_idents, env))*];
vec
}

fn emit(self, env: &soroban_sdk::Env) {
env.events().publish(self.topics(env), self.data(env));
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
#[cfg(any(test, feature = "testutils"))]
fn derive_test_impl(input: &DeriveInput) -> proc_macro2::TokenStream {
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
let name = &input.ident;
let ((_, topic_types), (_, data_types)) = event_struct_fields(input);
event_testutils(name, topic_types, data_types)
}

#[cfg(any(test, feature = "testutils"))]
fn event_testutils(
name: &Ident,
topic_types: Vec<&Type>,
data_types: Vec<&Type>,
) -> proc_macro2::TokenStream {
quote! {
impl stellar_axelar_std::events::EventTestutils for #name {
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(env).into_val(env), self.data(env).into_val(env)))
}

#[allow(unused_assignments)]
#[allow(unused_variables)]
#[allow(unused_mut)]
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::Vec<std::string::String> = std::vec![];

let event_name = topics.get(0).expect("event name topic missing");
topics_output.push(std::format!("{:?}", soroban_sdk::Symbol::try_from_val(env, &event_name)
.expect("event name should be a Symbol")));

let mut i = 1;
#(
let topic = topics.get(i).expect("the number of topics does not match this function's definition");
topics_output.push(std::format!("{:?}", <#topic_types>::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::Vec<std::string::String> = 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_types>::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(", ")
)
}
}
}
}

fn event_name_snake_case(input: &DeriveInput) -> String {
input
.attrs
.iter()
.find(|attr| attr.path().is_ident("event_name"))
.map(|attr| attr.parse_args::<LitStr>().unwrap().value())
.unwrap_or_else(|| {
input
.ident
.to_string()
.strip_suffix("Event")
.unwrap()
.to_snake_case()
})
}

fn event_struct_fields(
input: &DeriveInput,
) -> ((Vec<&Ident>, Vec<&Type>), (Vec<&Ident>, Vec<&Type>)) {
let syn::Data::Struct(data_struct) = &input.data else {
panic!("IntoEvent can only be derived for structs");
};

let mut topic_idents = Vec::new();
let mut topic_types = Vec::new();
let mut data_idents = Vec::new();
let mut data_types = Vec::new();

for field in data_struct.fields.iter() {
if let Some(ident) = field.ident.as_ref() {
if field.attrs.iter().any(|attr| attr.path().is_ident("data")) {
data_idents.push(ident);
data_types.push(&field.ty);
} else {
topic_idents.push(ident);
topic_types.push(&field.ty);
}
}
}

((topic_idents, topic_types), (data_idents, data_types))
}
Loading
Loading