Skip to content

feat: add Kamino Borrow visualizer preset#258

Merged
prasanna-anchorage merged 2 commits into
mainfrom
kamino-borrow
May 5, 2026
Merged

feat: add Kamino Borrow visualizer preset#258
prasanna-anchorage merged 2 commits into
mainfrom
kamino-borrow

Conversation

@kyle-anchorage
Copy link
Copy Markdown
Contributor

@kyle-anchorage kyle-anchorage commented Apr 17, 2026

Why this PR exists

Kamino Borrow is a Solana lending program (KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD) not currently supported by the parser. Without a preset, transactions signing against it render as raw hex in wallets instead of decoded instruction names, named accounts, and typed arguments.

What changed

  • New visualsign-solana preset kamino_borrow with the program's Anchor IDL embedded as kamino_borrow.json (55 instructions covering init/refresh/deposit/borrow/repay/liquidate and their V2 variants).
  • KaminoBorrowVisualizer implements InstructionVisualizer with VisualizerKind::Lending(\"Kamino Borrow\"), delegating parsing to solana_parser::parse_instruction_with_idl and mapping IDL account definitions to instruction accounts by index.
  • Registered in presets/mod.rs (alphabetical); build.rs auto-discovers the visualizer.
image

Why this is safe

  • Pure addition: no existing preset, shared helper, or trait is modified. Other programs continue resolving to their current visualizers; only calls to the new program ID change behavior.
  • Parsing failures (unknown discriminator, short data) fall back to a generic "Unknown Instruction" field set, so a malformed or future instruction still renders with program ID and raw hex rather than erroring.
  • The generic IDL pattern is shared with the existing jupiter_swap and prior squads_multisig presets — no Kamino-specific decoding or unsafe deserialization.

Checks run (by agent)

  • cargo fmt -p visualsign-solana
  • cargo clippy -p visualsign-solana --all-targets -- -D warnings
  • cargo test -p visualsign-solana (all 4 new tests + existing suites pass)
  • make -C src test (full workspace, no failures)

Manual steps needed (by human)

None — fully automated by CI.

How this is maintainable

  • Scaffolded via the /solana-add-idl skill, so a fresh agent can regenerate an equivalent preset from the skill's documented template.
  • The preset follows the same generic IDL shape (build_named_accounts / build_parsed_fields / build_fallback_fields / append_raw_data / format_arg_value) used by other IDL-based presets; touching one teaches you the rest.
  • Four unit tests pin the invariants: IDL loads, every instruction has an 8-byte Anchor discriminator, unknown discriminators error, and short data errors. A Kamino IDL update that drops or renames an instruction will surface in the discriminator test.
  • The IDL JSON is checked in verbatim; re-fetching and diffing it is the upgrade path when Kamino ships a new program version.

@github-actions
Copy link
Copy Markdown
Contributor

Hi @kyle-anchorage, thank you for your contribution!

It looks like this is your first time contributing. To get this PR merged, please review our Contributor License Agreement (CLA) here: https://ironcladapp.com/public-launch/6896309b0f158de8c7450de6

Once you have reviewed the agreement, a maintainer (@anchorageoss/maintainers) will need to approve your addition to our contributors list by commenting /approve-cla on this PR. Thank you!

@kyle-anchorage kyle-anchorage marked this pull request as ready for review April 27, 2026 18:38
Copilot AI review requested due to automatic review settings April 27, 2026 18:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Solana instruction visualizer preset for the Kamino Borrow (Kamino Lending) program so wallets can render decoded instruction names, accounts, and typed arguments instead of raw hex.

Changes:

  • Added kamino_borrow preset module and registered it in presets/mod.rs.
  • Implemented KaminoBorrowVisualizer using solana_parser::parse_instruction_with_idl plus basic IDL-account-name mapping.
  • Embedded the program’s Anchor IDL as kamino_borrow.json and added unit tests for IDL/discriminator invariants.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/chain_parsers/visualsign-solana/src/presets/mod.rs Registers the new kamino_borrow preset module.
src/chain_parsers/visualsign-solana/src/presets/kamino_borrow/mod.rs Implements the Kamino Borrow instruction visualizer + parsing/helpers + tests.
src/chain_parsers/visualsign-solana/src/presets/kamino_borrow/config.rs Adds integration config wiring for the Kamino Borrow program ID.
src/chain_parsers/visualsign-solana/src/presets/kamino_borrow/kamino_borrow.json Embeds the Kamino Borrow Anchor IDL used for decoding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +90 to +92
fn get_kamino_borrow_idl() -> Option<Idl> {
decode_idl_data(KAMINO_BORROW_IDL_JSON).ok()
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_kamino_borrow_idl() decodes the full embedded JSON on every call, which means every Kamino instruction visualization will re-parse a ~1.4k-line IDL. Cache the decoded Idl in a OnceLock (or similar) and return a reference/clone from there to avoid repeated JSON parsing overhead.

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +187
if let Ok(f) = create_text_field("Program", "Kamino Borrow") {
condensed_fields.push(f);
}
if let Ok(f) = create_text_field("Instruction", &parsed.instruction_name) {
condensed_fields.push(f);
}
for (key, value) in &parsed.program_call_args {
if let Ok(f) = create_text_field(key, &format_arg_value(value)) {
condensed_fields.push(f);
}
}

if let Ok(f) = create_text_field("Program ID", program_id) {
expanded_fields.push(f);
}
if let Ok(f) = create_text_field("Instruction", &parsed.instruction_name) {
expanded_fields.push(f);
}
if let Ok(f) = create_text_field("Discriminator", &parsed.discriminator) {
expanded_fields.push(f);
}

for (account_name, account_address) in &instruction.named_accounts {
if let Ok(f) = create_text_field(account_name, account_address) {
expanded_fields.push(f);
}
}

for (key, value) in &parsed.program_call_args {
if let Ok(f) = create_text_field(key, &format_arg_value(value)) {
expanded_fields.push(f);
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field-builder errors are silently ignored throughout build_parsed_fields (e.g., if let Ok(f) = create_text_field(...)). If any field creation fails, the UI will quietly drop fields rather than surfacing an error or falling back. Prefer propagating the error (?) so visualization fails deterministically (or explicitly fall back with a status field + raw data).

Suggested change
if let Ok(f) = create_text_field("Program", "Kamino Borrow") {
condensed_fields.push(f);
}
if let Ok(f) = create_text_field("Instruction", &parsed.instruction_name) {
condensed_fields.push(f);
}
for (key, value) in &parsed.program_call_args {
if let Ok(f) = create_text_field(key, &format_arg_value(value)) {
condensed_fields.push(f);
}
}
if let Ok(f) = create_text_field("Program ID", program_id) {
expanded_fields.push(f);
}
if let Ok(f) = create_text_field("Instruction", &parsed.instruction_name) {
expanded_fields.push(f);
}
if let Ok(f) = create_text_field("Discriminator", &parsed.discriminator) {
expanded_fields.push(f);
}
for (account_name, account_address) in &instruction.named_accounts {
if let Ok(f) = create_text_field(account_name, account_address) {
expanded_fields.push(f);
}
}
for (key, value) in &parsed.program_call_args {
if let Ok(f) = create_text_field(key, &format_arg_value(value)) {
expanded_fields.push(f);
}
condensed_fields.push(
create_text_field("Program", "Kamino Borrow")
.unwrap_or_else(|err| panic!("failed to build Kamino Borrow condensed field `Program`: {err}")),
);
condensed_fields.push(
create_text_field("Instruction", &parsed.instruction_name)
.unwrap_or_else(|err| panic!("failed to build Kamino Borrow condensed field `Instruction`: {err}")),
);
for (key, value) in &parsed.program_call_args {
condensed_fields.push(
create_text_field(key, &format_arg_value(value)).unwrap_or_else(|err| {
panic!("failed to build Kamino Borrow condensed argument field `{key}`: {err}")
}),
);
}
expanded_fields.push(
create_text_field("Program ID", program_id)
.unwrap_or_else(|err| panic!("failed to build Kamino Borrow expanded field `Program ID`: {err}")),
);
expanded_fields.push(
create_text_field("Instruction", &parsed.instruction_name)
.unwrap_or_else(|err| panic!("failed to build Kamino Borrow expanded field `Instruction`: {err}")),
);
expanded_fields.push(
create_text_field("Discriminator", &parsed.discriminator)
.unwrap_or_else(|err| panic!("failed to build Kamino Borrow expanded field `Discriminator`: {err}")),
);
for (account_name, account_address) in &instruction.named_accounts {
expanded_fields.push(
create_text_field(account_name, account_address).unwrap_or_else(|err| {
panic!("failed to build Kamino Borrow account field `{account_name}`: {err}")
}),
);
}
for (key, value) in &parsed.program_call_args {
expanded_fields.push(
create_text_field(key, &format_arg_value(value)).unwrap_or_else(|err| {
panic!("failed to build Kamino Borrow expanded argument field `{key}`: {err}")
}),
);

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +228
if let Ok(f) = create_raw_data_field(data, Some(hex_str.to_string())) {
fields.push(f);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append_raw_data silently ignores failures from create_raw_data_field. If raw-data field construction fails, the expanded view may lose the only universally reliable detail for unknown/failed parses. Consider propagating the error (so visualization fails loudly) or adding a deterministic fallback text field indicating raw-data rendering failed.

Suggested change
if let Ok(f) = create_raw_data_field(data, Some(hex_str.to_string())) {
fields.push(f);
match create_raw_data_field(data, Some(hex_str.to_string())) {
Ok(f) => fields.push(f),
Err(_) => {
if let Ok(fallback) = create_text_field(
"Raw Data",
&format!("Failed to render raw data field; hex: {}", hex_str),
) {
fields.push(fallback);
}
}

Copilot uses AI. Check for mistakes.
let idl_instruction = idl.instructions.iter().find(|inst| {
inst.discriminator
.as_ref()
.is_some_and(|disc| data.len() >= disc.len() && data[..disc.len()] == *disc)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The discriminator match in build_named_accounts uses manual slicing + equality. Elsewhere in this crate the pattern is data.len() >= 8 && &data[0..8] == disc.as_slice() (e.g., presets/unknown_program/mod.rs:321-324). Consider switching to data.starts_with(disc.as_slice()) (or the same 0..8 comparison) for consistency and readability.

Suggested change
.is_some_and(|disc| data.len() >= disc.len() && data[..disc.len()] == *disc)
.is_some_and(|disc| data.starts_with(disc.as_slice()))

Copilot uses AI. Check for mistakes.
kyle-anchorage and others added 2 commits May 5, 2026 14:46
HashMap iteration order is non-deterministic (Rust's randomized hasher),
which causes the rendered output's field ordering to vary between runs.
Switching named_accounts to BTreeMap matches the project's deterministic
serialization invariant (CLAUDE.md) and the precedent set by the merged
`dflow_aggregator` preset (PR #262).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@prasanna-anchorage prasanna-anchorage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — Kamino Borrow preset. Validated end-to-end via parser_cli decode of a real on-chain refreshReserve+depositReserveLiquidityAndObligationCollateral chain (output saved to /tmp/fixtures-review/258-kamino_borrow.json during the QA review run). Rebased onto current main; only conflict was the trivial alphabetical add in presets/mod.rs alongside kamino_farms/kamino_vault from #257.

@prasanna-anchorage prasanna-anchorage merged commit 469894f into main May 5, 2026
7 checks passed
@prasanna-anchorage prasanna-anchorage deleted the kamino-borrow branch May 5, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants