| 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. |
| user-invocable | true |
Add Solana IDL Visualizer Preset
You are scaffolding a new Solana program visualizer preset from an Anchor IDL.
Step 1: Gather Information
Ask the user for:
- Program address (base58 Solana program ID)
- Human-readable name (e.g. "Squads Multisig", "Marinade Finance", "Jupiter Swap")
- VisualizerKind — one of:
Dex, Lending, StakingPools, Payments
Derive these from the human name:
snake_name: lowercase with underscores (e.g. marinade_finance)
PascalName: PascalCase (e.g. MarinadeFinance)
SCREAMING_SNAKE: uppercase with underscores (e.g. MARINADE_FINANCE)
display_name: the human name as-is for display strings
Step 2: Fetch the IDL
Try these in order:
Option A: Local Anchor CLI
anchor idl fetch <PROGRAM_ID> --provider.cluster mainnet
Option B: Docker container
If anchor is not installed locally, use the project's Anchor CLI container:
docker images -q anchor-cli | grep -q . || \
docker build -t anchor-cli -f images/anchor-cli/Containerfile .
docker run --rm anchor-cli idl fetch <PROGRAM_ID> --provider.cluster mainnet
Option C: User-provided IDL
If both methods fail, ask the user to provide the IDL via:
- A local file path
- A URL to fetch
- Pasted JSON
Save the IDL to: src/chain_parsers/visualsign-solana/src/presets/{snake_name}/{snake_name}.json
Validation
The IDL JSON must have an instructions array. Verify this before proceeding. If it's missing, tell the user the IDL is invalid.
Step 3: Scaffold the Preset
Create directory: src/chain_parsers/visualsign-solana/src/presets/{snake_name}/
File: config.rs
use super::{SCREAMING_SNAKE}_PROGRAM_ID;
use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData};
use std::collections::HashMap;
pub struct {PascalName}Config;
impl SolanaIntegrationConfig for {PascalName}Config {
fn new() -> Self {
Self
}
fn data(&self) -> &SolanaIntegrationConfigData {
static DATA: std::sync::OnceLock<SolanaIntegrationConfigData> = std::sync::OnceLock::new();
DATA.get_or_init(|| {
let mut programs = HashMap::new();
let mut instructions = HashMap::new();
instructions.insert("*", vec!["*"]);
programs.insert({SCREAMING_SNAKE}_PROGRAM_ID, instructions);
SolanaIntegrationConfigData { programs }
})
}
}
File: mod.rs
Use the dflow_aggregator preset as a template: src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/mod.rs
Read that file for the exact structure, then generate a generic version with these substitutions:
- Replace
DflowAggregator / dflow_aggregator / DFLOW_AGGREGATOR with the appropriate casing of the new program name
- Replace the program ID string with the new program address
- Replace
"DFlow Aggregator" 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
Generic IDL pattern only:
- The generic scaffold uses the three helpers
dflow_aggregator defines: build_named_accounts, build_parsed_fields, and build_fallback_fields. All three work with any IDL.
- Two additional helpers —
append_raw_data (for byte-blob args) and format_arg_value (for custom scalar rendering) — are not present in dflow_aggregator. Add them when the target IDL needs them, copying the pattern from another preset such as kamino_vault or jupiter_earn.
- 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
Visualizer body must use the wire-data context API. At the top of visualize_tx_commands:
let program_id = context.resolve_program_id()?.to_string();
let accounts = context.resolve_accounts()?;
let data = context.data();
These three accessors replace the old context.current_instruction(). They surface
unresolved indices as Err(VisualSignError::DecodeError(...)) with the bad index
named, instead of returning a generic "no instruction found" failure. Use
context.instruction_index() for any "Instruction N" labels.
Required imports (at top of module, NOT inside functions):
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_sdk::instruction::AccountMeta;
use std::collections::BTreeMap;
use std::sync::OnceLock;
use visualsign::errors::VisualSignError;
use visualsign::field_builders::{create_raw_data_field, create_text_field};
use visualsign::{
AnnotatedPayloadField, SignablePayloadField, SignablePayloadFieldCommon,
SignablePayloadFieldListLayout, SignablePayloadFieldPreviewLayout, SignablePayloadFieldTextV2,
};
BTreeMap (not HashMap) keeps the rendered named-accounts order deterministic.
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
Step 4: Register in presets/mod.rs
Add pub mod {snake_name}; to src/chain_parsers/visualsign-solana/src/presets/mod.rs.
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/.
Step 5: Code Quality
Follow these rules in all generated code:
use statements at top of module, never inside functions
- Inline format strings:
format!("{variable}") not format!("{}", variable)
- Use
create_text_field and create_raw_data_field from visualsign::field_builders — never construct field structs directly
- For raw-data fields, pass
None as the second arg of create_raw_data_field unless you already have a precomputed hex string to reuse (e.g. one you built for fallback_text). Do not call hex::encode(data) solely to populate this arg — the helper falls back to the same lowercase byte-by-byte hex on None.
- ASCII only in user-visible strings:
>= not ≥, -> not →
- Rust edition 2024 on nightly
Step 6: Verify
Run these commands and fix any issues:
cargo fmt -p visualsign-solana
cargo clippy -p visualsign-solana --all-targets -- -D warnings
cargo clippy -p visualsign-solana --features diagnostics --all-targets -- -D warnings
cargo test -p visualsign-solana
cargo test -p visualsign-solana --features diagnostics
make -C src test
All must pass before the task is complete. Both feature configurations
(diagnostics on and off) need to compile and test cleanly because parser_app
builds without diagnostics while parser_cli builds with it.