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    // Build the WASM entry point. Event handlers return void; block handlers return i32;
198    // call handlers return void; file handlers return void.
199    let wasm_entry = if is_file_handler {
200        quote! {
201            #[cfg(target_arch = "wasm32")]
202            #[unsafe(no_mangle)]
203            pub extern "C" fn #fn_name(content_ptr: i32) {
204                let content = unsafe {
205                    graph_as_runtime::store_read::read_asc_bytes(content_ptr as u32)
206                };
207                let ctx = graphite::FileContext::new();
208                #impl_name(&content, &ctx);
209            }
210        }
211    } else if is_call_handler {
212        quote! {
213            #[cfg(target_arch = "wasm32")]
214            #[unsafe(no_mangle)]
215            pub extern "C" fn #fn_name(call_ptr: i32) {
216                let raw = unsafe {
217                    graph_as_runtime::ethereum::read_ethereum_call(call_ptr as u32)
218                };
219                let #param_name = match <#param_type as graph_as_runtime::ethereum::FromRawCall>::from_raw_call(&raw) {
220                    Ok(c) => c,
221                    Err(_) => return,
222                };
223                let ctx = graphite::CallContext {
224                    address:                  raw.address,
225                    block_hash:               raw.block_hash,
226                    block_number:             raw.block_number.clone(),
227                    block_timestamp:          raw.block_timestamp.clone(),
228                    block_gas_used:           raw.block_gas_used.clone(),
229                    block_gas_limit:          raw.block_gas_limit.clone(),
230                    block_difficulty:         raw.block_difficulty.clone(),
231                    block_base_fee_per_gas:   raw.block_base_fee_per_gas.clone(),
232                    tx_hash:                  raw.tx_hash,
233                    tx_index:                 raw.tx_index.clone(),
234                    from:                     raw.from,
235                    tx_to:                    raw.tx_to,
236                    tx_value:                 raw.tx_value.clone(),
237                    tx_gas_limit:             raw.tx_gas_limit.clone(),
238                    tx_gas_price:             raw.tx_gas_price.clone(),
239                    tx_nonce:                 raw.tx_nonce.clone(),
240                };
241                #impl_name(&#param_name, &ctx);
242            }
243        }
244    } else if is_block_handler {
245        quote! {
246            #[cfg(target_arch = "wasm32")]
247            #[unsafe(no_mangle)]
248            pub extern "C" fn #fn_name(event_ptr: i32) -> i32 {
249                let raw = unsafe {
250                    graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
251                };
252                let #param_name = match <#param_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
253                    Ok(e) => e,
254                    Err(_) => return 1,
255                };
256                let ctx = graphite::EventContext {
257                    address:                  raw.address,
258                    log_index:                raw.log_index.clone(),
259                    block_hash:               raw.block_hash,
260                    block_number:             raw.block_number.clone(),
261                    block_timestamp:          raw.block_timestamp.clone(),
262                    block_gas_used:           raw.block_gas_used.clone(),
263                    block_gas_limit:          raw.block_gas_limit.clone(),
264                    block_difficulty:         raw.block_difficulty.clone(),
265                    block_base_fee_per_gas:   raw.block_base_fee_per_gas.clone(),
266                    tx_hash:                  raw.tx_hash,
267                    tx_index:                 raw.tx_index.clone(),
268                    tx_from:                  raw.tx_from,
269                    tx_to:                    raw.tx_to,
270                    tx_value:                 raw.tx_value.clone(),
271                    tx_gas_limit:             raw.tx_gas_limit.clone(),
272                    tx_gas_price:             raw.tx_gas_price.clone(),
273                    tx_nonce:                 raw.tx_nonce.clone(),
274                    receipt:                  raw.receipt,
275                };
276                #impl_name(&#param_name, &ctx);
277                0
278            }
279        }
280    } else {
281        // Event handler — graph-node expects no return value (void).
282        quote! {
283            #[cfg(target_arch = "wasm32")]
284            #[unsafe(no_mangle)]
285            pub extern "C" fn #fn_name(event_ptr: i32) {
286                let raw = unsafe {
287                    graph_as_runtime::ethereum::read_ethereum_event(event_ptr as u32)
288                };
289                let #param_name = match <#param_type as graph_as_runtime::ethereum::FromRawEvent>::from_raw_event(&raw) {
290                    Ok(e) => e,
291                    Err(_) => return,
292                };
293                let ctx = graphite::EventContext {
294                    address:                  raw.address,
295                    log_index:                raw.log_index.clone(),
296                    block_hash:               raw.block_hash,
297                    block_number:             raw.block_number.clone(),
298                    block_timestamp:          raw.block_timestamp.clone(),
299                    block_gas_used:           raw.block_gas_used.clone(),
300                    block_gas_limit:          raw.block_gas_limit.clone(),
301                    block_difficulty:         raw.block_difficulty.clone(),
302                    block_base_fee_per_gas:   raw.block_base_fee_per_gas.clone(),
303                    tx_hash:                  raw.tx_hash,
304                    tx_index:                 raw.tx_index.clone(),
305                    tx_from:                  raw.tx_from,
306                    tx_to:                    raw.tx_to,
307                    tx_value:                 raw.tx_value.clone(),
308                    tx_gas_limit:             raw.tx_gas_limit.clone(),
309                    tx_gas_price:             raw.tx_gas_price.clone(),
310                    tx_nonce:                 raw.tx_nonce.clone(),
311                    receipt:                  raw.receipt,
312                };
313                #impl_name(&#param_name, &ctx);
314            }
315        }
316    };
317
318    // Choose the context type and the parameter type for the _impl function.
319    let (ctx_type, param_override) = if is_file_handler {
320        (quote! { graphite::FileContext }, Some(quote! { alloc::vec::Vec<u8> }))
321    } else if is_call_handler {
322        (quote! { graphite::CallContext }, None)
323    } else {
324        (quote! { graphite::EventContext }, None)
325    };
326
327    // For file handlers, the _impl param type is Vec<u8>; for others, use the declared type.
328    let impl_param_type = if let Some(ref override_ty) = param_override {
329        quote! { #override_ty }
330    } else {
331        quote! { #param_type }
332    };
333
334    let expanded = quote! {
335        // ---------------------------------------------------------------
336        // Implementation function — contains the user's handler body.
337        // In native builds the test harness calls this directly.
338        // ---------------------------------------------------------------
339        #fn_vis fn #impl_name(
340            #param_name: #impl_param_type,
341            ctx: &#ctx_type,
342        ) #fn_body
343
344        // ---------------------------------------------------------------
345        // Native (non-WASM) entry point — caller supplies event + context.
346        // Used by unit tests and the native test harness.
347        // ---------------------------------------------------------------
348        #[cfg(not(target_arch = "wasm32"))]
349        #fn_vis fn #fn_name(
350            #param_name: #impl_param_type,
351            ctx: &#ctx_type,
352        ) {
353            #impl_name(#param_name, ctx)
354        }
355
356        // ---------------------------------------------------------------
357        // WASM entry point — called by graph-node with an AscPtr to the
358        // event/block object in linear memory.
359        // ---------------------------------------------------------------
360        #wasm_entry
361    };
362
363    TokenStream::from(expanded)
364}
365
366/// Convert snake_case to camelCase for GraphQL field names.
367fn to_camel_case(s: &str) -> String {
368    let mut result = String::new();
369    let mut capitalize_next = false;
370
371    for c in s.chars() {
372        if c == '_' {
373            capitalize_next = true;
374        } else if capitalize_next {
375            result.push(c.to_ascii_uppercase());
376            capitalize_next = false;
377        } else {
378            result.push(c);
379        }
380    }
381
382    result
383}