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

Enforce Standard Logging Compliance by Abstraction #934

Merged
merged 12 commits into from
Nov 8, 2022
83 changes: 83 additions & 0 deletions near-sdk-macros/src/core_impl/event/mod.rs
Original file line number Diff line number Diff line change
@@ -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.",
austinabell marked this conversation as resolved.
Show resolved Hide resolved
)
.to_compile_error(),
);
}

if let Ok(mut input) = syn::parse::<ItemEnum>(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<LitStr> {
for attr in var.attrs.iter() {
if attr.path.is_ident("event_version") {
return attr.parse_args::<LitStr>().ok();
}
}
None
}

/// this function returns the `standard` value from `#[near_bindgen(event_json(standard = "nepXXX"))]`
fn get_standard_arg(args: &[syn::NestedMeta]) -> Option<LitStr> {
let mut standard: Option<LitStr> = 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
}
2 changes: 2 additions & 0 deletions near-sdk-macros/src/core_impl/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
129 changes: 128 additions & 1 deletion near-sdk-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
austinabell marked this conversation as resolved.
Show resolved Hide resolved
/// }
///
/// }
/// ```
#[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::<ItemStruct>(item.clone()) {
let ext_gen = generate_ext_structs(&input.ident, Some(&input.generics));
#[cfg(feature = "__abi-embed")]
Expand Down Expand Up @@ -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::<ItemEnum>(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<proc_macro2::TokenStream> = 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(),
)
}
}
2 changes: 1 addition & 1 deletion near-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
78 changes: 78 additions & 0 deletions near-sdk/src/types/event_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::test_utils::get_logs;
use crate::{near_bindgen, AccountId};

use crate as near_sdk;

#[near_bindgen(event_json(standard = "test_standard", random = "random"), other_random)]
pub enum TestEvents<'a, 'b, T>
where
T: crate::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>(String::from("string")).emit();

TestEvents::EmptyEvent::<String>.emit();

TestEvents::LifetimeTestA::<String>("lifetime").emit();

TestEvents::LifetimeTestB::<String>("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"}"#
);
}
3 changes: 3 additions & 0 deletions near-sdk/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pub use self::account_id::{AccountId, ParseAccountIdError};
mod gas;
pub use self::gas::Gas;

#[cfg(test)]
mod event_tests;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually the right place for the tests? Should we move it to near-sdk/tests?


mod error;
pub use self::error::Abort;
pub use self::error::FunctionError;
Expand Down