Complete Flow
Implement the full deposit flow either in an off-chain TypeScript client or via on-chain Rust CPI within your Anchor program.- TypeScript
- Rust
- Rust (CPI)
Import Dependencies
Import the required packages for Solana RPC communication, Kamino SDK operations, and Kit transaction building.import {
createSolanaRpc,
createSolanaRpcSubscriptions,
address,
pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory,
getSignatureFromTransaction,
} from '@solana/kit';
import { KaminoVault } from '@kamino-finance/klend-sdk';
import { parseKeypairFile } from '@kamino-finance/klend-sdk/dist/utils/signer.js';
import { Decimal } from 'decimal.js';
@solana/kit provides modern utilities for RPC, transaction building, and signing. @kamino-finance/klend-sdk contains vault operation methods.Load Keypair and Initialize Vault
Load the keypair from file, initialize RPC connections, and create the vault instance.const KEYPAIR_FILE = '/path/to/your/keypair.json';
const signer = await parseKeypairFile(KEYPAIR_FILE);
const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.mainnet-beta.solana.com');
const vault = new KaminoVault(
rpc,
address('HDsayqAsDWy3QvANGqh2yNraqcD8Fnjgh73Mhb3WRS5E') // USDC vault
);
parseKeypairFile loads an existing keypair from a JSON file.Build Deposit Instructions
Generate deposit instructions for the specified amount.const depositAmount = new Decimal(1.0);
const bundle = await vault.depositIxs(signer, depositAmount);
const instructions = [...(bundle.depositIxs || [])];
if (!instructions.length) {
throw new Error('No instructions returned by Kamino SDK');
}
The
depositIxs method returns the necessary instructions to deposit assets into the vault.Build and Send Transaction
Fetch the latest blockhash and construct the transaction message.const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(signer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx)
);
Kit’s
pipe function enables functional composition of transaction building steps for cleaner, more maintainable code.const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const signature = getSignatureFromTransaction(signedTransaction);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
commitment: 'confirmed',
skipPreflight: true,
});
console.log('Deposit successful! Signature:', signature);
The deposit is complete. Your assets are now deposited in the vault and earning yield.
A standalone off-chain Rust client that deposits into a Kamino Vault using the
kvault-interface crate. The crate’s helpers build the instruction for you — deriving every PDA and the per-reserve remaining_accounts from the on-chain vault state — so you never hand-assemble account lists or discriminators.Add Dependencies
[dependencies]
kvault-interface = "0.1.0"
solana-client = "2.3"
solana-sdk = "2.3"
solana-pubkey = "2.1"
spl-associated-token-account = "6"
kvault-interface re-exports ReserveInfo, VaultInfo, Fraction, and the program IDs, so you don’t need to depend on klend-interface directly for client code.Set Up RPC Client and Wallet
Load the Solana CLI wallet from~/.config/solana/id.json and create an RPC client.use std::str::FromStr;
use kvault_interface::{
from_account_data, helpers, pda, state::VaultState, ReserveInfo, VaultInfo, KVAULT_PROGRAM_ID,
};
use solana_client::rpc_client::RpcClient;
use solana_pubkey::Pubkey;
use solana_sdk::signer::{keypair::read_keypair_file, Signer};
use spl_associated_token_account::get_associated_token_address;
let rpc = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
let home = std::env::var("HOME").unwrap();
let signer = read_keypair_file(format!("{home}/.config/solana/id.json"))
.expect("Failed to load wallet from ~/.config/solana/id.json");
let owner = signer.pubkey();
The wallet needs at least ~0.01 SOL (transaction fees + ATA rent) and enough USDC to cover the deposit amount configured below.
Fetch Vault and Reserve Data
Load the vault account, then fetch every reserve the vault actively allocates to. The on-chain refresh that runs insidedeposit needs each reserve and its lending market, so VaultInfo collects them up front.// Steakhouse USDC vault
let vault_pubkey = Pubkey::from_str("HDsayqAsDWy3QvANGqh2yNraqcD8Fnjgh73Mhb3WRS5E").unwrap();
let vault_data = rpc.get_account(&vault_pubkey).unwrap();
let vault_state = from_account_data::<VaultState>(&vault_data.data).unwrap();
let mut reserve_infos = Vec::new();
for reserve_pubkey in VaultInfo::active_reserve_addresses(vault_state) {
let reserve_data = rpc.get_account(&reserve_pubkey).unwrap();
reserve_infos.push(ReserveInfo::from_account_data(reserve_pubkey, &reserve_data.data).unwrap());
}
let vault = VaultInfo::from_account_data(vault_pubkey, &vault_data.data, &reserve_infos).unwrap();
Derive User Token Accounts
Compute the user’s ATA for the underlying token (USDC) and for the vault shares mint. The shares mint is a PDA of the vault state, derived withpda::shares_mint.let user_token_ata = get_associated_token_address(&owner, &vault.token_mint);
let (shares_mint, _bump) = pda::shares_mint(&KVAULT_PROGRAM_ID, &vault_pubkey);
let user_shares_ata = get_associated_token_address(&owner, &shares_mint);
Create these ATAs with
spl_associated_token_account::instruction::create_associated_token_account_idempotent if they may not exist yet, and prepend those instructions to the transaction below.Build the Deposit Instruction
helpers::deposit::deposit returns a single instruction with the PDAs and per-reserve remaining_accounts already derived from VaultInfo.let ix = helpers::deposit::deposit(
&vault,
owner,
user_token_ata, // source: user's token account
user_shares_ata, // destination: user's shares account
1_000_000, // 1 USDC (6 decimals)
);
Send the Deposit Transaction
let message = solana_sdk::message::Message::new(&[ix], Some(&owner));
let recent_blockhash = rpc.get_latest_blockhash().unwrap();
let tx = solana_sdk::transaction::Transaction::new(&[&signer], message, recent_blockhash);
let signature = rpc.send_and_confirm_transaction(&tx).unwrap();
println!("Deposit successful! Signature: {signature}");
Depositing only mints shares to your share ATA. If the vault has a share farm (
VaultState::vault_farm is set), you must also stake those shares into the farm to earn vault-level rewards — staking is a separate Kamino Farms instruction, appended after this deposit.The deposit is complete. Your USDC has been transferred to the vault, and you’ve received kVUSDC shares redeemable at the current share price.
This example shows how an on-chain Anchor program can CPI into a Kamino Vault to deposit assets, using the
kvault-interface instructions builders. A PDA authority owns the user token and shares accounts and signs the CPI via invoke_signed — the builder assembles the discriminator and account metas, so you never hand-roll them.Add Dependencies
[dependencies]
kvault-interface = "0.1.0"
anchor-lang = "0.31"
anchor-spl = "0.31"
solana-program = "2.1"
Use Anchor 0.31+ here:
kvault-interface is built on the Solana 2.x crates (solana-pubkey/solana-instruction), and Anchor 0.31 is the first release on that line — so its Pubkey type matches the crate’s. Older Anchor (0.30) pulls Solana 1.x and won’t type-check against the builders.Define Program IDs, PDA Authority, and Handler
use anchor_lang::prelude::*;
use anchor_lang::solana_program::instruction::AccountMeta;
use anchor_spl::token::{Token, TokenAccount};
use kvault_interface::instructions::deposit::{deposit, DepositAccounts};
declare_id!("YourProgram1111111111111111111111111111111111");
const AUTHORITY_SEED: &[u8] = b"vault_authority";
// Mainnet program IDs — see accordion below for devnet equivalents
pub const KVAULT_PROGRAM_ID: Pubkey = pubkey!("KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd");
pub const KLEND_PROGRAM_ID: Pubkey = pubkey!("KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD");
#[program]
pub mod vault_deposit {
use super::*;
pub fn deposit_into_vault<'info>(
ctx: Context<'_, '_, '_, 'info, Deposit<'info>>,
max_amount: u64,
) -> Result<()> {
let vault_state_key = ctx.accounts.vault_state.key();
let authority_seeds: &[&[u8]] = &[
AUTHORITY_SEED,
vault_state_key.as_ref(),
&[ctx.bumps.authority],
];
Mainnet Program IDs
Mainnet Program IDs
| Constant | Address |
|---|---|
KVAULT_PROGRAM_ID | KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd |
KLEND_PROGRAM_ID | KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD |
Devnet Program IDs
Devnet Program IDs
| Constant | Address |
|---|---|
KVAULT_PROGRAM_ID | devkRngFnfp4gBc5a3LsadgbQKdPo8MSZ4prFiNSVmY |
KLEND_PROGRAM_ID | KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD |
Forward the Refresh Accounts
Klend reserve allocations live inremaining_accounts — writable reserve metas followed by readonly lending-market metas, in allocation-slot order. Forward them to the builder unchanged. let remaining_metas: Vec<AccountMeta> = ctx
.remaining_accounts
.iter()
.map(|a| AccountMeta {
pubkey: *a.key,
is_signer: a.is_signer,
is_writable: a.is_writable,
})
.collect();
Build the Deposit Instruction
deposit takes a DepositAccounts struct, the max_amount, and the refresh list. It derives the event-authority PDA and packs the discriminator + args for you. let ix = deposit(
DepositAccounts {
user: ctx.accounts.authority.key(),
vault_state: ctx.accounts.vault_state.key(),
token_vault: ctx.accounts.token_vault.key(),
token_mint: ctx.accounts.token_mint.key(),
base_vault_authority: ctx.accounts.base_vault_authority.key(),
shares_mint: ctx.accounts.shares_mint.key(),
user_token_ata: ctx.accounts.user_token_ata.key(),
user_shares_ata: ctx.accounts.user_shares_ata.key(),
klend_program: ctx.accounts.klend_program.key(),
token_program: ctx.accounts.token_program.key(),
shares_token_program: ctx.accounts.shares_token_program.key(),
},
max_amount,
remaining_metas,
);
Invoke Signed with the PDA
TheAccountInfos are supplied in the order the builder emits metas — the 11 named accounts, then the event-authority and kvault program (appended by the builder), then the forwarded remaining_accounts. let mut account_infos = vec![
ctx.accounts.authority.to_account_info(),
ctx.accounts.vault_state.to_account_info(),
ctx.accounts.token_vault.to_account_info(),
ctx.accounts.token_mint.to_account_info(),
ctx.accounts.base_vault_authority.to_account_info(),
ctx.accounts.shares_mint.to_account_info(),
ctx.accounts.user_token_ata.to_account_info(),
ctx.accounts.user_shares_ata.to_account_info(),
ctx.accounts.klend_program.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.shares_token_program.to_account_info(),
ctx.accounts.event_authority.to_account_info(),
ctx.accounts.kvault_program.to_account_info(),
];
account_infos.extend(ctx.remaining_accounts.iter().cloned());
solana_program::program::invoke_signed(&ix, &account_infos, &[authority_seeds])?;
Ok(())
}
}
Account Validation Struct
Deposit accounts
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(
mut,
seeds = [AUTHORITY_SEED, vault_state.key().as_ref()],
bump,
)]
pub authority: SystemAccount<'info>,
#[account(mut, token::authority = authority)]
pub user_token_ata: Account<'info, TokenAccount>,
#[account(mut, token::authority = authority)]
pub user_shares_ata: Account<'info, TokenAccount>,
/// CHECK: Kvault state account.
#[account(mut)]
pub vault_state: UncheckedAccount<'info>,
/// CHECK: Token vault owned by the Kvault program.
#[account(mut)]
pub token_vault: UncheckedAccount<'info>,
/// CHECK: Underlying token mint.
pub token_mint: UncheckedAccount<'info>,
/// CHECK: Kvault base vault authority PDA.
pub base_vault_authority: UncheckedAccount<'info>,
/// CHECK: Shares mint.
#[account(mut)]
pub shares_mint: UncheckedAccount<'info>,
/// CHECK: Event-emit PDA owned by the Kvault program.
pub event_authority: UncheckedAccount<'info>,
/// CHECK: The Kvault program.
#[account(address = KVAULT_PROGRAM_ID)]
pub kvault_program: UncheckedAccount<'info>,
/// CHECK: The Klend program.
#[account(address = KLEND_PROGRAM_ID)]
pub klend_program: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
pub shares_token_program: Program<'info, Token>,
}
Full Code Example
- TypeScript
import {
createSolanaRpc,
createSolanaRpcSubscriptions,
address,
pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory,
getSignatureFromTransaction,
} from '@solana/kit';
import { KaminoVault } from '@kamino-finance/klend-sdk';
import { parseKeypairFile } from '@kamino-finance/klend-sdk/dist/utils/signer.js';
import { Decimal } from 'decimal.js';
// Configuration - UPDATE THESE VALUES
const KEYPAIR_FILE = '/path/to/your/keypair.json';
// Load keypair from file
const signer = await parseKeypairFile(KEYPAIR_FILE);
// Initialize RPC and RPC Subscriptions
const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.mainnet-beta.solana.com');
const vault = new KaminoVault(
rpc,
address('HDsayqAsDWy3QvANGqh2yNraqcD8Fnjgh73Mhb3WRS5E') // USDC vault
);
// Build deposit instructions
const depositAmount = new Decimal(1.0);
const bundle = await vault.depositIxs(signer, depositAmount);
const instructions = [...(bundle.depositIxs || [])];
if (!instructions.length) {
throw new Error('No instructions returned by Kamino SDK');
}
// Build and sign transaction using functional pipe pattern
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(signer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx)
);
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
// Send and confirm transaction
const signature = getSignatureFromTransaction(signedTransaction);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
commitment: 'confirmed',
skipPreflight: true,
});
console.log('Deposit successful! Signature:', signature);