Skip to main content

graphite_macros/
lib.rs

1//! Proc macros for the Graphite subgraph SDK.
2//!
3//! Provides `#[derive(Entity)]` and `#[handler]` macros for ergonomic
4//! subgraph development.
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{DeriveInput, ItemFn, parse_macro_input};
9
10/// Derive macro for entity types.
11///
12/// Generates `Store` trait implementation with `load()`, `save()`, and `remove()` methods.
13///
14/// # Example
15///
16/// ```rust,ignore
17/// #[derive(Entity)]
18/// pub struct Transfer {
19///     #[id]
20///     id: String,
21///     from: Address,
22///     to: Address,
23///     value: BigInt,
24/// }
25/// ```
26///
27/// The struct must have exactly one field marked with `#[id]`.
28#[proc_macro_derive(Entity, attributes(id, graphite))]
29pub fn derive_entity(input: TokenStream) -> TokenStream {
30    let input = parse_macro_input!(input as DeriveInput);
31    let name = &input.ident;
32    let entity_type = name.to_string();
33
34    let fields = match &input.data {
35        syn::Data::Struct(data) => match &data.fields {
36            syn::Fields::Named(fields) => &fields.named,
37            _ => panic!("Entity derive only supports structs with named fields"),
38        },
39        _ => panic!("Entity derive only supports structs"),
40    };
41
42    // Find the #[id] field
43    let id_field = fields
44        .iter()
45        .find(|f| f.attrs.iter().any(|a| a.path().is_ident("id")))
46        .expect("Entity must have exactly one field marked with #[id]");
47    let id_field_name = id_field.ident.as_ref().unwrap();
48
49    // Generate field setters for to_entity
50    let field_setters = fields.iter().map(|f| {
51        let field_name = f.ident.as_ref().unwrap();
52        let field_name_str = to_camel_case(&field_name.to_string());
53        quote! {
54            entity.set(#field_name_str, self.#field_name.clone());
55        }
56    });
57
58    // Generate field getters for from_entity
59    let field_getters = fields.iter().map(|f| {
60        let field_name = f.ident.as_ref().unwrap();
61        let field_name_str = to_camel_case(&field_name.to_string());
62        let field_type = &f.ty;
63        quote! {
64            #field_name: entity
65                .get(#field_name_str)
66                .and_then(|v| <#field_type as graphite::store::FromValue>::from_value(v.clone()))
67                .ok_or_else(|| graphite::store::EntityError::MissingField(#field_name_str.into()))?
68        }
69    });
70
71    // Generate Default-like field initializers for new()
72    let field_defaults = fields.iter().map(|f| {
73        let field_name = f.ident.as_ref().unwrap();
74        if f.attrs.iter().any(|a| a.path().is_ident("id")) {
75            quote! { #field_name: id.into() }
76        } else {
77            quote! { #field_name: Default::default() }
78        }
79    });
80
81    let expanded = quote! {
82        impl #name {
83            /// Create a new instance with the given ID and default field values.
84            pub fn new(id: impl Into<String>) -> Self {
85                Self {
86                    #(#field_defaults),*
87                }
88            }
89
90            /// Load an entity from the store.
91            pub fn load<H: graphite::host::HostFunctions>(host: &H, id: &str) -> Option<Self> {
92                host.store_get(#entity_type, id)
93                    .and_then(|e| Self::from_entity(e).ok())
94            }
95
96            /// Save this entity to the store.
97            pub fn save<H: graphite::host::HostFunctions>(&self, host: &mut H) {
98                host.store_set(#entity_type, &self.id(), self.to_entity());
99            }
100
101            /// Remove this entity from the store.
102            pub fn remove<H: graphite::host::HostFunctions>(host: &mut H, id: &str) {
103                host.store_remove(#entity_type, id);
104            }
105        }
106
107        impl graphite::store::Store for #name {
108            const ENTITY_TYPE: &'static str = #entity_type;
109
110            fn id(&self) -> &str {
111                &self.#id_field_name
112            }
113
114            fn to_entity(&self) -> graphite::store::Entity {
115                let mut entity = graphite::store::Entity::new();
116                #(#field_setters)*
117                entity
118            }
119
120            fn from_entity(entity: graphite::store::Entity) -> Result<Self, graphite::store::EntityError> {
121                Ok(Self {
122                    #(#field_getters),*
123                })
124            }
125        }
126    };
127
128    TokenStream::from(expanded)
129}
130
131/// Attribute macro for handler functions.
132///
133/// Generates the `extern "C"` wrapper that graph-node calls, reading the
134/// EthereumEvent from AS memory via `graph_as_runtime::ethereum::read_ethereum_event`,
135/// constructing the typed event via `EventType::from_raw_event`, and delegating
136/// to the user's handler implementation.
137///
138/// graph-node enforces strict return-type rules on exported WASM functions:
139/// - **Event handlers** must return `()` (void) — use `#[handler]`
140/// - **Block handlers** must return `i32` — use `#[handler(block)]`
141///
142/// # Signature
143///
144/// The user's function must take two parameters:
145/// - First: the event/block type (e.g. `TransferEvent`) — read from AS memory
146/// - Second: `ctx: &graphite::EventContext` — block/tx metadata
147///
148/// # Examples
149///
150/// ```rust,ignore
151/// // Event handler — WASM export returns void
152/// #[handler]
153/// pub fn handle_transfer(event: &ERC20TransferEvent, ctx: &graphite::EventContext) {
154///     // Handler logic here
155/// }
156///
157/// // Block handler — WASM export returns i32
158/// #[handler(block)]
159/// pub fn handle_block(block: &EthereumBlock, ctx: &graphite::EventContext) {
160///     // Block handler logic here
161/// }
162/// ```
163#[proc_macro_attribute]
164pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
165    let attr_str = attr.to_string();
166    let attr_str = attr_str.trim();
167
168    // Detect the handler variant.
169    let is_block_handler = !attr.is_empty() && attr_str == "block";
170    let is_call_handler = !attr.is_empty() && attr_str == "call";
171    let is_file_handler = !attr.is_empty() && attr_str == "file";
172
173    let input = parse_macro_input!(item as ItemFn);
174    let fn_name = &input.sig.ident;
175    let fn_body = &input.block;
176    let fn_inputs = &input.sig.inputs;
177    let fn_vis = &input.vis;
178
179    // Extract the event/block parameter type from the first argument.
180    let event_param = fn_inputs
181        .first()
182        .expect("Handler must have at least one parameter (event)");
183    let (param_name, param_type) = match event_param {
184        syn::FnArg::Typed(pat_type) => {
185            let name = match &*pat_type.pat {
186                syn::Pat::Ident(ident) => &ident.ident,
187                _ => panic!("Expected identifier pattern for event parameter"),
188            };
189            (name, &pat_type.ty)
190        }
191        _ => panic!("Handler cannot have self parameter"),
192    };
193
194    // The impl function gets the original name suffixed with _impl.
195    let impl_name = syn::Ident::new(&format!("{}_impl", fn_name), fn_name.span());
196
197    // Strip leading & from the param type for FromRawEvent/FromRawCall trait bounds.
198    // Users write `event: &MyEvent` but the trait is impl'd on `MyEvent`, not `&MyEvent`.
199    let event_base_type: &syn::Type = match param_type.as_ref() {
200        syn::Type::Reference(r) => &r.elem,
201        other => other,
202    };
203
204    // Build the WASM entry point. Event handlers return void; block handlers return i32;
205    // call handlers return void; file handlers return void.
206    let wasm_entry = if is_file_handler {
207        quote! {
208            #[cfg(target_arch = "wasm32")]
209            #[unsafe(no_mangle)]
210            pub extern "C" fn #fn_name(content_ptr: i32) {
211                let content = unsafe {
212                    graph_as_runtime::store_read::read_asc_bytes(content_ptr as u32)
213                };
214                let ctx = graphite::FileContext::new();
215                #impl_name(content, &ctx);
216            }
217        }
218    } else if is_call_handler {
219        quote! {
220            #[cfg(target_arch = "wasm32")]
221            #[unsafe(no_mangle)]
222            pub extern "C" fn #fn_name(call_ptr: i32) {
223                let raw = unsafe {
224                    graph_as_runtime::ethereum::read_ethereum_call(call_ptr as u32)
225                };
226                let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawCall>::from_raw_call(&raw) {
227                    Ok(c) => c,
228                    Err(_) => return,
229                };
230                let ctx = graphite::CallContext {
231                    address:                  raw.address,
232                    block_hash:               raw.block_hash,
233                    block_number:             raw.block_number.clone(),
234                    block_timestamp:          raw.block_timestamp.clone(),
235                    block_gas_used:           raw.block_gas_used.clone(),
236                    block_gas_limit:          raw.block_gas_limit.clone(),
237                    block_difficulty:         raw.block_difficulty.clone(),
238                    block_base_fee_per_gas:   raw.block_base_fee_per_gas.clone(),
239                    tx_hash:                  raw.tx_hash,
240                    tx_index:                 raw.tx_index.clone(),
241                    from:                     raw.from,
242                    tx_to:                    raw.tx_to,
243                    tx_value:                 raw.tx_value.clone(),
244                    tx_gas_limit:             raw.tx_gas_limit.clone(),
245                    tx_gas_price:             raw.tx_gas_price.clone(),
246                    tx_nonce:                 raw.tx_nonce.clone(),
247                };
248                #impl_name(&#param_name, &ctx);
249            }
250        }
251    } else if is_block_handler {
252        quote! {
253            #[cfg(target_arch = "wasm32")]
254            #[unsafe(no_mangle)]
255            pub extern "C" fn #fn_name(event_ptr: i32) -> i32 {
256                let raw = unsafe {
257                    graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
258                };
259                let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
260                    Ok(e) => e,
261                    Err(_) => return 1,
262                };
263                let ctx = graphite::EventContext {
264                    address:                  raw.address,
265                    log_index:                raw.log_index.clone(),
266                    block_hash:               raw.block_hash,
267                    block_number:             raw.block_number.clone(),
268                    block_timestamp:          raw.block_timestamp.clone(),
269                    block_gas_used:           raw.block_gas_used.clone(),
270                    block_gas_limit:          raw.block_gas_limit.clone(),
271                    block_difficulty:         raw.block_difficulty.clone(),
272                    block_base_fee_per_gas:   raw.block_base_fee_per_gas.clone(),
273                    tx_hash:                  raw.tx_hash,
274                    tx_index:                 raw.tx_index.clone(),
275                    tx_from:                  raw.tx_from,
276                    tx_to:                    raw.tx_to,
277                    tx_value:                 raw.tx_value.clone(),
278                    tx_gas_limit:             raw.tx_gas_limit.clone(),
279                    tx_gas_price:             raw.tx_gas_price.clone(),
280                    tx_nonce:                 raw.tx_nonce.clone(),
281                    receipt:                  raw.receipt,
282                };
283                #impl_name(&#param_name, &ctx);
284                0
285            }
286        }
287    } else {
288        // Event handler — graph-node expects no return value (void).
289        quote! {
290            #[cfg(target_arch = "wasm32")]
291            #[unsafe(no_mangle)]
292            pub extern "C" fn #fn_name(event_ptr: i32) {
293                let raw = unsafe {
294                    graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
295                };
296                let #param_name = match <#event_base_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
297                    Ok(e) => e,
298                    Err(_) => return,
299                };
300                let ctx = graphite::EventContext {
301                    address:                  raw.address,
302                    log_index:                raw.log_index.clone(),
303                    block_hash:               raw.block_hash,
304                    block_number:             raw.block_number.clone(),
305                    block_timestamp:          raw.block_timestamp.clone(),
306                    block_gas_used:           raw.block_gas_used.clone(),
307                    block_gas_limit:          raw.block_gas_limit.clone(),
308                    block_difficulty:         raw.block_difficulty.clone(),
309                    block_base_fee_per_gas:   raw.block_base_fee_per_gas.clone(),
310                    tx_hash:                  raw.tx_hash,
311                    tx_index:                 raw.tx_index.clone(),
312                    tx_from:                  raw.tx_from,
313                    tx_to:                    raw.tx_to,
314                    tx_value:                 raw.tx_value.clone(),
315                    tx_gas_limit:             raw.tx_gas_limit.clone(),
316                    tx_gas_price:             raw.tx_gas_price.clone(),
317                    tx_nonce:                 raw.tx_nonce.clone(),
318                    receipt:                  raw.receipt,
319                };
320                #impl_name(&#param_name, &ctx);
321            }
322        }
323    };
324
325    // Choose the context type and the parameter type for the _impl function.
326    let (ctx_type, param_override) = if is_file_handler {
327        (quote! { graphite::FileContext }, Some(quote! { alloc::vec::Vec<u8> }))
328    } else if is_call_handler {
329        (quote! { graphite::CallContext }, None)
330    } else {
331        (quote! { graphite::EventContext }, None)
332    };
333
334    // For file handlers, the _impl param type is Vec<u8>; for others, use the declared type.
335    let impl_param_type = if let Some(ref override_ty) = param_override {
336        quote! { #override_ty }
337    } else {
338        quote! { #param_type }
339    };
340
341    let expanded = quote! {
342        // ---------------------------------------------------------------
343        // Implementation function — contains the user's handler body.
344        // In native builds the test harness calls this directly.
345        // ---------------------------------------------------------------
346        #fn_vis fn #impl_name(
347            #param_name: #impl_param_type,
348            ctx: &#ctx_type,
349        ) #fn_body
350
351        // ---------------------------------------------------------------
352        // Native (non-WASM) entry point — caller supplies event + context.
353        // Used by unit tests and the native test harness.
354        // ---------------------------------------------------------------
355        #[cfg(not(target_arch = "wasm32"))]
356        #fn_vis fn #fn_name(
357            #param_name: #impl_param_type,
358            ctx: &#ctx_type,
359        ) {
360            #impl_name(#param_name, ctx)
361        }
362
363        // ---------------------------------------------------------------
364        // WASM entry point — called by graph-node with an AscPtr to the
365        // event/block object in linear memory.
366        // ---------------------------------------------------------------
367        #wasm_entry
368    };
369
370    TokenStream::from(expanded)
371}
372
373/// Convert snake_case to camelCase for GraphQL field names.
374fn to_camel_case(s: &str) -> String {
375    let mut result = String::new();
376    let mut capitalize_next = false;
377
378    for c in s.chars() {
379        if c == '_' {
380            capitalize_next = true;
381        } else if capitalize_next {
382            result.push(c.to_ascii_uppercase());
383            capitalize_next = false;
384        } else {
385            result.push(c);
386        }
387    }
388
389    result
390}