UNPKG

@kamino-finance/klend-sdk

Version:

Typescript SDK for interacting with the Kamino Lending (klend) protocol

1,344 lines (1,226 loc) 126 kB
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);