feat: add Kamino Borrow visualizer preset#258
Conversation
|
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 |
There was a problem hiding this comment.
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_borrowpreset module and registered it inpresets/mod.rs. - Implemented
KaminoBorrowVisualizerusingsolana_parser::parse_instruction_with_idlplus basic IDL-account-name mapping. - Embedded the program’s Anchor IDL as
kamino_borrow.jsonand 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.
| fn get_kamino_borrow_idl() -> Option<Idl> { | ||
| decode_idl_data(KAMINO_BORROW_IDL_JSON).ok() | ||
| } |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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).
| 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}") | |
| }), | |
| ); |
| if let Ok(f) = create_raw_data_field(data, Some(hex_str.to_string())) { | ||
| fields.push(f); |
There was a problem hiding this comment.
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.
| 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); | |
| } | |
| } |
| let idl_instruction = idl.instructions.iter().find(|inst| { | ||
| inst.discriminator | ||
| .as_ref() | ||
| .is_some_and(|disc| data.len() >= disc.len() && data[..disc.len()] == *disc) |
There was a problem hiding this comment.
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.
| .is_some_and(|disc| data.len() >= disc.len() && data[..disc.len()] == *disc) | |
| .is_some_and(|disc| data.starts_with(disc.as_slice())) |
2eb60b5 to
cd17689
Compare
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>
cd17689 to
9468d5e
Compare
prasanna-anchorage
left a comment
There was a problem hiding this comment.
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.
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
visualsign-solanapresetkamino_borrowwith the program's Anchor IDL embedded askamino_borrow.json(55 instructions covering init/refresh/deposit/borrow/repay/liquidate and their V2 variants).KaminoBorrowVisualizerimplementsInstructionVisualizerwithVisualizerKind::Lending(\"Kamino Borrow\"), delegating parsing tosolana_parser::parse_instruction_with_idland mapping IDL account definitions to instruction accounts by index.presets/mod.rs(alphabetical);build.rsauto-discovers the visualizer.Why this is safe
jupiter_swapand priorsquads_multisigpresets — no Kamino-specific decoding or unsafe deserialization.Checks run (by agent)
cargo fmt -p visualsign-solanacargo clippy -p visualsign-solana --all-targets -- -D warningscargo 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
/solana-add-idlskill, so a fresh agent can regenerate an equivalent preset from the skill's documented template.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.