diff --git a/README.md b/README.md index 6a641e1..5871061 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Here's an example of an event handler that processes `TVLUpdated` events emitted use alloy::eips::BlockNumberOrTag; use ghost_crab::prelude::*; -#[handler(EtherFi.TVLUpdated)] +#[event_handler(EtherFi.TVLUpdated)] async fn EtherFiTVLUpdated(ctx: Context) { let block_number = ctx.log.block_number.unwrap() as i64; let current_tvl = event._currentTvl.to_string(); @@ -180,12 +180,12 @@ Here's an example on how to use templates: use alloy::eips::BlockNumberOrTag; use ghost_crab::prelude::*; -#[handler(ETHVault.Deposited)] +#[template(ETHVault.Deposited)] async fn ETHVaultDeposited(ctx: Context) { // Handler Logic } -#[handler(VaultsRegistry.VaultAdded)] +#[event_handler(VaultsRegistry.VaultAdded)] async fn VaultsRegistry(ctx: Context) { let vault = event.vault.to_string(); @@ -238,12 +238,10 @@ GhostCrab uses a configuration file to specify the data sources, templates, and In summary: - If you want to use an environment variable, you can use the `$ENV_VAR` syntax within the configuration file. -- If you want to create an event handler, you need to define a data source. This data source will be loaded by the proc macro `handler` (event handler). -- If you want to create a template, you need to define a template. This template will be loaded by the proc macro `handler` (event handler). +- If you want to create an event handler, you need to define a data source. This data source will be loaded by the proc macro `event_handler`. +- If you want to create a template, you need to define a template. This template will be loaded by the proc macro `template`. - If you want to create a block handler, you need to define a block handler. This block handler will be loaded by the procedural macro `block_handler`. -Note: the `handler` (event handler) proc macro, tries to look for a data source first, and if it doesn't find one, it will look for a template. - # Examples If you want to see some examples of how to use GhostCrab, you can check out our [indexers](https://github.com/stakelens/indexers) repo, where we maintain a collection of smart contracts indexers for our [staking analytics dashboard](https://stakelens.com). diff --git a/ghost-crab-macros/src/lib.rs b/ghost-crab-macros/src/lib.rs index e88338a..d5e35b4 100644 --- a/ghost-crab-macros/src/lib.rs +++ b/ghost-crab-macros/src/lib.rs @@ -2,7 +2,7 @@ extern crate proc_macro; use std::fs; use proc_macro::TokenStream; -use proc_macro2::Literal; +use proc_macro2::{Ident, Literal}; use quote::{format_ident, quote}; use syn::{parse_macro_input, ItemFn}; @@ -39,93 +39,80 @@ struct Config { // #[proc_macro_attribute] -pub fn handler(metadata: TokenStream, input: TokenStream) -> TokenStream { - let metadata_string = metadata.to_string(); - let mut metadata_split = metadata_string.split('.'); +pub fn event_handler(metadata: TokenStream, input: TokenStream) -> TokenStream { + let (name, event_name) = get_source_and_event(metadata); + let config = get_config(); - let name = metadata_split.next(); - let event_name = metadata_split.next(); + let source = config.data_sources.get(&name).expect("Source not found."); + let abi = Literal::string(&source.abi.clone()); - if name.is_none() { - panic!("The source is missing"); - } - - if event_name.is_none() { - panic!("The event name is missing"); - } + let parsed = parse_macro_input!(input as ItemFn); + let fn_name = parsed.sig.ident.clone(); + let fn_args = parsed.sig.inputs.clone(); + let fn_body = parsed.block.clone(); + let ctx = get_context_identifier(parsed); - // Checks that the metadata does not have more than 3 comma separated values - let should_be_none = metadata_split.next(); - if should_be_none.is_some() { - panic!("The metadata has too many values"); - } + let contract_name = format_ident!("{}Contract", fn_name); + let data_source = Literal::string(&name); - let name = name.unwrap(); - let name = String::from(name.trim()); + TokenStream::from(quote! { + sol!( + #[sol(rpc)] + #contract_name, + #abi + ); - let event_name = event_name.unwrap(); - let event_name = String::from(event_name.trim()); + pub struct #fn_name; - if name.is_empty() { - panic!("The source is empty"); - } + impl #fn_name { + pub fn new() -> Arc> { + Arc::new(Box::new(#fn_name {})) + } + } - if event_name.is_empty() { - panic!("The event name is empty"); - } + #[async_trait] + impl Handler for #fn_name { + async fn handle(&self, #fn_args) { + let decoded_log = #ctx + .log + .log_decode::<#contract_name::#event_name>() + .unwrap(); - let current_dir = std::env::current_dir().unwrap(); - let content = fs::read_to_string(current_dir.join("config.json")); + let event = decoded_log.data(); - let abi; - let is_template; + #fn_body + } - match content { - Ok(content) => { - let config: Config = serde_json::from_str(&content).unwrap(); - let source_data_source = config.data_sources.get(&name); - let source_template = config.templates.get(&name); + fn get_source(&self) -> String { + String::from(#data_source) + } - if source_data_source.is_none() && source_template.is_none() { - panic!("Source '{}' not found.", name); + fn is_template(&self) -> bool { + false } - if source_data_source.is_some() { - is_template = false; - abi = source_data_source.unwrap().abi.clone() - } else { - is_template = true; - abi = source_template.unwrap().abi.clone() + fn get_event_signature(&self) -> String { + #contract_name::#event_name::SIGNATURE.to_string() } } - Err(err) => { - panic!("Error reading the config.json file: {}", err); - } - }; + }) +} - let abi = Literal::string(&abi); - let event_name = syn::Ident::new(&event_name, proc_macro2::Span::call_site()); +#[proc_macro_attribute] +pub fn template(metadata: TokenStream, input: TokenStream) -> TokenStream { + let (name, event_name) = get_source_and_event(metadata); + let config = get_config(); + + let source = config.templates.get(&name).expect("Source not found."); + let abi = Literal::string(&source.abi.clone()); let parsed = parse_macro_input!(input as ItemFn); let fn_name = parsed.sig.ident.clone(); - let fn_body = parsed.block; let fn_args = parsed.sig.inputs.clone(); - - let first_input = parsed.sig.inputs[0].clone(); - let ctx; - - match first_input { - syn::FnArg::Typed(arg) => match *arg.pat { - syn::Pat::Ident(arg) => { - ctx = arg.ident; - } - _ => panic!("Malformed handler function arguments"), - }, - _ => panic!("Malformed handler function arguments"), - } + let fn_body = parsed.block.clone(); + let ctx = get_context_identifier(parsed); let contract_name = format_ident!("{}Contract", fn_name); - let data_source = Literal::string(&name); TokenStream::from(quote! { @@ -161,7 +148,7 @@ pub fn handler(metadata: TokenStream, input: TokenStream) -> TokenStream { } fn is_template(&self) -> bool { - #is_template + true } fn get_event_signature(&self) -> String { @@ -180,22 +167,12 @@ pub fn block_handler(metadata: TokenStream, input: TokenStream) -> TokenStream { panic!("The source is missing"); } - let current_dir = std::env::current_dir().unwrap(); - let content = fs::read_to_string(current_dir.join("config.json")); + let config = get_config(); + let source = config.block_handlers.get(name); - match content { - Ok(content) => { - let config: Config = serde_json::from_str(&content).unwrap(); - let source = config.block_handlers.get(name); - - if source.is_none() { - panic!("Source '{}' not found.", name); - } - } - Err(err) => { - panic!("Error reading the config.json file: {}", err); - } - }; + if source.is_none() { + panic!("Source '{}' not found.", name); + } let parsed = parse_macro_input!(input as ItemFn); let fn_name = parsed.sig.ident.clone(); @@ -225,3 +202,55 @@ pub fn block_handler(metadata: TokenStream, input: TokenStream) -> TokenStream { } }) } + +fn get_config() -> Config { + let current_dir = std::env::current_dir().expect("Current directory not found"); + let config_json_path = current_dir.join("config.json"); + let content = fs::read_to_string(config_json_path).expect("Error reading config file"); + let config: Config = serde_json::from_str(&content).expect("Error parsing config file"); + return config; +} + +fn get_source_and_event(metadata: TokenStream) -> (String, Ident) { + let metadata_string = metadata.to_string(); + let mut metadata_split = metadata_string.split('.'); + + let name = metadata_split.next().expect("The source is missing"); + let name = String::from(name.trim()); + + if name.is_empty() { + panic!("The source is empty"); + } + + let event_name = metadata_split.next().expect("The event name is missing"); + let event_name = String::from(event_name.trim()); + + if event_name.is_empty() { + panic!("The event name is empty"); + } + + // Checks that the metadata does not have more than 3 comma separated values + let should_be_none = metadata_split.next(); + if should_be_none.is_some() { + panic!("The metadata has too many values"); + } + + let event_name = syn::Ident::new(&event_name, proc_macro2::Span::call_site()); + return (name, event_name); +} + +fn get_context_identifier(parsed: ItemFn) -> Ident { + let first_input = parsed.sig.inputs[0].clone(); + + let ctx = if let syn::FnArg::Typed(pat_type) = first_input { + if let syn::Pat::Ident(pat_ident) = *pat_type.pat { + pat_ident.ident + } else { + panic!("Malformed handler function arguments") + } + } else { + panic!("Malformed handler function arguments") + }; + + return ctx; +} diff --git a/ghost-crab/src/prelude.rs b/ghost-crab/src/prelude.rs index 4f33f90..f5c758e 100644 --- a/ghost-crab/src/prelude.rs +++ b/ghost-crab/src/prelude.rs @@ -7,7 +7,8 @@ pub use alloy::{ }; pub use async_trait::async_trait; pub use ghost_crab_macros::block_handler; -pub use ghost_crab_macros::handler; +pub use ghost_crab_macros::event_handler; +pub use ghost_crab_macros::template; pub use std::sync::Arc; pub use tokio;