Skip to content

Commit

Permalink
Adds "template" proc-macro and renames "handle" to "event_handler"
Browse files Browse the repository at this point in the history
  • Loading branch information
luis-herasme committed Jul 19, 2024
1 parent 44e6bdf commit 9b46d78
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 90 deletions.
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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).
193 changes: 111 additions & 82 deletions ghost-crab-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<Box<(dyn Handler + Send + Sync)>> {
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! {
Expand Down Expand Up @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
3 changes: 2 additions & 1 deletion ghost-crab/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 9b46d78

Please sign in to comment.