I've been working on integration of lez-payment-streams into logos-delivery. I looked at whisper-wall as inspiration. Turns out it is non-trivial to pull (and parse) account data from LEZ. Whisper-wall uses C FFI for the whisper_wall SPEL program. This approach pre-dates this SPEL commit #154 - included into v0.2.0-rc.5 - that should simplify the process, but apparently the structure of my payment streams project doesn't fit into what IDL generation expects, and prevents me from taking advantage of that recent SPEL feature.
AI offered an explanation and a fix, but I feel it'd be impolite to push a fully AI-written PR here with very limited understanding on my part of what is going on. I'm not sure I have resources to establish such understanding, so I'm leaving the following AI-written breakdown hoping it could be helpful to people (or agents, for that matter) working closely with SPEL.
When account types annotated with #[account_type] are defined in a separate crate referenced via a local path = "..." dependency, the generated IDL's top-level accounts array is always empty. Only types defined directly in the file passed to generate_idl! are discovered.
Reproducer
A typical project structure:
my_program/
methods/guest/Cargo.toml # depends on my_program_core via path = "../../my_program_core"
methods/guest/src/bin/program.rs # #[lez_program], no #[account_type] here
my_program_core/src/lib.rs # #[account_type] on VaultConfig, StreamConfig, ...
examples/src/bin/generate_idl.rs # spel_framework::generate_idl!("../methods/guest/src/bin/program.rs")
Running cargo run --bin generate_idl produces an IDL with "accounts": [] even though my_program_core contains multiple #[account_type]-annotated types.
This is the standard structure for any non-trivial program that separates on-chain types into a shared core crate (for reuse in tests, clients, etc.).
Root causes
Two independent issues combine to cause the empty output.
Issue 1 — path-dep crates are not scanned.
expand_generate_idl in spel-framework-macros parses only the single .rs file passed to it. It never follows the crate's Cargo.toml to find local path dependencies. By contrast, spel-cli generate-idl already handles this correctly: it calls find_path_dep_dirs to collect local path entries from Cargo.toml, then passes them to generate_idl_from_file_with_deps, which recursively parses src/lib.rs of each dep and follows mod declarations.
The proc macro needs the same treatment. The CARGO_MANIFEST_DIR environment variable and the source file path are both available at macro expansion time, so the nearest Cargo.toml can be found by walking up from the source file.
Issue 2 — qualified attribute form not recognised.
has_account_type_attr in spel-framework-core/src/account_types.rs checks a.path().is_ident("account_type"). This matches only the bare form #[account_type]. When users write #[spel_framework_macros::account_type] (the fully-qualified path, which is idiomatic when importing via a path rather than a use declaration), the check returns false and the type is silently skipped. The fix is to also match any attribute path whose last segment is account_type.
Proposed fix
-
In spel-framework-core/src/account_types.rs, update has_account_type_attr to recognise both forms:
pub(crate) fn has_account_type_attr(attrs: &[Attribute]) -> bool {
attrs.iter().any(|a| {
let path = a.path();
path.is_ident("account_type")
|| path.segments.last().map_or(false, |s| s.ident == "account_type")
})
}
-
Expose collect_items_from_crate_dirs as pub in spel-framework-core/src/idl_gen.rs so the proc macro can reuse the existing recursive mod-following logic.
-
Add toml as a dependency of spel-framework-macros.
-
In spel-framework-macros/src/lib.rs, add helpers that mirror the spel-cli logic:
find_crate_manifest_macro(path) — walks up from the source file to the nearest Cargo.toml
find_path_dep_dirs_macro(path) — parses [dependencies] for path = "..." entries (excluding dev/build deps)
-
In expand_generate_idl, call these helpers and merge the collected items into all_items before calling collect_account_types.
Notes
- The fix has no effect on programs that define all account types in the guest file itself or use the bare
#[account_type] form — behaviour is unchanged in those cases.
- The
#[lez_program] macro path (expand_lez_program) has the same gap for the qualified attribute form, since it also calls collect_account_types. The attribute fix resolves it there too.
spel-cli generate-idl already produces the correct output because it uses generate_idl_from_file_with_deps. The gap is specific to the generate_idl! proc macro.
I've been working on integration of lez-payment-streams into logos-delivery. I looked at whisper-wall as inspiration. Turns out it is non-trivial to pull (and parse) account data from LEZ. Whisper-wall uses C FFI for the whisper_wall SPEL program. This approach pre-dates this SPEL commit #154 - included into v0.2.0-rc.5 - that should simplify the process, but apparently the structure of my payment streams project doesn't fit into what IDL generation expects, and prevents me from taking advantage of that recent SPEL feature.
AI offered an explanation and a fix, but I feel it'd be impolite to push a fully AI-written PR here with very limited understanding on my part of what is going on. I'm not sure I have resources to establish such understanding, so I'm leaving the following AI-written breakdown hoping it could be helpful to people (or agents, for that matter) working closely with SPEL.
When account types annotated with
#[account_type]are defined in a separate crate referenced via a localpath = "..."dependency, the generated IDL's top-levelaccountsarray is always empty. Only types defined directly in the file passed togenerate_idl!are discovered.Reproducer
A typical project structure:
Running
cargo run --bin generate_idlproduces an IDL with"accounts": []even thoughmy_program_corecontains multiple#[account_type]-annotated types.This is the standard structure for any non-trivial program that separates on-chain types into a shared core crate (for reuse in tests, clients, etc.).
Root causes
Two independent issues combine to cause the empty output.
Issue 1 — path-dep crates are not scanned.
expand_generate_idlinspel-framework-macrosparses only the single.rsfile passed to it. It never follows the crate'sCargo.tomlto find local path dependencies. By contrast,spel-cli generate-idlalready handles this correctly: it callsfind_path_dep_dirsto collect local path entries fromCargo.toml, then passes them togenerate_idl_from_file_with_deps, which recursively parsessrc/lib.rsof each dep and followsmoddeclarations.The proc macro needs the same treatment. The
CARGO_MANIFEST_DIRenvironment variable and the source file path are both available at macro expansion time, so the nearestCargo.tomlcan be found by walking up from the source file.Issue 2 — qualified attribute form not recognised.
has_account_type_attrinspel-framework-core/src/account_types.rschecksa.path().is_ident("account_type"). This matches only the bare form#[account_type]. When users write#[spel_framework_macros::account_type](the fully-qualified path, which is idiomatic when importing via a path rather than ausedeclaration), the check returns false and the type is silently skipped. The fix is to also match any attribute path whose last segment isaccount_type.Proposed fix
In
spel-framework-core/src/account_types.rs, updatehas_account_type_attrto recognise both forms:Expose
collect_items_from_crate_dirsaspubinspel-framework-core/src/idl_gen.rsso the proc macro can reuse the existing recursive mod-following logic.Add
tomlas a dependency ofspel-framework-macros.In
spel-framework-macros/src/lib.rs, add helpers that mirror thespel-clilogic:find_crate_manifest_macro(path)— walks up from the source file to the nearestCargo.tomlfind_path_dep_dirs_macro(path)— parses[dependencies]forpath = "..."entries (excluding dev/build deps)In
expand_generate_idl, call these helpers and merge the collected items intoall_itemsbefore callingcollect_account_types.Notes
#[account_type]form — behaviour is unchanged in those cases.#[lez_program]macro path (expand_lez_program) has the same gap for the qualified attribute form, since it also callscollect_account_types. The attribute fix resolves it there too.spel-cli generate-idlalready produces the correct output because it usesgenerate_idl_from_file_with_deps. The gap is specific to thegenerate_idl!proc macro.