Skip to content

Commit

Permalink
feat(minor-axelar-wasm-std-derive): derive macro IntoEvent (#734)
Browse files Browse the repository at this point in the history
Co-authored-by: Christian Gorenflo <[email protected]>
  • Loading branch information
fish-sammy and cgorenflo authored Jan 3, 2025
1 parent 7240b50 commit 780d7c3
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 269 deletions.
371 changes: 187 additions & 184 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ evm-gateway = { version = "^1.0.0", path = "packages/evm-gateway" }
gateway = { version = "^1.1.1", path = "contracts/gateway" }
gateway-api = { version = "^1.0.0", path = "packages/gateway-api" }
goldie = { version = "0.5" }
heck = "0.5.0"
hex = "0.4.3"
integration-tests = { version = "^1.0.0", path = "integration-tests" }
interchain-token-service = { version = "^1.0.0", path = "contracts/interchain-token-service" }
Expand All @@ -52,7 +53,7 @@ msgs-derive = { version = "^1.0.0", path = "packages/msgs-derive" }
multisig = { version = "^1.1.1", path = "contracts/multisig" }
multisig-prover = { version = "^1.1.1", path = "contracts/multisig-prover" }
num-traits = { version = "0.2.14", default-features = false }
quote = "1.0.36"
quote = "1.0.38"
rand = "0.8.5"
report = { version = "^1.0.0", path = "packages/report" }
rewards = { version = "^1.2.0", path = "contracts/rewards" }
Expand All @@ -61,7 +62,7 @@ router-api = { version = "^1.0.0", path = "packages/router-api" }
schemars = "0.8.10"
semver = "1.0"
serde = { version = "1.0.145", default-features = false, features = ["derive"] }
serde_json = "1.0.89"
serde_json = "1.0.134"
service-registry = { version = "^1.1.0", path = "contracts/service-registry" }
service-registry-api = { version = "^1.0.0", path = "packages/service-registry-api" }
sha3 = { version = "0.10.8", default-features = false, features = [] }
Expand All @@ -71,14 +72,15 @@ stellar-xdr = { version = "21.2.0" }
strum = { version = "0.25", default-features = false, features = ["derive"] }
sui-gateway = { version = "^1.0.0", path = "packages/sui-gateway" }
sui-types = { version = "^1.0.0", path = "packages/sui-types" }
syn = "2.0.68"
syn = "2.0.92"
thiserror = "1.0.61"
tofn = { version = "1.1" }
tokio = "1.38.0"
tokio-stream = "0.1.11"
tokio-util = "0.7.11"
voting-verifier = { version = "^1.1.0", path = "contracts/voting-verifier" }
axelar-core-std = { version = "^1.0.0", path = "packages/axelar-core-std" }
proc-macro2 = "1.0.92"

[workspace.lints.clippy]
arithmetic_side_effects = "deny"
Expand Down
2 changes: 1 addition & 1 deletion contracts/multisig-prover/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ multisig = { workspace = true, features = ["library"] }
report = { workspace = true }
router-api = { workspace = true }
semver = { workspace = true }
serde_json = "1.0.89"
serde_json = { workspace = true }
service-registry = { workspace = true }
service-registry-api = { workspace = true }
sha3 = { workspace = true }
Expand Down
39 changes: 4 additions & 35 deletions contracts/multisig-prover/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use axelar_wasm_std::IntoEvent;
use cosmwasm_std::Uint64;
use router_api::{ChainName, CrossChainId};

use crate::payload::PayloadId;

#[derive(IntoEvent)]
pub enum Event {
ProofUnderConstruction {
destination_chain: ChainName,
Expand All @@ -12,43 +14,9 @@ pub enum Event {
},
}

impl From<Event> for cosmwasm_std::Event {
fn from(other: Event) -> Self {
match other {
Event::ProofUnderConstruction {
destination_chain,
payload_id,
multisig_session_id,
msg_ids,
} => cosmwasm_std::Event::new("proof_under_construction")
.add_attribute(
"destination_chain",
serde_json::to_string(&destination_chain)
.expect("violated invariant: destination_chain is not serializable"),
)
.add_attribute(
"payload_id",
serde_json::to_string(&payload_id)
.expect("violated invariant: payload_id is not serializable"),
)
.add_attribute(
"multisig_session_id",
serde_json::to_string(&multisig_session_id)
.expect("violated invariant: multisig_session_id is not serializable"),
)
.add_attribute(
"message_ids",
serde_json::to_string(&msg_ids)
.expect("violated invariant: message_ids is not serializable"),
),
}
}
}

#[cfg(test)]
mod tests {
use router_api::Message;
use serde_json::to_string;

use super::*;
use crate::payload::Payload;
Expand Down Expand Up @@ -78,7 +46,8 @@ mod tests {
multisig_session_id: Uint64::new(2),
msg_ids: payload.message_ids().unwrap(),
};
let event = cosmwasm_std::Event::from(event);

assert!(to_string(&cosmwasm_std::Event::from(event)).is_ok());
goldie::assert_json!(event);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"type": "proof_under_construction",
"attributes": [
{
"key": "destination_chain",
"value": "\"avalanche\""
},
{
"key": "payload_id",
"value": "\"442440511db473dc80e1630be5e78133559e5af8027bb1cd6209fd8c5679381d\""
},
{
"key": "multisig_session_id",
"value": "\"2\""
},
{
"key": "msg_ids",
"value": "[{\"source_chain\":\"ethereum\",\"message_id\":\"some-id\"},{\"source_chain\":\"fantom\",\"message_id\":\"some-other-id\"}]"
}
]
}
3 changes: 3 additions & 0 deletions packages/axelar-wasm-std-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ proc-macro = true
[dependencies]
cosmwasm-std = { workspace = true }
error-stack = { workspace = true }
heck = { workspace = true }
itertools = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
report = { workspace = true }
syn = { workspace = true }
Expand Down
187 changes: 142 additions & 45 deletions packages/axelar-wasm-std-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use std::iter;

use heck::ToSnakeCase;
use itertools::Itertools;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::quote;
use syn::DeriveInput;
use syn::{DeriveInput, FieldsNamed, ItemEnum, Variant};

#[proc_macro_derive(IntoContractError)]
pub fn into_contract_error_derive(input: TokenStream) -> TokenStream {
Expand All @@ -21,80 +26,172 @@ pub fn into_contract_error_derive(input: TokenStream) -> TokenStream {
gen.into()
}

/// Derive macro to implement `From` for a struct to convert it into a `cosmwasm_std::Event`.
/// Derive macro to implement `From` for an enum to convert it into a `cosmwasm_std::Event`.
///
/// # Examples
///
/// ```
/// use std::collections::HashMap;
/// use std::collections::BTreeMap;
/// use serde::Serialize;
///
/// use axelar_wasm_std_derive::into_event;
/// use axelar_wasm_std_derive::IntoEvent;
///
/// #[derive(Serialize)]
/// struct SomeObject {
/// pub some_option: Option<String>,
/// pub some_other_option: Option<String>,
/// pub some_vec: Vec<String>,
/// pub some_map: HashMap<String, String>,
/// pub some_map: BTreeMap<String, String>,
/// }
///
/// #[derive(Serialize)]
/// #[into_event("some_event")]
/// struct SomeEvent {
/// pub some_uint: u64,
/// pub some_string: String,
/// pub some_bool: bool,
/// pub some_object: SomeObject,
/// #[derive(IntoEvent)]
/// enum SomeEvents {
/// SomeEmptyEvent,
/// SomeOtherEmptyEvent {},
/// SomeEvent {
/// some_uint: u64,
/// some_string: String,
/// some_bool: bool,
/// some_object: SomeObject,
/// },
/// }
///
/// let event = SomeEvent {
/// let actual = cosmwasm_std::Event::from(SomeEvents::SomeEmptyEvent);
/// let expected = cosmwasm_std::Event::new("some_empty_event");
/// assert_eq!(actual, expected);
///
/// let actual = cosmwasm_std::Event::from(SomeEvents::SomeOtherEmptyEvent {});
/// let expected = cosmwasm_std::Event::new("some_other_empty_event");
/// assert_eq!(actual, expected);
///
/// let actual = cosmwasm_std::Event::from(SomeEvents::SomeEvent {
/// some_uint: 42,
/// some_string: "string".to_string(),
/// some_string: "some string".to_string(),
/// some_bool: true,
/// some_object: SomeObject {
/// some_option: Some("some".to_string()),
/// some_option: Some("some option".to_string()),
/// some_other_option: None,
/// some_vec: vec!["a".to_string(), "b".to_string()],
/// some_map: [("a".to_string(), "b".to_string()), ("c".to_string(), "d".to_string()), ("e".to_string(), "f".to_string())].into_iter().collect(),
/// }
/// };
/// let actual_event = cosmwasm_std::Event::from(event);
/// let expected_event = cosmwasm_std::Event::new("some_event")
/// },
/// });
/// let expected = cosmwasm_std::Event::new("some_event")
/// .add_attribute("some_uint", "42")
/// .add_attribute("some_string", "\"some string\"")
/// .add_attribute("some_bool", "true")
/// .add_attribute("some_object", r#"{"some_map":{"a":"b","c":"d","e":"f"},"some_option":"some","some_other_option":null,"some_vec":["a","b"]}"#)
/// .add_attribute("some_string", "\"string\"")
/// .add_attribute("some_uint", "42");
/// .add_attribute("some_object", "{\"some_option\":\"some option\",\"some_other_option\":null,\"some_vec\":[\"a\",\"b\"],\"some_map\":{\"a\":\"b\",\"c\":\"d\",\"e\":\"f\"}}");
/// assert_eq!(actual, expected);
/// ```
///
/// ```compile_fail
/// # use axelar_wasm_std_derive::IntoEvent;
///
/// assert_eq!(actual_event, expected_event);
/// # #[derive(IntoEvent)] // should not compile because the event is not an enum
/// # struct SomeStructEvent {
/// # pub some_uint: u64,
/// # }
/// ```
#[proc_macro_attribute]
pub fn into_event(arg: TokenStream, input: TokenStream) -> TokenStream {
let event_name = syn::parse_macro_input!(arg as syn::LitStr);
let input = syn::parse_macro_input!(input as syn::ItemStruct);

let event_struct = input.ident.clone();

TokenStream::from(quote! {
#input

impl From<&#event_struct> for cosmwasm_std::Event {
fn from(event: &#event_struct) -> Self {
let json_value = serde_json::to_value(event).expect("failed to serialize event");
let attributes = json_value
.as_object()
.expect("event must be a json object")
.into_iter()
.map(|(key, value)| cosmwasm_std::Attribute::new(key, value.to_string()));

cosmwasm_std::Event::new(#event_name.to_string()).add_attributes(attributes)
///
/// ```compile_fail
/// # use axelar_wasm_std_derive::IntoEvent;
///
/// # #[derive(IntoEvent)] // should not compile because the event has some unnamed field
/// # enum SomeEventWithUnnamedField {
/// # Uint,
/// # Named { some_uint: u64 },
/// # Unnamed(u64),
/// # }
/// ```
#[proc_macro_derive(IntoEvent)]
pub fn into_event(input: TokenStream) -> TokenStream {
let ItemEnum {
variants,
ident: event_enum,
..
} = syn::parse_macro_input!(input as syn::ItemEnum);

try_into_event(event_enum, variants)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}

fn try_into_event(
event_enum: Ident,
variants: impl IntoIterator<Item = Variant>,
) -> Result<TokenStream2, syn::Error> {
let variant_matches: Vec<_> = variants
.into_iter()
.map(|variant| match variant.fields {
syn::Fields::Named(fields) => Ok(match_structured_variant(
&event_enum,
&variant.ident,
fields,
)),
syn::Fields::Unit => Ok(match_unit_variant(&event_enum, &variant.ident)),
syn::Fields::Unnamed(_) => Err(syn::Error::new(
Span::call_site(),
"unnamed fields are not supported",
)),
})
.try_collect()?;

Ok(quote! {
impl From<&#event_enum> for cosmwasm_std::Event {
fn from(event: &#event_enum) -> Self {
match event {
#(#variant_matches),*
}
}
}

impl From<#event_struct> for cosmwasm_std::Event {
fn from(event: #event_struct) -> Self {
impl From<#event_enum> for cosmwasm_std::Event {
fn from(event: #event_enum) -> Self {
(&event).into()
}
}
})
}

fn match_structured_variant(
event_enum: &Ident,
variant_name: &Ident,
fields: FieldsNamed,
) -> TokenStream2 {
let event_name = variant_name.to_string().to_snake_case();

// we know these are named fields, so flat_map is a safe operation to get all the identifiers
let field_names = fields
.named
.into_iter()
.flat_map(|field| field.ident)
.collect_vec();

let new_event = quote! {
#event_enum::#variant_name { #(#field_names), * } => cosmwasm_std::Event::new(#event_name)
};

let add_attributes = field_names.iter().map(|field_name| {
let field_name_str = field_name.to_string();
let attribute_name = field_name_str.to_snake_case();
// compute the error message outside the quote! so the resulting string will be baked in at compile time
let error_message = format!("failed to serialize event field {}", field_name_str);

quote! {
add_attribute(#attribute_name, serde_json::to_string(#field_name).expect(#error_message))
}
});

let variant_pattern = iter::once(new_event).chain(add_attributes);

quote! {
#(#variant_pattern).*
}
}

fn match_unit_variant(event_enum: &Ident, variant_name: &Ident) -> TokenStream2 {
let event_name = variant_name.to_string().to_snake_case();

quote! {
#event_enum::#variant_name => cosmwasm_std::Event::new(#event_name)
}
}
2 changes: 1 addition & 1 deletion packages/msgs-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ axelar-wasm-std = { workspace = true }
cosmwasm-std = { workspace = true }
error-stack = { workspace = true }
itertools = { workspace = true }
proc-macro2 = "1.0.85"
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }

Expand Down

0 comments on commit 780d7c3

Please sign in to comment.