diff --git a/near-sdk-macros/src/core_impl/event/mod.rs b/near-sdk-macros/src/core_impl/event/mod.rs new file mode 100644 index 000000000..7bfc698e6 --- /dev/null +++ b/near-sdk-macros/src/core_impl/event/mod.rs @@ -0,0 +1,83 @@ +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{parse_quote, ItemEnum, LitStr}; + +/// this function is used to inject serialization macros and the `near_sdk::EventMetadata` macro. +/// In addition, this function extracts the event's `standard` value and injects it as a constant to be used by +/// the `near_sdk::EventMetadata` derive macro +pub(crate) fn near_events(attr: TokenStream, item: TokenStream) -> TokenStream { + // get standard from attr args + let standard = get_standard_arg(&syn::parse_macro_input!(attr as syn::AttributeArgs)); + if standard.is_none() { + return TokenStream::from( + syn::Error::new( + Span::call_site(), + "Near events must have a `standard` value as an argument for `event_json` in the `near_bindgen` arguments. The value must be a string literal, e.g. \"nep999\", \"mintbase-marketplace\".", + ) + .to_compile_error(), + ); + } + + if let Ok(mut input) = syn::parse::(item) { + let name = &input.ident; + let standard_name = format!("{}_event_standard", name); + let standard_ident = syn::Ident::new(&standard_name, Span::call_site()); + // NearEvent Macro handles implementation + input + .attrs + .push(parse_quote! (#[derive(near_sdk::serde::Serialize, near_sdk::EventMetadata)])); + input.attrs.push(parse_quote! (#[serde(crate="near_sdk::serde")])); + input.attrs.push(parse_quote! (#[serde(tag = "event", content = "data")])); + input.attrs.push(parse_quote! (#[serde(rename_all = "snake_case")])); + + TokenStream::from(quote! { + const #standard_ident: &'static str = #standard; + #input + }) + } else { + TokenStream::from( + syn::Error::new( + Span::call_site(), + "`#[near_bindgen(event_json(standard = \"nepXXX\"))]` can only be used as an attribute on enums.", + ) + .to_compile_error(), + ) + } +} + +/// This function returns the `version` value from `#[event_version("x.x.x")]`. +/// used by `near_sdk::EventMetadata` +pub(crate) fn get_event_version(var: &syn::Variant) -> Option { + for attr in var.attrs.iter() { + if attr.path.is_ident("event_version") { + return attr.parse_args::().ok(); + } + } + None +} + +/// this function returns the `standard` value from `#[near_bindgen(event_json(standard = "nepXXX"))]` +fn get_standard_arg(args: &[syn::NestedMeta]) -> Option { + let mut standard: Option = None; + for arg in args.iter() { + if let syn::NestedMeta::Meta(syn::Meta::List(syn::MetaList { path, nested, .. })) = arg { + if path.is_ident("event_json") { + for event_arg in nested.iter() { + if let syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue { + path, + lit: syn::Lit::Str(value), + .. + })) = event_arg + { + if path.is_ident("standard") { + standard = Some(value.to_owned()); + break; + } + } + } + } + } + } + standard +} diff --git a/near-sdk-macros/src/core_impl/mod.rs b/near-sdk-macros/src/core_impl/mod.rs index 47a0b33a9..abe7439ac 100644 --- a/near-sdk-macros/src/core_impl/mod.rs +++ b/near-sdk-macros/src/core_impl/mod.rs @@ -1,9 +1,11 @@ #[cfg(any(feature = "__abi-embed", feature = "__abi-generate"))] pub(crate) mod abi; mod code_generator; +mod event; mod info_extractor; mod metadata; mod utils; pub(crate) use code_generator::*; +pub(crate) use event::{get_event_version, near_events}; pub(crate) use info_extractor::*; pub(crate) use metadata::metadata_visitor::MetadataVisitor; diff --git a/near-sdk-macros/src/lib.rs b/near-sdk-macros/src/lib.rs index 420cf6b2d..2444085b9 100644 --- a/near-sdk-macros/src/lib.rs +++ b/near-sdk-macros/src/lib.rs @@ -43,8 +43,51 @@ use syn::{parse_quote, File, ItemEnum, ItemImpl, ItemStruct, ItemTrait, WhereCla /// pub fn some_function(&self) {} /// } /// ``` +/// +/// Events Standard: +/// +/// By passing `event_json` as an argument `near_bindgen` will generate the relevant code to format events +/// according to NEP-297 +/// +/// For parameter serialization, this macro will generate a wrapper struct to include the NEP-297 standard fields `standard` and `version +/// as well as include serialization reformatting to include the `event` and `data` fields automatically. +/// The `standard` and `version` values must be included in the enum and variant declaration (see example below). +/// By default this will be JSON deserialized with `serde` +/// +/// +/// # Examples +/// +/// ```ignore +/// use near_sdk::near_bindgen; +/// +/// #[near_bindgen(event_json(standard = "nepXXX"))] +/// pub enum MyEvents { +/// #[event_version("1.0.0")] +/// Swap { token_in: AccountId, token_out: AccountId, amount_in: u128, amount_out: u128 }, +/// +/// #[event_version("2.0.0")] +/// StringEvent(String), +/// +/// #[event_version("3.0.0")] +/// EmptyEvent +/// } +/// +/// #[near_bindgen] +/// impl Contract { +/// pub fn some_function(&self) { +/// MyEvents::StringEvent( +/// String::from("some_string") +/// ).emit(); +/// } +/// +/// } +/// ``` #[proc_macro_attribute] -pub fn near_bindgen(_attr: TokenStream, item: TokenStream) -> TokenStream { +pub fn near_bindgen(attr: TokenStream, item: TokenStream) -> TokenStream { + if attr.to_string().contains("event_json") { + return core_impl::near_events(attr, item); + } + if let Ok(input) = syn::parse::(item.clone()) { let ext_gen = generate_ext_structs(&input.ident, Some(&input.generics)); #[cfg(feature = "__abi-embed")] @@ -316,3 +359,87 @@ pub fn function_error(item: TokenStream) -> TokenStream { } }) } + +/// NOTE: This is an internal implementation for `#[near_bindgen(events(standard = ...))]` attribute. +/// +/// This derive macro is used to inject the necessary wrapper and logic to auto format +/// standard event logs. The other appropriate attribute macros are not injected with this macro. +/// Required attributes below: +/// ```ignore +/// #[derive(near_sdk::serde::Serialize, std::clone::Clone)] +/// #[serde(crate="near_sdk::serde")] +/// #[serde(tag = "event", content = "data")] +/// #[serde(rename_all="snake_case")] +/// pub enum MyEvent { +/// Event +/// } +/// ``` +#[proc_macro_derive(EventMetadata, attributes(event_version))] +pub fn derive_event_attributes(item: TokenStream) -> TokenStream { + if let Ok(input) = syn::parse::(item) { + let name = &input.ident; + // get `standard` const injected from `near_events` + let standard_name = format!("{}_event_standard", name); + let standard_ident = syn::Ident::new(&standard_name, Span::call_site()); + // version from each attribute macro + let mut event_meta: Vec = vec![]; + for var in &input.variants { + if let Some(version) = core_impl::get_event_version(var) { + let var_ident = &var.ident; + event_meta.push(quote! { + #name::#var_ident { .. } => {(#standard_ident.to_string(), #version.to_string())} + }) + } else { + return TokenStream::from( + syn::Error::new( + Span::call_site(), + "Near events must have `event_version`. Must have a single string literal value.", + ) + .to_compile_error(), + ); + } + } + + // handle lifetimes, generics, and where clauses + let (impl_generics, type_generics, where_clause) = &input.generics.split_for_impl(); + // add `'near_event` lifetime for user defined events + let mut generics = input.generics.clone(); + let event_lifetime = syn::Lifetime::new("'near_event", Span::call_site()); + generics + .params + .insert(0, syn::GenericParam::Lifetime(syn::LifetimeDef::new(event_lifetime.clone()))); + let (custom_impl_generics, ..) = generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #name #type_generics #where_clause { + fn emit(&self) { + let (standard, version): (String, String) = match self { + #(#event_meta),* + }; + + #[derive(near_sdk::serde::Serialize)] + #[serde(crate="near_sdk::serde")] + #[serde(rename_all="snake_case")] + struct EventBuilder #custom_impl_generics #where_clause { + standard: String, + version: String, + #[serde(flatten)] + event_data: &#event_lifetime #name #type_generics + } + let event = EventBuilder { standard, version, event_data: self }; + let json = near_sdk::serde_json::to_string(&event) + .unwrap_or_else(|_| near_sdk::env::abort()); + near_sdk::env::log_str(&format!("EVENT_JSON:{}", json)); + } + } + }) + } else { + TokenStream::from( + syn::Error::new( + Span::call_site(), + "EventMetadata can only be used as a derive on enums.", + ) + .to_compile_error(), + ) + } +} diff --git a/near-sdk/src/lib.rs b/near-sdk/src/lib.rs index 185f6700a..b305ec016 100644 --- a/near-sdk/src/lib.rs +++ b/near-sdk/src/lib.rs @@ -6,7 +6,7 @@ extern crate quickcheck; pub use near_sdk_macros::{ - ext_contract, near_bindgen, BorshStorageKey, FunctionError, PanicOnDefault, + ext_contract, near_bindgen, BorshStorageKey, EventMetadata, FunctionError, PanicOnDefault, }; pub mod store; diff --git a/near-sdk/tests/event_tests.rs b/near-sdk/tests/event_tests.rs new file mode 100644 index 000000000..62356dd51 --- /dev/null +++ b/near-sdk/tests/event_tests.rs @@ -0,0 +1,76 @@ +use near_sdk::test_utils::get_logs; +use near_sdk::{near_bindgen, AccountId}; + +#[near_bindgen(event_json(standard = "test_standard", random = "random"), other_random)] +pub enum TestEvents<'a, 'b, T> +where + T: near_sdk::serde::Serialize, +{ + #[event_version("1.0.0")] + Swap { token_in: AccountId, token_out: AccountId, amount_in: u128, amount_out: u128, test: T }, + + #[event_version("2.0.0")] + StringEvent(String), + + #[event_version("3.0.0")] + EmptyEvent, + + #[event_version("4.0.0")] + LifetimeTestA(&'a str), + + #[event_version("5.0.0")] + LifetimeTestB(&'b str), +} + +#[near_bindgen(event_json(standard = "another_standard"))] +pub enum AnotherEvent { + #[event_version("1.0.0")] + Test, +} + +#[test] +fn test_json_emit() { + let token_in: AccountId = "wrap.near".parse().unwrap(); + let token_out: AccountId = "test.near".parse().unwrap(); + let amount_in: u128 = 100; + let amount_out: u128 = 200; + TestEvents::Swap { token_in, token_out, amount_in, amount_out, test: String::from("tst") } + .emit(); + + TestEvents::StringEvent::(String::from("string")).emit(); + + TestEvents::EmptyEvent::.emit(); + + TestEvents::LifetimeTestA::("lifetime").emit(); + + TestEvents::LifetimeTestB::("lifetime_b").emit(); + + AnotherEvent::Test.emit(); + + let logs = get_logs(); + + assert_eq!( + logs[0], + r#"EVENT_JSON:{"standard":"test_standard","version":"1.0.0","event":"swap","data":{"token_in":"wrap.near","token_out":"test.near","amount_in":100,"amount_out":200,"test":"tst"}}"# + ); + assert_eq!( + logs[1], + r#"EVENT_JSON:{"standard":"test_standard","version":"2.0.0","event":"string_event","data":"string"}"# + ); + assert_eq!( + logs[2], + r#"EVENT_JSON:{"standard":"test_standard","version":"3.0.0","event":"empty_event"}"# + ); + assert_eq!( + logs[3], + r#"EVENT_JSON:{"standard":"test_standard","version":"4.0.0","event":"lifetime_test_a","data":"lifetime"}"# + ); + assert_eq!( + logs[4], + r#"EVENT_JSON:{"standard":"test_standard","version":"5.0.0","event":"lifetime_test_b","data":"lifetime_b"}"# + ); + assert_eq!( + logs[5], + r#"EVENT_JSON:{"standard":"another_standard","version":"1.0.0","event":"test"}"# + ); +}