@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
1,344 lines (1,226 loc) • 126 kB
text/typescript
import dotenv from 'dotenv';
import { Command } from 'commander';
import { Address, address, generateKeyPairSigner, Instruction, TransactionSigner } from '@solana/kit';
import {
AssetReserveConfigCli,
calculateAPYFromAPR,
createLookupTableIx,
DEFAULT_CU_PER_TX,
DEFAULT_PUBLIC_KEY,
DEFAULT_RECENT_SLOT_DURATION_MS,
encodeTokenName,
extendLookupTableIxs,
getKvaultGlobalConfigPda,
getMedianSlotDurationInMsFromLastEpochs,
globalConfigPda,
initLookupTableIx,
KaminoManager,
KaminoMarket,
KaminoReserve,
KaminoVault,
KaminoVaultConfig,
KVaultGlobalConfig,
lamportsToDecimal,
LendingMarket,
parseBooleanFlag,
parseTokenSymbol,
parseZeroPaddedUtf8,
programDataPda,
Reserve,
ReserveAllocationConfig,
ReserveWithAddress,
sleep,
} from '../lib';
import {
BorrowRateCurve,
CurvePointFields,
PriceHeuristic,
ReserveConfig,
ReserveConfigFields,
ScopeConfiguration,
TokenInfo,
WithdrawalCaps,
} from '../@codegen/klend/types';
import { Fraction } from '../classes/fraction';
import Decimal from 'decimal.js';
import BN from 'bn.js';
import { PythConfiguration, SwitchboardConfiguration, UpdateReserveWhitelistMode } from '../@codegen/kvault/types';
import { ReserveWhitelistEntry } from '../@codegen/kvault/accounts';
import { getReserveWhitelistEntryPda } from '../classes/vault';
import { getMarketsFromApi } from '../utils/api';
import * as fs from 'fs';
import { MarketWithAddress } from '../utils/managerTypes';
import { ManagementFeeBps, PendingVaultAdmin, PerformanceFeeBps } from '../@codegen/kvault/types/VaultConfigField';
import { getAccountOwner } from '../utils/rpc';
import { fetchMint, findAssociatedTokenPda } from '@solana-program/token-2022';
import { initEnv, ManagerEnv } from './tx/ManagerEnv';
import { processTx } from './tx/processor';
import { getPriorityFeeAndCuIxs } from '../client/tx/priorityFee';
import { fetchAddressLookupTable, fetchAllAddressLookupTable } from '@solana-program/address-lookup-table';
import { noopSigner, parseKeypairFile } from '../utils/signer';
dotenv.config({
path: `.env${process.env.ENV ? '.' + process.env.ENV : ''}`,
});
async function main() {
const commands = new Command();
commands.name('kamino-manager-cli').description('CLI to interact with the kvaults and klend programs');
commands
.command('create-market')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.action(async ({ mode, staging, devnet, multisig }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig pubkey is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, undefined, undefined, devnet);
const admin = await env.getSigner();
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
const { market: marketKp, ixs: createMarketIxs } = await kaminoManager.createMarketIxs({
admin,
});
await processTx(
env.c,
admin,
[
...createMarketIxs,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode,
[]
);
mode === 'execute' && console.log('Market created:', marketKp.address);
});
commands
.command('add-asset-to-market')
.requiredOption('--market <string>', 'Market address to add asset to')
.requiredOption('--mint <string>', 'Reserve liquidity token mint')
.requiredOption('--reserve-config-path <string>', 'Path for the reserve config')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(
'--global-admin <string>',
'Global admin signer (keypair path in execute/simulate modes, pubkey in multisig mode)'
)
.option('--reserve-key-path <string>', 'Path to the reserve key pair file')
.option(`--staging`, 'If true, will use the staging programs')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.action(async ({ market, mint, reserveConfigPath, mode, staging, globalAdmin, multisig, reserveKeyPath }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig pubkey is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms);
const tokenMint = address(mint);
const marketAddress = address(market);
const existingMarket = await KaminoMarket.load(
env.c.rpc,
marketAddress,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
false
);
if (existingMarket === null) {
throw new Error(`Market ${marketAddress} does not exist`);
}
const signer = await env.getSigner({ market: existingMarket });
const mintAccount = await fetchMint(env.c.rpc, mint);
const tokenMintProgramId = mintAccount.programAddress;
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
const reserveConfigFromFile = JSON.parse(fs.readFileSync(reserveConfigPath, 'utf8'));
const reserveConfig = parseReserveConfigFromFile(reserveConfigFromFile);
const assetConfig = new AssetReserveConfigCli(tokenMint, tokenMintProgramId, reserveConfig);
const [adminAta] = await findAssociatedTokenPda({
mint: tokenMint,
owner: signer.address,
tokenProgram: tokenMintProgramId,
});
let globalAdminSigner: TransactionSigner | undefined = undefined;
if (globalAdmin) {
globalAdminSigner =
mode === 'multisig' ? noopSigner(address(globalAdmin)) : await parseKeypairFile(globalAdmin as string);
}
let reserveKeypair: TransactionSigner | undefined = undefined;
if (reserveKeyPath) {
reserveKeypair = await parseKeypairFile(reserveKeyPath);
} else {
reserveKeypair = await generateKeyPairSigner();
}
const { createReserveIxs, configUpdateIxs } = await kaminoManager.addAssetToMarketIxs({
admin: signer,
adminLiquiditySource: adminAta,
marketAddress: marketAddress,
assetConfig: assetConfig,
reserveKeypair,
globalAdminSigner,
});
console.log('reserve: ', reserveKeypair.address);
await processTx(
env.c,
signer,
[
...createReserveIxs,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode,
[]
);
const [lut, createLutIxs] = await createUpdateReserveConfigLutIxs(env, marketAddress, reserveKeypair.address);
await processTx(
env.c,
signer,
[
...createLutIxs,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode
);
const lutAcc = await fetchAddressLookupTable(env.c.rpc, lut);
// Split config update instructions into chunks to avoid transaction size limits
const CHUNK_SIZE = 8;
for (let i = 0; i < configUpdateIxs.length; i += CHUNK_SIZE) {
const chunk = configUpdateIxs.slice(i, i + CHUNK_SIZE);
await processTx(
env.c,
signer,
[
...chunk.map((ix) => ix.ix),
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
computeUnits: 400_000,
}),
],
mode,
[lutAcc]
);
}
mode === 'execute' &&
console.log(
'Reserve Created with config:',
JSON.parse(JSON.stringify(reserveConfig)),
'\nreserve address:',
reserveKeypair.address
);
});
commands
.command('update-reserve-config')
.requiredOption('--reserve <string>', 'Reserve address')
.requiredOption('--reserve-config-path <string>', 'Path for the reserve config')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(
'--global-admin <string>',
'Global admin signer (keypair path in execute/simulate modes, pubkey in multisig mode)'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.action(async ({ reserve, reserveConfigPath, mode, staging, globalAdmin, multisig }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig pubkey is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms);
const reserveAddress = address(reserve);
const reserveState = await Reserve.fetch(env.c.rpc, reserveAddress, env.klendProgramId);
if (reserveState === null) {
throw new Error(`Reserve ${reserveAddress} not found`);
}
const marketAddress = reserveState.lendingMarket;
const marketState = await KaminoMarket.load(
env.c.rpc,
marketAddress,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
false
);
if (marketState === null) {
throw new Error(`Market ${marketAddress} not found`);
}
const signer = await env.getSigner({ market: marketState });
const marketWithAddress: MarketWithAddress = {
address: marketAddress,
state: marketState.state,
};
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
const reserveConfigFromFile = JSON.parse(fs.readFileSync(reserveConfigPath, 'utf8'));
const reserveConfig = parseReserveConfigFromFile(reserveConfigFromFile);
const updateIxs = await kaminoManager.updateReserveIxs(
signer,
marketWithAddress,
reserveAddress,
reserveConfig,
reserveState,
globalAdmin
);
// Split config update instructions into chunks to avoid transaction size limits
const CHUNK_SIZE = 8;
for (let i = 0; i < updateIxs.length; i += CHUNK_SIZE) {
const chunk = updateIxs.slice(i, i + CHUNK_SIZE);
await processTx(
env.c,
signer,
[
...chunk.map((ix) => ix.ix),
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
computeUnits: 400_000,
}),
],
mode,
[]
);
}
mode === 'execute' && console.log('Reserve Updated with config -> ', JSON.parse(JSON.stringify(reserveConfig)));
});
commands
.command('download-reserve-config')
.requiredOption('--reserve <string>', 'Reserve address')
.option(`--staging`, 'If true, will use the staging programs')
.action(async ({ reserve, staging }) => {
const env = await initEnv(undefined, staging);
const reserveAddress = address(reserve);
const reserveState = await Reserve.fetch(env.c.rpc, reserveAddress, env.klendProgramId);
if (!reserveState) {
throw new Error('Reserve not found');
}
fs.mkdirSync('./configs/' + reserveState.lendingMarket, { recursive: true });
const decoder = new TextDecoder('utf-8');
const reserveName = decoder.decode(Uint8Array.from(reserveState.config.tokenInfo.name)).replace(/\0/g, '');
const reserveConfigDisplay = parseReserveConfigToFile(reserveState.config);
fs.writeFileSync(
'./configs/' + reserveState.lendingMarket + '/' + reserveName + '.json',
JSON.stringify(reserveConfigDisplay, null, 2)
);
});
commands
.command('init-kvault-global-config')
.requiredOption(
'--mode <string>',
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.option('--signer-path <string>', 'If set, it will use the provided signer')
.action(async ({ mode, staging, devnet, multisig, signerPath }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, signerPath, undefined, devnet);
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
let signer: TransactionSigner | undefined = undefined;
if (signerPath) {
signer = await parseKeypairFile(signerPath);
} else {
const programData = await programDataPda(env.kvaultProgramId);
const programDataInfo = await env.c.rpc.getAccountInfo(programData).send();
if (programDataInfo === null) {
throw new Error('KVault program data not found');
}
const programAdmin = programDataInfo.value?.owner.toString();
if (!programAdmin) {
throw new Error('Program admin not found');
}
signer = noopSigner(address(programAdmin));
}
const ix = await kaminoManager.initKvaultGlobalConfigIx(signer);
await processTx(
env.c,
signer,
[
ix,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode,
[]
);
mode === 'execute' && console.log('KVault global config initialized');
});
commands
.command('update-kvault-global-config')
.requiredOption(
'--mode <string>',
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.requiredOption('--field <string>', 'The field to update')
.requiredOption('--value <string>', 'The value to update the field to')
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.option('--signer-path <string>', 'If set, it will use the provided signer')
.action(async ({ mode, field, value, staging, devnet, multisig, signerPath }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, signerPath, undefined, devnet);
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
let signer: TransactionSigner | undefined = undefined;
if (signerPath) {
signer = await parseKeypairFile(signerPath);
} else {
const globalConfigAddress = await getKvaultGlobalConfigPda(env.kvaultProgramId);
const globalConfigState = await KVaultGlobalConfig.fetch(env.c.rpc, globalConfigAddress);
if (!globalConfigState) {
throw new Error('Global config not found');
}
signer = noopSigner(address(globalConfigState.globalAdmin));
}
const vaultClient = kaminoManager.getKaminoVaultClient();
const ix = await vaultClient.updateGlobalConfigIx(field, value);
await processTx(env.c, signer, [ix, ...getPriorityFeeAndCuIxs({ priorityFeeMultiplier: 2500 })], mode, []);
mode === 'execute' && console.log(`Global config updated to ${value} for field ${field}`);
});
commands
.command('accept-kvault-global-config-ownership')
.requiredOption(
'--mode <string>',
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.option('--signer-path <string>', 'If set, it will use the provided signer')
.action(async ({ mode, staging, devnet, multisig, signerPath }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, undefined, undefined, devnet);
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
let signer: TransactionSigner | undefined = undefined;
if (signerPath) {
signer = await parseKeypairFile(signerPath);
} else {
const globalConfigAddress = await getKvaultGlobalConfigPda(env.kvaultProgramId);
const globalConfigState = await KVaultGlobalConfig.fetch(env.c.rpc, globalConfigAddress);
if (!globalConfigState) {
throw new Error('Global config not found');
}
signer = noopSigner(address(globalConfigState.pendingAdmin));
}
const vaultClient = kaminoManager.getKaminoVaultClient();
const ix = await vaultClient.acceptGlobalConfigOwnershipIx(signer);
await processTx(env.c, signer, [ix, ...getPriorityFeeAndCuIxs({ priorityFeeMultiplier: 2500 })], mode, []);
mode === 'execute' && console.log('Global config ownership accepted');
});
commands
.command('create-vault')
.requiredOption('--mint <string>', 'Vault token mint')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.requiredOption('--name <string>', 'The onchain name of the strat')
.requiredOption('--tokenName <string>', 'The name of the token in the vault')
.requiredOption('--extraTokenName <string>', 'The extra string appended to the token symbol')
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.action(async ({ mint, mode, name, tokenName, extraTokenName, staging, devnet, multisig }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, undefined, undefined, devnet);
const tokenMint = address(mint);
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
const admin = await env.getSigner();
const tokenProgramID = await getAccountOwner(env.c.rpc, tokenMint);
const kaminoVaultConfig = new KaminoVaultConfig({
admin,
tokenMint: tokenMint,
tokenMintProgramId: tokenProgramID,
performanceFeeRatePercentage: new Decimal(0.0),
managementFeeRatePercentage: new Decimal(0.0),
name,
vaultTokenSymbol: tokenName,
vaultTokenName: extraTokenName,
});
const useDevnetFarms = devnet ? true : false;
const { vault: vaultKp, initVaultIxs: instructions } = await kaminoManager.createVaultIxs(
kaminoVaultConfig,
useDevnetFarms
);
await processTx(
env.c,
admin,
[
...instructions.createAtaIfNeededIxs,
...instructions.initVaultIxs,
instructions.createLUTIx,
instructions.setFarmToVaultIx,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode,
[]
);
await sleep(2000);
// create the farm
await processTx(
env.c,
admin,
[
...instructions.createVaultFarm.setupFarmIxs,
...instructions.createVaultFarm.updateFarmIxs,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode,
[]
);
await sleep(2000);
await processTx(
env.c,
admin,
[
...instructions.populateLUTIxs,
...instructions.cleanupIxs,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode,
[]
);
await processTx(
env.c,
admin,
[
instructions.initSharesMetadataIx,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode,
[]
);
mode === 'execute' && console.log('Vault created:', vaultKp.address);
});
commands
.command('set-shares-metadata')
.requiredOption('--vault <string>', 'Vault address')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.requiredOption('--symbol <string>', 'The symbol of the kVault token')
.requiredOption('--extraName <string>', 'The name of the kVault token, appended to the symbol')
.option(`--staging`, 'If true, will use the staging programs')
.option(`--CU <number>`, 'The number of compute units to use for the transaction')
.action(async ({ vault, mode, symbol, extraName, staging, CU: cu }) => {
const env = await initEnv(undefined, staging);
const kVault = new KaminoVault(env.c.rpc, address(vault));
const computeUnits = cu ? cu : DEFAULT_CU_PER_TX;
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
const vaultState = await kVault.getState();
const signer = await env.getSigner({ vaultState });
const ix = await kaminoManager.getSetSharesMetadataIx(signer, kVault, symbol, extraName);
await processTx(
env.c,
signer,
[
ix,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
computeUnits,
}),
],
mode,
[]
);
});
commands
.command('update-vault-pending-admin')
.requiredOption('--vault <string>', 'Vault address')
.requiredOption('--new-admin <string>', 'Pubkey of the new admin')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--CU <number>`, 'The number of compute units to use for the transaction')
.action(async ({ vault, newAdmin, mode, staging, devnet, CU: cu }) => {
const env = await initEnv(staging, undefined, undefined, undefined, devnet);
const vaultAddress = address(vault);
const computeUnits = cu ? cu : DEFAULT_CU_PER_TX;
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
const kaminoVault = new KaminoVault(env.c.rpc, vaultAddress, undefined, env.kvaultProgramId);
const vaultState = await kaminoVault.getState();
const signer = await env.getSigner({ vaultState });
const instructions = await kaminoManager.updateVaultConfigIxs(
kaminoVault,
new PendingVaultAdmin(),
newAdmin,
signer,
undefined,
true
);
await processTx(
env.c,
signer,
[
instructions.updateVaultConfigIx,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
computeUnits,
}),
],
mode,
[]
);
mode === 'execute' && console.log(`Pending admin updated to ${newAdmin}`);
});
commands
.command('update-vault-config')
.requiredOption('--vault <string>', 'Vault address')
.requiredOption('--field <string>', 'The field to update')
.requiredOption('--value <string>', 'The value to update the field to')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--skip-lut-update`, 'If set, it will skip the LUT update')
.option(
`--lutSigner <string>`,
'If set, it will use the provided signer instead of the default one for the LUT update'
)
.option(
`--global-admin <string>`,
'Global admin signer (keypair path in execute/simulate modes, pubkey in multisig mode). Required when setting AllowInvestInWhitelistedReservesOnly or AllowAllocationsInWhitelistedReservesOnly to false'
)
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.option(
`--error-on-override`,
'If set, it will throw an error if the vault already has a farm, if you want to override it set errorOnOverride to false'
)
.option(`--CU <number>`, 'The number of compute units to use for the transaction')
.action(
async ({
vault,
field,
value,
mode,
staging,
devnet,
skipLutUpdate,
lutSigner,
globalAdmin,
multisig,
errorOnOverride,
CU: cu,
}) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig pubkey is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, undefined, undefined, devnet);
const computeUnits = cu ? cu : DEFAULT_CU_PER_TX;
const vaultAddress = address(vault);
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId
);
const kaminoVault = new KaminoVault(env.c.rpc, vaultAddress, undefined, env.kvaultProgramId);
const vaultState = await kaminoVault.getState();
// Use global admin signer if provided, otherwise fall back to vault admin/noop signer depending on mode
let signer;
if (mode === 'multisig' && globalAdmin) {
signer = noopSigner(address(globalAdmin));
} else if (globalAdmin) {
signer = await parseKeypairFile(globalAdmin as string);
} else {
signer = await env.getSigner({ vaultState });
}
let lutSignerOrUndefined = undefined;
if (lutSigner) {
lutSignerOrUndefined = await parseKeypairFile(lutSigner as string);
}
const shouldSkipLutUpdate = !!skipLutUpdate;
const instructions = await kaminoManager.updateVaultConfigIxs(
kaminoVault,
field,
value,
signer,
lutSignerOrUndefined,
shouldSkipLutUpdate,
errorOnOverride
);
await processTx(
env.c,
signer,
[
instructions.updateVaultConfigIx,
...instructions.updateLUTIxs,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
computeUnits,
}),
],
mode,
[]
);
mode === 'execute' && console.log('Vault updated');
}
);
commands
.command('add-update-whitelisted-reserve')
.requiredOption('--reserve <string>', 'Reserve address to whitelist')
.requiredOption('--whitelist-mode <string>', 'Whitelist mode: "Invest" or "AddAllocation"')
.requiredOption(
'--value <string>',
'Value: "1" or "true" to add to whitelist, "0" or "false" to remove from whitelist'
)
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.requiredOption(
'--global-admin <string>',
'Global admin signer (keypair path in execute/simulate modes, pubkey in multisig mode)'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.option(`--CU <number>`, 'The number of compute units to use for the transaction')
.action(async ({ reserve, whitelistMode, value, mode, globalAdmin, staging, devnet, multisig, CU: cu }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig pubkey is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, undefined, undefined, devnet);
const computeUnits = cu ? cu : DEFAULT_CU_PER_TX;
const reserveAddress = address(reserve);
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
// Parse the value (1/true = add, 0/false = remove)
const flagValue = parseBooleanFlag(value);
// Parse the whitelist mode
let whitelistModeEnum;
if (whitelistMode === 'Invest') {
whitelistModeEnum = new UpdateReserveWhitelistMode.Invest([flagValue]);
} else if (whitelistMode === 'AddAllocation') {
whitelistModeEnum = new UpdateReserveWhitelistMode.AddAllocation([flagValue]);
} else {
throw new Error(`Invalid whitelist mode '${whitelistMode}'. Expected 'Invest' or 'AddAllocation'.`);
}
let globalAdminSigner;
if (mode === 'multisig') {
globalAdminSigner = noopSigner(address(globalAdmin));
} else {
globalAdminSigner = await parseKeypairFile(globalAdmin as string);
}
const instruction = await kaminoManager.addUpdateWhitelistedReserveIx(
reserveAddress,
whitelistModeEnum,
globalAdminSigner
);
await processTx(
env.c,
globalAdminSigner,
[
instruction,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
computeUnits,
}),
],
mode,
[]
);
mode === 'execute' &&
console.log(
`Reserve ${reserveAddress} whitelisted for ${whitelistMode} with value ${flagValue ? 'ALLOW' : 'DENY'}`
);
});
commands
.command('whitelist-reserves')
.requiredOption(
'--reserves-file <string>',
'Path to a file containing newline-separated reserve addresses to whitelist'
)
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.requiredOption(
'--global-admin <string>',
'Global admin signer (keypair path in execute/simulate modes, pubkey in multisig mode)'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.option(`--CU <number>`, 'The number of compute units to use for the transaction')
.action(async ({ reservesFile, mode, globalAdmin, staging, multisig, CU: cu }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig pubkey is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms);
const computeUnits = cu ? cu : DEFAULT_CU_PER_TX;
const fileContent = fs.readFileSync(reservesFile, 'utf-8');
const reserveAddresses = fileContent
.split('\n')
.map((r: string) => r.trim())
.filter((r: string) => r.length > 0)
.map((r: string) => address(r));
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
let globalAdminSigner;
if (mode === 'multisig') {
globalAdminSigner = noopSigner(address(globalAdmin));
} else {
globalAdminSigner = await parseKeypairFile(globalAdmin as string);
}
const instructions: Instruction[] = [];
for (const reserveAddress of reserveAddresses) {
let instruction = await kaminoManager.addUpdateWhitelistedReserveIx(
reserveAddress,
new UpdateReserveWhitelistMode.Invest([1]),
globalAdminSigner
);
instructions.push(instruction);
instruction = await kaminoManager.addUpdateWhitelistedReserveIx(
reserveAddress,
new UpdateReserveWhitelistMode.AddAllocation([1]),
globalAdminSigner
);
instructions.push(instruction);
}
// batch the instructions in groups of 6
const batchSize = 6;
const batchInstructions: Instruction[][] = [];
for (let i = 0; i < instructions.length; i += batchSize) {
batchInstructions.push(instructions.slice(i, i + batchSize));
}
for (const batch of batchInstructions) {
await processTx(
env.c,
globalAdminSigner,
[...batch, ...getPriorityFeeAndCuIxs({ priorityFeeMultiplier: 2500, computeUnits })],
mode,
[]
);
}
});
commands
.command('backfill-whitelisted-reserves')
.option(
'--value <string>',
'Value: "1" or "true" to add to whitelist, "0" or "false" to remove from whitelist',
'1'
)
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.requiredOption(
'--global-admin <string>',
'Global admin signer (keypair path in execute/simulate modes, pubkey in multisig mode)'
)
.option(
`--markets <string>`,
'Comma-separated list of market addresses. If not provided, all markets will be processed'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.option(`--CU <number>`, 'The number of compute units to use for the transaction')
.action(async ({ value, mode, globalAdmin, markets, staging, devnet, multisig, CU: cu }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig pubkey is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, undefined, undefined, devnet);
const computeUnits = cu ? cu : DEFAULT_CU_PER_TX;
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
let globalAdminSigner;
if (mode === 'multisig') {
globalAdminSigner = noopSigner(address(globalAdmin));
} else {
globalAdminSigner = await parseKeypairFile(globalAdmin as string);
}
// Get markets to process
let marketsToProcess: KaminoMarket[];
if (markets) {
const marketAddresses = markets.split(',').map((m: string) => address(m.trim()));
console.log(`Processing ${marketAddresses.length} specified markets...`);
marketsToProcess = await Promise.all(
marketAddresses.map(async (marketAddress: Address) => {
const market = await KaminoMarket.load(
env.c.rpc,
marketAddress,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId
);
if (!market) {
throw new Error(`Market ${marketAddress} not found`);
}
return market;
})
);
} else {
console.log('Fetching all markets...');
marketsToProcess = await kaminoManager.getAllMarkets(env.klendProgramId);
console.log(`Found ${marketsToProcess.length} markets`);
}
// Collect all reserves from all markets
const allReserves: Address[] = [];
for (const market of marketsToProcess) {
const marketName = parseTokenSymbol(market.state.name);
const reserveAddresses = Array.from(market.reserves.keys());
console.log(`Market ${market.getAddress()} (${marketName}): ${reserveAddresses.length} reserves`);
allReserves.push(...reserveAddresses);
}
console.log(`\nTotal reserves to whitelist: ${allReserves.length}`);
console.log(`Whitelist modes: Invest AND AddAllocation (both will be set)`);
// Parse the value (1/true = add, 0/false = remove)
const flagValue = parseBooleanFlag(value);
console.log(`Action: ${flagValue ? 'ADD to whitelist' : 'REMOVE from whitelist'}`);
if (mode === 'simulate') {
console.log('\nSimulation mode - no transactions will be executed');
}
// Create whitelist mode enums for both Invest and AddAllocation
const investModeEnum = new UpdateReserveWhitelistMode.Invest([flagValue]);
const addAllocationModeEnum = new UpdateReserveWhitelistMode.AddAllocation([flagValue]);
// Process each reserve with both whitelist modes
let successCount = 0;
let errorCount = 0;
for (const reserveAddress of allReserves) {
try {
const investInstruction = await kaminoManager.addUpdateWhitelistedReserveIx(
reserveAddress,
investModeEnum,
globalAdminSigner
);
const addAllocationInstruction = await kaminoManager.addUpdateWhitelistedReserveIx(
reserveAddress,
addAllocationModeEnum,
globalAdminSigner
);
await processTx(
env.c,
globalAdminSigner,
[
investInstruction,
addAllocationInstruction,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
computeUnits,
}),
],
mode,
[]
);
successCount++;
console.log(`✓ [${successCount}/${allReserves.length}] ${reserveAddress} (Invest + AddAllocation)`);
} catch (error) {
errorCount++;
console.error(`✗ Failed to whitelist ${reserveAddress}:`, error);
}
}
console.log(`\nBackfill complete!`);
console.log(`Success: ${successCount} reserves (both modes set)`);
console.log(`Errors: ${errorCount}`);
});
commands
.command('is-reserve-whitelisted')
.requiredOption('--reserve <string>', 'Reserve address to check')
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.action(async ({ reserve, staging, devnet }) => {
const env = await initEnv(staging, undefined, undefined, undefined, devnet);
const reserveAddress = address(reserve);
const pda = await getReserveWhitelistEntryPda(reserveAddress, env.kvaultProgramId);
const entry = await ReserveWhitelistEntry.fetch(env.c.rpc, pda, env.kvaultProgramId);
if (!entry) {
console.log(`Reserve ${reserveAddress}`);
console.log(` PDA: ${pda} (not initialized)`);
console.log(` whitelistInvest: 0`);
console.log(` whitelistAddAllocation: 0`);
} else {
console.log(`Reserve ${reserveAddress}`);
console.log(` PDA: ${pda}`);
console.log(` tokenMint: ${entry.tokenMint}`);
console.log(` whitelistInvest: ${entry.whitelistInvest}`);
console.log(` whitelistAddAllocation: ${entry.whitelistAddAllocation}`);
}
});
commands
.command('check-whitelist-for-mint')
.requiredOption('--mint <string>', 'Token mint address to check across all UI markets')
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.action(async ({ mint, staging, devnet }) => {
const env = await initEnv(staging, undefined, undefined, undefined, devnet);
const tokenMint = address(mint);
const marketsConfig = await getMarketsFromApi({ api: { programId: env.klendProgramId } });
console.log(`Found ${marketsConfig.length} UI markets from CDN\n`);
const markets = await Promise.all(
marketsConfig.map(async (cfg) => {
const market = await KaminoMarket.load(
env.c.rpc,
address(cfg.lendingMarket),
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId
);
return { cfg, market };
})
);
// Collect all reserves for this mint across all markets
const reserveEntries: { marketName: string; marketAddress: string; reserveAddress: Address; symbol: string }[] =
[];
for (const { cfg, market } of markets) {
if (!market) continue;
const reserve = market.getReserveByMint(tokenMint);
if (reserve) {
reserveEntries.push({
marketName: cfg.name,
marketAddress: cfg.lendingMarket,
reserveAddress: reserve.address,
symbol: reserve.symbol,
});
}
}
if (reserveEntries.length === 0) {
console.log(`No reserves found for mint ${tokenMint} in any UI market`);
return;
}
// Derive all PDAs and batch-fetch whitelist entries
const pdas = await Promise.all(
reserveEntries.map((e) => getReserveWhitelistEntryPda(e.reserveAddress, env.kvaultProgramId))
);
const entries = await ReserveWhitelistEntry.fetchMultiple(env.c.rpc, pdas, env.kvaultProgramId);
let missingCount = 0;
for (let i = 0; i < reserveEntries.length; i++) {
const r = reserveEntries[i];
const entry = entries[i];
const pda = pdas[i];
const invest = entry ? entry.whitelistInvest : 0;
const addAlloc = entry ? entry.whitelistAddAllocation : 0;
const pdaStatus = entry ? 'initialized' : 'NOT initialized';
const isMissing = !entry || invest === 0 || addAlloc === 0;
if (isMissing) {
missingCount++;
console.log(
`[MISSING] ${r.symbol} reserve ${r.reserveAddress} in market "${r.marketName}" (${r.marketAddress})`
);
console.log(` PDA: ${pda} (${pdaStatus})`);
console.log(` whitelistInvest: ${invest}`);
console.log(` whitelistAddAllocation: ${addAlloc}`);
console.log('');
}
}
if (missingCount === 0) {
console.log(
`All ${reserveEntries.length} reserves for mint ${tokenMint} are fully whitelisted (Invest + AddAllocation)`
);
} else {
console.log(`${missingCount}/${reserveEntries.length} reserves need whitelisting`);
}
});
commands
.command('update-vault-mgmt-fee')
.requiredOption('--vault <string>', 'Vault address')
.requiredOption('--fee-bps <string>', 'Management fee to set (in basis points)')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--CU <number>`, 'The number of compute units to use for the transaction')
.action(async ({ vault, feeBps, mode, staging, devnet, CU: cu }) => {
const env = await initEnv(staging, undefined, undefined, undefined, devnet);
const vaultAddress = address(vault);
const computeUnits = cu ? cu : DEFAULT_CU_PER_TX;
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
const kaminoVault = new KaminoVault(env.c.rpc, vaultAddress, undefined, env.kvaultProgramId);
const vaultState = await kaminoVault.getState();
const signer = await env.getSigner({ vaultState });
const instructions = await kaminoManager.updateVaultConfigIxs(
kaminoVault,
new ManagementFeeBps(),
feeBps,
signer
);
await processTx(
env.c,
signer,
[
instructions.updateVaultConfigIx,
...instructions.updateLUTIxs,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
computeUnits,
}),
],
mode,
[]
);
mode === 'execute' && console.log('Management fee updated');
});
commands
.command('insert-into-lut')
.requiredOption('--lut <string>', 'Lookup table address')
.requiredOption('--addresses <string>', 'The addresses to insert into the LUT, space separated')
.requiredOption(
`--mode <string>`,
'simulate|multisig|execute - simulate - to print txn simulation and to get tx simulation link in explorer, execute - execute tx, multisig - to get bs58 tx for multisig usage'
)
.option(`--staging`, 'If true, will use the staging programs')
.option(`--devnet`, 'If true, will use devnet programs and RPC')
.option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
.option(`--signer <string>`, 'If set, it will use the provided signer instead of the default one')
.action(async ({ lut, addresses, mode, staging, devnet, multisig, signer }) => {
if (mode === 'multisig' && !multisig) {
throw new Error('If using multisig mode, multisig is required');
}
const ms = multisig ? address(multisig) : undefined;
const env = await initEnv(staging, ms, undefined, undefined, devnet);
const lutAddress = address(lut);
let txSigner = await env.getSigner();
// if the signer is provided (path to a keypair) we use it, otherwise we use the default one
if (signer) {
txSigner = await parseKeypairFile(signer as string);
}
const addressesArr = addresses.split(' ').map((a: string) => address(a));
const kaminoManager = new KaminoManager(
env.c.rpc,
DEFAULT_RECENT_SLOT_DURATION_MS,
env.klendProgramId,
env.kvaultProgramId,
undefined,
env.farmsProgramId
);
const instructions = await kaminoManager.insertIntoLutIxs(txSigner, lutAddress, addressesArr);
await processTx(
env.c,
txSigner,
[
...instructions,
...getPriorityFeeAndCuIxs({
priorityFeeMultiplier: 2500,
}),
],
mode,
[]
);
mode === 'execute' && console.log('Management fee updated');
});
commands.command('create-lut').action(async () => {
const env = await initEnv(false);