From 2b04c96829650bea7c154f2cf8ea9d45b6b27716 Mon Sep 17 00:00:00 2001 From: Kyle Kamimoto Date: Thu, 7 May 2026 17:51:08 +0000 Subject: [PATCH] add kamino limit orders --- .../src/presets/kamino_limit/config.rs | 22 + .../presets/kamino_limit/kamino_limit.json | 1799 +++++++++++++++++ .../src/presets/kamino_limit/mod.rs | 285 +++ .../visualsign-solana/src/presets/mod.rs | 1 + 4 files changed, 2107 insertions(+) create mode 100644 src/chain_parsers/visualsign-solana/src/presets/kamino_limit/config.rs create mode 100644 src/chain_parsers/visualsign-solana/src/presets/kamino_limit/kamino_limit.json create mode 100644 src/chain_parsers/visualsign-solana/src/presets/kamino_limit/mod.rs diff --git a/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/config.rs b/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/config.rs new file mode 100644 index 00000000..9c34c2ce --- /dev/null +++ b/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/config.rs @@ -0,0 +1,22 @@ +use super::KAMINO_LIMIT_PROGRAM_ID; +use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; +use std::collections::HashMap; + +pub struct KaminoLimitConfig; + +impl SolanaIntegrationConfig for KaminoLimitConfig { + fn new() -> Self { + Self + } + + fn data(&self) -> &SolanaIntegrationConfigData { + static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); + DATA.get_or_init(|| { + let mut programs = HashMap::new(); + let mut instructions = HashMap::new(); + instructions.insert("*", vec!["*"]); + programs.insert(KAMINO_LIMIT_PROGRAM_ID, instructions); + SolanaIntegrationConfigData { programs } + }) + } +} diff --git a/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/kamino_limit.json b/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/kamino_limit.json new file mode 100644 index 00000000..338de1b5 --- /dev/null +++ b/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/kamino_limit.json @@ -0,0 +1,1799 @@ +{ + "accounts": [ + { + "name": "Order", + "type": { + "fields": [ + { + "name": "globalConfig", + "type": "publicKey" + }, + { + "name": "maker", + "type": "publicKey" + }, + { + "name": "inputMint", + "type": "publicKey" + }, + { + "name": "inputMintProgramId", + "type": "publicKey" + }, + { + "name": "outputMint", + "type": "publicKey" + }, + { + "name": "outputMintProgramId", + "type": "publicKey" + }, + { + "docs": [ + "The amount of input token the maker wants to swap" + ], + "name": "initialInputAmount", + "type": "u64" + }, + { + "docs": [ + "The amount of output token the maker wants to receive" + ], + "name": "expectedOutputAmount", + "type": "u64" + }, + { + "docs": [ + "The amount of input token remaining to be swapped" + ], + "name": "remainingInputAmount", + "type": "u64" + }, + { + "docs": [ + "The amount of output token that the maker has received so far" + ], + "name": "filledOutputAmount", + "type": "u64" + }, + { + "docs": [ + "The amount of tips the maker is due to receive for this order -", + "in lamports, stored in the pda_authority account" + ], + "name": "tipAmount", + "type": "u64" + }, + { + "docs": [ + "The number of times the order has been filled" + ], + "name": "numberOfFills", + "type": "u64" + }, + { + "name": "orderType", + "type": "u8" + }, + { + "name": "status", + "type": "u8" + }, + { + "name": "inVaultBump", + "type": "u8" + }, + { + "docs": [ + "This is normally set to 0, but can be set to 1 to indicate that the", + "order is part of a flash operation, in whcih case the order can not be", + "modified until the flash operation is completed." + ], + "name": "flashIxLock", + "type": "u8" + }, + { + "name": "permissionless", + "type": "u8" + }, + { + "name": "padding0", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "lastUpdatedTimestamp", + "type": "u64" + }, + { + "docs": [ + "This is only used for flash operations, and is set to the blanance on the start", + "operation, and than back to 0 on the end operation. It is used to compute the difference", + "between start and end balances in order to compute the amount received from a potential swap" + ], + "name": "flashStartTakerOutputBalance", + "type": "u64" + }, + { + "name": "counterparty", + "type": "publicKey" + }, + { + "name": "padding", + "type": { + "array": [ + "u64", + 15 + ] + } + } + ], + "kind": "struct" + } + }, + { + "name": "UserSwapBalancesState", + "type": { + "fields": [ + { + "name": "userLamports", + "type": "u64" + }, + { + "name": "inputTaBalance", + "type": "u64" + }, + { + "name": "outputTaBalance", + "type": "u64" + } + ], + "kind": "struct" + } + }, + { + "name": "GlobalConfig", + "type": { + "fields": [ + { + "name": "emergencyMode", + "type": "u8" + }, + { + "name": "flashTakeOrderBlocked", + "type": "u8" + }, + { + "name": "newOrdersBlocked", + "type": "u8" + }, + { + "name": "ordersTakingBlocked", + "type": "u8" + }, + { + "name": "hostFeeBps", + "type": "u16" + }, + { + "name": "padding0", + "type": { + "array": [ + "u8", + 2 + ] + } + }, + { + "docs": [ + "The number of seconds after an order has been updated before it can be closed" + ], + "name": "orderCloseDelaySeconds", + "type": "u64" + }, + { + "name": "padding1", + "type": { + "array": [ + "u64", + 9 + ] + } + }, + { + "docs": [ + "The total amount of lamports that were present in the pda_authority last", + "time a program instructions which alters the pda_authority account was", + "executed" + ], + "name": "pdaAuthorityPreviousLamportsBalance", + "type": "u64" + }, + { + "docs": [ + "The total amount of tips that have been paid out - should be at least", + "as much as the total lamports present in the pda_authority account" + ], + "name": "totalTipAmount", + "type": "u64" + }, + { + "docs": [ + "The amount of tips the host is due to receive -", + "in lamports, stored in the pda_authority account" + ], + "name": "hostTipAmount", + "type": "u64" + }, + { + "name": "pdaAuthority", + "type": "publicKey" + }, + { + "name": "pdaAuthorityBump", + "type": "u64" + }, + { + "name": "adminAuthority", + "type": "publicKey" + }, + { + "name": "adminAuthorityCached", + "type": "publicKey" + }, + { + "name": "txnFeeCost", + "type": "u64" + }, + { + "name": "ataCreationCost", + "type": "u64" + }, + { + "name": "padding2", + "type": { + "array": [ + "u64", + 241 + ] + } + } + ], + "kind": "struct" + } + } + ], + "errors": [ + { + "code": 6000, + "msg": "Order can't be canceled", + "name": "OrderCanNotBeCanceled" + }, + { + "code": 6001, + "msg": "Order not active", + "name": "OrderNotActive" + }, + { + "code": 6002, + "msg": "Invalid admin authority", + "name": "InvalidAdminAuthority" + }, + { + "code": 6003, + "msg": "Invalid pda authority", + "name": "InvalidPdaAuthority" + }, + { + "code": 6004, + "msg": "Invalid config option", + "name": "InvalidConfigOption" + }, + { + "code": 6005, + "msg": "Order owner account is not the order owner", + "name": "InvalidOrderOwner" + }, + { + "code": 6006, + "msg": "Out of range integral conversion attempted", + "name": "OutOfRangeIntegralConversion" + }, + { + "code": 6007, + "msg": "Invalid boolean flag, valid values are 0 and 1", + "name": "InvalidFlag" + }, + { + "code": 6008, + "msg": "Mathematical operation with overflow", + "name": "MathOverflow" + }, + { + "code": 6009, + "msg": "Order input amount invalid", + "name": "OrderInputAmountInvalid" + }, + { + "code": 6010, + "msg": "Order output amount invalid", + "name": "OrderOutputAmountInvalid" + }, + { + "code": 6011, + "msg": "Host fee bps must be between 0 and 10000", + "name": "InvalidHostFee" + }, + { + "code": 6012, + "msg": "Conversion between integers failed", + "name": "IntegerOverflow" + }, + { + "code": 6013, + "msg": "Tip balance less than accounted tip", + "name": "InvalidTipBalance" + }, + { + "code": 6014, + "msg": "Tip transfer amount is less than expected", + "name": "InvalidTipTransferAmount" + }, + { + "code": 6015, + "msg": "Host tup amount is less than accounted for", + "name": "InvalidHostTipBalance" + }, + { + "code": 6016, + "msg": "Order within flash operation - all otehr actions are blocked", + "name": "OrderWithinFlashOperation" + }, + { + "code": 6017, + "msg": "CPI not allowed", + "name": "CPINotAllowed" + }, + { + "code": 6018, + "msg": "Flash take_order is blocked", + "name": "FlashTakeOrderBlocked" + }, + { + "code": 6019, + "msg": "Some unexpected instructions are present in the tx. Either before or after the flash ixs, or some ix target the same program between", + "name": "FlashTxWithUnexpectedIxs" + }, + { + "code": 6020, + "msg": "Flash ixs initiated without the closing ix in the transaction", + "name": "FlashIxsNotEnded" + }, + { + "code": 6021, + "msg": "Flash ixs ended without the starting ix in the transaction", + "name": "FlashIxsNotStarted" + }, + { + "code": 6022, + "msg": "Some accounts differ between the two flash ixs", + "name": "FlashIxsAccountMismatch" + }, + { + "code": 6023, + "msg": "Some args differ between the two flash ixs", + "name": "FlashIxsArgsMismatch" + }, + { + "code": 6024, + "msg": "Order is not within flash operation", + "name": "OrderNotWithinFlashOperation" + }, + { + "code": 6025, + "msg": "Emergency mode is enabled", + "name": "EmergencyModeEnabled" + }, + { + "code": 6026, + "msg": "Creating new ordersis blocked", + "name": "CreatingNewOrdersBlocked" + }, + { + "code": 6027, + "msg": "Orders taking is blocked", + "name": "OrderTakingBlocked" + }, + { + "code": 6028, + "msg": "Order input amount larger than the remaining", + "name": "OrderInputAmountTooLarge" + }, + { + "code": 6029, + "msg": "Permissionless order taking not enabled, please provide permission account", + "name": "PermissionRequiredPermissionlessNotEnabled" + }, + { + "code": 6030, + "msg": "Permission address does not match order address", + "name": "PermissionDoesNotMatchOrder" + }, + { + "code": 6031, + "msg": "Invalid ata address", + "name": "InvalidAtaAddress" + }, + { + "code": 6032, + "msg": "Maker output ata required when output mint is not WSOL", + "name": "MakerOutputAtaRequired" + }, + { + "code": 6033, + "msg": "Intermediary output token account required when output mint is WSOL", + "name": "IntermediaryOutputTokenAccountRequired" + }, + { + "code": 6034, + "msg": "Not enough balance for rent", + "name": "NotEnoughBalanceForRent" + }, + { + "code": 6035, + "msg": "Order can not be closed - Not enough time passed since last update", + "name": "NotEnoughTimePassedSinceLastUpdate" + }, + { + "code": 6036, + "msg": "Order input and output mints are the same", + "name": "OrderSameMint" + }, + { + "code": 6037, + "msg": "Mint has a token (2022) extension that is not supported", + "name": "UnsupportedTokenExtension" + }, + { + "code": 6038, + "msg": "Can't have an spl token mint with a t22 account", + "name": "InvalidTokenAccount" + }, + { + "code": 6039, + "msg": "The order type is invalid", + "name": "OrderTypeInvalid" + }, + { + "code": 6040, + "msg": "Token account is not initialized", + "name": "UninitializedTokenAccount" + }, + { + "code": 6041, + "msg": "Account is not owned by the token program", + "name": "InvalidTokenAccountOwner" + }, + { + "code": 6042, + "msg": "Account is not a valid token account", + "name": "InvalidAccount" + }, + { + "code": 6043, + "msg": "Token account has incorrect mint", + "name": "InvalidTokenMint" + }, + { + "code": 6044, + "msg": "Token account has incorrect authority", + "name": "InvalidTokenAuthority" + }, + { + "code": 6045, + "msg": "The provided parameter type is invalid", + "name": "InvalidParameterType" + }, + { + "code": 6046, + "msg": "The counterparty is not the taker", + "name": "CounterpartyDisallowed" + }, + { + "code": 6047, + "msg": "The swap input amount is larger than the maximum allowed", + "name": "SwapInputAmountTooLarge" + }, + { + "code": 6048, + "msg": "The swap output amount is smaller than the minimum allowed", + "name": "SwapOutputAmountTooSmall" + }, + { + "code": 6049, + "msg": "The swap input balance change is positive, expected negative", + "name": "SwapInputInvalidBalanceChange" + }, + { + "code": 6050, + "msg": "The swap output balance change is negative, expected positive", + "name": "SwapOutputInvalidBalanceChange" + } + ], + "events": [ + { + "fields": [ + { + "index": false, + "name": "initialInputAmount", + "type": "u64" + }, + { + "index": false, + "name": "expectedOutputAmount", + "type": "u64" + }, + { + "index": false, + "name": "remainingInputAmount", + "type": "u64" + }, + { + "index": false, + "name": "filledOutputAmount", + "type": "u64" + }, + { + "index": false, + "name": "tipAmount", + "type": "u64" + }, + { + "index": false, + "name": "numberOfFills", + "type": "u64" + }, + { + "index": false, + "name": "onEventOutputAmountFilled", + "type": "u64" + }, + { + "index": false, + "name": "onEventTipAmount", + "type": "u64" + }, + { + "index": false, + "name": "orderType", + "type": "u8" + }, + { + "index": false, + "name": "status", + "type": "u8" + }, + { + "index": false, + "name": "lastUpdatedTimestamp", + "type": "u64" + } + ], + "name": "OrderDisplay" + }, + { + "fields": [ + { + "index": false, + "name": "userLamportsBefore", + "type": "u64" + }, + { + "index": false, + "name": "inputTaBalanceBefore", + "type": "u64" + }, + { + "index": false, + "name": "outputTaBalanceBefore", + "type": "u64" + }, + { + "index": false, + "name": "userLamportsAfter", + "type": "u64" + }, + { + "index": false, + "name": "inputTaBalanceAfter", + "type": "u64" + }, + { + "index": false, + "name": "outputTaBalanceAfter", + "type": "u64" + }, + { + "index": false, + "name": "swapProgram", + "type": "publicKey" + }, + { + "index": false, + "name": "simulatedSwapAmountOut", + "type": "u64" + }, + { + "index": false, + "name": "simulatedTs", + "type": "u64" + }, + { + "index": false, + "name": "minimumAmountOut", + "type": "u64" + }, + { + "index": false, + "name": "swapAmountIn", + "type": "u64" + }, + { + "index": false, + "name": "simulatedAmountOutNextBest", + "type": "u64" + }, + { + "index": false, + "name": "aggregator", + "type": "u8" + }, + { + "index": false, + "name": "nextBestAggregator", + "type": "u8" + } + ], + "name": "UserSwapBalanceDiffs" + } + ], + "instructions": [ + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "adminAuthority" + }, + { + "isMut": true, + "isSigner": false, + "name": "pdaAuthority" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + } + ], + "args": [], + "name": "initializeGlobalConfig" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "payer" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + }, + { + "isMut": false, + "isSigner": false, + "name": "pdaAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "mint" + }, + { + "isMut": true, + "isSigner": false, + "name": "vault" + }, + { + "isMut": false, + "isSigner": false, + "name": "tokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + } + ], + "args": [], + "name": "initializeVault" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "maker" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + }, + { + "isMut": false, + "isSigner": false, + "name": "pdaAuthority" + }, + { + "isMut": true, + "isSigner": false, + "name": "order" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputMint" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputMint" + }, + { + "isMut": true, + "isSigner": false, + "name": "makerAta" + }, + { + "isMut": true, + "isSigner": false, + "name": "inputVault" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "eventAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "program" + } + ], + "args": [ + { + "name": "inputAmount", + "type": "u64" + }, + { + "name": "outputAmount", + "type": "u64" + }, + { + "name": "orderType", + "type": "u8" + } + ], + "name": "createOrder" + }, + { + "accounts": [ + { + "isMut": false, + "isSigner": true, + "name": "maker" + }, + { + "isMut": false, + "isSigner": false, + "name": "globalConfig" + }, + { + "isMut": true, + "isSigner": false, + "name": "order" + } + ], + "args": [ + { + "name": "mode", + "type": "u16" + }, + { + "name": "value", + "type": "bytes" + } + ], + "name": "updateOrder" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "maker" + }, + { + "isMut": true, + "isSigner": false, + "name": "order" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + }, + { + "isMut": true, + "isSigner": false, + "name": "pdaAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputMint" + }, + { + "docs": [ + "- required only for indexing the order state from the instruction" + ], + "isMut": false, + "isSigner": false, + "name": "outputMint" + }, + { + "isMut": true, + "isSigner": false, + "name": "makerInputAta" + }, + { + "isMut": true, + "isSigner": false, + "name": "inputVault" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "eventAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "program" + } + ], + "args": [], + "name": "closeOrderAndClaimTip" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "taker" + }, + { + "isMut": true, + "isSigner": false, + "name": "maker" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + }, + { + "isMut": true, + "isSigner": false, + "name": "pdaAuthority" + }, + { + "isMut": true, + "isSigner": false, + "name": "order" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputMint" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputMint" + }, + { + "isMut": true, + "isSigner": false, + "name": "inputVault" + }, + { + "isMut": true, + "isSigner": false, + "name": "takerInputAta" + }, + { + "isMut": true, + "isSigner": false, + "name": "takerOutputAta" + }, + { + "isMut": true, + "isOptional": true, + "isSigner": false, + "name": "intermediaryOutputTokenAccount" + }, + { + "isMut": true, + "isOptional": true, + "isSigner": false, + "name": "makerOutputAta" + }, + { + "isMut": false, + "isSigner": false, + "name": "expressRelay" + }, + { + "isMut": false, + "isSigner": false, + "name": "expressRelayMetadata" + }, + { + "isMut": false, + "isSigner": false, + "name": "sysvarInstructions" + }, + { + "isMut": false, + "isOptional": true, + "isSigner": false, + "name": "permission" + }, + { + "isMut": false, + "isSigner": false, + "name": "configRouter" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "rent" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "eventAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "program" + } + ], + "args": [ + { + "name": "inputAmount", + "type": "u64" + }, + { + "name": "minOutputAmount", + "type": "u64" + }, + { + "name": "tipAmountPermissionlessTaking", + "type": "u64" + } + ], + "name": "takeOrder" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "taker" + }, + { + "isMut": true, + "isSigner": false, + "name": "maker" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + }, + { + "isMut": true, + "isSigner": false, + "name": "pdaAuthority" + }, + { + "isMut": true, + "isSigner": false, + "name": "order" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputMint" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputMint" + }, + { + "isMut": true, + "isSigner": false, + "name": "inputVault" + }, + { + "isMut": true, + "isSigner": false, + "name": "takerInputAta" + }, + { + "isMut": true, + "isSigner": false, + "name": "takerOutputAta" + }, + { + "isMut": true, + "isOptional": true, + "isSigner": false, + "name": "intermediaryOutputTokenAccount" + }, + { + "isMut": true, + "isOptional": true, + "isSigner": false, + "name": "makerOutputAta" + }, + { + "isMut": false, + "isSigner": false, + "name": "expressRelay" + }, + { + "isMut": false, + "isSigner": false, + "name": "expressRelayMetadata" + }, + { + "isMut": false, + "isSigner": false, + "name": "sysvarInstructions" + }, + { + "isMut": false, + "isOptional": true, + "isSigner": false, + "name": "permission" + }, + { + "isMut": false, + "isSigner": false, + "name": "configRouter" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "rent" + }, + { + "isMut": false, + "isSigner": false, + "name": "eventAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "program" + } + ], + "args": [ + { + "name": "inputAmount", + "type": "u64" + }, + { + "name": "minOutputAmount", + "type": "u64" + }, + { + "name": "tipAmountPermissionlessTaking", + "type": "u64" + } + ], + "name": "flashTakeOrderStart" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "taker" + }, + { + "isMut": true, + "isSigner": false, + "name": "maker" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + }, + { + "isMut": true, + "isSigner": false, + "name": "pdaAuthority" + }, + { + "isMut": true, + "isSigner": false, + "name": "order" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputMint" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputMint" + }, + { + "isMut": true, + "isSigner": false, + "name": "inputVault" + }, + { + "isMut": true, + "isSigner": false, + "name": "takerInputAta" + }, + { + "isMut": true, + "isSigner": false, + "name": "takerOutputAta" + }, + { + "isMut": true, + "isOptional": true, + "isSigner": false, + "name": "intermediaryOutputTokenAccount" + }, + { + "isMut": true, + "isOptional": true, + "isSigner": false, + "name": "makerOutputAta" + }, + { + "isMut": false, + "isSigner": false, + "name": "expressRelay" + }, + { + "isMut": false, + "isSigner": false, + "name": "expressRelayMetadata" + }, + { + "isMut": false, + "isSigner": false, + "name": "sysvarInstructions" + }, + { + "isMut": false, + "isOptional": true, + "isSigner": false, + "name": "permission" + }, + { + "isMut": false, + "isSigner": false, + "name": "configRouter" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputTokenProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "rent" + }, + { + "isMut": false, + "isSigner": false, + "name": "eventAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "program" + } + ], + "args": [ + { + "name": "inputAmount", + "type": "u64" + }, + { + "name": "minOutputAmount", + "type": "u64" + }, + { + "name": "tipAmountPermissionlessTaking", + "type": "u64" + } + ], + "name": "flashTakeOrderEnd" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "adminAuthority" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + } + ], + "args": [ + { + "name": "mode", + "type": "u16" + }, + { + "name": "value", + "type": { + "array": [ + "u8", + 128 + ] + } + } + ], + "name": "updateGlobalConfig" + }, + { + "accounts": [ + { + "isMut": false, + "isSigner": true, + "name": "adminAuthorityCached" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + } + ], + "args": [], + "name": "updateGlobalConfigAdmin" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "adminAuthority" + }, + { + "isMut": true, + "isSigner": false, + "name": "globalConfig" + }, + { + "isMut": true, + "isSigner": false, + "name": "pdaAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + } + ], + "args": [], + "name": "withdrawHostTip" + }, + { + "accounts": [ + { + "accounts": [ + { + "isMut": false, + "isSigner": true, + "name": "maker" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputMint" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputMint" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTa" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputTa" + }, + { + "docs": [ + "if it's not the pda it doesn't matter" + ], + "isMut": false, + "isOptional": true, + "isSigner": false, + "name": "pdaReferrer" + }, + { + "isMut": false, + "isSigner": false, + "name": "swapProgramId" + } + ], + "name": "baseAccounts" + }, + { + "isMut": true, + "isSigner": false, + "name": "userSwapBalanceState" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "rent" + }, + { + "isMut": false, + "isSigner": false, + "name": "sysvarInstructions" + }, + { + "isMut": false, + "isSigner": false, + "name": "eventAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "program" + } + ], + "args": [], + "name": "logUserSwapBalancesStart" + }, + { + "accounts": [ + { + "accounts": [ + { + "isMut": false, + "isSigner": true, + "name": "maker" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputMint" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputMint" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTa" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputTa" + }, + { + "docs": [ + "if it's not the pda it doesn't matter" + ], + "isMut": false, + "isOptional": true, + "isSigner": false, + "name": "pdaReferrer" + }, + { + "isMut": false, + "isSigner": false, + "name": "swapProgramId" + } + ], + "name": "baseAccounts" + }, + { + "isMut": true, + "isSigner": false, + "name": "userSwapBalanceState" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "rent" + }, + { + "isMut": false, + "isSigner": false, + "name": "sysvarInstructions" + }, + { + "isMut": false, + "isSigner": false, + "name": "eventAuthority" + }, + { + "isMut": false, + "isSigner": false, + "name": "program" + } + ], + "args": [ + { + "name": "simulatedSwapAmountOut", + "type": "u64" + }, + { + "name": "simulatedTs", + "type": "u64" + }, + { + "name": "minimumAmountOut", + "type": "u64" + }, + { + "name": "swapAmountIn", + "type": "u64" + }, + { + "name": "simulatedAmountOutNextBest", + "type": "u64" + }, + { + "name": "aggregator", + "type": "u8" + }, + { + "name": "nextBestAggregator", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 2 + ] + } + } + ], + "name": "logUserSwapBalancesEnd" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "maker" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTa" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputTa" + }, + { + "isMut": true, + "isSigner": false, + "name": "userSwapBalanceState" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "rent" + }, + { + "isMut": false, + "isSigner": false, + "name": "sysvarInstructions" + } + ], + "args": [], + "name": "assertUserSwapBalancesStart" + }, + { + "accounts": [ + { + "isMut": true, + "isSigner": true, + "name": "maker" + }, + { + "isMut": false, + "isSigner": false, + "name": "inputTa" + }, + { + "isMut": false, + "isSigner": false, + "name": "outputTa" + }, + { + "isMut": true, + "isSigner": false, + "name": "userSwapBalanceState" + }, + { + "isMut": false, + "isSigner": false, + "name": "systemProgram" + }, + { + "isMut": false, + "isSigner": false, + "name": "rent" + }, + { + "isMut": false, + "isSigner": false, + "name": "sysvarInstructions" + } + ], + "args": [ + { + "name": "maxInputAmountChange", + "type": "u64" + }, + { + "name": "minOutputAmountChange", + "type": "u64" + } + ], + "name": "assertUserSwapBalancesEnd" + } + ], + "name": "limo", + "types": [ + { + "name": "OrderStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Active" + }, + { + "name": "Filled" + }, + { + "name": "Cancelled" + } + ] + } + }, + { + "name": "OrderType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Vanilla" + } + ] + } + }, + { + "name": "UpdateGlobalConfigMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateEmergencyMode" + }, + { + "name": "UpdateFlashTakeOrderBlocked" + }, + { + "name": "UpdateBlockNewOrders" + }, + { + "name": "UpdateBlockOrderTaking" + }, + { + "name": "UpdateHostFeeBps" + }, + { + "name": "UpdateAdminAuthorityCached" + }, + { + "name": "UpdateOrderTakingPermissionless" + }, + { + "name": "UpdateOrderCloseDelaySeconds" + }, + { + "name": "UpdateTxnFeeCost" + }, + { + "name": "UpdateAtaCreationCost" + } + ] + } + }, + { + "name": "UpdateGlobalConfigValue", + "type": { + "kind": "enum", + "variants": [ + { + "fields": [ + "bool" + ], + "name": "Bool" + }, + { + "fields": [ + "u16" + ], + "name": "U16" + }, + { + "fields": [ + "u64" + ], + "name": "U64" + }, + { + "fields": [ + "publicKey" + ], + "name": "Pubkey" + } + ] + } + }, + { + "name": "UpdateOrderMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdatePermissionless" + }, + { + "name": "UpdateCounterparty" + } + ] + } + } + ], + "version": "0.1.0" +} diff --git a/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/mod.rs b/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/mod.rs new file mode 100644 index 00000000..6b37c46c --- /dev/null +++ b/src/chain_parsers/visualsign-solana/src/presets/kamino_limit/mod.rs @@ -0,0 +1,285 @@ +//! Kamino Limit Orders preset implementation for Solana + +mod config; + +use crate::core::{ + InstructionVisualizer, SolanaIntegrationConfig, VisualizerContext, VisualizerKind, +}; +use config::KaminoLimitConfig; +use solana_parser::{ + Idl, SolanaParsedInstructionData, decode_idl_data, parse_instruction_with_idl, +}; +use std::collections::BTreeMap; +use visualsign::errors::VisualSignError; +use visualsign::field_builders::{create_raw_data_field, create_text_field}; +use visualsign::{ + AnnotatedPayloadField, SignablePayloadField, SignablePayloadFieldCommon, + SignablePayloadFieldListLayout, SignablePayloadFieldPreviewLayout, SignablePayloadFieldTextV2, +}; + +pub(crate) const KAMINO_LIMIT_PROGRAM_ID: &str = "LiMoM9rMhrdYrfzUCxQppvxCSG1FcrUK9G8uLq4A1GF"; + +const KAMINO_LIMIT_DISPLAY_NAME: &str = "Kamino Limit Orders"; + +const KAMINO_LIMIT_IDL_JSON: &str = include_str!("kamino_limit.json"); + +static KAMINO_LIMIT_CONFIG: KaminoLimitConfig = KaminoLimitConfig; + +pub struct KaminoLimitVisualizer; + +impl InstructionVisualizer for KaminoLimitVisualizer { + fn visualize_tx_commands( + &self, + context: &VisualizerContext, + ) -> Result { + let instruction = context + .current_instruction() + .ok_or_else(|| VisualSignError::MissingData("No instruction found".into()))?; + + let parsed_result = parse_kamino_limit_instruction(&instruction.data); + let program_id_str = instruction.program_id.to_string(); + + let (condensed_fields, expanded_fields, title_text) = match &parsed_result { + Ok(parsed) => { + let named_accounts = match load_kamino_limit_idl() { + Some(idl) => { + build_named_accounts(idl, &instruction.data, &instruction.accounts) + } + None => BTreeMap::new(), + }; + ( + build_condensed_fields(&parsed.instruction_name)?, + build_parsed_fields( + &program_id_str, + parsed, + &named_accounts, + &instruction.data, + )?, + format!("{KAMINO_LIMIT_DISPLAY_NAME}: {}", parsed.instruction_name), + ) + } + Err(_) => ( + build_fallback_condensed_fields()?, + build_fallback_fields(&program_id_str, &instruction.data)?, + format!("{KAMINO_LIMIT_DISPLAY_NAME}: Unknown Instruction"), + ), + }; + + let preview_layout = SignablePayloadFieldPreviewLayout { + title: Some(SignablePayloadFieldTextV2 { text: title_text }), + subtitle: Some(SignablePayloadFieldTextV2 { + text: String::new(), + }), + condensed: Some(SignablePayloadFieldListLayout { + fields: condensed_fields, + }), + expanded: Some(SignablePayloadFieldListLayout { + fields: expanded_fields, + }), + }; + + let fallback_text = format!( + "Program ID: {program_id_str}\nData: {}", + hex::encode(&instruction.data) + ); + + Ok(AnnotatedPayloadField { + static_annotation: None, + dynamic_annotation: None, + signable_payload_field: SignablePayloadField::PreviewLayout { + common: SignablePayloadFieldCommon { + label: format!("Instruction {}", context.instruction_index() + 1), + fallback_text, + }, + preview_layout, + }, + }) + } + + fn get_config(&self) -> Option<&dyn SolanaIntegrationConfig> { + Some(&KAMINO_LIMIT_CONFIG) + } + + fn kind(&self) -> VisualizerKind { + VisualizerKind::Dex(KAMINO_LIMIT_DISPLAY_NAME) + } +} + +/// Load and cache the bundled Kamino Limit Orders IDL. +fn load_kamino_limit_idl() -> Option<&'static Idl> { + static IDL: std::sync::OnceLock> = std::sync::OnceLock::new(); + IDL.get_or_init(|| decode_idl_data(KAMINO_LIMIT_IDL_JSON).ok()) + .as_ref() +} + +/// Parse an instruction's data using the bundled IDL. +fn parse_kamino_limit_instruction( + data: &[u8], +) -> Result { + if data.len() < 8 { + return Err(VisualSignError::DecodeError( + "instruction data shorter than 8-byte discriminator".into(), + )); + } + + let idl = load_kamino_limit_idl().ok_or_else(|| { + VisualSignError::DecodeError("failed to load Kamino Limit Orders IDL".into()) + })?; + + parse_instruction_with_idl(data, KAMINO_LIMIT_PROGRAM_ID, idl) + .map_err(|e| VisualSignError::DecodeError(e.to_string())) +} + +/// Build a map of IDL account name to pubkey by zipping instruction accounts with IDL accounts. +fn build_named_accounts( + idl: &Idl, + instruction_data: &[u8], + instruction_accounts: &[solana_sdk::instruction::AccountMeta], +) -> BTreeMap { + let mut named = BTreeMap::new(); + + let Some(idl_instruction) = idl.instructions.iter().find(|inst| { + inst.discriminator + .as_ref() + .map(|d| instruction_data.len() >= 8 && &instruction_data[0..8] == d.as_slice()) + .unwrap_or(false) + }) else { + return named; + }; + + for (idx, account_meta) in instruction_accounts.iter().enumerate() { + if let Some(idl_account) = idl_instruction.accounts.get(idx) { + named.insert(idl_account.name.clone(), account_meta.pubkey.to_string()); + } + } + + named +} + +/// Build the expanded fields shown when the IDL parsed successfully. +fn build_parsed_fields( + program_id: &str, + parsed: &SolanaParsedInstructionData, + named_accounts: &BTreeMap, + data: &[u8], +) -> Result, VisualSignError> { + let mut fields = vec![ + create_text_field("Program ID", program_id)?, + create_text_field("Instruction", &parsed.instruction_name)?, + create_text_field("Discriminator", &parsed.discriminator)?, + ]; + + let mut account_entries: Vec<(&String, &String)> = named_accounts.iter().collect(); + account_entries.sort_by(|a, b| a.0.cmp(b.0)); + for (name, address) in account_entries { + fields.push(create_text_field(name, address)?); + } + + let mut arg_entries: Vec<(&String, &serde_json::Value)> = + parsed.program_call_args.iter().collect(); + arg_entries.sort_by(|a, b| a.0.cmp(b.0)); + for (name, value) in arg_entries { + fields.push(create_text_field(name, &format_arg_value(value))?); + } + + append_raw_data(&mut fields, data)?; + + Ok(fields) +} + +/// Build the expanded fields shown when IDL parsing failed. +fn build_fallback_fields( + program_id: &str, + data: &[u8], +) -> Result, VisualSignError> { + let mut fields = vec![ + create_text_field("Program ID", program_id)?, + create_text_field("Status", "IDL parsing failed - showing raw data")?, + ]; + append_raw_data(&mut fields, data)?; + Ok(fields) +} + +/// Build the condensed fields when parsing succeeded. +fn build_condensed_fields( + instruction_name: &str, +) -> Result, VisualSignError> { + Ok(vec![ + create_text_field("Program", KAMINO_LIMIT_DISPLAY_NAME)?, + create_text_field("Instruction", instruction_name)?, + ]) +} + +/// Build the condensed fields when parsing failed. +fn build_fallback_condensed_fields() -> Result, VisualSignError> { + Ok(vec![create_text_field( + "Program", + KAMINO_LIMIT_DISPLAY_NAME, + )?]) +} + +fn append_raw_data( + fields: &mut Vec, + data: &[u8], +) -> Result<(), VisualSignError> { + fields.push(create_raw_data_field(data, Some(hex::encode(data)))?); + Ok(()) +} + +fn format_arg_value(value: &serde_json::Value) -> String { + match value { + serde_json::Value::String(s) => s.clone(), + other => other.to_string(), + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +mod tests { + use super::*; + + #[test] + fn test_kamino_limit_idl_loads() { + let idl = load_kamino_limit_idl().expect("IDL should load"); + assert!( + !idl.instructions.is_empty(), + "IDL should declare at least one instruction" + ); + } + + #[test] + fn test_kamino_limit_idl_has_discriminators() { + let idl = load_kamino_limit_idl().expect("IDL should load"); + for instruction in &idl.instructions { + let discriminator = instruction.discriminator.as_ref().unwrap_or_else(|| { + panic!("instruction '{}' missing discriminator", instruction.name) + }); + assert_eq!( + discriminator.len(), + 8, + "instruction '{}' has non-8-byte discriminator", + instruction.name + ); + } + } + + #[test] + fn test_unknown_discriminator_returns_error() { + let garbage = [0xFFu8; 9]; + let result = parse_kamino_limit_instruction(&garbage); + assert!( + result.is_err(), + "garbage discriminator should not match any instruction" + ); + } + + #[test] + fn test_short_data_returns_error() { + let too_short = [0x01u8, 0x02, 0x03]; + let result = parse_kamino_limit_instruction(&too_short); + assert!( + result.is_err(), + "data shorter than discriminator should return error" + ); + } +} diff --git a/src/chain_parsers/visualsign-solana/src/presets/mod.rs b/src/chain_parsers/visualsign-solana/src/presets/mod.rs index 83f98299..ddfa9ddc 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/mod.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/mod.rs @@ -7,6 +7,7 @@ pub mod jupiter_perps; pub mod jupiter_swap; pub mod kamino_borrow; pub mod kamino_farms; +pub mod kamino_limit; pub mod kamino_vault; pub mod metadao_conditional_vault; pub mod metadao_futarchy;