@anywalletkit/protocol-lending-kamino-solana
Kamino for @anywalletkit/wallet-solana accounts, two
LendingProtocols in one package:
Install
npm install @anywalletkit/protocol-lending-kamino-solana @anywalletkit/core
# or: pnpm add @anywalletkit/protocol-lending-kamino-solana @anywalletkit/core · yarn add @anywalletkit/protocol-lending-kamino-solana @anywalletkit/core
On npm: @anywalletkit/protocol-lending-kamino-solana, published from the release workflow with provenance.
KaminoVaultProtocolSolana— one Kamino Earn vault (kVault):supplydeposits the vault's asset for shares (kept in the wallet, where the guard proves they arrive;skipFarm: falsestakes them in the vault farm as the Kamino app does — the guard then pins the stake to this vault's farm and the wallet's farm state, but cannot count the shares),withdrawredeems shares for the asset. The simplest Earn product;borrow/repayreject.KaminoMarketProtocolSolana— one Kamino Lend market (the Main Market by default):supply/withdrawpost and take collateral on the wallet's obligation,borrow/repaytake and return a reserve's liquidity against it. Tokens are named by mint.
How a transaction is trusted
Kamino's official API builds every transaction (POST /ktx/… — the same
builder the Kamino app and SDK use: farms, lookup tables, obligation setup).
The module never signs it as-is. The wallet's serialized-transaction guard
(wallet-solana's checkSerialized / sendSerialized) proves it first:
- every program it invokes is on the Kamino allowlist — Kamino Lend, kVault, Farms, the address-lookup-table program and the token / system / compute budget / memo programs, nothing else;
- every instruction is one the operation takes (
marketRules/vaultRules), with the accounts that decide where value goes pinned: the wallet, its vanilla obligation, the configured market or vault, the reserve and mint the call names, the wallet's own token accounts, this vault's farm. A deposit carries refreshes, set-up and the deposit; a withdrawal inside it, a borrow inside a repay, a token transfer, aninvestor a reward harvest is refused (unexpected-instruction); - the compiled-transaction checks pass (this wallet is the fee payer, no foreign co-signers, no authority or delegate changes);
- a simulation with post-state accounts shows exactly the balance changes
the module declared: on a deposit the amount out and at most rent in SOL
on top, on a withdrawal / borrow the asset in at the minimum
(
slippageBps, default 0.5 %) — a borrow at most what was asked — no other token account of ours touched.
Only after all four does the account sign. A first Lend deposit also pays the
rent of the obligation, user metadata and a per-user lookup table
(firstDepositAllowanceLamports, default 0.045 SOL); later ones only the
usual rentAllowanceLamports (0.01 SOL). Whether it is a first deposit is
read from the chain (does the vanilla obligation's account exist).
The bounds come from the chain, read through the account
(getAccountInfos, @anywalletkit/wallet-solana 0.4+): a vault's share
price from its state and the reserves it invests in, the wallet's shares
from its shares account and farm stake, a Lend 'max' from the obligation
(debt with the reserve's accrued interest), and the reserve the API names
must be this market's and lend that token. An API figure that disagrees —
a tokensPerShare more than 1 % off, a reported deposit or debt that is not
the obligation's (the API's per-token legs, or the cTokens and scaled debt in
its raw obligation state, valued at the reserves' on-chain rates), any
negative figure — is refused, never used. A Lend market's getPosition reads
the wallet's obligations from the chain too: the API only names them (each
must be the wallet's on this market; the vanilla one is read even when the
API leaves it out), and every leg is what it redeems for, or owes, at its
reserve's on-chain rate. Other reads (getVault, a vault's getPosition,
APY, TVL, LTVs, USD values) stay the API's; mint decimals are read when the
account has an RPC, else taken from a table of Kamino's listed mints.
Usage
import WalletManagerSolana from '@anywalletkit/wallet-solana'
import { KaminoApi, KaminoVaultProtocolSolana, KaminoMarketProtocolSolana } from '@anywalletkit/protocol-lending-kamino-solana'
const account = await new WalletManagerSolana(seed, { provider: 'https://…' }).getAccount(0)
const USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
// Discovery: the USDC Earn vaults, biggest first
const api = new KaminoApi()
const vaults = (await api.listVaults()).filter((v) => v.tokenMint === USDC).sort((a, b) => Number(b.sharesIssued - a.sharesIssued))
// Earn vault
// `asset`: the API's answer must name it, and deposits need it (or a `token` per call). The vault's
// address, program and shares mint (the PDA ["shares", vault]) are checked too, before anything is built.
const vault = new KaminoVaultProtocolSolana(account, { vault: vaults[0].address, asset: USDC })
await vault.getVault() // asset, shares, APY (net, %), fees, TVL, minimums, allocations
await vault.quoteSupply({ amount: 5_000_000n }) // { fee, expectedShares } — built, proven, not signed
await vault.supply({ amount: 5_000_000n }) // { hash, fee }
await vault.getPosition() // { shares, stakedShares, unstakedShares, assets }
await vault.withdraw({ amount: 'max' }) // every share → asset, min = assets × (1 − slippage)
// Lend market
const market = new KaminoMarketProtocolSolana(account) // Main Market
await market.getReserves() // symbol, mint, supply / borrow APY (%), max LTV, TVL, utilization
await market.supply({ token: USDC, amount: 5_000_000n }) // collateral on the wallet's obligation
await market.borrow({ token: USDC, amount: 1_000_000n })
await market.getPosition() // { obligations, supplied: {mint: units}, borrowed } — from the chain
await market.repay({ token: USDC, amount: 'max' })
await market.withdraw({ token: USDC, amount: 'max' })
A mint with several reserves in a market (the Main Market has four USDC
reserves) resolves to the deepest one by supplied value; pin another with
reserves: { [mint]: reserveAddress }.
Config
| option | default | |
|---|---|---|
api | https://api.kamino.finance | endpoint or a KaminoApi |
slippageBps | 50 | floor on what the wallet must receive (withdraw, borrow); ceiling on a repay |
rentAllowanceLamports | 0.01 SOL | SOL a transaction may spend beyond the amount |
memo | — | SPL memo the API adds |
vault: vault, asset, skipFarm | — / — / true | the kVault; the asset it must take; keep shares in the wallet (false: stake them in this vault's farm; the guard then proves the spend and pins the stake to the wallet's farm state) |
market: market, reserves, firstDepositAllowanceLamports | Main Market / — / 0.045 SOL |
Verify
pnpm test (fixtures copied from live API responses). pnpm verify:live [address] [rpcUrl] [lendAddress] reads markets, reserves and vaults from the
API, then builds a vault deposit and a Lend deposit for the address, and a Lend
withdraw for a wallet with USDC supplied on the main market, and runs each
through the guard as a read-only account (no keys, no writes); that wallet's
getPosition must match its obligation on chain. A holder whose USDC account
is frozen is named and refused before anything is built; one without USDC
skips the deposits and still runs the withdraw.
Program ids: Kamino Lend KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD, kVault
KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd, Farms
FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr. instructionName() names the
Anchor instructions the API's transactions carry, for review screens;
decodeVault / decodeReserve / decodeObligation / decodeFarmUser read
the accounts the bounds come from, and vaultTokensPerShareWad / debtNow
compute from them what the module does.