Skip to main content

substreams_macro/
lib.rs

1use proc_macro::TokenStream;
2
3mod assertions;
4mod config;
5mod errors;
6mod handler;
7mod store;
8
9/// Marks a function as a Substreams map handler, generating the necessary WASM boilerplate.
10///
11/// # Panic Handling
12///
13/// Panics in handlers are trapped by the Substreams Engine and reported as deterministic
14/// errors back to the user. This includes panics from `Result::Err` returns (which the
15/// generated code converts to panics) and any explicit `panic!()` calls in your handler.
16///
17/// # Options
18///
19/// The macro accepts the following comma-separated options:
20///
21/// | Option | Description |
22/// |--------|-------------|
23/// | `no_testable` | Disables generation of the testable `__impl_<name>` function |
24/// | `keep_empty_output` | Prevents calling `substreams::skip_empty_output()` |
25///
26/// # Basic Usage
27///
28/// ```ignore
29/// #[substreams::handlers::map]
30/// fn map_transfers(blk: eth::Block) -> Result<pb::Transfers, Error> {
31///     // handler logic
32/// }
33/// ```
34///
35/// # Generated Code (default)
36///
37/// By default, the macro generates two functions:
38/// 1. A testable `__impl_<name>` function with the original signature (always compiled)
39/// 2. A WASM export function that calls the testable function (only on `wasm32` target)
40///
41/// For the example above, the macro generates:
42///
43/// ```ignore
44/// // Testable function - always available, can be called directly in tests
45/// pub fn __impl_map_transfers(blk: eth::Block) -> Result<pb::Transfers, Error> {
46///     // user's handler body
47/// }
48///
49/// // WASM export - only compiled for wasm32 target
50/// #[cfg(target_arch = "wasm32")]
51/// #[no_mangle]
52/// pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
53///     substreams::register_panic_hook();
54///     let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
55///         .unwrap_or_else(|_| panic!("Unable to decode..."));
56///     substreams::skip_empty_output();
57///     let result = __impl_map_transfers(blk);
58///     if result.is_err() {
59///         panic!("{:?}", result.unwrap_err())
60///     }
61///     substreams::output(result.expect("already checked"));
62/// }
63/// ```
64///
65/// # Testing Your Handlers
66///
67/// You can test your handler directly using the generated `__impl_` function:
68///
69/// ```ignore
70/// #[test]
71/// fn test_map_transfers() {
72///     let blk = eth::Block::default();
73///     // Call the testable function directly
74///     let result = __impl_map_transfers(blk);
75///     assert!(result.is_ok());
76///
77///     // Or use the test_map! macro for convenience
78///     let result = substreams::test_map!(map_transfers(blk));
79///     assert!(result.is_ok());
80/// }
81/// ```
82///
83/// # Disabling Testable Generation
84///
85/// Use `no_testable` to generate only the WASM export (legacy behavior):
86///
87/// ```ignore
88/// #[substreams::handlers::map(no_testable)]
89/// fn map_transfers(blk: eth::Block) -> Result<pb::Transfers, Error> {
90///     // handler logic
91/// }
92/// ```
93///
94/// This generates a single function without the `__impl_` wrapper:
95///
96/// ```ignore
97/// #[no_mangle]
98/// pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
99///     substreams::register_panic_hook();
100///     let func = || -> Result<pb::Transfers, Error> {
101///         let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
102///             .unwrap_or_else(|_| panic!("Unable to decode..."));
103///         // user's handler body
104///     };
105///     substreams::skip_empty_output();
106///     let result = func();
107///     if result.is_err() {
108///         panic!("{:?}", result.unwrap_err())
109///     }
110///     substreams::output(result.expect("already checked"));
111/// }
112/// ```
113///
114/// # Combining Options
115///
116/// Options can be combined with commas:
117///
118/// ```ignore
119/// #[substreams::handlers::map(no_testable, keep_empty_output)]
120/// fn map_transfers(blk: eth::Block) -> Result<pb::Transfers, Error> {
121///     // handler logic
122/// }
123/// ```
124///
125/// # Supported Return Types
126///
127/// - `Result<T, Error>` - Panics on error, outputs `T` on success
128/// - `Result<Option<T>, Error>` - Panics on error, outputs `T` if `Some`, nothing if `None`
129/// - `Option<T>` - Outputs `T` if `Some`, nothing if `None`
130/// - `T` - Outputs `T` directly
131#[proc_macro_attribute]
132pub fn map(args: TokenStream, item: TokenStream) -> TokenStream {
133    let options = config::HandlerOptions::parse(&args.to_string())
134        .unwrap_or_else(|e| panic!("Invalid arguments for map macro: {}", e));
135    handler::main(item.into(), config::ModuleType::Map, options).into()
136}
137
138/// Marks a function as a Substreams store handler, generating the necessary WASM boilerplate.
139///
140/// # Panic Handling
141///
142/// Panics in handlers are trapped by the Substreams Engine and reported as deterministic
143/// errors back to the user. This includes any explicit `panic!()` calls in your handler.
144///
145/// # Options
146///
147/// The macro accepts the following comma-separated options:
148///
149/// | Option | Description |
150/// |--------|-------------|
151/// | `keep_empty_output` | Prevents calling `substreams::skip_empty_output()` |
152///
153/// **Note**: The `testable` option is not currently supported for store handlers.
154///
155/// # Usage
156///
157/// ```ignore
158/// use substreams::store::StoreAddInt64;
159///
160/// #[substreams::handlers::store]
161/// fn store_transfers(transfers: pb::Transfers, store: StoreAddInt64) {
162///     // store logic
163/// }
164/// ```
165///
166/// Store handlers must not return a value. The writable store parameter is automatically
167/// instantiated by the macro.
168#[proc_macro_attribute]
169pub fn store(args: TokenStream, item: TokenStream) -> TokenStream {
170    let options = config::HandlerOptions::parse(&args.to_string())
171        .unwrap_or_else(|e| panic!("Invalid arguments for store macro: {}", e));
172    handler::main(item.into(), config::ModuleType::Store, options).into()
173}
174
175// todo: remove this once satisfied with implementation of StoreDelete
176#[proc_macro_derive(StoreWriter)]
177pub fn derive(input: TokenStream) -> TokenStream {
178    store::main(input)
179}
180
181/// Test helper macro that transforms a handler call to use the `__impl_` testable function.
182///
183/// By default, `#[substreams::handlers::map]` generates a testable `__impl_<name>` function
184/// alongside the WASM export. This macro provides a convenient way to call the testable
185/// function in tests.
186///
187/// # Example
188///
189/// ```ignore
190/// use substreams::test_map;
191///
192/// #[substreams::handlers::map]
193/// fn map_transfers(blk: eth::Block) -> Result<Events, Error> {
194///     // handler logic
195/// }
196///
197/// #[test]
198/// fn test_map_transfers() {
199///     let blk = eth::Block::default();
200///     let result = test_map!(map_transfers(blk));
201///     assert!(result.is_ok());
202/// }
203/// ```
204///
205/// The macro transforms `test_map!(map_transfers(blk))` into `__impl_map_transfers(blk)`.
206#[proc_macro]
207pub fn test_map(input: TokenStream) -> TokenStream {
208    use proc_macro2::TokenStream as TokenStream2;
209    use quote::quote;
210    use syn::{parse_macro_input, Expr, ExprCall, ExprPath};
211
212    let call = parse_macro_input!(input as ExprCall);
213
214    // Extract the function path from the call
215    let func_expr = &*call.func;
216    let args = &call.args;
217
218    match func_expr {
219        Expr::Path(ExprPath { path, .. }) => {
220            // Get the last segment (function name) and prepend __impl_
221            let mut new_path = path.clone();
222            if let Some(last_segment) = new_path.segments.last_mut() {
223                let new_name = quote::format_ident!("__impl_{}", last_segment.ident);
224                last_segment.ident = new_name;
225            }
226
227            let result: TokenStream2 = quote! {
228                #new_path(#args)
229            };
230            result.into()
231        }
232        _ => {
233            panic!("test_map! expects a function call expression, e.g., test_map!(handler(args))")
234        }
235    }
236}
237
238#[cfg(test)]
239mod test {
240    use crate::{
241        assertions::assert_ast_eq,
242        config::{HandlerOptions, ModuleType},
243        handler::main,
244    };
245    use quote::quote;
246
247    fn opts(keep_empty_output: bool, no_testable: bool) -> HandlerOptions {
248        HandlerOptions {
249            keep_empty_output,
250            no_testable,
251        }
252    }
253
254    // Tests for default behavior (testable enabled)
255    #[test]
256    fn test_map_default_plain() {
257        let item = quote! {
258            fn map_transfers(blk: eth::Block) -> pb::Custom {
259                unimplemented!("do something");
260            }
261        };
262
263        assert_ast_eq(
264            main(item, ModuleType::Map, opts(true, false)).into(),
265            quote! {
266                #[no_mangle]
267                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
268                    substreams::register_panic_hook();
269                    let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
270                        .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
271                    let result = __impl_map_transfers(blk);
272                    substreams::output(result);
273                }
274
275                pub fn __impl_map_transfers(blk: eth::Block) -> pb::Custom {
276                    unimplemented!("do something");
277                }
278            },
279        );
280    }
281
282    #[test]
283    fn test_map_default_mut() {
284        let item = quote! {
285            fn map_transfers(mut blk: eth::Block) -> pb::Custom {
286                unimplemented!("do something");
287            }
288        };
289
290        assert_ast_eq(
291            main(item, ModuleType::Map, opts(true, false)).into(),
292            quote! {
293                #[no_mangle]
294                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
295                    substreams::register_panic_hook();
296                    let mut blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
297                        .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
298                    let result = __impl_map_transfers(blk);
299                    substreams::output(result);
300                }
301
302                pub fn __impl_map_transfers(mut blk: eth::Block) -> pb::Custom {
303                    unimplemented!("do something");
304                }
305            },
306        );
307    }
308
309    #[test]
310    fn test_map_default_result() {
311        let item = quote! {
312            fn map_transfers(blk: eth::Block) -> Result<pb::Custom, Error> {
313                unimplemented!("do something");
314            }
315        };
316
317        assert_ast_eq(
318            main(item, ModuleType::Map, opts(true, false)).into(),
319            quote! {
320                #[no_mangle]
321                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
322                    substreams::register_panic_hook();
323                    let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
324                        .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
325                    let result = __impl_map_transfers(blk);
326                    if result.is_err() {
327                        panic!("{:?}", result.unwrap_err())
328                    }
329                    substreams::output(result.expect("already checked that result is not an error"));
330                }
331
332                pub fn __impl_map_transfers(blk: eth::Block) -> Result<pb::Custom, Error> {
333                    unimplemented!("do something");
334                }
335            },
336        );
337    }
338
339    #[test]
340    fn test_map_default_with_string_param() {
341        let item = quote! {
342            fn map_transfers(params: String, blk: eth::Block) -> Result<pb::Custom, Error> {
343                unimplemented!("do something");
344            }
345        };
346
347        assert_ast_eq(
348            main(item, ModuleType::Map, opts(true, false)).into(),
349            quote! {
350                #[no_mangle]
351                pub extern "C" fn map_transfers(params_ptr: *mut u8, params_len: usize, blk_ptr: *mut u8, blk_len: usize) {
352                    substreams::register_panic_hook();
353                    let params: String = std::mem::ManuallyDrop::new(unsafe { String::from_raw_parts(params_ptr, params_len, params_len) }).to_string();
354                    let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
355                        .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
356                    let result = __impl_map_transfers(params, blk);
357                    if result.is_err() {
358                        panic!("{:?}", result.unwrap_err())
359                    }
360                    substreams::output(result.expect("already checked that result is not an error"));
361                }
362
363                pub fn __impl_map_transfers(params: String, blk: eth::Block) -> Result<pb::Custom, Error> {
364                    unimplemented!("do something");
365                }
366            },
367        );
368    }
369
370    #[test]
371    fn test_map_default_with_readable_store() {
372        let item = quote! {
373            fn map_transfers(blk: eth::Block, store: StoreGetProto<pb::Pairs>) -> Result<pb::Custom, Error> {
374                unimplemented!("do something");
375            }
376        };
377
378        assert_ast_eq(
379            main(item, ModuleType::Map, opts(true, false)).into(),
380            quote! {
381                #[no_mangle]
382                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize, store_idx: u32) {
383                    substreams::register_panic_hook();
384                    let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
385                        .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
386                    let store: StoreGetProto<pb::Pairs> = StoreGetProto::new(store_idx);
387                    let result = __impl_map_transfers(blk, store);
388                    if result.is_err() {
389                        panic!("{:?}", result.unwrap_err())
390                    }
391                    substreams::output(result.expect("already checked that result is not an error"));
392                }
393
394                pub fn __impl_map_transfers(blk: eth::Block, store: StoreGetProto<pb::Pairs>) -> Result<pb::Custom, Error> {
395                    unimplemented!("do something");
396                }
397            },
398        );
399    }
400
401    #[test]
402    fn test_map_default_with_writable_store() {
403        let item = quote! {
404            fn map_transfers(blk: eth::Block, output: StoreAddInt64) -> Result<pb::Custom, Error> {
405                unimplemented!("do something");
406            }
407        };
408
409        assert_ast_eq(
410            main(item, ModuleType::Map, opts(true, false)).into(),
411            quote! {
412                #[no_mangle]
413                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
414                    substreams::register_panic_hook();
415                    let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
416                        .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
417                    let output: StoreAddInt64 = StoreAddInt64::new();
418                    let result = __impl_map_transfers(blk, output);
419                    if result.is_err() {
420                        panic!("{:?}", result.unwrap_err())
421                    }
422                    substreams::output(result.expect("already checked that result is not an error"));
423                }
424
425                pub fn __impl_map_transfers(blk: eth::Block, output: StoreAddInt64) -> Result<pb::Custom, Error> {
426                    unimplemented!("do something");
427                }
428            },
429        );
430    }
431
432    // Tests for no_testable option (legacy behavior)
433    #[test]
434    fn test_map_no_testable_plain() {
435        let item = quote! {
436            fn map_transfers(blk: eth::Block) -> pb::Custom {
437                unimplemented!("do something");
438            }
439        };
440
441        assert_ast_eq(
442            main(item, ModuleType::Map, opts(true, true)).into(),
443            quote! {
444                #[no_mangle]
445                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
446                    substreams::register_panic_hook();
447                    let func = || -> pb::Custom {
448                        let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
449                            .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
450                        let result = {
451                            unimplemented!("do something");
452                        };
453                        result
454                    };
455                    let result = func();
456                    substreams::output(result);
457                }
458            },
459        );
460    }
461
462    #[test]
463    fn test_map_no_testable_option() {
464        let item = quote! {
465            fn map_transfers(blk: eth::Block) -> Option<pb::Custom> {
466                unimplemented!("do something");
467            }
468        };
469
470        assert_ast_eq(
471            main(item, ModuleType::Map, opts(true, true)).into(),
472            quote! {
473                #[no_mangle]
474                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
475                    substreams::register_panic_hook();
476                    let func = || -> Option<pb::Custom> {
477                        let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
478                            .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
479                        let result = { unimplemented!("do something"); };
480                        result
481                    };
482
483                    let result = func();
484                    if let Some(value) = result {
485                        substreams::output(value);
486                    }
487                }
488            },
489        );
490    }
491
492    #[test]
493    fn test_map_no_testable_result_option() {
494        let item = quote! {
495            fn map_transfers(blk: eth::Block) -> Result<Option<pb::Custom>> {
496                unimplemented!("do something");
497            }
498        };
499
500        assert_ast_eq(
501            main(item.clone(), ModuleType::Map, opts(true, true)).into(),
502            quote! {
503                #[no_mangle]
504                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
505                    substreams::register_panic_hook();
506                    let func = || -> Result<Option<pb::Custom> > {
507                        let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
508                            .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
509                        let result = { unimplemented!("do something"); };
510                        result
511                    };
512
513                    let result = func();
514                    if result.is_err() {
515                        panic!("{:?}", result.unwrap_err())
516                    }
517                    if let Some(inner) = result.expect("already checked that result is not an error") {
518                        substreams::output(inner);
519                    }
520                }
521            },
522        );
523
524        assert_ast_eq(
525            main(item, ModuleType::Map, opts(false, true)).into(),
526            quote! {
527                #[no_mangle]
528                pub extern "C" fn map_transfers(blk_ptr: *mut u8, blk_len: usize) {
529                    substreams::register_panic_hook();
530                    let func = || -> Result<Option<pb::Custom> > {
531                        let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
532                            .unwrap_or_else(|_| panic!("Unable to decode Protobuf data ({} bytes) to '{}' message's struct", blk_len, stringify!(eth::Block)));
533                        let result = { unimplemented!("do something"); };
534                        result
535                    };
536
537                    substreams :: skip_empty_output () ;
538                    let result = func();
539                    if result.is_err() {
540                        panic!("{:?}", result.unwrap_err())
541                    }
542                    if let Some(inner) = result.expect("already checked that result is not an error") {
543                        substreams::output(inner);
544                    }
545                }
546            },
547        );
548    }
549
550    #[test]
551    fn test_store_handler() {
552        let item = quote! {
553            fn store_values(blk: eth::Block, store: StoreAddInt64) {
554                unimplemented!("do something");
555            }
556        };
557
558        assert_ast_eq(
559            main(item.clone(), ModuleType::Store, opts(true, false)).into(),
560            quote! {
561                #[no_mangle]
562                pub extern "C" fn store_values(blk_ptr: *mut u8, blk_len: usize) {
563                    substreams::register_panic_hook();
564                    let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
565                        .unwrap_or_else(|_|
566                            panic!(
567                                "Unable to decode Protobuf data ({} bytes) to '{}' message's struct",
568                                blk_len, stringify!(eth::Block)
569                            )
570                        );
571                    let store: StoreAddInt64 = StoreAddInt64::new();
572                    let result = {
573                        unimplemented!("do something");
574                    };
575                    result
576                }
577            },
578        );
579
580        assert_ast_eq(
581            main(item, ModuleType::Store, opts(false, false)).into(),
582            quote! {
583                #[no_mangle]
584                    pub extern "C" fn store_values(blk_ptr: *mut u8, blk_len: usize) {
585                    substreams::register_panic_hook();
586                    let blk: eth::Block = substreams::proto::decode_ptr(blk_ptr, blk_len)
587                        .unwrap_or_else(|_|
588                            panic!(
589                                "Unable to decode Protobuf data ({} bytes) to '{}' message's struct",
590                                blk_len, stringify!(eth::Block)
591                            )
592                        );
593                    let store: StoreAddInt64 = StoreAddInt64::new();
594                    substreams :: skip_empty_output () ;
595                    let result = {
596                        unimplemented!("do something");
597                    };
598                    result
599                }
600            },
601        );
602    }
603}