From 739d5778b93b90587cdcb3a33a3a3a8d70e22c8d Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Fri, 8 May 2026 15:04:17 -0400 Subject: [PATCH 1/3] feat(surfpool,skill): auto-discover preset IDLs and fix solana-add-idl template gaps Wires the surfpool harness to cover skill-generated presets via reflection, and fixes three correctness gaps in the solana-add-idl skill that would have produced code that does not compile. Reflection: - build.rs scans `src/presets//.json` and emits `pub const PRESET_IDLS: &[(&str, &str)]` to OUT_DIR; lib.rs re-exports it. Drop a JSON file at the matching path and it appears here on the next build with no manual registration. - tests/surfpool_fuzz.rs::surfpool_preset_idls iterates PRESET_IDLS and runs each through the same roundtrip used for the named upstream-IDL tests. Currently empty (no preset ships an embedded IDL today); will pick up the first skill-generated preset. - run_idl_roundtrip and the idl_test! macro hoisted from surfpool_fuzz.rs into tests/common/mod.rs so future per-preset test files can reuse them. #[macro_export] + $crate::common::... path makes the macro work for any test that does `mod common;`. Skill template fixes (.claude/skills/solana-add-idl/SKILL.md): - Removed the squads_multisig template reference -- that preset does not exist in this repo. Replaced with unknown_program, which is the actual working IDL-parsing reference. - Removed references to five solana_parser helpers (build_named_accounts, build_parsed_fields, build_fallback_fields, append_raw_data, format_arg_value) that do not exist. Documented the manual named_accounts loop pattern that unknown_program uses. - Added explicit step for declaring `pub(crate) const {SCREAMING_SNAKE}_PROGRAM_ID: &str` in the preset's mod.rs -- the generated config.rs references it via `use super::` but the previous skill never told contributors where to put it. - Removed the obsolete `idl_test!` registration step (replaced by PRESET_IDLS auto-discovery) and added a verification command that greps the generated preset_idls.rs. - Documented what's already auto-covered (proptest is generative, cargo-fuzz is generative, surfpool roundtrip is reflective) vs. what still needs hand-written assertions (semantic_pipeline.rs). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/solana-add-idl/SKILL.md | 69 +++++++++----- src/chain_parsers/visualsign-solana/build.rs | 43 +++++++++ .../visualsign-solana/src/lib.rs | 3 + .../visualsign-solana/tests/common/mod.rs | 71 +++++++++++++- .../visualsign-solana/tests/surfpool_fuzz.rs | 94 ++++++------------- 5 files changed, 192 insertions(+), 88 deletions(-) diff --git a/.claude/skills/solana-add-idl/SKILL.md b/.claude/skills/solana-add-idl/SKILL.md index 16b42b80..8941cd44 100644 --- a/.claude/skills/solana-add-idl/SKILL.md +++ b/.claude/skills/solana-add-idl/SKILL.md @@ -86,29 +86,30 @@ impl SolanaIntegrationConfig for {PascalName}Config { ### File: `mod.rs` -Use the squads_multisig preset as a template: `src/chain_parsers/visualsign-solana/src/presets/squads_multisig/mod.rs` +Use `src/chain_parsers/visualsign-solana/src/presets/unknown_program/mod.rs` as the working reference for the IDL parsing pattern — it is the preset that actually exercises `parse_instruction_with_idl` against a runtime-supplied IDL today. Your preset is the same pattern with the IDL **embedded at compile time** and the program ID hardcoded. -Read that file for the exact structure, then generate a generic version with these substitutions: -- Replace `SquadsMultisig` / `squads_multisig` / `SQUADS_MULTISIG` with the appropriate casing of the new program name -- Replace the program ID string with the new program address -- Replace `"Squads Multisig"` display strings with `{display_name}` -- Replace IDL file reference: `include_str!("{snake_name}.json")` -- Keep the `kind()` method returning the user's chosen `VisualizerKind` variant with `display_name` as the `&'static str` argument +Substitutions to make when adapting it: +- **Hardcode the program ID const** at the top of `mod.rs`: + ```rust + pub(crate) const {SCREAMING_SNAKE}_PROGRAM_ID: &str = "{base58_program_id}"; + ``` + This is what `config.rs` resolves via `use super::{SCREAMING_SNAKE}_PROGRAM_ID;`. See `presets/jupiter_swap/mod.rs` line ~24 for the canonical placement. +- **Embed the IDL** via `const IDL_JSON: &str = include_str!("{snake_name}.json");` and replace any runtime `idl_registry.get_idl(...)` lookup with `decode_idl_data(IDL_JSON)?`. +- **Rename the visualizer/config/static**: `UnknownProgramVisualizer` → `{PascalName}Visualizer`, `UnknownProgramConfig` → `{PascalName}Config`, `UNKNOWN_PROGRAM_CONFIG` → `{SCREAMING_SNAKE}_CONFIG`. +- **`kind()`** returns your chosen `VisualizerKind` variant: `VisualizerKind::{Kind}("{display_name}")`. +- **Drop the no-IDL fallback path** (`create_unknown_program_preview_layout`) — for an IDL-driven preset, return `Err(VisualSignError::DecodeError(...))` if parsing fails. Do not display raw bytes as a substitute. -**Important — generic IDL pattern only:** -- DO NOT copy any squads-specific code (e.g. `VaultTransactionMessage` decoding, inner instruction handling) -- The generic scaffold uses `build_named_accounts`, `build_parsed_fields`, `build_fallback_fields`, `append_raw_data`, `format_arg_value` — all of which work with any IDL -- The parse function should: check `data.len() < 8`, load IDL, call `parse_instruction_with_idl`, call `build_named_accounts`, return a struct with parsed data + named accounts +**Building `named_accounts` — what the IDL gives you and what you build manually** -**Required imports** (at top of module, NOT inside functions): +`parse_instruction_with_idl` returns a `SolanaParsedInstructionData` whose `named_accounts` field is empty by default. There is no `build_named_accounts` helper in `solana_parser`; you build the map yourself by matching the on-chain instruction's accounts against the IDL instruction's account list, in order. The reference loop is in `unknown_program::try_parse_with_idl` (search for `named_accounts` in that file). Copy that loop verbatim — it is the supported pattern. + +**Required imports** (at top of module, NOT inside functions; only symbols that actually exist in the current dependency graph): ```rust use crate::core::{ InstructionVisualizer, SolanaIntegrationConfig, VisualizerContext, VisualizerKind, }; use config::{PascalName}Config; -use solana_parser::{ - Idl, SolanaParsedInstructionData, decode_idl_data, parse_instruction_with_idl, -}; +use solana_parser::{SolanaParsedInstructionData, decode_idl_data, parse_instruction_with_idl}; use std::collections::HashMap; use visualsign::errors::VisualSignError; use visualsign::field_builders::{create_raw_data_field, create_text_field}; @@ -119,10 +120,10 @@ use visualsign::{ ``` **Required tests** (in `#[cfg(test)] mod tests`): -- `test_{snake_name}_idl_loads` — IDL loads and has instructions -- `test_{snake_name}_idl_has_discriminators` — every instruction has an 8-byte discriminator -- `test_unknown_discriminator_returns_error` — garbage 9-byte data returns error -- `test_short_data_returns_error` — 3-byte data returns error +- `test_{snake_name}_idl_loads` — `decode_idl_data(IDL_JSON)` succeeds and `instructions` is non-empty +- `test_{snake_name}_idl_has_discriminators` — every instruction in the IDL has an 8-byte discriminator + +Crash-safety against unknown discriminators / short data is **already covered**: by `tests/fuzz_idl_parsing.rs` (proptest, generative — exercises arbitrary discriminator/data combinations) and by `tests/surfpool_fuzz.rs::surfpool_preset_idls` (auto-iterates `PRESET_IDLS`). Do not duplicate those assertions in the preset's own test module. ## Step 4: Register in presets/mod.rs @@ -130,9 +131,24 @@ Add `pub mod {snake_name};` to `src/chain_parsers/visualsign-solana/src/presets/ **Keep entries in alphabetical order.** The existing entries are sorted — insert the new module in the correct position. -No other registration is needed. `build.rs` auto-discovers `{PascalName}Visualizer` from any directory under `src/presets/`. +No other registration is needed for the visualizer itself. `build.rs` auto-discovers `{PascalName}Visualizer` from any directory under `src/presets/`. + +## Step 5: Test coverage — what's auto-discovered, what isn't + +You do **not** need to edit any test file. The harness picks up the new IDL by reflection: + +- **`build.rs`** scans `src/presets//.json` and emits `pub const PRESET_IDLS: &[(&str, &str)]` exposed from the library. The IDL JSON file you saved in Step 2 is the only input. +- **`tests/surfpool_fuzz.rs::surfpool_preset_idls`** iterates `PRESET_IDLS` and runs each through `run_idl_roundtrip` against a `surfpool` mainnet fork (decode IDL → build synthetic tx with the first instruction's discriminator → convert → assert non-empty payload). The new preset is exercised on every run with no test-file edit. +- **Proptest (`tests/fuzz_idl_parsing.rs`)** is *generative*. Strategies in `solana_parser_fuzz_core::proptest` synthesize arbitrary IDL shapes and feed them through `decode_idl_data` / `parse_instruction_with_idl`. New IDLs are covered structurally by the existing strategies — no per-IDL registration, ever. +- **cargo-fuzz (`fuzz/fuzz_targets/`)** runs against random byte streams through `transaction_string_to_visual_sign`. Same story: generative, no per-IDL registration. + +Tests that *do* need hand-written assertions (and therefore can't be auto-discovered): -## Step 5: Code Quality +- **`tests/semantic_pipeline.rs`** — correctness assertions on parsed-field shape, label text, amounts, etc. These are program-specific. If the new preset's behavior matters in CI beyond "doesn't crash on a roundtrip," add a fixture-based test here. Otherwise the auto-roundtrip is enough. + +CI: `surfpool_preset_idls` is `#[ignore]`. It runs when the PR carries the `surfpool` label (see `.github/workflows/surfpool-solana.yml`); local runs need `HELIUS_API_KEY`. + +## Step 6: Code Quality Follow these rules in all generated code: - `use` statements at top of module, never inside functions @@ -141,7 +157,7 @@ Follow these rules in all generated code: - ASCII only in user-visible strings: `>=` not `≥`, `->` not `→` - Rust edition 2024 on nightly -## Step 6: Verify +## Step 7: Verify Run these commands and fix any issues: @@ -153,3 +169,12 @@ make -C src test ``` All must pass before the task is complete. + +To confirm the surfpool roundtrip picked up the new preset's IDL via auto-discovery, run: + +```bash +cargo build -p visualsign-solana +grep -- '"{snake_name}"' src/chain_parsers/visualsign-solana/target/debug/build/visualsign-solana-*/out/preset_idls.rs +``` + +The `PRESET_IDLS` slice should contain a `("{snake_name}", include_str!(...))` entry. If it doesn't, the IDL JSON file is at the wrong path — `build.rs` looks for exactly `src/presets/{snake_name}/{snake_name}.json`. diff --git a/src/chain_parsers/visualsign-solana/build.rs b/src/chain_parsers/visualsign-solana/build.rs index 516c1f8c..22b9d530 100644 --- a/src/chain_parsers/visualsign-solana/build.rs +++ b/src/chain_parsers/visualsign-solana/build.rs @@ -24,6 +24,18 @@ fn main() { ); fs::write(out_dir.join("generated_visualizers.rs"), code).unwrap(); + + let preset_idls = collect_preset_idls(); + let preset_idls_code = format!( + "/// Preset IDLs auto-discovered from `src/presets//.json`.\n\ + ///\n\ + /// Generated by `build.rs`. Drop a JSON file at the matching path and\n\ + /// it appears here on the next build -- no manual registration. Used\n\ + /// by the surfpool roundtrip test to exercise every embedded IDL.\n\ + pub const PRESET_IDLS: &[(&str, &str)] = &[\n {}\n];\n", + preset_idls.join(",\n ") + ); + fs::write(out_dir.join("preset_idls.rs"), preset_idls_code).unwrap(); } fn collect_visualizers() -> Vec { @@ -68,6 +80,37 @@ fn collect_visualizers() -> Vec { .collect() } +fn collect_preset_idls() -> Vec { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let presets_dir = PathBuf::from(&manifest_dir).join("src/presets"); + let Ok(entries) = fs::read_dir(&presets_dir) else { + return Vec::new(); + }; + let mut idls: Vec = entries + .filter_map(|entry| { + let path = entry.ok()?.path(); + if !path.is_dir() { + return None; + } + let dir_name = path.file_name()?.to_str()?.to_string(); + let json_path = path.join(format!("{dir_name}.json")); + if !json_path.exists() { + return None; + } + // include_str! takes a literal; emit the absolute path as-is. The + // `cargo:rerun-if-changed=src/presets` directive triggers a rebuild + // when the JSON file is added, removed, or modified. + Some(format!( + "(\"{name}\", include_str!(\"{path}\"))", + name = dir_name, + path = json_path.display() + )) + }) + .collect(); + idls.sort(); + idls +} + fn to_pascal_case(s: &str) -> String { s.split('_') .map(|w| { diff --git a/src/chain_parsers/visualsign-solana/src/lib.rs b/src/chain_parsers/visualsign-solana/src/lib.rs index 3cd6eeb3..dbb3222e 100644 --- a/src/chain_parsers/visualsign-solana/src/lib.rs +++ b/src/chain_parsers/visualsign-solana/src/lib.rs @@ -7,6 +7,9 @@ pub mod utils; pub use core::*; pub use utils::*; +// Auto-discovered preset IDLs. See `build.rs::collect_preset_idls`. +include!(concat!(env!("OUT_DIR"), "/preset_idls.rs")); + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { diff --git a/src/chain_parsers/visualsign-solana/tests/common/mod.rs b/src/chain_parsers/visualsign-solana/tests/common/mod.rs index 2e05b781..674fe87a 100644 --- a/src/chain_parsers/visualsign-solana/tests/common/mod.rs +++ b/src/chain_parsers/visualsign-solana/tests/common/mod.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; +use base64::Engine; use generated::parser::{ChainMetadata, Idl as ProtoIdl, SolanaMetadata, chain_metadata}; use solana_parser::decode_idl_data; use solana_parser::solana::structs::Idl; @@ -11,10 +12,12 @@ use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::message::Message; use solana_sdk::pubkey::Pubkey; use solana_sdk::transaction::Transaction as SolanaTransaction; -use visualsign::vsptrait::VisualSignOptions; +use solana_test_utils::{SurfpoolConfig, SurfpoolManager}; +use visualsign::vsptrait::{Transaction, VisualSignConverter, VisualSignOptions}; use visualsign::{ AnnotatedPayloadField, SignablePayload, SignablePayloadField, SignablePayloadFieldPreviewLayout, }; +use visualsign_solana::{SolanaTransactionWrapper, SolanaVisualSignConverter}; /// Decode an IDL JSON string, extract the discriminator for the instruction at /// `inst_idx`, and return `(idl, data)` where `data` = discriminator ++ `arg_bytes`. @@ -162,3 +165,69 @@ pub fn load_idl_from_env() -> Option<(String, solana_parser::solana::structs::Id } } } + +// ── Surfpool roundtrip ──────────────────────────────────────────────────────── + +/// Per-IDL roundtrip: decode the IDL, build a synthetic transaction whose data +/// starts with the first instruction's discriminator, run it through the +/// visual-sign converter, and assert the payload is non-empty. +/// +/// Network-bound: starts a `surfpool` mainnet fork and requires the `surfpool` +/// binary on `$PATH`. Callers are responsible for marking their tests with +/// `#[ignore]`. Use the `idl_test!` macro for the standard wrapper. +pub async fn run_idl_roundtrip(idl_label: &str, idl_json: &str) { + // Distinguish the three failure modes explicitly so a red test names the + // IDL and the actual cause (decode rejection from a malformed IDL, empty + // instruction list, or a missing discriminator). + let idl = decode_idl_data(idl_json) + .unwrap_or_else(|e| panic!("{idl_label}: decode_idl_data rejected the IDL: {e}")); + assert!( + !idl.instructions.is_empty(), + "{idl_label}: IDL has no instructions" + ); + let disc = idl.instructions[0] + .discriminator + .as_ref() + .unwrap_or_else(|| panic!("{idl_label}: instructions[0] has no discriminator")); + let mut data = disc.clone(); + data.extend_from_slice(&[0u8; 32]); + + let _manager = SurfpoolManager::start(SurfpoolConfig::default()) + .await + .expect("surfpool should start"); + + let program_id = Pubkey::new_unique(); + let tx = build_transaction(program_id, vec![Pubkey::new_unique()], data); + let tx_bytes = bincode::serialize(&tx).expect("tx should serialize"); + let tx_b64 = base64::engine::general_purpose::STANDARD.encode(&tx_bytes); + + let wrapper = SolanaTransactionWrapper::from_string(&tx_b64) + .expect("from_string should succeed for a valid base64 transaction"); + + let options = options_with_idl(&program_id, idl_json, "test_program"); + let payload = SolanaVisualSignConverter + .to_visual_sign_payload(wrapper, options) + .expect("converter should succeed"); + + assert!( + !payload.fields.is_empty(), + "payload must contain at least one field" + ); +} + +/// Generate a `#[tokio::test] #[ignore]` that runs `run_idl_roundtrip` against +/// the provided IDL string. Works for both upstream `embedded_idls` consts and +/// vsp-local IDL JSON via `include_str!`. +/// +/// The macro is hoisted from `common/mod.rs`, so any test file that uses it +/// must declare the module with `#[macro_use] mod common;`. +#[macro_export] +macro_rules! idl_test { + ($name:ident, $idl:expr) => { + #[tokio::test(flavor = "multi_thread")] + #[ignore] + async fn $name() { + $crate::common::run_idl_roundtrip(stringify!($name), $idl).await; + } + }; +} diff --git a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs index 2a7147bb..d35d750f 100644 --- a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs +++ b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs @@ -2,10 +2,9 @@ //! Surfpool-backed integration tests for the Solana visual-sign parser. //! //! Tests are network-bound (start a `surfpool` mainnet fork; require the -//! `surfpool` binary on `$PATH`) and are therefore `#[ignore]`. Each test -//! references a `solana_parser::solana::embedded_idls::*` const directly, -//! so the IDL contents are baked in at compile time -- no filesystem -//! lookup, no env var, no `cargo metadata`. +//! `surfpool` binary on `$PATH`) and are therefore `#[ignore]`. The roundtrip +//! body and the `idl_test!` macro live in `tests/common/mod.rs` so other test +//! files (e.g. preset-specific surfpool tests) can reuse them. //! //! Run all surfpool tests: //! @@ -21,22 +20,21 @@ //! cargo test ... --test surfpool_fuzz surfpool_idl_jupiter -- --ignored //! ``` //! -//! Adding a new IDL: once it's exposed as a `pub const` in -//! `solana_parser::solana::embedded_idls`, add an `idl_test!(name, CONST)` -//! line below; cargo's harness picks it up. +//! Adding a new IDL: +//! - Upstream `solana_parser::solana::embedded_idls::*`: add a `use` import and +//! an `idl_test!(name, CONST)` line below. +//! - Vsp-local preset IDL (e.g. one added by the `solana-add-idl` skill): +//! drop `/.json` into `src/presets/`. `build.rs` discovers it +//! and `surfpool_preset_idls` (below) iterates it on every run -- no test- +//! file edit required. mod common; -use common::{build_transaction, options_with_idl}; -use solana_parser::decode_idl_data; use solana_parser::solana::embedded_idls::{ APE_PRO_IDL, CANDY_MACHINE_IDL, DRIFT_IDL, JUPITER_AGG_V6_IDL, JUPITER_IDL, JUPITER_LIMIT_IDL, KAMINO_IDL, LIFINITY_IDL, METEORA_IDL, OPENBOOK_IDL, ORCA_IDL, RAYDIUM_IDL, STABBLE_IDL, }; -use solana_sdk::pubkey::Pubkey; use solana_test_utils::{SurfpoolConfig, SurfpoolManager}; -use visualsign::vsptrait::{Transaction, VisualSignConverter}; -use visualsign_solana::{SolanaTransactionWrapper, SolanaVisualSignConverter}; /// Smoke test: start surfpool, verify the RPC responds, let `Drop` tear it down. #[tokio::test(flavor = "multi_thread")] @@ -57,59 +55,6 @@ async fn surfpool_lifecycle() { ); } -/// Per-IDL roundtrip: decode the IDL, build a synthetic transaction whose data -/// starts with the first instruction's discriminator, run it through the -/// visual-sign converter, and assert the payload is non-empty. -async fn run_idl_roundtrip(idl_label: &str, idl_json: &str) { - // Distinguish the three failure modes explicitly so a red test names the - // IDL and the actual cause (decode rejection from a malformed IDL, empty - // instruction list, or a missing discriminator). - let idl = decode_idl_data(idl_json) - .unwrap_or_else(|e| panic!("{idl_label}: decode_idl_data rejected the IDL: {e}")); - assert!( - !idl.instructions.is_empty(), - "{idl_label}: IDL has no instructions" - ); - let disc = idl.instructions[0] - .discriminator - .as_ref() - .unwrap_or_else(|| panic!("{idl_label}: instructions[0] has no discriminator")); - let mut data = disc.clone(); - data.extend_from_slice(&[0u8; 32]); - - let _manager = SurfpoolManager::start(SurfpoolConfig::default()) - .await - .expect("surfpool should start"); - - let program_id = Pubkey::new_unique(); - let tx = build_transaction(program_id, vec![Pubkey::new_unique()], data); - let tx_bytes = bincode::serialize(&tx).expect("tx should serialize"); - let tx_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &tx_bytes); - - let wrapper = SolanaTransactionWrapper::from_string(&tx_b64) - .expect("from_string should succeed for a valid base64 transaction"); - - let options = options_with_idl(&program_id, idl_json, "test_program"); - let payload = SolanaVisualSignConverter - .to_visual_sign_payload(wrapper, options) - .expect("converter should succeed"); - - assert!( - !payload.fields.is_empty(), - "payload must contain at least one field" - ); -} - -macro_rules! idl_test { - ($name:ident, $idl:expr) => { - #[tokio::test(flavor = "multi_thread")] - #[ignore] - async fn $name() { - run_idl_roundtrip(stringify!($name), $idl).await; - } - }; -} - // `collision.json` and `cyclic.json` exist in `solana_parser`'s `idls/` // directory but are negative test fixtures (duplicate type names / cyclic // type refs); they're rejected by `decode_idl_data` and therefore not @@ -128,3 +73,22 @@ idl_test!(surfpool_idl_openbook, OPENBOOK_IDL); idl_test!(surfpool_idl_orca, ORCA_IDL); idl_test!(surfpool_idl_raydium, RAYDIUM_IDL); idl_test!(surfpool_idl_stabble, STABBLE_IDL); + +/// Auto-discovered preset IDLs: every `src/presets//.json` file +/// that `build.rs` finds is exercised here through the same roundtrip used +/// by the named `idl_test!` invocations above. The skill (and any future +/// contributor) only needs to drop the JSON file -- this test picks it up +/// without any code edit. Empty when no presets ship an IDL JSON. +#[tokio::test(flavor = "multi_thread")] +#[ignore] +async fn surfpool_preset_idls() { + if visualsign_solana::PRESET_IDLS.is_empty() { + // Nothing to do: no preset has an embedded IDL JSON yet. Don't fail + // the test -- it'd be an unhelpful red whenever the upstream stack + // ships before the first preset IDL lands. + return; + } + for (name, idl_json) in visualsign_solana::PRESET_IDLS { + common::run_idl_roundtrip(&format!("preset_{name}"), idl_json).await; + } +} From a45e514536bce723b992da975a0427b705809c90 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Fri, 8 May 2026 15:39:04 -0400 Subject: [PATCH 2/3] feat(solana): auto-generate presets/mod.rs and refresh skill scope Removes the last manual touchpoint outside the preset directory: build.rs now scans `src/presets//` for `mod.rs` and emits one `#[path = "..."] pub mod ;` per match into `OUT_DIR/presets_mod.rs`. `presets/mod.rs` becomes a single `include!`. Drop a preset directory in place and it wires up on the next build with no edit. The `#[path]` attribute is required because `include!` substitutes the generated text at the OUT_DIR file's location, so the bare `pub mod foo;` would otherwise resolve to `OUT_DIR/foo.rs`. Pinning the absolute source path fixes resolution and keeps spans correct. A `.join("mod.rs").exists()` guard skips any subdirectory that isn't a real module. SKILL.md scope refresh: - Frontmatter description: drop "registers the preset" (now reflective), state explicitly that this scaffolds a structural decoder and that semantic refinement is a follow-up workflow. - New "Scope" section after the H1 enumerates what the skill produces (decoded args, named accounts, auto-registration, auto-test-coverage) and what it deliberately does not (domain labels, token resolution, per-instruction dispatch, semantic assertions). Points at `presets/jupiter_swap/mod.rs` as the fully-semantic exemplar. - Step 4 rewritten: registration is now automatic; nothing to edit. - Step 5 promotes the `tests/semantic_pipeline.rs` mention from a bullet to its own subsection clarifying the auto-roundtrip asserts no semantic correctness -- by design. - New Step 8: "What's next -- semantic refinement (optional)" spells out the three contributor paths (ship as-is, hand-extend modelled on jupiter_swap, wait for `solana-refine-idl-preset`) and lists the patterns to copy from jupiter_swap. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/solana-add-idl/SKILL.md | 75 +++++++++++++++---- src/chain_parsers/visualsign-solana/build.rs | 45 +++++++++++ .../visualsign-solana/src/presets/mod.rs | 13 ++-- 3 files changed, 111 insertions(+), 22 deletions(-) diff --git a/.claude/skills/solana-add-idl/SKILL.md b/.claude/skills/solana-add-idl/SKILL.md index 8941cd44..2d669ee3 100644 --- a/.claude/skills/solana-add-idl/SKILL.md +++ b/.claude/skills/solana-add-idl/SKILL.md @@ -1,6 +1,6 @@ --- name: solana-add-idl -description: Add a new Solana program IDL-based visualizer preset. Fetches IDL on-chain or accepts user-provided IDL, then scaffolds config.rs, mod.rs, and registers the preset. +description: Scaffold a structural IDL-driven Solana visualizer preset. Fetches the IDL (on-chain or user-provided), drops it into the preset directory, and writes a generic decoder. Registration is fully reflective via build.rs. Semantic refinement (domain labels, token resolution) is a follow-up workflow. user-invocable: true --- @@ -8,6 +8,33 @@ user-invocable: true You are scaffolding a new Solana program visualizer preset from an Anchor IDL. +## Scope: what this skill produces and doesn't produce + +This skill scaffolds a **structurally correct, semantically generic** preset. + +What you get: + +- Binary instruction decoded against the IDL via `parse_instruction_with_idl` +- Each on-chain account paired with its IDL-declared name +- Each instruction argument shown as a `text` field with the raw decoded value +- Auto-registered in `available_visualizers()` and `PRESET_IDLS` by `build.rs` reflection — no edits to `presets/mod.rs` or any test file +- Crash-safety auto-covered by `tests/fuzz_idl_parsing.rs` (proptest, generative), `fuzz/fuzz_targets/` (cargo-fuzz, generative), and `tests/surfpool_fuzz.rs::surfpool_preset_idls` (reflective) + +What it deliberately does **not** produce: + +- Domain-specific labels — e.g. `"Swap 1.5 USDC for 0.001 SOL"` rather than `in_token=Pubkey(...), amount_in=1500000, ...` +- Token metadata resolution (mint decimals, symbol lookups) — amounts render as raw integers +- Per-instruction display logic — every instruction goes through the same generic path +- Cross-instruction correlation (e.g. CPI inner-instruction handling) +- Account-role disambiguation beyond IDL parameter names +- Semantic correctness assertions in `tests/semantic_pipeline.rs` — those are program-specific and hand-written + +The skill's output is the equivalent of a typed-decoder dump: correct, but not yet wallet-readable. + +For a **fully semantic** preset to model after, read `src/chain_parsers/visualsign-solana/src/presets/jupiter_swap/mod.rs`. It hand-rolls a `JupiterSwapInstruction` enum, resolves token metadata via `get_token_info`, and uses format strings like `"Swap {amount} {in_token} for {amount} {out_token}"`. That's the destination; this skill produces the starting point. + +Semantic refinement is intended as a separate workflow (planned: `solana-refine-idl-preset` skill). Until that exists, contributors who want wallet-readable output extend the generated `mod.rs` by hand using `jupiter_swap` as the reference. See **Step 7: What's next** at the end of this skill. + ## Step 1: Gather Information Ask the user for: @@ -125,28 +152,30 @@ use visualsign::{ Crash-safety against unknown discriminators / short data is **already covered**: by `tests/fuzz_idl_parsing.rs` (proptest, generative — exercises arbitrary discriminator/data combinations) and by `tests/surfpool_fuzz.rs::surfpool_preset_idls` (auto-iterates `PRESET_IDLS`). Do not duplicate those assertions in the preset's own test module. -## Step 4: Register in presets/mod.rs +## Step 4: Registration is automatic — nothing to edit -Add `pub mod {snake_name};` to `src/chain_parsers/visualsign-solana/src/presets/mod.rs`. +`presets/mod.rs` is generated by `build.rs` (it `include!`s a file that emits `#[path = ...] pub mod ;` per direct subdirectory of `src/presets/` containing a `mod.rs`). Drop your preset directory in place; the next build picks it up. -**Keep entries in alphabetical order.** The existing entries are sorted — insert the new module in the correct position. +`build.rs` also discovers your `{PascalName}Visualizer` for `available_visualizers()` and (because Step 2 saved an IDL JSON) adds an entry to `PRESET_IDLS`. -No other registration is needed for the visualizer itself. `build.rs` auto-discovers `{PascalName}Visualizer` from any directory under `src/presets/`. +Skip ahead — there's no edit to make in this step. -## Step 5: Test coverage — what's auto-discovered, what isn't +## Step 5: Test coverage — what's auto-discovered, what's program-specific -You do **not** need to edit any test file. The harness picks up the new IDL by reflection: +You do **not** need to edit any test file for crash-safety coverage. The harness picks up the new IDL by reflection: -- **`build.rs`** scans `src/presets//.json` and emits `pub const PRESET_IDLS: &[(&str, &str)]` exposed from the library. The IDL JSON file you saved in Step 2 is the only input. -- **`tests/surfpool_fuzz.rs::surfpool_preset_idls`** iterates `PRESET_IDLS` and runs each through `run_idl_roundtrip` against a `surfpool` mainnet fork (decode IDL → build synthetic tx with the first instruction's discriminator → convert → assert non-empty payload). The new preset is exercised on every run with no test-file edit. -- **Proptest (`tests/fuzz_idl_parsing.rs`)** is *generative*. Strategies in `solana_parser_fuzz_core::proptest` synthesize arbitrary IDL shapes and feed them through `decode_idl_data` / `parse_instruction_with_idl`. New IDLs are covered structurally by the existing strategies — no per-IDL registration, ever. -- **cargo-fuzz (`fuzz/fuzz_targets/`)** runs against random byte streams through `transaction_string_to_visual_sign`. Same story: generative, no per-IDL registration. +- **`build.rs`** emits `pub const PRESET_IDLS: &[(&str, &str)]` from `src/presets//.json` (saved in Step 2). The slice is re-exported from the library. +- **`tests/surfpool_fuzz.rs::surfpool_preset_idls`** iterates `PRESET_IDLS` and runs each through `run_idl_roundtrip` against a `surfpool` mainnet fork (decode IDL → build synthetic tx with the first instruction's discriminator → convert → assert non-empty payload). Picked up on every run with no test-file edit. +- **Proptest (`tests/fuzz_idl_parsing.rs`)** is *generative*: strategies in `solana_parser_fuzz_core::proptest` synthesize arbitrary IDL shapes and feed them through `decode_idl_data` / `parse_instruction_with_idl`. Covers your IDL structurally without registration. +- **cargo-fuzz (`fuzz/fuzz_targets/`)** is also *generative* — random byte streams through `transaction_string_to_visual_sign`. No per-IDL registration. -Tests that *do* need hand-written assertions (and therefore can't be auto-discovered): +CI: `surfpool_preset_idls` is `#[ignore]`. It runs when the PR carries the `surfpool` label (see `.github/workflows/surfpool-solana.yml`); local runs need `HELIUS_API_KEY`. -- **`tests/semantic_pipeline.rs`** — correctness assertions on parsed-field shape, label text, amounts, etc. These are program-specific. If the new preset's behavior matters in CI beyond "doesn't crash on a roundtrip," add a fixture-based test here. Otherwise the auto-roundtrip is enough. +### Semantic correctness is NOT auto-covered -CI: `surfpool_preset_idls` is `#[ignore]`. It runs when the PR carries the `surfpool` label (see `.github/workflows/surfpool-solana.yml`); local runs need `HELIUS_API_KEY`. +The auto-roundtrip only asserts the converter doesn't crash. It does **not** assert the displayed fields look correct semantically — that's by design (this skill produces a generic decoder, not a semantic one). + +If the preset needs CI-level semantic guarantees (specific label text, amount formatting, multi-instruction flows, fixture-based snapshot expectations), add a hand-written test in `tests/semantic_pipeline.rs` modelled after the existing `JUPITER_IDL` / `RAYDIUM_IDL` blocks. Otherwise, ship as-is — semantic refinement is a separate workflow (see Step 8). ## Step 6: Code Quality @@ -178,3 +207,21 @@ grep -- '"{snake_name}"' src/chain_parsers/visualsign-solana/target/debug/build/ ``` The `PRESET_IDLS` slice should contain a `("{snake_name}", include_str!(...))` entry. If it doesn't, the IDL JSON file is at the wrong path — `build.rs` looks for exactly `src/presets/{snake_name}/{snake_name}.json`. + +## Step 8: What's next — semantic refinement (optional, follow-up) + +Your preset compiles, registers, and survives a roundtrip. A wallet user signing one of these transactions will, however, see raw arg names and integer values, not a recognizable summary. The skill's scope ends here. To make the preset wallet-readable, three options: + +1. **Ship as-is.** For low-traffic programs or where structural display is enough, this is acceptable — the new preset is strictly better than the `unknown_program` fallback. + +2. **Hand-extend the generated `mod.rs`**, modelled after `presets/jupiter_swap/mod.rs`. The patterns to copy: + - Replace the wildcard `"*": ["*"]` in `config.rs` with explicit instruction names so each instruction can be dispatched separately. + - Introduce a `{PascalName}Instruction` enum with one variant per IDL instruction you care about. See `JupiterSwapInstruction` for the shape (named fields like `in_token`, `out_token`, `slippage_bps`). + - Add a `parse_{snake_name}_instruction` helper that dispatches on the 8-byte discriminator and decodes args into the enum. + - Add a `format_{snake_name}_instruction` helper that turns the enum into a human string. Use `get_token_info` from `crate::utils` to resolve mint decimals and symbols for amount fields. + - Replace generic `create_text_field` calls with semantic ones — `create_amount_field` for token quantities, `create_address_field` for accounts you want clickable in the UI. + - Add a fixture test in `tests/semantic_pipeline.rs` asserting the formatted output for one or two real on-chain transactions. + +3. **Wait for `solana-refine-idl-preset`** — a planned follow-up skill that automates the structural-to-semantic transition. Tracked as future work; not yet available. + +Until option 3 exists, option 2 is the path. The structural decoder this skill produced is the scaffolding the semantic layer goes on top of, not a replacement for it. diff --git a/src/chain_parsers/visualsign-solana/build.rs b/src/chain_parsers/visualsign-solana/build.rs index 22b9d530..106d0271 100644 --- a/src/chain_parsers/visualsign-solana/build.rs +++ b/src/chain_parsers/visualsign-solana/build.rs @@ -36,6 +36,17 @@ fn main() { preset_idls.join(",\n ") ); fs::write(out_dir.join("preset_idls.rs"), preset_idls_code).unwrap(); + + let preset_mods = collect_preset_mods(); + let preset_mods_code = format!( + "// Auto-generated by `build.rs`. One `pub mod` per directory under\n\ + // `src/presets/` that contains a `mod.rs`. Drop in a new preset\n\ + // directory and it shows up here on the next build -- no manual\n\ + // edit of `presets/mod.rs` required.\n\ + {}\n", + preset_mods.join("\n") + ); + fs::write(out_dir.join("presets_mod.rs"), preset_mods_code).unwrap(); } fn collect_visualizers() -> Vec { @@ -80,6 +91,40 @@ fn collect_visualizers() -> Vec { .collect() } +fn collect_preset_mods() -> Vec { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let presets_dir = PathBuf::from(&manifest_dir).join("src/presets"); + let Ok(entries) = fs::read_dir(&presets_dir) else { + return Vec::new(); + }; + let mut mods: Vec = entries + .filter_map(|entry| { + let path = entry.ok()?.path(); + if !path.is_dir() { + return None; + } + // Only emit `pub mod` for directories that actually contain a + // mod.rs. An empty subdirectory would otherwise produce a + // confusing compile error pointing at generated code. + let mod_rs = path.join("mod.rs"); + if !mod_rs.exists() { + return None; + } + let dir_name = path.file_name()?.to_str()?.to_string(); + // The generated file lives in OUT_DIR; `mod` resolution is + // relative to *its* location, so without `#[path]` rustc looks + // for `OUT_DIR/.rs`. The absolute path attribute pins + // resolution to the real source tree. + Some(format!( + "#[path = \"{path}\"]\npub mod {dir_name};", + path = mod_rs.display() + )) + }) + .collect(); + mods.sort(); + mods +} + fn collect_preset_idls() -> Vec { let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let presets_dir = PathBuf::from(&manifest_dir).join("src/presets"); diff --git a/src/chain_parsers/visualsign-solana/src/presets/mod.rs b/src/chain_parsers/visualsign-solana/src/presets/mod.rs index d6474f51..8db6c2ed 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/mod.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/mod.rs @@ -1,8 +1,5 @@ -pub mod associated_token_account; -pub mod compute_budget; -pub mod jupiter_swap; -pub mod stakepool; -pub mod swig_wallet; -pub mod system; -pub mod token_2022; -pub mod unknown_program; +// Module declarations are auto-generated by `build.rs`. The generated +// file emits one `pub mod ;` per direct subdirectory of +// `src/presets/` that contains a `mod.rs`. Drop in a new preset and +// it's wired up on the next build -- no manual edit here. +include!(concat!(env!("OUT_DIR"), "/presets_mod.rs")); From 256cf09b2ff47c991954dc193bfbe1d17d85a8fc Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Fri, 8 May 2026 22:54:32 -0400 Subject: [PATCH 3/3] fix(solana): address code-review feedback on PR #289 Four fixes prompted by review: - SKILL.md scope section pointed at "Step 7: What's next" but the actual section is Step 8 (Step 7 is "Verify"). Now correct. - SKILL.md Step 5 referenced a "JUPITER_IDL block" in tests/semantic_pipeline.rs that does not exist -- Jupiter has its own dedicated preset and is not exercised in semantic_pipeline.rs. Replaced with RAYDIUM_IDL / ORCA_IDL, which are real. - The idl_test! macro docstring claimed test files "must declare the module with `#[macro_use] mod common;`". With #[macro_export] that is unnecessary and surfpool_fuzz.rs uses plain `mod common;`. Rewrote the docstring to reflect that any sibling test file gets the macro at the test binary's crate root for free. - surfpool_preset_idls started a fresh SurfpoolManager for every preset in the loop, paying the fork-startup cost N times. Split run_idl_roundtrip into a setup-free run_idl_roundtrip_inner so the iterating test can share one manager across all presets. Per-IDL named tests via idl_test! still get their own manager, which is fine for isolated runs. Plus, three new build.rs unit tests that lock in the reflection contract: collect_preset_mods emits #[path]-pinned `pub mod` lines, collect_preset_mods skips dirs without mod.rs, collect_preset_idls output is tuple-shaped and sorted. These mirror the existing test_collect_visualizers_* tests in the same file. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/solana-add-idl/SKILL.md | 4 +- src/chain_parsers/visualsign-solana/build.rs | 64 +++++++++++++++++++ .../visualsign-solana/tests/common/mod.rs | 28 +++++--- .../visualsign-solana/tests/surfpool_fuzz.rs | 8 ++- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/.claude/skills/solana-add-idl/SKILL.md b/.claude/skills/solana-add-idl/SKILL.md index 2d669ee3..fd2b20d8 100644 --- a/.claude/skills/solana-add-idl/SKILL.md +++ b/.claude/skills/solana-add-idl/SKILL.md @@ -33,7 +33,7 @@ The skill's output is the equivalent of a typed-decoder dump: correct, but not y For a **fully semantic** preset to model after, read `src/chain_parsers/visualsign-solana/src/presets/jupiter_swap/mod.rs`. It hand-rolls a `JupiterSwapInstruction` enum, resolves token metadata via `get_token_info`, and uses format strings like `"Swap {amount} {in_token} for {amount} {out_token}"`. That's the destination; this skill produces the starting point. -Semantic refinement is intended as a separate workflow (planned: `solana-refine-idl-preset` skill). Until that exists, contributors who want wallet-readable output extend the generated `mod.rs` by hand using `jupiter_swap` as the reference. See **Step 7: What's next** at the end of this skill. +Semantic refinement is intended as a separate workflow (planned: `solana-refine-idl-preset` skill). Until that exists, contributors who want wallet-readable output extend the generated `mod.rs` by hand using `jupiter_swap` as the reference. See **Step 8: What's next** at the end of this skill. ## Step 1: Gather Information @@ -175,7 +175,7 @@ CI: `surfpool_preset_idls` is `#[ignore]`. It runs when the PR carries the `surf The auto-roundtrip only asserts the converter doesn't crash. It does **not** assert the displayed fields look correct semantically — that's by design (this skill produces a generic decoder, not a semantic one). -If the preset needs CI-level semantic guarantees (specific label text, amount formatting, multi-instruction flows, fixture-based snapshot expectations), add a hand-written test in `tests/semantic_pipeline.rs` modelled after the existing `JUPITER_IDL` / `RAYDIUM_IDL` blocks. Otherwise, ship as-is — semantic refinement is a separate workflow (see Step 8). +If the preset needs CI-level semantic guarantees (specific label text, amount formatting, multi-instruction flows, fixture-based snapshot expectations), add a hand-written test in `tests/semantic_pipeline.rs` modelled after the existing `RAYDIUM_IDL` / `ORCA_IDL` blocks. Otherwise, ship as-is — semantic refinement is a separate workflow (see Step 8). ## Step 6: Code Quality diff --git a/src/chain_parsers/visualsign-solana/build.rs b/src/chain_parsers/visualsign-solana/build.rs index 106d0271..618a40ea 100644 --- a/src/chain_parsers/visualsign-solana/build.rs +++ b/src/chain_parsers/visualsign-solana/build.rs @@ -205,4 +205,68 @@ mod tests { "Should have at least one visualizer" ); } + + #[test] + fn test_collect_preset_mods_emits_path_attribute_per_preset() { + // Every preset directory with a mod.rs should produce one + // `#[path = ...] pub mod ;` line. Without the path + // attribute, `include!`'d `pub mod` would resolve relative to + // OUT_DIR and break the build. + let mods = collect_preset_mods(); + assert!(!mods.is_empty(), "Should discover at least one preset"); + for line in &mods { + assert!( + line.starts_with("#[path = \""), + "Each entry must pin the source path: {line}" + ); + assert!( + line.contains("pub mod "), + "Each entry must declare a module: {line}" + ); + } + // Sorted so diffs stay stable as presets are added. + let mut sorted = mods.clone(); + sorted.sort(); + assert_eq!(mods, sorted, "collect_preset_mods output must be sorted"); + } + + #[test] + fn test_collect_preset_mods_skips_dirs_without_mod_rs() { + // The current preset tree has a mod.rs in every directory. + // If any future scaffolding leaves a JSON-only directory behind, + // this guard prevents it from producing a confusing build break. + let mods = collect_preset_mods(); + for line in &mods { + // Extract the path between #[path = "..."] + let start = line.find('"').unwrap() + 1; + let end = line[start..].find('"').unwrap() + start; + let path = &line[start..end]; + assert!( + std::path::Path::new(path).exists(), + "Generated #[path] points at a non-existent file: {path}" + ); + } + } + + #[test] + fn test_collect_preset_idls_returns_only_dirs_with_matching_json() { + // Currently no preset ships an embedded IDL JSON, so the slice + // is expected to be empty. When the first preset adds one, this + // test will need updating -- which is the point: it locks in + // the discovery contract. + let idls = collect_preset_idls(); + for entry in &idls { + assert!( + entry.starts_with('('), + "Entries must be tuple-shaped: {entry}" + ); + assert!( + entry.contains("include_str!"), + "Entries must embed JSON: {entry}" + ); + } + let mut sorted = idls.clone(); + sorted.sort(); + assert_eq!(idls, sorted, "collect_preset_idls output must be sorted"); + } } diff --git a/src/chain_parsers/visualsign-solana/tests/common/mod.rs b/src/chain_parsers/visualsign-solana/tests/common/mod.rs index 674fe87a..b6009385 100644 --- a/src/chain_parsers/visualsign-solana/tests/common/mod.rs +++ b/src/chain_parsers/visualsign-solana/tests/common/mod.rs @@ -175,10 +175,23 @@ pub fn load_idl_from_env() -> Option<(String, solana_parser::solana::structs::Id /// Network-bound: starts a `surfpool` mainnet fork and requires the `surfpool` /// binary on `$PATH`. Callers are responsible for marking their tests with /// `#[ignore]`. Use the `idl_test!` macro for the standard wrapper. +/// +/// To loop over many IDLs without paying the surfpool startup cost per IDL, +/// start one `SurfpoolManager` yourself and call `run_idl_roundtrip_inner` +/// in the loop. pub async fn run_idl_roundtrip(idl_label: &str, idl_json: &str) { - // Distinguish the three failure modes explicitly so a red test names the - // IDL and the actual cause (decode rejection from a malformed IDL, empty - // instruction list, or a missing discriminator). + let _manager = SurfpoolManager::start(SurfpoolConfig::default()) + .await + .expect("surfpool should start"); + run_idl_roundtrip_inner(idl_label, idl_json); +} + +/// Body of `run_idl_roundtrip` minus the `SurfpoolManager` start. Use when +/// running many IDLs in sequence under a single shared manager. +pub fn run_idl_roundtrip_inner(idl_label: &str, idl_json: &str) { + // Three failure modes are distinguished explicitly so a red test names + // the IDL and the actual cause (decode rejection from a malformed IDL, + // empty instruction list, or a missing discriminator). let idl = decode_idl_data(idl_json) .unwrap_or_else(|e| panic!("{idl_label}: decode_idl_data rejected the IDL: {e}")); assert!( @@ -192,10 +205,6 @@ pub async fn run_idl_roundtrip(idl_label: &str, idl_json: &str) { let mut data = disc.clone(); data.extend_from_slice(&[0u8; 32]); - let _manager = SurfpoolManager::start(SurfpoolConfig::default()) - .await - .expect("surfpool should start"); - let program_id = Pubkey::new_unique(); let tx = build_transaction(program_id, vec![Pubkey::new_unique()], data); let tx_bytes = bincode::serialize(&tx).expect("tx should serialize"); @@ -219,8 +228,9 @@ pub async fn run_idl_roundtrip(idl_label: &str, idl_json: &str) { /// the provided IDL string. Works for both upstream `embedded_idls` consts and /// vsp-local IDL JSON via `include_str!`. /// -/// The macro is hoisted from `common/mod.rs`, so any test file that uses it -/// must declare the module with `#[macro_use] mod common;`. +/// Any sibling test file can call this macro unqualified after `mod common;` — +/// `#[macro_export]` puts it at the test binary's crate root, so neither +/// `#[macro_use]` nor an explicit `use` is required. #[macro_export] macro_rules! idl_test { ($name:ident, $idl:expr) => { diff --git a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs index d35d750f..114ee92a 100644 --- a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs +++ b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs @@ -79,6 +79,9 @@ idl_test!(surfpool_idl_stabble, STABBLE_IDL); /// by the named `idl_test!` invocations above. The skill (and any future /// contributor) only needs to drop the JSON file -- this test picks it up /// without any code edit. Empty when no presets ship an IDL JSON. +/// +/// Shares one `SurfpoolManager` across all preset IDLs to avoid paying the +/// fork-startup cost N times when there are many presets. #[tokio::test(flavor = "multi_thread")] #[ignore] async fn surfpool_preset_idls() { @@ -88,7 +91,10 @@ async fn surfpool_preset_idls() { // ships before the first preset IDL lands. return; } + let _manager = SurfpoolManager::start(SurfpoolConfig::default()) + .await + .expect("surfpool should start"); for (name, idl_json) in visualsign_solana::PRESET_IDLS { - common::run_idl_roundtrip(&format!("preset_{name}"), idl_json).await; + common::run_idl_roundtrip_inner(&format!("preset_{name}"), idl_json); } }