feat(solana): core::arg_rendering + Marginfi preset; bootstrap-only solana-add-idl skill#375
Conversation
|
Finding from commit 3 (regeneration test): The skill's use std::collections::HashMap;
// ...
let mut programs = HashMap::new();
let mut instructions = HashMap::new();But pub struct SolanaIntegrationConfigData {
pub programs: BTreeMap<&'static str, BTreeMap<&'static str, Vec<&'static str>>>,
}A contributor following the template verbatim would get a type mismatch compile error. The template needs to be updated to |
|
Finding from commit 3 (regeneration test): Step 4 of the skill is stale — Step 4 currently says:
But //! ... Do NOT edit this file — it has no hand-maintained content.
include!(concat!(env!(\OUT_DIR\), \/generated_presets_mod.rs\));
|
|
Finding from commit 3 (regeneration test): skill requires The updated skill prescribes: use crate::core::{
InstructionView, ...
};
// ...
let view = InstructionView::from_context(context);
The skill should document this precondition — either a note at the top ("requires PR #367 / the |
|
Comparison: generated vs prior neutral_trade and onre_app implementation (template gap fix) Finding that triggered this commit: the generated template was missing Fix applied:
After fix, the generated expanded view is now consistent with the prior implementations on this point, while still being strictly better on: OnceLock caching, recursive push_arg_fields, BTreeMap for named_accounts, Remaining Account N handling, and InstructionView pattern. |
…y-name subtitle in neutral_trade and onre_app Code review (PR #375) identified that regenerated neutral_trade and onre_app silently regressed from the display-name subtitle to an empty string, because the dflow_aggregator template (which always had an empty subtitle) was used verbatim. Fix: add subtitle as a gathered input in Step 1 (defaulting to display_name), template it in Step 3, and restore "Neutral Trade" / "Onre App" in the affected presets. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…p; expand skill required-tests list Code review (PR #375) identified that neutral_trade and onre_app were missing the six rendering tests present in dflow_aggregator. Backfill push_arg_fields (scalars, arrays, objects, empty collections), build_named_accounts_surfaces_extra_accounts, and remaining_account_label_is_human_readable into both presets. Also update SKILL.md: expand required-tests list to include all ten tests, add field_label_value and serde_json/IdlSource imports to the prescribed test helpers, and document the build_named_accounts signature with argument order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Validates and updates the solana-add-idl scaffolding skill by regenerating multiple Solana IDL-based presets to match the newer InstructionView::from_context() pattern (graceful handling for v0 + ALT / unresolved accounts), plus aligning config generation and test-helper conventions.
Changes:
- Regenerated
onre_app,neutral_trade, anddflow_aggregatorpreset implementations to useInstructionView, IDL caching viaOnceLock,BTreeMapnamed accounts, anddummy_account_stringstest helpers. - Improved expanded/fallback field rendering consistency (e.g., ensuring expanded views include a human-readable
Programfield). - Updated
.claude/skills/solana-add-idl/SKILL.mdto document the new patterns (BTreeMap config, no manualpresets/mod.rsregistration, additional tests, etc.).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/chain_parsers/visualsign-solana/src/presets/onre_app/mod.rs |
Regenerated IDL visualizer using InstructionView, cached IDL, fallback rendering, recursive arg field rendering, and expanded “Remaining Account N” support + tests. |
src/chain_parsers/visualsign-solana/src/presets/onre_app/config.rs |
Simplifies config map construction (type inference) while keeping BTreeMap backing. |
src/chain_parsers/visualsign-solana/src/presets/neutral_trade/mod.rs |
Regenerated visualizer and tests in the same InstructionView/fallback/recursive-args pattern. |
src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/mod.rs |
Small regeneration deltas: formatting, expanded/fallback “Program” field, and test helper conversion to dummy_account_strings. |
.claude/skills/solana-add-idl/SKILL.md |
Updates skill instructions to match current preset patterns (InstructionView, BTreeMap config, no manual module registration, extended required tests). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| **Subtitle field uses `{subtitle_text}`.** In the `preview_layout` construction inside `visualize_tx_commands`, set the subtitle to the value gathered in Step 1: | ||
| ```rust | ||
| subtitle: Some(SignablePayloadFieldTextV2 { | ||
| text: "{subtitle_text}".to_string(), | ||
| }), | ||
| ``` | ||
| If the user provided no subtitle (or said to leave it empty), use `String::new()` instead. |
There was a problem hiding this comment.
Fixed in 76e32a3 — replaced the single "{subtitle_text}".to_string() placeholder with two explicit forms: one showing a custom subtitle as a string literal with .to_string(), and one showing the empty-default case as String::new() directly. This matches what the generated presets actually emit.
Review summaryStacked on: #367 (fix/362-idl-preset-alt-accounts — introduces What this PR doesValidates the Skill changes (SKILL.md)
Generated preset changes vs prior implementationsAll three presets are strictly better than the hand-written versions they replace:
Strengths
|
684ae17 to
41c82e4
Compare
…y-name subtitle in neutral_trade and onre_app Code review (PR #375) identified that regenerated neutral_trade and onre_app silently regressed from the display-name subtitle to an empty string, because the dflow_aggregator template (which always had an empty subtitle) was used verbatim. Fix: add subtitle as a gathered input in Step 1 (defaulting to display_name), template it in Step 3, and restore "Neutral Trade" / "Onre App" in the affected presets. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…p; expand skill required-tests list Code review (PR #375) identified that neutral_trade and onre_app were missing the six rendering tests present in dflow_aggregator. Backfill push_arg_fields (scalars, arrays, objects, empty collections), build_named_accounts_surfaces_extra_accounts, and remaining_account_label_is_human_readable into both presets. Also update SKILL.md: expand required-tests list to include all ten tests, add field_label_value and serde_json/IdlSource imports to the prescribed test helpers, and document the build_named_accounts signature with argument order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Rebased onto The diff is now just the real change — 5 files: Verified locally against the merged baseline:
Ready for re-review. |
…ly skill Additive only -- no existing preset is modified. Consolidates the solana-add-idl skill work into a single PR. Code: - Add `core::arg_rendering::format_arg_value` (+ charset_safe, bytes_as_hex): a charset-safe, single-field-per-arg renderer (byte arrays -> 0x hex) for IDL-based presets. New presets import it from crate::core; existing presets keep their current renderers untouched. - Add Marginfi IDL-based preset as a live validation of the skill; auto-registered via build.rs. Skill (SKILL.md): - Dispatch scaffolding as a Sonnet subagent (model enforced at the API layer); compare mode dispatches Sonnet + Opus and diffs outputs. - Import format_arg_value from crate::core rather than embedding it. - Bootstrap-only scope: scaffold a NEW preset; never regenerate an existing one. - Subtitle convention (Step 1 + preview_layout emission). - Live-fuzz validation via the surfpool label (Step 3). - "Improving this skill": two non-destructive validation loops -- corpus diff (regenerate to a scratch dir, diff vs committed, fix the skill) and Sonnet-vs-Opus model compare. Neither overwrites a committed preset. Closes #397 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
41c82e4 to
3cc61da
Compare
|
Consolidating the What changed in approach: we're dropping the regenerate-existing-presets validation (the delete+regenerate of dflow/neutral_trade/onre_app) in favor of bootstrap-then-own: the skill scaffolds a new preset and never regenerates an owned one. Net effect here is additive only — Preserved from the prior branch: the regeneration-loop framing you'd added (reframed non-destructive — regenerate to a scratch dir, diff vs committed, fix the skill), plus the subtitle convention and surfpool live-fuzz docs. Added the Sonnet-subagent dispatch + compare mode (closes #397). Follow-up (separate issue, not this PR): neutral_trade + onre_app still use the buggy Tests: 270/270, clippy clean (default + diagnostics). Requesting your review given the merge-approval policy. |
|
Follow-up on the Best fix — make it type-driven, not value-driven. pub fn format_arg_value(value: &serde_json::Value, ty: Option<&IdlType>) -> String {
match (value, ty) {
(serde_json::Value::Array(items), Some(IdlType::Array(inner, _) | IdlType::Vec(inner)))
if matches!(**inner, IdlType::U8) =>
{
bytes_as_hex(items).unwrap_or_else(|| "[]".to_string())
}
(serde_json::Value::Array(items), _) => {
let inner: Vec<String> = items.iter().map(|v| format_arg_value(v, None)).collect();
format!("[{}]", inner.join(","))
}
// ... other arms unchanged, ty ignored for non-array cases
}
}This makes If threading the type isn't worth the churn right now, a strictly safer heuristic than "all elements ≤255" is to also gate on length, since deliberate byte-blobs (seeds, hashes, pubkey-shaped fields) are essentially always ≥16 bytes, while meaningful small-integer parameter arrays (bps, weights, fee tiers) are almost always shorter than that in practice: fn bytes_as_hex(items: &[serde_json::Value]) -> Option<String> {
if items.len() < 16 { // short arrays are far more likely semantic values than byte blobs
return None;
}
...
}That's still a heuristic (a legitimate 16+ element |
|
Fleshed out the type-driven version as a concrete suggestion (not applying it myself — this is for you to evaluate/apply).
use solana_parser::solana::structs::IdlType;
/// `ty` is the arg's declared IDL type, when known — it decides whether an array
/// collapses to hex or renders as a bracketed list. Byte classification is
/// type-driven: an array only becomes hex when the IDL says its elements are
/// `u8`, never merely because every decoded value happens to be `<= 255`.
///
/// `ty` is `None` for values reached through a struct/enum field (resolving
/// `IdlType::Defined` against the IDL's `types` registry isn't done here).
/// That's safe by construction: without positive type evidence, arrays never
/// collapse to hex, they just render as a (possibly longer) bracketed list.
pub fn format_arg_value(value: &serde_json::Value, ty: Option<&IdlType>) -> String {
match value {
serde_json::Value::String(s) => charset_safe(s),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Array(items) => {
let element_type = array_element_type(ty);
if is_byte_element_type(element_type) {
if let Some(hex) = bytes_as_hex(items) {
return hex;
}
}
let inner: Vec<String> = items
.iter()
.map(|v| format_arg_value(v, element_type))
.collect();
format!("[{}]", inner.join(","))
}
serde_json::Value::Object(map) => {
let inner: Vec<String> = map
.iter()
.map(|(k, v)| format!("{}:{}", charset_safe(k), format_arg_value(v, None)))
.collect();
format!("{{{}}}", inner.join(","))
}
}
}
fn array_element_type(ty: Option<&IdlType>) -> Option<&IdlType> {
match ty {
Some(IdlType::Array(inner, _) | IdlType::Vec(inner)) => Some(inner.as_ref()),
_ => None,
}
}
fn is_byte_element_type(ty: Option<&IdlType>) -> bool {
matches!(ty, Some(IdlType::U8))
}
fn charset_safe(text: &str) -> String {
text.chars()
.filter(|&c| c == ' ' || (c.is_ascii_graphic() && c != '"' && c != '\\'))
.collect()
}
/// Encodes `items` as `0x`-prefixed hex. Returns `None` only if an element
/// doesn't actually fit in a `u8` (defensive: shouldn't happen once the caller
/// has confirmed the IDL element type is `u8`).
fn bytes_as_hex(items: &[serde_json::Value]) -> Option<String> {
let mut bytes = Vec::with_capacity(items.len());
for item in items {
let byte = item.as_u64().filter(|n| *n <= u8::MAX as u64)? as u8;
bytes.push(byte);
}
Some(format!("0x{}", hex::encode(bytes)))
}Notes on that sketch:
use solana_parser::solana::structs::{IdlInstruction, IdlType};
fn parse_marginfi_instruction(
data: &[u8],
accounts: &[String],
) -> Result<MarginfiParsedInstruction, Box<dyn std::error::Error>> {
if data.len() < 8 {
return Err("Invalid instruction data length".into());
}
let idl = get_marginfi_idl().ok_or("Marginfi IDL not available")?;
let parsed = parse_instruction_with_idl(data, MARGINFI_PROGRAM_ID, idl)?;
let idl_instruction = find_idl_instruction(data, idl);
let (named_accounts, extra_accounts) = build_named_accounts(idl_instruction, accounts);
let arg_types = idl_instruction
.map(|inst| {
inst.args
.iter()
.map(|field| (field.name.clone(), field.r#type.clone()))
.collect()
})
.unwrap_or_default();
Ok(MarginfiParsedInstruction { parsed, named_accounts, extra_accounts, arg_types })
}
fn find_idl_instruction<'a>(data: &[u8], idl: &'a Idl) -> Option<&'a IdlInstruction> {
idl.instructions.iter().find(|inst| {
inst.discriminator
.as_ref()
.is_some_and(|disc| data.get(..disc.len()) == Some(disc.as_slice()))
})
}
// build_named_accounts takes `Option<&IdlInstruction>` directly now (reuses the
// lookup above instead of re-finding it), and MarginfiParsedInstruction gains
// `arg_types: BTreeMap<String, IdlType>`.Then in for (key, value) in &parsed.program_call_args {
let ty = instruction.arg_types.get(key);
condensed_fields.push(create_text_field(key, &format_arg_value(value, ty))?);
}(same for the Checked marginfi's own IDL: this repo's only array-typed instruction arg is |
|
One more generalization on the type-driven suggestion above: move the instruction/arg-type lookup into // src/chain_parsers/visualsign-solana/src/core/arg_rendering.rs (or core/mod.rs)
use solana_parser::solana::structs::{Idl, IdlInstruction, IdlType};
use std::collections::BTreeMap;
pub fn find_idl_instruction<'a>(data: &[u8], idl: &'a Idl) -> Option<&'a IdlInstruction> {
idl.instructions.iter().find(|inst| {
inst.discriminator
.as_ref()
.is_some_and(|disc| data.get(..disc.len()) == Some(disc.as_slice()))
})
}
pub fn arg_types_for(idl_instruction: Option<&IdlInstruction>) -> BTreeMap<String, IdlType> {
idl_instruction
.map(|inst| {
inst.args
.iter()
.map(|field| (field.name.clone(), field.r#type.clone()))
.collect()
})
.unwrap_or_default()
}Then let idl_instruction = find_idl_instruction(data, idl);
let (named_accounts, extra_accounts) = build_named_accounts(idl_instruction, accounts);
let arg_types = arg_types_for(idl_instruction);Worth folding the call pattern into the SKILL.md scaffolding prompt too (Step C / "Arg rendering" section), so every future (Opened a separate tracking issue for migrating the 17 existing presets, which each have their own local |
Summary
Single, additive PR consolidating the
solana-add-idlskill work (supersedes #401, closed into this).core::arg_rendering— addsformat_arg_value(+charset_safe,bytes_as_hex): a charset-safe, single-field-per-arg renderer (byte arrays ->0xhex). New presets import it fromcrate::core; existing presets are left untouched.build.rs, as a live validation of the skill.format_arg_valuefromcrate::core; bootstrap-only scope (scaffold a new preset, never regenerate an existing one); subtitle convention; surfpool live-fuzz validation; and an "Improving this skill" section with two non-destructive validation loops (corpus diff + model compare).No existing preset is modified — clean on
main.Test plan
cargo clippy -p visualsign-solana --all-targets -- -D warningscargo clippy -p visualsign-solana --features diagnostics --all-targets -- -D warningscargo test -p visualsign-solana— 270 passedcargo test -p visualsign-solana --features diagnosticssurfpoollabel to run live fuzz CI (incl. Marginfi)Closes #397