Withdraw assets from Kamino Earn vaults to redeem your shares for underlying tokens. The SDK handles transaction building and vault interaction.Documentation Index
Fetch the complete documentation index at: https://kamino.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
Complete Flow
Implement the full withdrawal 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 Withdraw Instructions
Generate withdraw instructions including optional unstaking instructions.const withdrawAmount = new Decimal(1.0);
const bundle = await vault.withdrawIxs(signer, withdrawAmount);
const instructions = [...(bundle.unstakeFromFarmIfNeededIxs || []), ...(bundle.withdrawIxs || [])];
if (!instructions.length) {
throw new Error('No instructions returned by Kamino SDK');
}
The
withdrawIxs method returns both unstaking and withdraw instructions. The bundle ensures optimal handling by automatically unstaking deposited assets when needed. The amount represents vault shares to redeem.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('Withdraw successful! Signature:', signature);
The withdrawal is complete. Your vault shares have been redeemed for the underlying assets.
A standalone off-chain Rust client that calls the Kamino Vault program directly with your wallet keypair. Burns kVUSDC vault shares to redeem the underlying USDC at the current share price. Uses the same
Cargo.toml and wallet setup as the deposit client; running deposit at least once leaves the kVUSDC shares this client burns.Add Dependencies
[dependencies]
solana-client = "3"
solana-instruction = "3"
solana-keypair = "3"
solana-message = "3"
solana-pubkey = "3"
solana-signer = "3"
solana-transaction = "3"
Set Up RPC Client and Wallet
Load the Solana CLI wallet from~/.config/solana/id.json and create an RPC client.use solana_client::rpc_client::RpcClient;
use solana_keypair::read_keypair_file;
use solana_signer::Signer;
let rpc = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
let home = std::env::var("HOME").unwrap();
let user = read_keypair_file(format!("{}/.config/solana/id.json", home))
.expect("Failed to load wallet from ~/.config/solana/id.json");
Define Program IDs and Vault Constants
Add the Steakhouse USDC vault state, allocated klend reserves, lending markets, and the withdraw discriminator.use solana_pubkey::{pubkey, Pubkey};
const KVAULT_PROGRAM_ID: Pubkey = pubkey!("KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd");
const KLEND_PROGRAM_ID: Pubkey = pubkey!("KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD");
const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
const ATA_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111");
const INSTRUCTIONS_SYSVAR: Pubkey = pubkey!("Sysvar1nstructions1111111111111111111111111");
const VAULT_STATE: Pubkey = pubkey!("HDsayqAsDWy3QvANGqh2yNraqcD8Fnjgh73Mhb3WRS5E");
const TOKEN_VAULT: Pubkey = pubkey!("CKTEDx5z19CntAB9B66AxuS98S1NuCgMvfpsew7TQwi");
const TOKEN_MINT: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const SHARES_MINT: Pubkey = pubkey!("7D8C5pDFxug58L9zkwK7bCiDg4kD4AygzbcZUmf5usHS");
const BASE_VAULT_AUTHORITY: Pubkey = pubkey!("AyY6VCkHfTWdFs7SqBbu6AnCqLUhgzVHBzW3WcJu5Jc8");
const GLOBAL_CONFIG: Pubkey = pubkey!("BKyTcUe6daNG8HbgBix2ugdRHbykG2dK9hPBBqhUyoEX");
const RESERVES: &[Pubkey] = &[
pubkey!("Ga4rZytCpq1unD4DbEJ5bkHeUz9g3oh9AAFEi6vSauXp"),
pubkey!("D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59"),
pubkey!("9FRZvAsjDJ6WM8BJ2S45h9PoDCLAq8DNY9zZDX7MyGzT"),
pubkey!("Atj6UREVWa7WxbF2EMKNyfmYUY1U1txughe2gjhcPDCo"),
];
const LENDING_MARKETS: &[Pubkey] = &[
pubkey!("DxXdAyU3kCjnyggvHmY5nAwg5cRbbmdyX3npfDMjjMek"),
pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF"),
pubkey!("GMqmFygF5iSm5nkckYU6tieggFcR42SyjkkhK5rswFRs"),
pubkey!("6WEGfej9B9wjxRs6t4BYpb9iCXd8CpTpJ8fVSNzHCC5y"),
];
const REDEEM_RESERVE: Pubkey = pubkey!("D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59");
const REDEEM_CTOKEN_VAULT: Pubkey = pubkey!("CZg8x8oqB7FYUfURq15F5AcjRTymcXsc8ann76CrpJrf");
const REDEEM_LENDING_MARKET: Pubkey = pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF");
const RESERVE_LIQUIDITY_SUPPLY_OFFSET: usize = 160;
const RESERVE_COLLATERAL_MINT_OFFSET: usize = 2560;
const WITHDRAW_DISCRIMINATOR: [u8; 8] = [183, 18, 70, 156, 148, 109, 161, 34];
const SHARES_AMOUNT: u64 = 959_664;
WITHDRAW_DISCRIMINATOR is the 8-byte instruction selector kvault expects on the wire: the first 8 bytes of sha256("global:withdraw"). SHARES_AMOUNT is in raw kVUSDC units (6 decimals); 959_664 ≈ all the shares from a 1 USDC deposit (returns ≈ 0.9997 USDC after fees).REDEEM_* are the klend-side accounts kvault uses when tokenAvailable is too low to satisfy a withdrawal directly — kvault then redeems cTokens from a klend reserve.Derive User Token Accounts
Compute the user’s USDC and kVUSDC ATAs and the kvault event-authority PDA.let user_token_ata = ata(&user.pubkey(), &TOKEN_MINT);
let user_shares_ata = ata(&user.pubkey(), &SHARES_MINT);
let (event_authority, _) =
Pubkey::find_program_address(&[b"__event_authority"], &KVAULT_PROGRAM_ID);
fn ata(owner: &Pubkey, mint: &Pubkey) -> Pubkey {
Pubkey::find_program_address(
&[owner.as_ref(), TOKEN_PROGRAM.as_ref(), mint.as_ref()],
&ATA_PROGRAM,
)
.0
}
Send Setup Transaction
Make sure the USDC ATA exists. Thecreate_idempotent instruction is a no-op if the ATA is already there.use solana_instruction::{AccountMeta, Instruction};
use solana_message::{Message, VersionedMessage};
use solana_transaction::versioned::VersionedTransaction;
let setup_ix = ata_create_idempotent_ix(&user.pubkey(), &user.pubkey(), &TOKEN_MINT);
let blockhash = rpc.get_latest_blockhash().unwrap();
let setup_msg = Message::new_with_blockhash(&[setup_ix], Some(&user.pubkey()), &blockhash);
let setup_tx = VersionedTransaction::try_new(VersionedMessage::Legacy(setup_msg), &[&user]).unwrap();
rpc.send_and_confirm_transaction(&setup_tx).unwrap();
fn ata_create_idempotent_ix(payer: &Pubkey, owner: &Pubkey, mint: &Pubkey) -> Instruction {
let ata_addr = ata(owner, mint);
Instruction {
program_id: ATA_PROGRAM,
accounts: vec![
AccountMeta::new(*payer, true),
AccountMeta::new(ata_addr, false),
AccountMeta::new_readonly(*owner, false),
AccountMeta::new_readonly(*mint, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM, false),
AccountMeta::new_readonly(TOKEN_PROGRAM, false),
],
data: vec![1],
}
}
Look Up Redemption Reserve Klend Accounts
Fetch the redemption reserve account, parseliquidity.supplyVault and collateral.mint at fixed offsets, and derive the lending_market_authority PDA from the klend program seeds.let reserve_acct = rpc.get_account(&REDEEM_RESERVE).unwrap();
let reserve_liquidity_supply = read_pubkey(&reserve_acct.data, RESERVE_LIQUIDITY_SUPPLY_OFFSET);
let reserve_collateral_mint = read_pubkey(&reserve_acct.data, RESERVE_COLLATERAL_MINT_OFFSET);
let (lending_market_authority, _) = Pubkey::find_program_address(
&[b"lma", REDEEM_LENDING_MARKET.as_ref()],
&KLEND_PROGRAM_ID,
);
fn read_pubkey(data: &[u8], offset: usize) -> Pubkey {
Pubkey::new_from_array(data[offset..offset + 32].try_into().unwrap())
}
Build the Withdraw Instruction
Buildkamino_vault::withdraw with the discriminator, the shares amount, and the full set of account metas — using the dynamically resolved klend accounts in the reserve-group slots.let withdraw_ix = build_withdraw_ix(
&user.pubkey(),
&user_token_ata,
&user_shares_ata,
&event_authority,
&lending_market_authority,
&reserve_liquidity_supply,
&reserve_collateral_mint,
SHARES_AMOUNT,
);
fn build_withdraw_ix(
user: &Pubkey,
user_token_ata: &Pubkey,
user_shares_ata: &Pubkey,
event_authority: &Pubkey,
lending_market_authority: &Pubkey,
reserve_liquidity_supply: &Pubkey,
reserve_collateral_mint: &Pubkey,
shares_amount: u64,
) -> Instruction {
let mut data = Vec::with_capacity(16);
data.extend_from_slice(&WITHDRAW_DISCRIMINATOR);
data.extend_from_slice(&shares_amount.to_le_bytes());
let mut metas = vec![
AccountMeta::new(*user, true),
AccountMeta::new(VAULT_STATE, false),
AccountMeta::new_readonly(GLOBAL_CONFIG, false),
AccountMeta::new(TOKEN_VAULT, false),
AccountMeta::new_readonly(BASE_VAULT_AUTHORITY, false),
AccountMeta::new(*user_token_ata, false),
AccountMeta::new(TOKEN_MINT, false),
AccountMeta::new(*user_shares_ata, false),
AccountMeta::new(SHARES_MINT, false),
AccountMeta::new_readonly(TOKEN_PROGRAM, false),
AccountMeta::new_readonly(TOKEN_PROGRAM, false),
AccountMeta::new_readonly(KLEND_PROGRAM_ID, false),
AccountMeta::new_readonly(*event_authority, false),
AccountMeta::new_readonly(KVAULT_PROGRAM_ID, false),
AccountMeta::new(VAULT_STATE, false),
AccountMeta::new(REDEEM_RESERVE, false),
AccountMeta::new(REDEEM_CTOKEN_VAULT, false),
AccountMeta::new_readonly(REDEEM_LENDING_MARKET, false),
AccountMeta::new_readonly(*lending_market_authority, false),
AccountMeta::new(*reserve_liquidity_supply, false),
AccountMeta::new(*reserve_collateral_mint, false),
AccountMeta::new_readonly(TOKEN_PROGRAM, false),
AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR, false),
AccountMeta::new_readonly(*event_authority, false),
AccountMeta::new_readonly(KVAULT_PROGRAM_ID, false),
];
for r in RESERVES {
metas.push(AccountMeta::new(*r, false));
}
for m in LENDING_MARKETS {
metas.push(AccountMeta::new_readonly(*m, false));
}
Instruction {
program_id: KVAULT_PROGRAM_ID,
accounts: metas,
data,
}
}
Instruction data is
[discriminator (8 bytes) | shares_amount (u64 LE, 8 bytes)].Send the Withdraw Transaction
Send the withdraw transaction and print the resulting signature.let blockhash = rpc.get_latest_blockhash().unwrap();
let msg = Message::new_with_blockhash(&[withdraw_ix], Some(&user.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&user]).unwrap();
let sig = rpc.send_and_confirm_transaction(&tx).unwrap();
println!("Withdraw tx: {}", sig);
The withdrawal is complete. Your kVUSDC shares have been burned and the underlying USDC has been transferred to your wallet at the current share price.
This example shows how an on-chain Anchor program can CPI into a Kamino Vault to withdraw assets. A PDA authority owns the user token and shares vaults and signs the CPI via
invoke_signed.Add Dependencies
[dependencies]
anchor-lang = "0.30"
anchor-spl = "0.30"
solana-program = "2.1"
Define Program IDs and Discriminator
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
instruction::{AccountMeta, Instruction},
program::invoke_signed,
};
use anchor_spl::token::{Token, TokenAccount};
declare_id!("YourProgram1111111111111111111111111111111111");
const AUTHORITY_SEED: &[u8] = b"vault_authority";
pub const KVAULT_PROGRAM_ID: Pubkey = pubkey!("KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd");
pub const KLEND_PROGRAM_ID: Pubkey = pubkey!("KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD");
// sha256("global:withdraw")[..8]
pub const KVAULT_WITHDRAW_DISCRIMINATOR: [u8; 8] = [183, 18, 70, 156, 148, 109, 161, 34];
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 |
Define the Withdraw Handler
#[program]
pub mod kvault_ops {
use super::*;
pub fn withdraw<'info>(ctx: Context<'info, Withdraw<'info>>, shares_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],
];
Build the Withdraw Instruction Data
Pack the discriminator +shares_amount (little-endian u64) as instruction data. // data = [discriminator (8 bytes) | shares_amount as u64 LE (8 bytes)]
let mut data = Vec::with_capacity(16);
data.extend_from_slice(&KVAULT_WITHDRAW_DISCRIMINATOR);
data.extend_from_slice(&shares_amount.to_le_bytes());
Assemble Account Metas
Withdraw flattens two IDL groups into 25AccountMetas. Some accounts repeat across groups: vault_state at indices 1 & 14, and event_authority / kvault_program at the end of each group. let metas = vec![
// withdrawFromAvailable (14)
AccountMeta::new(ctx.accounts.authority.key(), true),
AccountMeta::new(ctx.accounts.vault_state.key(), false),
AccountMeta::new_readonly(ctx.accounts.global_config.key(), false),
AccountMeta::new(ctx.accounts.token_vault.key(), false),
AccountMeta::new_readonly(ctx.accounts.base_vault_authority.key(), false),
AccountMeta::new(ctx.accounts.user_token_ata.key(), false),
AccountMeta::new(ctx.accounts.token_mint.key(), false),
AccountMeta::new(ctx.accounts.user_shares_ata.key(), false),
AccountMeta::new(ctx.accounts.shares_mint.key(), false),
AccountMeta::new_readonly(ctx.accounts.token_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.shares_token_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.klend_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.event_authority.key(), false),
AccountMeta::new_readonly(ctx.accounts.kvault_program.key(), false),
// withdrawFromReserveAccounts (9) — vault_state repeats
AccountMeta::new(ctx.accounts.vault_state.key(), false),
AccountMeta::new(ctx.accounts.reserve.key(), false),
AccountMeta::new(ctx.accounts.ctoken_vault.key(), false),
AccountMeta::new_readonly(ctx.accounts.lending_market.key(), false),
AccountMeta::new_readonly(ctx.accounts.lending_market_authority.key(), false),
AccountMeta::new(ctx.accounts.reserve_liquidity_supply.key(), false),
AccountMeta::new(ctx.accounts.reserve_collateral_mint.key(), false),
AccountMeta::new_readonly(ctx.accounts.reserve_collateral_token_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.instruction_sysvar_account.key(), false),
// trailing — event_authority and kvault_program repeat
AccountMeta::new_readonly(ctx.accounts.event_authority.key(), false),
AccountMeta::new_readonly(ctx.accounts.kvault_program.key(), false),
];
let ix = Instruction {
program_id: KVAULT_PROGRAM_ID,
accounts: metas,
data,
};
Invoke Signed and Sign with the PDA
TheAccountInfos must be supplied in the same order as the AccountMetas above — including the repeated entries. let account_infos = vec![
ctx.accounts.authority.to_account_info(),
ctx.accounts.vault_state.to_account_info(),
ctx.accounts.global_config.to_account_info(),
ctx.accounts.token_vault.to_account_info(),
ctx.accounts.base_vault_authority.to_account_info(),
ctx.accounts.user_token_ata.to_account_info(),
ctx.accounts.token_mint.to_account_info(),
ctx.accounts.user_shares_ata.to_account_info(),
ctx.accounts.shares_mint.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.shares_token_program.to_account_info(),
ctx.accounts.klend_program.to_account_info(),
ctx.accounts.event_authority.to_account_info(),
ctx.accounts.kvault_program.to_account_info(),
ctx.accounts.vault_state.to_account_info(),
ctx.accounts.reserve.to_account_info(),
ctx.accounts.ctoken_vault.to_account_info(),
ctx.accounts.lending_market.to_account_info(),
ctx.accounts.lending_market_authority.to_account_info(),
ctx.accounts.reserve_liquidity_supply.to_account_info(),
ctx.accounts.reserve_collateral_mint.to_account_info(),
ctx.accounts
.reserve_collateral_token_program
.to_account_info(),
ctx.accounts.instruction_sysvar_account.to_account_info(),
ctx.accounts.event_authority.to_account_info(),
ctx.accounts.kvault_program.to_account_info(),
];
invoke_signed(&ix, &account_infos, &[authority_seeds])?;
Ok(())
}
}
Account Validation Struct
Withdraw accounts
#[derive(Accounts)]
pub struct Withdraw<'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: Kvault global config account.
pub global_config: UncheckedAccount<'info>,
/// CHECK: Token vault owned by the Kvault program.
#[account(mut)]
pub token_vault: UncheckedAccount<'info>,
/// CHECK: Kvault base vault authority PDA.
pub base_vault_authority: UncheckedAccount<'info>,
/// CHECK: Underlying token mint.
#[account(mut)]
pub token_mint: 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: Klend reserve account.
#[account(mut)]
pub reserve: UncheckedAccount<'info>,
/// CHECK: cToken vault owned by the Kvault program.
#[account(mut)]
pub ctoken_vault: UncheckedAccount<'info>,
/// CHECK: Klend lending market.
pub lending_market: UncheckedAccount<'info>,
/// CHECK: Klend lending market authority PDA.
pub lending_market_authority: UncheckedAccount<'info>,
/// CHECK: Reserve's underlying liquidity supply vault.
#[account(mut)]
pub reserve_liquidity_supply: UncheckedAccount<'info>,
/// CHECK: Reserve's cToken mint.
#[account(mut)]
pub reserve_collateral_mint: UncheckedAccount<'info>,
/// CHECK: Reserve's cToken program.
pub reserve_collateral_token_program: UncheckedAccount<'info>,
/// CHECK: Solana instructions sysvar.
#[account(address = pubkey!("Sysvar1nstructions1111111111111111111111111"))]
pub instruction_sysvar_account: 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
- Rust
- Rust (CPI)
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 withdraw instructions (includes optional unstaking)
const withdrawAmount = new Decimal(1.0);
const bundle = await vault.withdrawIxs(signer, withdrawAmount);
const instructions = [...(bundle.unstakeFromFarmIfNeededIxs || []), ...(bundle.withdrawIxs || [])];
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('Withdraw successful! Signature:', signature);
use solana_client::rpc_client::RpcClient;
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::read_keypair_file;
use solana_message::{Message, VersionedMessage};
use solana_pubkey::{pubkey, Pubkey};
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
const KVAULT_PROGRAM_ID: Pubkey = pubkey!("KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd");
const KLEND_PROGRAM_ID: Pubkey = pubkey!("KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD");
const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
const ATA_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
const SYSTEM_PROGRAM: Pubkey = pubkey!("11111111111111111111111111111111");
const INSTRUCTIONS_SYSVAR: Pubkey = pubkey!("Sysvar1nstructions1111111111111111111111111");
const VAULT_STATE: Pubkey = pubkey!("HDsayqAsDWy3QvANGqh2yNraqcD8Fnjgh73Mhb3WRS5E");
const TOKEN_VAULT: Pubkey = pubkey!("CKTEDx5z19CntAB9B66AxuS98S1NuCgMvfpsew7TQwi");
const TOKEN_MINT: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const SHARES_MINT: Pubkey = pubkey!("7D8C5pDFxug58L9zkwK7bCiDg4kD4AygzbcZUmf5usHS");
const BASE_VAULT_AUTHORITY: Pubkey = pubkey!("AyY6VCkHfTWdFs7SqBbu6AnCqLUhgzVHBzW3WcJu5Jc8");
const GLOBAL_CONFIG: Pubkey = pubkey!("BKyTcUe6daNG8HbgBix2ugdRHbykG2dK9hPBBqhUyoEX");
const RESERVES: &[Pubkey] = &[
pubkey!("Ga4rZytCpq1unD4DbEJ5bkHeUz9g3oh9AAFEi6vSauXp"),
pubkey!("D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59"),
pubkey!("9FRZvAsjDJ6WM8BJ2S45h9PoDCLAq8DNY9zZDX7MyGzT"),
pubkey!("Atj6UREVWa7WxbF2EMKNyfmYUY1U1txughe2gjhcPDCo"),
];
const LENDING_MARKETS: &[Pubkey] = &[
pubkey!("DxXdAyU3kCjnyggvHmY5nAwg5cRbbmdyX3npfDMjjMek"),
pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF"),
pubkey!("GMqmFygF5iSm5nkckYU6tieggFcR42SyjkkhK5rswFRs"),
pubkey!("6WEGfej9B9wjxRs6t4BYpb9iCXd8CpTpJ8fVSNzHCC5y"),
];
const REDEEM_RESERVE: Pubkey = pubkey!("D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59");
const REDEEM_CTOKEN_VAULT: Pubkey = pubkey!("CZg8x8oqB7FYUfURq15F5AcjRTymcXsc8ann76CrpJrf");
const REDEEM_LENDING_MARKET: Pubkey = pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF");
const RESERVE_LIQUIDITY_SUPPLY_OFFSET: usize = 160;
const RESERVE_COLLATERAL_MINT_OFFSET: usize = 2560;
const WITHDRAW_DISCRIMINATOR: [u8; 8] = [183, 18, 70, 156, 148, 109, 161, 34];
const SHARES_AMOUNT: u64 = 959_664;
fn main() {
let rpc = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
let home = std::env::var("HOME").unwrap();
let user = read_keypair_file(format!("{}/.config/solana/id.json", home))
.expect("Failed to load wallet from ~/.config/solana/id.json");
println!("User wallet: {}", user.pubkey());
let user_token_ata = ata(&user.pubkey(), &TOKEN_MINT);
let user_shares_ata = ata(&user.pubkey(), &SHARES_MINT);
let (event_authority, _) =
Pubkey::find_program_address(&[b"__event_authority"], &KVAULT_PROGRAM_ID);
let setup_ix = ata_create_idempotent_ix(&user.pubkey(), &user.pubkey(), &TOKEN_MINT);
let blockhash = rpc.get_latest_blockhash().unwrap();
let setup_msg = Message::new_with_blockhash(&[setup_ix], Some(&user.pubkey()), &blockhash);
let setup_tx = VersionedTransaction::try_new(VersionedMessage::Legacy(setup_msg), &[&user]).unwrap();
rpc.send_and_confirm_transaction(&setup_tx).unwrap();
let reserve_acct = rpc.get_account(&REDEEM_RESERVE).unwrap();
let reserve_liquidity_supply = read_pubkey(&reserve_acct.data, RESERVE_LIQUIDITY_SUPPLY_OFFSET);
let reserve_collateral_mint = read_pubkey(&reserve_acct.data, RESERVE_COLLATERAL_MINT_OFFSET);
let (lending_market_authority, _) = Pubkey::find_program_address(
&[b"lma", REDEEM_LENDING_MARKET.as_ref()],
&KLEND_PROGRAM_ID,
);
let withdraw_ix = build_withdraw_ix(
&user.pubkey(),
&user_token_ata,
&user_shares_ata,
&event_authority,
&lending_market_authority,
&reserve_liquidity_supply,
&reserve_collateral_mint,
SHARES_AMOUNT,
);
let blockhash = rpc.get_latest_blockhash().unwrap();
let msg = Message::new_with_blockhash(&[withdraw_ix], Some(&user.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&user]).unwrap();
let sig = rpc.send_and_confirm_transaction(&tx).unwrap();
println!("Withdraw tx: {}", sig);
}
fn ata(owner: &Pubkey, mint: &Pubkey) -> Pubkey {
Pubkey::find_program_address(
&[owner.as_ref(), TOKEN_PROGRAM.as_ref(), mint.as_ref()],
&ATA_PROGRAM,
)
.0
}
fn read_pubkey(data: &[u8], offset: usize) -> Pubkey {
Pubkey::new_from_array(data[offset..offset + 32].try_into().unwrap())
}
fn ata_create_idempotent_ix(payer: &Pubkey, owner: &Pubkey, mint: &Pubkey) -> Instruction {
let ata_addr = ata(owner, mint);
Instruction {
program_id: ATA_PROGRAM,
accounts: vec![
AccountMeta::new(*payer, true),
AccountMeta::new(ata_addr, false),
AccountMeta::new_readonly(*owner, false),
AccountMeta::new_readonly(*mint, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM, false),
AccountMeta::new_readonly(TOKEN_PROGRAM, false),
],
data: vec![1],
}
}
fn build_withdraw_ix(
user: &Pubkey,
user_token_ata: &Pubkey,
user_shares_ata: &Pubkey,
event_authority: &Pubkey,
lending_market_authority: &Pubkey,
reserve_liquidity_supply: &Pubkey,
reserve_collateral_mint: &Pubkey,
shares_amount: u64,
) -> Instruction {
let mut data = Vec::with_capacity(16);
data.extend_from_slice(&WITHDRAW_DISCRIMINATOR);
data.extend_from_slice(&shares_amount.to_le_bytes());
let mut metas = vec![
AccountMeta::new(*user, true),
AccountMeta::new(VAULT_STATE, false),
AccountMeta::new_readonly(GLOBAL_CONFIG, false),
AccountMeta::new(TOKEN_VAULT, false),
AccountMeta::new_readonly(BASE_VAULT_AUTHORITY, false),
AccountMeta::new(*user_token_ata, false),
AccountMeta::new(TOKEN_MINT, false),
AccountMeta::new(*user_shares_ata, false),
AccountMeta::new(SHARES_MINT, false),
AccountMeta::new_readonly(TOKEN_PROGRAM, false),
AccountMeta::new_readonly(TOKEN_PROGRAM, false),
AccountMeta::new_readonly(KLEND_PROGRAM_ID, false),
AccountMeta::new_readonly(*event_authority, false),
AccountMeta::new_readonly(KVAULT_PROGRAM_ID, false),
AccountMeta::new(VAULT_STATE, false),
AccountMeta::new(REDEEM_RESERVE, false),
AccountMeta::new(REDEEM_CTOKEN_VAULT, false),
AccountMeta::new_readonly(REDEEM_LENDING_MARKET, false),
AccountMeta::new_readonly(*lending_market_authority, false),
AccountMeta::new(*reserve_liquidity_supply, false),
AccountMeta::new(*reserve_collateral_mint, false),
AccountMeta::new_readonly(TOKEN_PROGRAM, false),
AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR, false),
AccountMeta::new_readonly(*event_authority, false),
AccountMeta::new_readonly(KVAULT_PROGRAM_ID, false),
];
for r in RESERVES {
metas.push(AccountMeta::new(*r, false));
}
for m in LENDING_MARKETS {
metas.push(AccountMeta::new_readonly(*m, false));
}
Instruction {
program_id: KVAULT_PROGRAM_ID,
accounts: metas,
data,
}
}
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
instruction::{AccountMeta, Instruction},
program::invoke_signed,
};
use anchor_spl::token::{Token, TokenAccount};
declare_id!("YourProgram1111111111111111111111111111111111");
const AUTHORITY_SEED: &[u8] = b"vault_authority";
pub const KVAULT_PROGRAM_ID: Pubkey = pubkey!("KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd");
pub const KLEND_PROGRAM_ID: Pubkey = pubkey!("KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD");
// sha256("global:withdraw")[..8]
pub const KVAULT_WITHDRAW_DISCRIMINATOR: [u8; 8] = [183, 18, 70, 156, 148, 109, 161, 34];
#[program]
pub mod kvault_ops {
use super::*;
pub fn withdraw<'info>(ctx: Context<'info, Withdraw<'info>>, shares_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],
];
// data = [discriminator (8 bytes) | shares_amount as u64 LE (8 bytes)]
let mut data = Vec::with_capacity(16);
data.extend_from_slice(&KVAULT_WITHDRAW_DISCRIMINATOR);
data.extend_from_slice(&shares_amount.to_le_bytes());
// 25 AccountMetas in IDL flattened order. Some accounts repeat across
// groups (vault_state at 1 & 14; event_authority at 12 & 23;
// kvault_program at 13 & 24).
let metas = vec![
// withdrawFromAvailable (14)
AccountMeta::new(ctx.accounts.authority.key(), true),
AccountMeta::new(ctx.accounts.vault_state.key(), false),
AccountMeta::new_readonly(ctx.accounts.global_config.key(), false),
AccountMeta::new(ctx.accounts.token_vault.key(), false),
AccountMeta::new_readonly(ctx.accounts.base_vault_authority.key(), false),
AccountMeta::new(ctx.accounts.user_token_ata.key(), false),
AccountMeta::new(ctx.accounts.token_mint.key(), false),
AccountMeta::new(ctx.accounts.user_shares_ata.key(), false),
AccountMeta::new(ctx.accounts.shares_mint.key(), false),
AccountMeta::new_readonly(ctx.accounts.token_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.shares_token_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.klend_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.event_authority.key(), false),
AccountMeta::new_readonly(ctx.accounts.kvault_program.key(), false),
// withdrawFromReserveAccounts (9) — vault_state repeats
AccountMeta::new(ctx.accounts.vault_state.key(), false),
AccountMeta::new(ctx.accounts.reserve.key(), false),
AccountMeta::new(ctx.accounts.ctoken_vault.key(), false),
AccountMeta::new_readonly(ctx.accounts.lending_market.key(), false),
AccountMeta::new_readonly(ctx.accounts.lending_market_authority.key(), false),
AccountMeta::new(ctx.accounts.reserve_liquidity_supply.key(), false),
AccountMeta::new(ctx.accounts.reserve_collateral_mint.key(), false),
AccountMeta::new_readonly(ctx.accounts.reserve_collateral_token_program.key(), false),
AccountMeta::new_readonly(ctx.accounts.instruction_sysvar_account.key(), false),
// trailing — event_authority and kvault_program repeat
AccountMeta::new_readonly(ctx.accounts.event_authority.key(), false),
AccountMeta::new_readonly(ctx.accounts.kvault_program.key(), false),
];
let ix = Instruction {
program_id: KVAULT_PROGRAM_ID,
accounts: metas,
data,
};
let account_infos = vec![
ctx.accounts.authority.to_account_info(),
ctx.accounts.vault_state.to_account_info(),
ctx.accounts.global_config.to_account_info(),
ctx.accounts.token_vault.to_account_info(),
ctx.accounts.base_vault_authority.to_account_info(),
ctx.accounts.user_token_ata.to_account_info(),
ctx.accounts.token_mint.to_account_info(),
ctx.accounts.user_shares_ata.to_account_info(),
ctx.accounts.shares_mint.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.shares_token_program.to_account_info(),
ctx.accounts.klend_program.to_account_info(),
ctx.accounts.event_authority.to_account_info(),
ctx.accounts.kvault_program.to_account_info(),
ctx.accounts.vault_state.to_account_info(),
ctx.accounts.reserve.to_account_info(),
ctx.accounts.ctoken_vault.to_account_info(),
ctx.accounts.lending_market.to_account_info(),
ctx.accounts.lending_market_authority.to_account_info(),
ctx.accounts.reserve_liquidity_supply.to_account_info(),
ctx.accounts.reserve_collateral_mint.to_account_info(),
ctx.accounts
.reserve_collateral_token_program
.to_account_info(),
ctx.accounts.instruction_sysvar_account.to_account_info(),
ctx.accounts.event_authority.to_account_info(),
ctx.accounts.kvault_program.to_account_info(),
];
invoke_signed(&ix, &account_infos, &[authority_seeds])?;
Ok(())
}
}
#[derive(Accounts)]
pub struct Withdraw<'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: Kvault global config account.
pub global_config: UncheckedAccount<'info>,
/// CHECK: Token vault owned by the Kvault program.
#[account(mut)]
pub token_vault: UncheckedAccount<'info>,
/// CHECK: Kvault base vault authority PDA.
pub base_vault_authority: UncheckedAccount<'info>,
/// CHECK: Underlying token mint.
#[account(mut)]
pub token_mint: 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: Klend reserve account.
#[account(mut)]
pub reserve: UncheckedAccount<'info>,
/// CHECK: cToken vault owned by the Kvault program.
#[account(mut)]
pub ctoken_vault: UncheckedAccount<'info>,
/// CHECK: Klend lending market.
pub lending_market: UncheckedAccount<'info>,
/// CHECK: Klend lending market authority PDA.
pub lending_market_authority: UncheckedAccount<'info>,
/// CHECK: Reserve's underlying liquidity supply vault.
#[account(mut)]
pub reserve_liquidity_supply: UncheckedAccount<'info>,
/// CHECK: Reserve's cToken mint.
#[account(mut)]
pub reserve_collateral_mint: UncheckedAccount<'info>,
/// CHECK: Reserve's cToken program.
pub reserve_collateral_token_program: UncheckedAccount<'info>,
/// CHECK: Solana instructions sysvar.
#[account(address = pubkey!("Sysvar1nstructions1111111111111111111111111"))]
pub instruction_sysvar_account: 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>,
}