UNPKG

@kamino-finance/klend-sdk

Version:

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

1,146 lines (1,059 loc) 88 kB
import { Address, Instruction, generateKeyPairSigner, TransactionSigner, Slot, address, Rpc, SolanaRpcApi, GetAccountInfoApi, GetProgramAccountsDatasizeFilter, GetProgramAccountsMemcmpFilter, ProgramDerivedAddress, Base58EncodedBytes, getBase58Decoder, } from '@solana/kit'; import { KaminoVault, KaminoVaultClient, KaminoVaultConfig, kaminoVaultId, MarketOverview, PendingRewardsForUserInVault, ReserveAllocationConfig, ReserveOverview, SimulatedVaultHoldingsWithEarnedInterest, VaultFees, VaultFeesPct, VaultHolder, VaultHoldings, VaultHoldingsWithUSDValue, VaultOverview, VaultReserveTotalBorrowedAndInvested, } from './vault'; import { AddAssetToMarketParams, AllOracleAccounts, CdnResources, CreateKaminoMarketParams, createReserveIxs, DEFAULT_PUBLIC_KEY, DEFAULT_RECENT_SLOT_DURATION_MS, ENV, getAllLendingMarketAccounts, getAllOracleAccounts, getAllReserveAccounts, getReserveOracleConfigs, getTokenOracleDataSync, initLendingMarket, InitLendingMarketAccounts, InitLendingMarketArgs, insertIntoLookupTableIxs, KaminoMarket, KaminoReserve, LendingMarket, lendingMarketAuthPda, MarketWithAddress, parseForChangesReserveConfigAndGetIxs, parseOracleType, parseTokenSymbol, Reserve, ReserveConfigUpdateIx, ReserveWithAddress, ScopeOracleConfig, setOrAppend, updateLendingMarket, UpdateLendingMarketAccounts, UpdateLendingMarketArgs, updateLendingMarketOwner, UpdateLendingMarketOwnerAccounts, } from '../lib'; import { PROGRAM_ID } from '../@codegen/klend/programId'; import { Scope, U16_MAX } from '@kamino-finance/scope-sdk'; import { TokenMetadatas } from '@kamino-finance/scope-sdk/dist/@codegen/scope/accounts/TokenMetadatas'; import BN from 'bn.js'; import { ReserveConfig, UpdateLendingMarketMode, UpdateLendingMarketModeKind } from '../@codegen/klend/types'; import Decimal from 'decimal.js'; import { VaultState } from '../@codegen/kvault/accounts'; import { getProgramAccounts } from '../utils'; import { UpdateReserveWhitelistModeKind, VaultConfigField, VaultConfigFieldKind } from '../@codegen/kvault/types'; import { AcceptVaultOwnershipIxs, APYs, CreateVaultFarm, DepositIxs, DisinvestAllReservesIxs, InitVaultIxs, ReserveAllocationOverview, SyncVaultLUTIxs, UpdateReserveAllocationIxs, UpdateVaultConfigIxs, UserSharesForVault, VaultComputedAllocation, VaultReleaseCheckResult, WithdrawAndBlockReserveIxs, WithdrawIxs, } from './vault_types'; import { FarmIncentives, Farms, FarmState } from '@kamino-finance/farms-sdk/dist'; import { getSquadsMultisigAdminsAndThreshold, walletIsSquadsMultisig, WalletType } from '../utils/multisig'; import { decodeVaultState } from '../utils/vault'; import { noopSigner } from '../utils/signer'; import { getCreateAccountInstruction, SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; import { SYSVAR_RENT_ADDRESS } from '@solana/sysvars'; import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; import type { AccountInfoBase, AccountInfoWithJsonData, AccountInfoWithPubkey } from '@solana/rpc-types'; import { arrayElementConfigItems, ConfigUpdater } from './configItems'; import { OracleMappings } from '@kamino-finance/scope-sdk/dist/@codegen/scope/accounts'; import { getReserveFarmRewardsAPY as getReserveFarmRewardsAPYUtils, ReserveIncentives } from '../utils/farmUtils'; import { fetchKaminoCdnData } from '../utils/readCdnData'; const base58Decoder = getBase58Decoder(); /** * KaminoManager is a class that provides a high-level interface to interact with the Kamino Lend and Kamino Vault programs, in order to create and manage a market, as well as vaults */ export class KaminoManager { private readonly _rpc: Rpc<SolanaRpcApi>; private readonly _kaminoVaultProgramId: Address; private readonly _kaminoLendProgramId: Address; private readonly _farmsProgramId?: Address; private readonly _vaultClient: KaminoVaultClient; recentSlotDurationMs: number; constructor( rpc: Rpc<SolanaRpcApi>, recentSlotDurationMs?: number, kaminoLendProgramId?: Address, kaminoVaultProgramId?: Address, cdnResources?: CdnResources, farmsProgramId?: Address ) { this._rpc = rpc; this.recentSlotDurationMs = recentSlotDurationMs ?? DEFAULT_RECENT_SLOT_DURATION_MS; this._kaminoVaultProgramId = kaminoVaultProgramId ? kaminoVaultProgramId : kaminoVaultId; this._kaminoLendProgramId = kaminoLendProgramId ? kaminoLendProgramId : PROGRAM_ID; this._farmsProgramId = farmsProgramId; this._vaultClient = new KaminoVaultClient( rpc, this.recentSlotDurationMs, this._kaminoVaultProgramId, this._kaminoLendProgramId, cdnResources, farmsProgramId ); } getRpc() { return this._rpc; } getProgramID() { return this._kaminoVaultProgramId; } /** * This is a function that helps quickly setting up a reserve for an asset with a default config. The config can be modified later on. * @param params.admin - the admin of the market * @returns market keypair - keypair used for market account creation -> to be signed with when executing the transaction * @returns ixs - an array of ixs for creating and initializing the market account */ async createMarketIxs(params: CreateKaminoMarketParams): Promise<{ market: TransactionSigner; ixs: Instruction[] }> { const marketAccount = await generateKeyPairSigner(); const size = BigInt(LendingMarket.layout.span + 8); const [lendingMarketAuthority] = await lendingMarketAuthPda(marketAccount.address, this._kaminoLendProgramId); const createMarketIxs: Instruction[] = []; createMarketIxs.push( getCreateAccountInstruction({ payer: params.admin, newAccount: marketAccount, space: size, lamports: await this._rpc.getMinimumBalanceForRentExemption(size).send(), programAddress: this._kaminoLendProgramId, }) ); const accounts: InitLendingMarketAccounts = { lendingMarketOwner: params.admin, lendingMarket: marketAccount.address, lendingMarketAuthority: lendingMarketAuthority, systemProgram: SYSTEM_PROGRAM_ADDRESS, rent: SYSVAR_RENT_ADDRESS, }; const args: InitLendingMarketArgs = { quoteCurrency: Array(32).fill(0), }; createMarketIxs.push(initLendingMarket(args, accounts, undefined, this._kaminoLendProgramId)); return { market: marketAccount, ixs: createMarketIxs }; } /** * This is a function that helps quickly setting up a reserve for an asset with a default config. The config can be modified later on. * @param params.admin - the admin of the reserve * @param params.marketAddress - the market to create a reserve for, only the market admin can create a reserve for the market * @param params.assetConfig - an object that helps generate a default reserve config with some inputs which have to be configured before calling this function * @returns reserve - keypair used for reserve creation -> to be signed with when executing the transaction * @returns txnIxs - an array of arrays of ixs -> first array for reserve creation, second for updating it with correct params */ async addAssetToMarketIxs(params: AddAssetToMarketParams): Promise<{ createReserveIxs: Instruction[]; configUpdateIxs: ReserveConfigUpdateIx[]; }> { const market = await LendingMarket.fetch(this._rpc, params.marketAddress, this._kaminoLendProgramId); if (!market) { throw new Error('Market not found'); } const marketWithAddress: MarketWithAddress = { address: params.marketAddress, state: market }; const reserve = await Reserve.fetch(this._rpc, params.reserveKeypair.address, this._kaminoLendProgramId); let createReserveInstructions: Instruction[] = []; if (!reserve) { createReserveInstructions = await createReserveIxs( this._rpc, params.admin, params.adminLiquiditySource, params.marketAddress, params.assetConfig.mint, params.assetConfig.mintTokenProgram, params.reserveKeypair, this._kaminoLendProgramId ); } else { console.log('Reserve already exists, skipping creation'); } const configUpdateIxs = await this.updateReserveIxs( params.admin, marketWithAddress, params.reserveKeypair.address, params.assetConfig.getReserveConfig(), undefined, params.globalAdminSigner ); return { createReserveIxs: createReserveInstructions, configUpdateIxs }; } /** * This method initializes the kvault global config (one off, needs to be signed by program owner) * @param admin - the admin of the kvault program * @returns - an instruction to initialize the kvault global config */ async initKvaultGlobalConfigIx(admin: TransactionSigner) { return this._vaultClient.initKvaultGlobalConfigIx(admin); } /** * This method will create a vault with a given config. The config can be changed later on, but it is recommended to set it up correctly from the start * @param vaultConfig - the config object used to create a vault * @returns vault: the keypair of the vault, used to sign the initialization transaction; initVaultIxs: a struct with ixs to initialize the vault and its lookup table + populateLUTIxs, a list to populate the lookup table which has to be executed in a separate transaction */ async createVaultIxs( vaultConfig: KaminoVaultConfig, useDevnetFarms: boolean = false, slot?: Slot ): Promise<{ vault: TransactionSigner; lut: Address; initVaultIxs: InitVaultIxs }> { return this._vaultClient.createVaultIxs(vaultConfig, useDevnetFarms, slot); } /** * This method creates a farm for a vault * @param admin - the admin of the vault * @param vault - the vault to create a farm for (the vault should be already initialized) * @returns a struct with the farm, the setup farm ixs and the update farm ixs */ async createVaultFarmIxs(admin: TransactionSigner, vault: KaminoVault): Promise<CreateVaultFarm> { const vaultState = await vault.getState(); if (!vaultState) { throw new Error('Vault not initialized'); } if (vaultState.vaultFarm !== DEFAULT_PUBLIC_KEY) { throw new Error('Vault already has a farm'); } return this._vaultClient.createVaultFarm(admin, vault.address, vaultState.sharesMint); } /** * This method creates an instruction to set the shares metadata for a vault * @param authority - the vault admin * @param vault - the vault to set the shares metadata for * @param tokenName - the name of the token in the vault (symbol; e.g. "USDC" which becomes "kVUSDC") * @param extraName - the extra string appended to the prefix("Kamino Vault USDC <extraName>") * @returns - an instruction to set the shares metadata for the vault */ async getSetSharesMetadataIx( authority: TransactionSigner, vault: KaminoVault, tokenName: string, extraName: string, metadataProgramId?: Address, kvaultProgramId?: Address ) { const vaultState = await vault.getState(); return this._vaultClient.getSetSharesMetadataIx( this._rpc, authority, vault.address, vaultState.sharesMint, vaultState.baseVaultAuthority, tokenName, extraName, metadataProgramId, kvaultProgramId ); } /** * This method updates the vault reserve allocation cofnig for an exiting vault reserve, or adds a new reserve to the vault if it does not exist. * @param vault - vault to be updated * @param reserveAllocationConfig - new reserve allocation config * @param [signer] - optional parameter to pass a different signer for the instruction. If not provided, the admin of the vault will be used * @returns - a struct with an instruction to update the reserve allocation and an optional list of instructions to update the lookup table for the allocation changes */ async updateVaultReserveAllocationIxs( vault: KaminoVault, reserveAllocationConfig: ReserveAllocationConfig, signer?: TransactionSigner ): Promise<UpdateReserveAllocationIxs> { return this._vaultClient.updateReserveAllocationIxs(vault, reserveAllocationConfig, signer); } /** * This method updates the unallocated weight and cap of a vault (both are optional, if not provided the current values will be used) * @param vault - the vault to update the unallocated weight and cap for * @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs * @param [unallocatedWeight] - the new unallocated weight to set. If not provided, the current unallocated weight will be used * @param [unallocatedCap] - the new unallocated cap to set. If not provided, the current unallocated cap will be used * @returns - a list of instructions to update the unallocated weight and cap */ async updateVaultUnallocatedWeightAndCapIxs( vault: KaminoVault, vaultAdminAuthority?: TransactionSigner, unallocatedWeight?: BN, unallocatedCap?: BN ): Promise<Instruction[]> { return this._vaultClient.updateVaultUnallocatedWeightAndCapIxs( vault, vaultAdminAuthority, unallocatedWeight, unallocatedCap ); } /** * This method removes a reserve from the vault allocation strategy if already part of the allocation strategy * @param vault - vault to remove the reserve from * @param reserve - reserve to remove from the vault allocation strategy * @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs * @returns - an instruction to remove the reserve from the vault allocation strategy or undefined if the reserve is not part of the allocation strategy */ async removeReserveFromAllocationIx( vault: KaminoVault, reserve: Address, vaultAdminAuthority?: TransactionSigner ): Promise<Instruction | undefined> { return this._vaultClient.removeReserveFromAllocationIx(vault, reserve, vaultAdminAuthority); } /** * This method sets weight to 0, remove tokens and remove from allocation a reserve from the vault * @param signer - signer to use for the transaction * @param kaminoVault - vault to remove the reserve from * @param reserveAddress - reserve to remove from the vault allocation strategy * @param [reserveState] - optional parameter to pass a reserve state. If not provided, the reserve will be fetched from the connection * @param [currentSlot] - optional slot. If not provided, the latest confirmed slot will be fetched * @returns - an array of instructions to set the reserve allocation to 0, invest the reserve if it has tokens, and remove the reserve from the allocation */ async fullRemoveReserveFromVaultIxs( signer: TransactionSigner, kaminoVault: KaminoVault, reserveAddress: Address, reserveState?: Reserve, currentSlot?: Slot ): Promise<Instruction[]> { const connection = this.getRpc(); const vaultState = await kaminoVault.getState(); const allocations = this.getVaultReserves(vaultState); if (!allocations.includes(reserveAddress)) { throw new Error('Reserve not found in vault allocations'); } const fetchedReserveState = reserveState ?? (await Reserve.fetch(connection, reserveAddress, this._kaminoLendProgramId)); if (!fetchedReserveState) { throw new Error('Reserve not found'); } const reserveWithAddress: ReserveWithAddress = { address: reserveAddress, state: fetchedReserveState, }; const kaminoReserve = await KaminoReserve.initializeFromAddress( reserveAddress, connection, this.recentSlotDurationMs, fetchedReserveState ); const reserveAllocationConfig = new ReserveAllocationConfig(reserveWithAddress, 0, new Decimal(0)); const setAllocationToZeroIx = await this.updateVaultReserveAllocationIxs( kaminoVault, reserveAllocationConfig, signer ); const investIx = await this.investSingleReserveIxs(signer, kaminoVault, reserveWithAddress); const removeAllocationIx = await this.removeReserveFromAllocationIx(kaminoVault, reserveAddress, signer); const ixs = [setAllocationToZeroIx.updateReserveAllocationIx]; const slot = currentSlot ?? (await connection.getSlot({ commitment: 'confirmed' }).send()); const suppliedInReserve = this.getSuppliedInReserve(vaultState, slot, kaminoReserve); if (suppliedInReserve.gt(new Decimal(0))) { ixs.push(...investIx); } if (removeAllocationIx) { ixs.push(removeAllocationIx); } return ixs; } /** * This method withdraws all the funds from a reserve and blocks it from being invested by setting its weight and ctoken allocation to 0 * @param vault - the vault to withdraw the funds from * @param reserve - the reserve to withdraw the funds from * @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs * @returns - a struct with an instruction to update the reserve allocation and an optional list of instructions to update the lookup table for the allocation changes */ async withdrawEverythingAndBlockInvestReserve( vault: KaminoVault, reserve: Address, vaultAdminAuthority?: TransactionSigner ): Promise<WithdrawAndBlockReserveIxs> { return this._vaultClient.withdrawEverythingAndBlockInvestReserve(vault, reserve, vaultAdminAuthority); } /** * This method withdraws all the funds from all the reserves and blocks them from being invested by setting their weight and ctoken allocation to 0 * @param vault - the vault to withdraw the invested funds from * @param [vaultReservesMap] - optional parameter to pass a map of the vault reserves. If not provided, the reserves will be loaded from the vault * @param [payer] - optional parameter to pass a different payer for the transaction. If not provided, the admin of the vault will be used; this is the payer for the invest ixs and it should have an ATA and some lamports (2x no_of_reserves) of the token vault * @returns - a struct with an instruction to update the reserve allocation and an optional list of instructions to update the lookup table for the allocation changes */ async withdrawEverythingFromAllReservesAndBlockInvest( vault: KaminoVault, vaultReservesMap?: Map<Address, KaminoReserve>, payer?: TransactionSigner ): Promise<WithdrawAndBlockReserveIxs> { return this._vaultClient.withdrawEverythingFromAllReservesAndBlockInvest(vault, vaultReservesMap, payer); } /** * This method disinvests all the funds from all the reserves and set their weight to 0; for vaults that are managed by external bot/crank, the bot can change the weight and invest in the reserves again * @param vault - the vault to disinvest the invested funds from * @param [vaultReservesMap] - optional parameter to pass a map of the vault reserves. If not provided, the reserves will be loaded from the vault * @param [payer] - optional parameter to pass a different payer for the transaction. If not provided, the admin of the vault will be used; this is the payer for the invest ixs and it should have an ATA and some lamports (2x no_of_reserves) of the token vault * @returns - a struct with an instruction to update the reserve allocations to 0 weight and a list of instructions to disinvest the funds in the reserves */ async disinvestAllReservesIxs( vault: KaminoVault, vaultReservesMap?: Map<Address, KaminoReserve>, payer?: TransactionSigner ): Promise<DisinvestAllReservesIxs> { return this._vaultClient.disinvestAllReservesIxs(vault, vaultReservesMap, payer); } // async closeVault(vault: KaminoVault): Promise<TransactionInstruction> { // return this._vaultClient.closeVaultIx(vault); // } /** * This method retruns the reserve config for a given reserve * @param reserve - reserve to get the config for * @param [reserveState] - optional reserve state. If provided, the fetch will be skipped * @returns - the reserve config */ async getReserveConfig(reserve: Address, reserveState?: Reserve): Promise<ReserveConfig> { const state = reserveState ?? (await Reserve.fetch(this._rpc, reserve)); if (!state) { throw new Error('Reserve not found'); } return state.config; } /** * This function enables the update of the scope oracle configuration. In order to get a list of scope prices, getScopeOracleConfigs can be used * @param lendingMarketOwner - market admin * @param market - lending market which owns the reserve * @param reserve - reserve which to be updated * @param oraclePrices - scope OraclePrices account pubkey * @param scopeOracleConfig - new scope oracle config * @param scopeTwapConfig - new scope twap config * @param maxAgeBufferSeconds - buffer to be added to onchain max_age - if oracle price is older than that, txns interacting with the reserve will fail * @returns - an array of instructions used update the oracle configuration */ async updateReserveScopeOracleConfigurationIxs( lendingMarketOwner: TransactionSigner, market: MarketWithAddress, reserve: ReserveWithAddress, oraclePrices: Address, scopeOracleConfig: ScopeOracleConfig, scopeTwapConfig?: ScopeOracleConfig, maxAgeBufferSeconds: number = 20 ): Promise<Instruction[]> { const reserveConfig = reserve.state.config; let scopeTwapId = U16_MAX; if (scopeTwapConfig) { scopeTwapId = scopeTwapConfig.oracleId; // if(scopeTwapConfig.twapSourceId !== scopeOracleConfig.oracleId) { // throw new Error('Twap source id must match oracle id'); // } } const { scopeConfiguration } = getReserveOracleConfigs({ scopePriceConfigAddress: oraclePrices, scopeChain: [scopeOracleConfig.oracleId], scopeTwapChain: [scopeTwapId], }); const newReserveConfig = new ReserveConfig({ ...reserveConfig, tokenInfo: { ...reserveConfig.tokenInfo, scopeConfiguration: scopeConfiguration, // TODO: Decide if we want to keep this maxAge override for twap & price maxAgeTwapSeconds: scopeTwapConfig ? new BN(scopeTwapConfig.max_age + maxAgeBufferSeconds) : reserveConfig.tokenInfo.maxAgeTwapSeconds, maxAgePriceSeconds: new BN(scopeOracleConfig.max_age + maxAgeBufferSeconds), }, }); const updateIxs = await this.updateReserveIxs( lendingMarketOwner, market, reserve.address, newReserveConfig, reserve.state ); return updateIxs.map((item) => item.ix); } /** * This function updates the given reserve with a new config. It updates fields which differ between given reserve config and existing reserve config * @param lendingMarketOwner - market authority * @param marketWithAddress - the market that owns the reserve to be updated * @param reserve - the reserve to be updated * @param config - the new reserve configuration to be used for the update * @param reserveStateOverride - the reserve state, useful to provide, if already fetched outside this method, in order to avoid an extra rpc call to fetch it. Make sure the reserveConfig has not been updated since fetching the reserveState that you pass in. * @param globalAdminSigner - optional global admin signer for config modes that require it * @returns - an array of update instructions with metadata indicating if global admin is required as signer. * If there are many fields that are being updated, multiple transactions might be required to fit all ixs. */ async updateReserveIxs( lendingMarketOwner: TransactionSigner, marketWithAddress: MarketWithAddress, reserve: Address, config: ReserveConfig, reserveStateOverride?: Reserve, globalAdminSigner?: TransactionSigner ): Promise<ReserveConfigUpdateIx[]> { const reserveState = reserveStateOverride ? reserveStateOverride : (await Reserve.fetch(this._rpc, reserve, this._kaminoLendProgramId))!; const ixs: ReserveConfigUpdateIx[] = []; ixs.push( ...(await parseForChangesReserveConfigAndGetIxs( marketWithAddress, reserveState, reserve, config, this._kaminoLendProgramId, lendingMarketOwner, globalAdminSigner )) ); return ixs; } /** * This function creates instructions to deposit into a vault. It will also create ATA creation instructions for the vault shares that the user receives in return * @param user - user to deposit * @param vault - vault to deposit into (if the state is not provided, it will be fetched) * @param tokenAmount - token amount to be deposited, in decimals (will be converted in lamports) * @param [vaultReservesMap] - optional parameter; a hashmap from each reserve pubkey to the reserve state. Optional. If provided the function will be significantly faster as it will not have to fetch the reserves * @param [farmState] - the state of the vault farm, if the vault has a farm. Optional. If not provided, it will be fetched * @returns - an instance of DepositIxs which contains the instructions to deposit in vault and the instructions to stake the shares in the farm if the vault has a farm */ async depositToVaultIxs( user: TransactionSigner, vault: KaminoVault, tokenAmount: Decimal, vaultReservesMap?: Map<Address, KaminoReserve>, farmState?: FarmState, payer?: TransactionSigner ): Promise<DepositIxs> { return this._vaultClient.depositIxs(user, vault, tokenAmount, vaultReservesMap, farmState, payer); } /** * This function creates instructions to buy shares (i.e. deposit) into a vault. It will also create ATA creation instructions for the vault shares that the user receives in return * @param user - user to nuy shares * @param vault - vault to buy shares from (if the state is not provided, it will be fetched) * @param tokenAmount - token amount to be swapped for shares, in decimals (will be converted in lamports) * @param [vaultReservesMap] - optional parameter; a hashmap from each reserve pubkey to the reserve state. Optional. If provided the function will be significantly faster as it will not have to fetch the reserves * @param [farmState] - the state of the vault farm, if the vault has a farm. Optional. If not provided, it will be fetched * @param [payer] - optional parameter to pass a different payer for ATA creation rent. If not provided, the user will be used * @returns - an instance of DepositIxs which contains the instructions to buy shares in vault and the instructions to stake the shares in the farm if the vault has a farm */ async buyVaultSharesIxs( user: TransactionSigner, vault: KaminoVault, tokenAmount: Decimal, vaultReservesMap?: Map<Address, KaminoReserve>, farmState?: FarmState, payer?: TransactionSigner ): Promise<DepositIxs> { return this._vaultClient.buySharesIxs(user, vault, tokenAmount, vaultReservesMap, farmState, payer); } /** * This function creates instructions to stake the shares in the vault farm if the vault has a farm * @param user - user to stake * @param vault - vault to deposit into its farm (if the state is not provided, it will be fetched) * @param [sharesAmount] - token amount to be deposited, in decimals (will be converted in lamports). Optional. If not provided, the user's share balance will be used * @param [farmState] - the state of the vault farm, if the vault has a farm. Optional. If not provided, it will be fetched * @returns - a list of instructions for the user to stake shares into the vault's farm, including the creation of prerequisite accounts if needed */ async stakeSharesIxs( user: TransactionSigner, vault: KaminoVault, sharesAmount?: Decimal, farmState?: FarmState ): Promise<Instruction[]> { return this._vaultClient.stakeSharesIxs(user, vault, sharesAmount, farmState); } /** * Update a field of the vault. If the field is a pubkey it will return an extra instruction to add that account into the lookup table * @param vault the vault to update * @param mode the field to update (based on VaultConfigFieldKind enum) * @param value the value to update the field with * @param [signer] the signer of the transaction. Optional. If not provided the admin of the vault will be used. It should be used when changing the admin of the vault if we want to build or batch multiple ixs in the same tx * @param [lutIxsSigner] the signer of the transaction to be used for the lookup table instructions. Optional. If not provided the admin of the vault will be used. It should be used when changing the admin of the vault if we want to build or batch multiple ixs in the same tx * @param [skipLutUpdate] if true, the lookup table instructions will not be included in the returned instructions * @param errorOnOverride throw error if vault already has a farm * @returns a struct that contains the instruction to update the field and an optional list of instructions to update the lookup table */ async updateVaultConfigIxs( vault: KaminoVault, mode: VaultConfigFieldKind | string, value: string, signer?: TransactionSigner, lutIxsSigner?: TransactionSigner, skipLutUpdate: boolean = false, errorOnOverride: boolean = true ): Promise<UpdateVaultConfigIxs> { if (typeof mode === 'string') { const field = VaultConfigField.fromDecoded({ [mode]: '' }); return this._vaultClient.updateVaultConfigIxs( vault, field, value, signer, lutIxsSigner, skipLutUpdate, errorOnOverride ); } return this._vaultClient.updateVaultConfigIxs( vault, mode, value, signer, lutIxsSigner, skipLutUpdate, errorOnOverride ); } /** * Add or update a reserve whitelist entry. This controls whether the reserve is whitelisted for adding/updating * allocations or for invest, depending on the mode parameter. * * @param reserve - Address of the reserve to whitelist * @param mode - The whitelist mode: either 'Invest' or 'AddAllocation' with a value (1 = add, 0 = remove) * @param globalAdmin - The global admin that signs the transaction * @returns - An instruction to add/update the whitelisted reserve entry */ async addUpdateWhitelistedReserveIx( reserve: Address, mode: UpdateReserveWhitelistModeKind, globalAdmin: TransactionSigner ): Promise<Instruction> { return this._vaultClient.addUpdateWhitelistedReserveIx(reserve, mode, globalAdmin); } /** Sets the farm where the shares can be staked. This is store in vault state and a vault can only have one farm, so the new farm will ovveride the old farm * @param vault - vault to set the farm for * @param farm - the farm where the vault shares can be staked * @param [errorOnOverride] - if true, the function will throw an error if the vault already has a farm. If false, it will override the farm * @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs * @param [lutIxsSigner] (optional) signer of the LUT ixs * @param skipLutUpdate if true, the lookup table instructions will not be included in the returned instructions */ async setVaultFarmIxs( vault: KaminoVault, farm: Address, errorOnOverride: boolean = true, vaultAdminAuthority?: TransactionSigner, lutIxsSigner?: TransactionSigner, skipLutUpdate: boolean = false ): Promise<UpdateVaultConfigIxs> { return this._vaultClient.setVaultFarmIxs( vault, farm, errorOnOverride, vaultAdminAuthority, lutIxsSigner, skipLutUpdate ); } /** * This function creates the instruction for the `pendingAdmin` of the vault to accept to become the owner of the vault (step 2/2 of the ownership transfer) * @param vault - vault to change the ownership for * @param [pendingAdmin] - pending vault admin - a noop vaultAdminAuthority is provided when absent for multisigs * @returns - an instruction to accept the ownership of the vault and a list of instructions to update the lookup table */ async acceptVaultOwnershipIxs( vault: KaminoVault, pendingAdmin?: TransactionSigner, slot?: Slot ): Promise<AcceptVaultOwnershipIxs> { return this._vaultClient.acceptVaultOwnershipIxs(vault, pendingAdmin, slot); } /** * This function creates the instruction for the admin to give up a part of the pending fees (which will be accounted as part of the vault) * @param vault - vault to give up pending fees for * @param maxAmountToGiveUp - the maximum amount of fees to give up, in tokens * @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs * @returns - an instruction to give up the specified pending fees */ async giveUpPendingFeesIx( vault: KaminoVault, maxAmountToGiveUp: Decimal, vaultAdminAuthority?: TransactionSigner ): Promise<Instruction> { return this._vaultClient.giveUpPendingFeesIx(vault, maxAmountToGiveUp, vaultAdminAuthority); } /** * This function will return the missing ATA creation instructions, as well as one or multiple withdraw instructions, based on how many reserves it's needed to withdraw from. This might have to be split in multiple transactions * @param user - user to withdraw * @param vault - vault to withdraw from * @param shareAmount - share amount to withdraw (in tokens, not lamports), in order to withdraw everything, any value > user share amount * @param slot - current slot, used to estimate the interest earned in the different reserves with allocation from the vault * @param [vaultReservesMap] - optional parameter; a hashmap from each reserve pubkey to the reserve state. If provided the function will be significantly faster as it will not have to fetch the reserves * @param [farmState] - the state of the vault farm, if the vault has a farm. Optional. If not provided, it will be fetched * @returns an array of instructions to create missing ATAs if needed and the withdraw instructions */ async withdrawFromVaultIxs( user: TransactionSigner, vault: KaminoVault, shareAmount: Decimal, slot: Slot, vaultReservesMap?: Map<Address, KaminoReserve>, farmState?: FarmState, payer?: TransactionSigner ): Promise<WithdrawIxs> { return this._vaultClient.withdrawIxs(user, vault, shareAmount, slot, vaultReservesMap, farmState, payer); } /** * This function will return the missing ATA creation instructions, as well as one or multiple withdraw instructions, based on how many reserves it's needed to withdraw from. This might have to be split in multiple transactions * @param user - user to sell shares for vault tokens * @param vault - vault to sell shares from * @param shareAmount - share amount to sell (in tokens, not lamports), in order to withdraw everything, any value > user share amount * @param slot - current slot, used to estimate the interest earned in the different reserves with allocation from the vault * @param [vaultReservesMap] - optional parameter; a hashmap from each reserve pubkey to the reserve state. If provided the function will be significantly faster as it will not have to fetch the reserves * @param [farmState] - the state of the vault farm, if the vault has a farm. Optional. If not provided, it will be fetched * @param [payer] - optional parameter to pass a different payer for ATA creation rent. If not provided, the user will be used * @returns an array of instructions to create missing ATAs if needed and the withdraw instructions */ async sellVaultSharesIxs( user: TransactionSigner, vault: KaminoVault, shareAmount: Decimal, slot: Slot, vaultReservesMap?: Map<Address, KaminoReserve>, farmState?: FarmState, payer?: TransactionSigner ): Promise<WithdrawIxs> { return this._vaultClient.sellSharesIxs(user, vault, shareAmount, slot, vaultReservesMap, farmState, payer); } /** * This method withdraws all the pending fees from the vault to the owner's token ATA * @param vault - vault for which the admin withdraws the pending fees * @param slot - current slot, used to estimate the interest earned in the different reserves with allocation from the vault * @param [vaultAdminAuthority] - vault admin - a noop vaultAdminAuthority is provided when absent for multisigs * @param [vaultReservesMap] - optional parameter; a hashmap from each reserve pubkey to the reserve state. If provided the function will be significantly faster as it will not have to fetch the reserves * @returns - list of instructions to withdraw all pending fees, including the ATA creation instructions if needed */ async withdrawPendingFeesIxs( vault: KaminoVault, slot?: Slot, vaultAdminAuthority?: TransactionSigner, vaultReservesMap?: Map<Address, KaminoReserve> ): Promise<Instruction[]> { return this._vaultClient.withdrawPendingFeesIxs(vault, slot, vaultReservesMap, vaultAdminAuthority); } /** * This method inserts the missing keys from the provided keys into an existent lookup table * @param payer - payer wallet pubkey * @param lut - lookup table to insert the keys into * @param keys - keys to insert into the lookup table * @param [accountsInLUT] - the existent accounts in the lookup table. Optional. If provided, the function will not fetch the accounts in the lookup table * @returns - an array of instructions to insert the missing keys into the lookup table */ async insertIntoLutIxs( payer: TransactionSigner, lut: Address, keys: Address[], accountsInLUT?: Address[] ): Promise<Instruction[]> { return insertIntoLookupTableIxs(this._vaultClient.getConnection(), payer, lut, keys, accountsInLUT); } /** * Sync a vault for lookup table; create and set the LUT for the vault if needed and fill it with all the needed accounts * @param authority - vault admin * @param vault the vault to sync and set the LUT for if needed * @param vaultReserves optional; the state of the reserves in the vault allocation * @returns a struct that contains a list of ix to create the LUT and assign it to the vault if needed + a list of ixs to insert all the accounts in the LUT */ async syncVaultLUTIxs( authority: TransactionSigner, vault: KaminoVault, vaultReserves?: Map<Address, KaminoReserve>, slot?: Slot ): Promise<SyncVaultLUTIxs> { return this._vaultClient.syncVaultLookupTableIxs(authority, vault, vaultReserves, slot); } /** * This method calculates the token per share value. This will always change based on interest earned from the vault, but calculating it requires a bunch of rpc requests. Caching this for a short duration would be optimal * @param vault - vault to calculate tokensPerShare for * @param [slot] - the slot at which we retrieve the tokens per share. Optional. If not provided, the function will fetch the current slot * @param [vaultReservesMap] - hashmap from each reserve pubkey to the reserve state. Optional. If provided the function will be significantly faster as it will not have to fetch the reserves * @param [currentSlot] - the latest confirmed slot. Optional. If provided the function will be faster as it will not have to fetch the latest slot * @returns - token per share value */ async getTokensPerShareSingleVault( vault: KaminoVault, slot?: Slot, vaultReservesMap?: Map<Address, KaminoReserve>, currentSlot?: Slot ): Promise<Decimal> { return this._vaultClient.getTokensPerShareSingleVault(vault, slot, vaultReservesMap, currentSlot); } /** * This method calculates the price of one vault share(kToken) * @param vault - vault to calculate sharePrice for * @param tokenPrice - the price of the vault token (e.g. SOL) in USD * @param [slot] - the slot at which we retrieve the tokens per share. Optional. If not provided, the function will fetch the current slot * @param [vaultReservesMap] - hashmap from each reserve pubkey to the reserve state. Optional. If provided the function will be significantly faster as it will not have to fetch the reserves * @param [currentSlot] - the latest confirmed slot. Optional. If provided the function will be faster as it will not have to fetch the latest slot * @returns - share value in USD */ async getSharePriceInUSD( vault: KaminoVault, tokenPrice: Decimal, slot?: Slot, vaultReservesMap?: Map<Address, KaminoReserve>, currentSlot?: Slot ): Promise<Decimal> { const tokensPerShare = await this.getTokensPerShareSingleVault(vault, slot, vaultReservesMap, currentSlot); return tokensPerShare.mul(tokenPrice); } /** * This method returns the user shares balance for a given vault * @param user - user to calculate the shares balance for * @param vault - vault to calculate shares balance for * @returns - a struct of user share balance (staked in vault farm if the vault has a farm and unstaked) in decimal (not lamports) */ async getUserSharesBalanceSingleVault(user: Address, vault: KaminoVault): Promise<UserSharesForVault> { return this._vaultClient.getUserSharesBalanceSingleVault(user, vault); } /** * This method returns the user shares balance for all existing vaults * @param user - user to calculate the shares balance for * @param vaultsOverride - the kamino vaults if already fetched, in order to reduce rpc calls * @returns - hash map with keyh as vault address and value as user share balance in decimal (not lamports) */ async getUserSharesBalanceAllVaults( user: Address, vaultsOverride?: KaminoVault[] ): Promise<Map<Address, UserSharesForVault>> { return this._vaultClient.getUserSharesBalanceAllVaults(user, vaultsOverride); } /** * This method returns the management and performance fee percentages * @param vaultState - vault to retrieve the fees percentages from * @returns - VaultFeesPct containing management and performance fee percentages */ getVaultFeesPct(vaultState: VaultState): VaultFeesPct { return this._vaultClient.getVaultFeesPct(vaultState); } /** * This method returns the vault name * @param vaultState - vault to retrieve the onchain name for * @returns - the vault name as string */ getDecodedVaultName(vaultState: VaultState): string { return this._vaultClient.decodeVaultName(vaultState.name); } /** * @returns - the KaminoVault client */ getKaminoVaultClient(): KaminoVaultClient { return this._vaultClient; } /** * Get all vaults * @returns an array of all vaults */ async getAllVaults(): Promise<KaminoVault[]> { return this._vaultClient.getAllVaults(); } /** * Get all lending markets * @returns an array of all lending markets */ async getAllMarkets(programId: Address = this._kaminoLendProgramId): Promise<KaminoMarket[]> { // Get all lending markets const marketGenerator = getAllLendingMarketAccounts(this.getRpc(), programId); const lendingMarketPairs: [Address, LendingMarket][] = []; for await (const pair of marketGenerator) { lendingMarketPairs.push(pair); } // Get all reserves const allReserveAccounts = getAllReserveAccounts(this.getRpc(), programId); const reservePairs: [Address, Reserve][] = []; for await (const pair of allReserveAccounts) { reservePairs.push(pair); } const allReserves = reservePairs.map(([, reserve]) => reserve); // Get all oracle accounts const [allOracleAccounts, cdnResourcesData] = await Promise.all([ getAllOracleAccounts(this.getRpc(), allReserves), fetchKaminoCdnData(), ]); // Group reserves by market const marketToReserve = new Map<Address, ReserveWithAddress[]>(); for (const [reserveAddress, reserveState] of reservePairs) { const marketAddress = reserveState.lendingMarket; if (!marketToReserve.has(marketAddress)) { marketToReserve.set(marketAddress, [ { address: reserveAddress, state: reserveState, }, ]); } else { marketToReserve.get(marketAddress)?.push({ address: reserveAddress, state: reserveState, }); } } const combinedMarkets = lendingMarketPairs.map(([pubkey, market]) => { const reserves = marketToReserve.get(pubkey); const reservesByAddress = new Map<Address, KaminoReserve>(); if (!reserves) { console.log(`Market ${pubkey.toString()} ${parseTokenSymbol(market.name)} has no reserves`); } else { const reservesAndOracles = getTokenOracleDataSync(allOracleAccounts, reserves); reservesAndOracles.forEach(([{ address: reserveAddress, state: reserve }, oracle]) => { if (!oracle) { console.log('Manager > getAllMarkets: oracle not found for reserve', reserve.config.tokenInfo.name); return; } const kaminoReserve = KaminoReserve.initialize( reserveAddress, reserve, oracle, this.getRpc(), this.recentSlotDurationMs, cdnResourcesData ); reservesByAddress.set(kaminoReserve.address, kaminoReserve); }); } return KaminoMarket.loadWithReserves(this.getRpc(), market, reservesByAddress, pubkey, this.recentSlotDurationMs); }); return combinedMarkets; } /** * Get all vaults for owner * @param owner the pubkey of the vaults owner * @returns an array of all vaults owned by a given pubkey */ async getAllVaultsForOwner(owner: Address): Promise<KaminoVault[]> { const size = VaultState.layout.span + 8; const filters: (GetProgramAccountsDatasizeFilter | GetProgramAccountsMemcmpFilter)[] = [ { dataSize: BigInt(size), }, { memcmp: { offset: 0n, bytes: base58Decoder.decode(VaultState.discriminator) as Base58EncodedBytes, encoding: 'base58', }, }, { memcmp: { offset: 8n, bytes: owner.toString() as Base58EncodedBytes, encoding: 'base58', }, }, ]; const kaminoVaults = await getProgramAccounts(this._rpc, this._kaminoVaultProgramId, size, filters); return kaminoVaults.map((kaminoVault) => { const kaminoVaultAccount = decodeVaultState(kaminoVault.data); if (!kaminoVaultAccount) { throw Error(`kaminoVault with pubkey ${kaminoVault.address} could not be decoded`); } return KaminoVault.loadWithClientAndState(this._vaultClient, kaminoVault.address, kaminoVaultAccount); }); } /** * Get a list of kaminoVaults * @param vaults - a list of vaults to get the states for; if not provided, all vaults will be fetched * @returns a list of KaminoVaults */ async getVaults(vaults?: Array<Address>): Promise<Array<KaminoVault | null>> { return this._vaultClient.getVaults(vaults); } /** * Get all token accounts that hold shares for a specific share mint * @param shareMint * @returns an array of all holders tokenAccounts pubkeys and their account info */ async getShareTokenAccounts( shareMint: Address ): Promise<AccountInfoWithPubkey<AccountInfoBase & AccountInfoWithJsonData>[]> { //how to get all token accounts for specific mint: https://spl.solana.com/token#finding-all-token-accounts-for-a-specific-mint //get it from the hardcoded token program and create a filter with the actual mint address //datasize:165 filter selects all token accounts, memcmp filter selects based on the mint address withing each token account return this._rpc .getProgramAccounts(TOKEN_PROGRAM_ADDRESS, { filters: [ { dataSize: 165n }, { memcmp: { offset: 0n, bytes: shareMint.toString() as Base58EncodedBytes, encoding: 'base58' } }, ], encoding: 'jsonParsed', }) .send(); } /** * Get all token accounts that hold shares for a specific vault; if you already have the vault state use it in the param so you don't have to fetch it again * @param vault * @returns an array of all holders tokenAccounts pubkeys and their account info */ async getVaultTokenAccounts( vault: KaminoVault ): Promise<AccountInfoWithPubkey<AccountInfoBase & AccountInfoWithJsonData>[]> { const vaultState = await vault.getState(); return this.getShareTokenAccounts(vaultState.sharesMint); } /** * Get all vault token holders * @param vault * @returns an array of all vault holders with their pubkeys and amounts */ getVaultHolders = async (vault: KaminoVault): Promise<VaultHolder[]> => { await vault.getState(); const tokenAccounts = await this.getVaultTokenAccounts(vault); const result: VaultHolder[] = []; for (const tokenAccount of tokenAccounts) { const accountData = tokenAccount.account.data as Readonly<{ parsed: { info: { owner: string; tokenAmount: { uiAmountString: string; }; }; type: string; }; program: string; space: bigint; }>; result.push({ holderPubkey: address(accountData.parsed.info.owner), amount: new Decimal(accountData.parsed.info.tokenAmount.uiAmountString), }); } return result; }; /** * Get all vaults for a given token * @param token - the token to get all vaults for * @returns an array of all vaul