@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
1,280 lines (1,137 loc) • 131 kB
text/typescript
import { BN } from '@coral-xyz/anchor';
import {
AccountMeta,
AddressLookupTableProgram,
Connection,
GetProgramAccountsResponse,
Keypair,
PublicKey,
SystemProgram,
SYSVAR_INSTRUCTIONS_PUBKEY,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import {
createCloseAccountInstruction,
getAssociatedTokenAddressSync,
NATIVE_MINT,
TOKEN_PROGRAM_ID,
unpackAccount,
} from '@solana/spl-token';
import {
getAssociatedTokenAddress,
getTransferWsolIxs,
getTokenOracleData,
KaminoMarket,
KaminoReserve,
lamportsToDecimal,
PubkeyHashMap,
Reserve,
UserState,
} from '../lib';
import {
DepositAccounts,
DepositArgs,
giveUpPendingFees,
GiveUpPendingFeesAccounts,
GiveUpPendingFeesArgs,
initVault,
InitVaultAccounts,
invest,
InvestAccounts,
removeAllocation,
RemoveAllocationAccounts,
updateAdmin,
UpdateAdminAccounts,
updateReserveAllocation,
UpdateReserveAllocationAccounts,
UpdateReserveAllocationArgs,
updateVaultConfig,
UpdateVaultConfigAccounts,
UpdateVaultConfigArgs,
WithdrawAccounts,
WithdrawArgs,
withdrawFromAvailable,
WithdrawFromAvailableAccounts,
WithdrawFromAvailableArgs,
withdrawPendingFees,
WithdrawPendingFeesAccounts,
} from '../idl_codegen_kamino_vault/instructions';
import { VaultConfigField, VaultConfigFieldKind } from '../idl_codegen_kamino_vault/types';
import { VaultState } from '../idl_codegen_kamino_vault/accounts';
import Decimal from 'decimal.js';
import {
bpsToPct,
decodeVaultName,
getTokenBalanceFromAccountInfoLamports,
numberToLamportsDecimal,
parseTokenSymbol,
pubkeyHashMapToJson,
} from './utils';
import { deposit } from '../idl_codegen_kamino_vault/instructions';
import { withdraw } from '../idl_codegen_kamino_vault/instructions';
import { PROGRAM_ID } from '../idl_codegen/programId';
import { ReserveWithAddress } from './reserve';
import { Fraction } from './fraction';
import {
createAtasIdempotent,
createWsolAtaIfMissing,
getKVaultSharesMetadataPda,
lendingMarketAuthPda,
PublicKeySet,
SECONDS_PER_YEAR,
U64_MAX,
VAULT_INITIAL_DEPOSIT,
} from '../utils';
import bs58 from 'bs58';
import { getAccountOwner, getProgramAccounts } from '../utils/rpc';
import {
AcceptVaultOwnershipIxs,
APYs,
DepositIxs,
InitVaultIxs,
ReserveAllocationOverview,
SyncVaultLUTIxs,
UpdateReserveAllocationIxs,
UpdateVaultConfigIxs,
UserSharesForVault,
WithdrawAndBlockReserveIxs,
WithdrawIxs,
} from './vault_types';
import { batchFetch, collToLamportsDecimal, ZERO } from '@kamino-finance/kliquidity-sdk';
import { FullBPSDecimal } from '@kamino-finance/kliquidity-sdk/dist/utils/CreationParameters';
import { Farms, FarmState } from '@kamino-finance/farms-sdk/dist';
import { getAccountsInLUT, initLookupTableIx } from '../utils/lookupTable';
import {
getFarmStakeIxs,
getFarmUnstakeAndWithdrawIxs,
getSharesInFarmUserPosition,
getUserSharesInFarm,
} from './farm_utils';
import { getInitializeKVaultSharesMetadataIx, getUpdateSharesMetadataIx, resolveMetadata } from '../utils/metadata';
import { decodeVaultState } from '../utils/vault';
export const kaminoVaultId = new PublicKey('KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd');
export const kaminoVaultStagingId = new PublicKey('stKvQfwRsQiKnLtMNVLHKS3exFJmZFsgfzBPWHECUYK');
const TOKEN_VAULT_SEED = 'token_vault';
const CTOKEN_VAULT_SEED = 'ctoken_vault';
const BASE_VAULT_AUTHORITY_SEED = 'authority';
const SHARES_SEED = 'shares';
const EVENT_AUTHORITY_SEED = '__event_authority';
export const METADATA_SEED = 'metadata';
export const METADATA_PROGRAM_ID: PublicKey = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
export const INITIAL_DEPOSIT_LAMPORTS = 1000;
/**
* KaminoVaultClient is a class that provides a high-level interface to interact with the Kamino Vault program.
*/
export class KaminoVaultClient {
private readonly _connection: Connection;
private readonly _kaminoVaultProgramId: PublicKey;
private readonly _kaminoLendProgramId: PublicKey;
recentSlotDurationMs: number;
constructor(
connection: Connection,
recentSlotDurationMs: number,
kaminoVaultprogramId?: PublicKey,
kaminoLendProgramId?: PublicKey
) {
this._connection = connection;
this.recentSlotDurationMs = recentSlotDurationMs;
this._kaminoVaultProgramId = kaminoVaultprogramId ? kaminoVaultprogramId : kaminoVaultId;
this._kaminoLendProgramId = kaminoLendProgramId ? kaminoLendProgramId : PROGRAM_ID;
}
getConnection() {
return this._connection;
}
getProgramID() {
return this._kaminoVaultProgramId;
}
hasFarm() {
return;
}
/**
* Prints a vault in a human readable form
* @param vaultPubkey - the address of the vault
* @param [vaultState] - optional parameter to pass the vault state directly; this will save a network call
* @returns - void; prints the vault to the console
*/
async printVault(vaultPubkey: PublicKey, vaultState?: VaultState) {
const vault = vaultState ? vaultState : await VaultState.fetch(this.getConnection(), vaultPubkey);
if (!vault) {
console.log(`Vault ${vaultPubkey.toString()} not found`);
return;
}
const kaminoVault = new KaminoVault(vaultPubkey, vault, this._kaminoVaultProgramId);
const vaultName = this.decodeVaultName(vault.name);
const slot = await this.getConnection().getSlot('confirmed');
const tokensPerShare = await this.getTokensPerShareSingleVault(kaminoVault, slot);
const holdings = await this.getVaultHoldings(vault, slot);
const sharesIssued = new Decimal(vault.sharesIssued.toString()!).div(
new Decimal(vault.sharesMintDecimals.toString())
);
console.log('Name: ', vaultName);
console.log('Shares issued: ', sharesIssued);
printHoldings(holdings);
console.log('Tokens per share: ', tokensPerShare);
}
/**
* 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): Promise<{ vault: Keypair; initVaultIxs: InitVaultIxs }> {
const vaultState = Keypair.generate();
const size = VaultState.layout.span + 8;
const createVaultIx = SystemProgram.createAccount({
fromPubkey: vaultConfig.admin,
newAccountPubkey: vaultState.publicKey,
lamports: await this.getConnection().getMinimumBalanceForRentExemption(size),
space: size,
programId: this._kaminoVaultProgramId,
});
const tokenVault = PublicKey.findProgramAddressSync(
[Buffer.from(TOKEN_VAULT_SEED), vaultState.publicKey.toBytes()],
this._kaminoVaultProgramId
)[0];
const baseVaultAuthority = PublicKey.findProgramAddressSync(
[Buffer.from(BASE_VAULT_AUTHORITY_SEED), vaultState.publicKey.toBytes()],
this._kaminoVaultProgramId
)[0];
const sharesMint = PublicKey.findProgramAddressSync(
[Buffer.from(SHARES_SEED), vaultState.publicKey.toBytes()],
this._kaminoVaultProgramId
)[0];
let adminTokenAccount: PublicKey;
const prerequisiteIxs: TransactionInstruction[] = [];
const cleanupIxs: TransactionInstruction[] = [];
if (vaultConfig.tokenMint.equals(NATIVE_MINT)) {
const { wsolAta, createAtaIxs, closeAtaIxs } = await createWsolAtaIfMissing(
this.getConnection(),
new Decimal(VAULT_INITIAL_DEPOSIT),
vaultConfig.admin
);
adminTokenAccount = wsolAta;
prerequisiteIxs.push(...createAtaIxs);
cleanupIxs.push(...closeAtaIxs);
} else {
adminTokenAccount = getAssociatedTokenAddressSync(
vaultConfig.tokenMint,
vaultConfig.admin,
false,
vaultConfig.tokenMintProgramId
);
}
const initVaultAccounts: InitVaultAccounts = {
adminAuthority: vaultConfig.admin,
vaultState: vaultState.publicKey,
baseTokenMint: vaultConfig.tokenMint,
tokenVault,
baseVaultAuthority,
sharesMint,
systemProgram: SystemProgram.programId,
rent: SYSVAR_RENT_PUBKEY,
tokenProgram: vaultConfig.tokenMintProgramId,
sharesTokenProgram: TOKEN_PROGRAM_ID,
adminTokenAccount,
};
const initVaultIx = initVault(initVaultAccounts, this._kaminoVaultProgramId);
// create and set up the vault lookup table
const slot = await this.getConnection().getSlot();
const [createLUTIx, lut] = initLookupTableIx(vaultConfig.admin, slot);
const accountsToBeInserted = [
vaultConfig.admin,
vaultState.publicKey,
vaultConfig.tokenMint,
vaultConfig.tokenMintProgramId,
baseVaultAuthority,
sharesMint,
SystemProgram.programId,
SYSVAR_RENT_PUBKEY,
TOKEN_PROGRAM_ID,
this._kaminoLendProgramId,
SYSVAR_INSTRUCTIONS_PUBKEY,
];
const insertIntoLUTIxs = await this.insertIntoLookupTableIxs(vaultConfig.admin, lut, accountsToBeInserted, []);
const setLUTIx = this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.publicKey,
new VaultConfigField.LookupTable(),
lut.toString()
);
const ixs = [createVaultIx, initVaultIx, setLUTIx];
if (vaultConfig.getPerformanceFeeBps() > 0) {
const setPerformanceFeeIx = this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.publicKey,
new VaultConfigField.PerformanceFeeBps(),
vaultConfig.getPerformanceFeeBps().toString()
);
ixs.push(setPerformanceFeeIx);
}
if (vaultConfig.getManagementFeeBps() > 0) {
const setManagementFeeIx = this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.publicKey,
new VaultConfigField.ManagementFeeBps(),
vaultConfig.getManagementFeeBps().toString()
);
ixs.push(setManagementFeeIx);
}
if (vaultConfig.name && vaultConfig.name.length > 0) {
const setNameIx = this.updateUninitialisedVaultConfigIx(
vaultConfig.admin,
vaultState.publicKey,
new VaultConfigField.Name(),
vaultConfig.name
);
ixs.push(setNameIx);
}
const metadataIx = await this.getSetSharesMetadataIx(
this.getConnection(),
vaultConfig.admin,
vaultState.publicKey,
sharesMint,
baseVaultAuthority,
vaultConfig.vaultTokenSymbol,
vaultConfig.vaultTokenName
);
return {
vault: vaultState,
initVaultIxs: {
createAtaIfNeededIxs: prerequisiteIxs,
initVaultIxs: ixs,
createLUTIx,
populateLUTIxs: insertIntoLUTIxs,
cleanupIxs,
initSharesMetadataIx: metadataIx,
},
};
}
/**
* This method creates an instruction to set the shares metadata for a vault
* @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(
connection: Connection,
vaultAdmin: PublicKey,
vault: PublicKey,
sharesMint: PublicKey,
baseVaultAuthority: PublicKey,
tokenName: string,
extraName: string
) {
const [sharesMintMetadata] = getKVaultSharesMetadataPda(sharesMint);
const { name, symbol, uri } = resolveMetadata(sharesMint, extraName, tokenName);
const ix =
(await connection.getAccountInfo(sharesMintMetadata)) === null
? await getInitializeKVaultSharesMetadataIx(
connection,
vaultAdmin,
vault,
sharesMint,
baseVaultAuthority,
name,
symbol,
uri
)
: await getUpdateSharesMetadataIx(
connection,
vaultAdmin,
vault,
sharesMint,
baseVaultAuthority,
name,
symbol,
uri
);
return ix;
}
/**
* 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 updateReserveAllocationIxs(
vault: KaminoVault,
reserveAllocationConfig: ReserveAllocationConfig,
signer?: PublicKey
): Promise<UpdateReserveAllocationIxs> {
const vaultState: VaultState = await vault.getState(this.getConnection());
const reserveState: Reserve = reserveAllocationConfig.getReserveState();
const cTokenVault = getCTokenVaultPda(
vault.address,
reserveAllocationConfig.getReserveAddress(),
this._kaminoVaultProgramId
);
const allocationSigner = signer ? signer : vaultState.vaultAdminAuthority;
const updateReserveAllocationAccounts: UpdateReserveAllocationAccounts = {
signer: allocationSigner,
vaultState: vault.address,
baseVaultAuthority: vaultState.baseVaultAuthority,
reserveCollateralMint: reserveState.collateral.mintPubkey,
reserve: reserveAllocationConfig.getReserveAddress(),
ctokenVault: cTokenVault,
systemProgram: SystemProgram.programId,
rent: SYSVAR_RENT_PUBKEY,
reserveCollateralTokenProgram: TOKEN_PROGRAM_ID,
};
const updateReserveAllocationArgs: UpdateReserveAllocationArgs = {
weight: new BN(reserveAllocationConfig.targetAllocationWeight),
cap: new BN(reserveAllocationConfig.getAllocationCapLamports().floor().toString()),
};
const updateReserveAllocationIx = updateReserveAllocation(
updateReserveAllocationArgs,
updateReserveAllocationAccounts,
this._kaminoVaultProgramId
);
const accountsToAddToLUT = [
reserveAllocationConfig.getReserveAddress(),
cTokenVault,
...this.getReserveAccountsToInsertInLut(reserveState),
];
const lendingMarketAuth = lendingMarketAuthPda(reserveState.lendingMarket, this._kaminoLendProgramId)[0];
accountsToAddToLUT.push(lendingMarketAuth);
const insertIntoLUTIxs = await this.insertIntoLookupTableIxs(
vaultState.vaultAdminAuthority,
vaultState.vaultLookupTable,
accountsToAddToLUT
);
const updateReserveAllocationIxs: UpdateReserveAllocationIxs = {
updateReserveAllocationIx,
updateLUTIxs: insertIntoLUTIxs,
};
return updateReserveAllocationIxs;
}
/**
* 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 payer - the payer of the transaction. 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 withdrawEverythingAndBlockInvestReserve(
vault: KaminoVault,
reserve: PublicKey,
payer?: PublicKey
): Promise<WithdrawAndBlockReserveIxs> {
const vaultState = await vault.getState(this.getConnection());
const reserveIsPartOfAllocation = vaultState.vaultAllocationStrategy.some((allocation) =>
allocation.reserve.equals(reserve)
);
const withdrawAndBlockReserveIxs: WithdrawAndBlockReserveIxs = {
updateReserveAllocationIxs: [],
investIxs: [],
};
if (!reserveIsPartOfAllocation) {
return withdrawAndBlockReserveIxs;
}
const reserveState = await Reserve.fetch(this.getConnection(), reserve);
if (!reserveState) {
return withdrawAndBlockReserveIxs;
}
const reserveWithAddress: ReserveWithAddress = {
address: reserve,
state: reserveState,
};
const reserveAllocationConfig = new ReserveAllocationConfig(reserveWithAddress, 0, new Decimal(0));
// update allocation to have 0 weight and 0 cap
const updateAllocIxs = await this.updateReserveAllocationIxs(vault, reserveAllocationConfig);
const investPayer = payer ? payer : vaultState.vaultAdminAuthority;
const investIx = await this.investSingleReserveIxs(investPayer, vault, reserveWithAddress);
withdrawAndBlockReserveIxs.updateReserveAllocationIxs = [updateAllocIxs.updateReserveAllocationIx];
withdrawAndBlockReserveIxs.investIxs = investIx;
return withdrawAndBlockReserveIxs;
}
/**
* 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?: PubkeyHashMap<PublicKey, KaminoReserve>,
payer?: PublicKey
): Promise<WithdrawAndBlockReserveIxs> {
const vaultState = await vault.getState(this.getConnection());
const reserves = this.getVaultReserves(vaultState);
const withdrawAndBlockReserveIxs: WithdrawAndBlockReserveIxs = {
updateReserveAllocationIxs: [],
investIxs: [],
};
if (!vaultReservesMap) {
vaultReservesMap = await this.loadVaultReserves(vaultState);
}
for (const reserve of reserves) {
const reserveWithAddress: ReserveWithAddress = {
address: reserve,
state: vaultReservesMap.get(reserve)!.state,
};
const reserveAllocationConfig = new ReserveAllocationConfig(reserveWithAddress, 0, new Decimal(0));
// update allocation to have 0 weight and 0 cap
const updateAllocIxs = await this.updateReserveAllocationIxs(vault, reserveAllocationConfig);
withdrawAndBlockReserveIxs.updateReserveAllocationIxs.push(updateAllocIxs.updateReserveAllocationIx);
}
const investPayer = payer ? payer : vaultState.vaultAdminAuthority;
const investIxs = await this.investAllReservesIxs(investPayer, vault);
withdrawAndBlockReserveIxs.investIxs = investIxs;
return withdrawAndBlockReserveIxs;
}
/**
* 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
* @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: PublicKey
): Promise<TransactionInstruction | undefined> {
const vaultState = await vault.getState(this.getConnection());
const reserveIsPartOfAllocation = vaultState.vaultAllocationStrategy.some((allocation) =>
allocation.reserve.equals(reserve)
);
if (!reserveIsPartOfAllocation) {
return undefined;
}
const accounts: RemoveAllocationAccounts = {
vaultAdminAuthority: vaultState.vaultAdminAuthority,
vaultState: vault.address,
reserve,
};
return removeAllocation(accounts);
}
/**
* 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
* @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,
value: string,
signer?: PublicKey
): Promise<UpdateVaultConfigIxs> {
const vaultState: VaultState = await vault.getState(this.getConnection());
const updateVaultConfigAccs: UpdateVaultConfigAccounts = {
vaultAdminAuthority: vaultState.vaultAdminAuthority,
vaultState: vault.address,
klendProgram: this._kaminoLendProgramId,
};
if (signer) {
updateVaultConfigAccs.vaultAdminAuthority = signer;
}
const updateVaultConfigArgs: UpdateVaultConfigArgs = {
entry: mode,
data: Buffer.from([0]),
};
if (isNaN(+value)) {
if (mode.kind === new VaultConfigField.Name().kind) {
const data = Array.from(this.encodeVaultName(value));
updateVaultConfigArgs.data = Buffer.from(data);
} else {
const data = new PublicKey(value);
updateVaultConfigArgs.data = data.toBuffer();
}
} else {
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64LE(BigInt(value.toString()));
updateVaultConfigArgs.data = buffer;
}
const vaultReserves = this.getVaultReserves(vaultState);
const vaultReservesState = await this.loadVaultReserves(vaultState);
let vaultReservesAccountMetas: AccountMeta[] = [];
let vaultReservesLendingMarkets: AccountMeta[] = [];
vaultReserves.forEach((reserve) => {
const reserveState = vaultReservesState.get(reserve);
if (reserveState === undefined) {
throw new Error(`Reserve ${reserve.toBase58()} not found`);
}
vaultReservesAccountMetas = vaultReservesAccountMetas.concat([
{ pubkey: reserve, isSigner: false, isWritable: true },
]);
vaultReservesLendingMarkets = vaultReservesLendingMarkets.concat([
{ pubkey: reserveState.state.lendingMarket, isSigner: false, isWritable: false },
]);
});
const updateVaultConfigIx = updateVaultConfig(
updateVaultConfigArgs,
updateVaultConfigAccs,
this._kaminoVaultProgramId
);
updateVaultConfigIx.keys = updateVaultConfigIx.keys.concat(vaultReservesAccountMetas);
updateVaultConfigIx.keys = updateVaultConfigIx.keys.concat(vaultReservesLendingMarkets);
const updateLUTIxs: TransactionInstruction[] = [];
if (mode.kind === new VaultConfigField.PendingVaultAdmin().kind) {
const newPubkey = new PublicKey(value);
const insertIntoLutIxs = await this.insertIntoLookupTableIxs(
vaultState.vaultAdminAuthority,
vaultState.vaultLookupTable,
[newPubkey]
);
updateLUTIxs.push(...insertIntoLutIxs);
} else if (mode.kind === new VaultConfigField.Farm().kind) {
const keysToAddToLUT = [new PublicKey(value)];
// if the farm already exist we want to read its state to add it to the LUT
try {
const farmState = await FarmState.fetch(this.getConnection(), keysToAddToLUT[0]);
keysToAddToLUT.push(
farmState!.farmVault,
farmState!.farmVaultsAuthority,
farmState!.token.mint,
farmState!.scopePrices,
farmState!.globalConfig
);
const insertIntoLutIxs = await this.insertIntoLookupTableIxs(
vaultState.vaultAdminAuthority,
vaultState.vaultLookupTable,
keysToAddToLUT
);
updateLUTIxs.push(...insertIntoLutIxs);
} catch (error) {
console.log(`Error fetching farm ${keysToAddToLUT[0].toString()} state`, error);
}
}
const updateVaultConfigIxs: UpdateVaultConfigIxs = {
updateVaultConfigIx,
updateLUTIxs,
};
return updateVaultConfigIxs;
}
/** 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
*/
async setVaultFarmIxs(
vault: KaminoVault,
farm: PublicKey,
errorOnOverride: boolean = true
): Promise<UpdateVaultConfigIxs> {
const vaultHasFarm = await vault.hasFarm(this.getConnection());
if (vaultHasFarm && errorOnOverride) {
throw new Error('Vault already has a farm, if you want to override it set errorOnOverride to false');
}
return this.updateVaultConfigIxs(vault, new VaultConfigField.Farm(), farm.toBase58());
}
/**
* This method updates the vault config for a vault that
* @param vault - address of vault to be updated
* @param mode - the field to be updated
* @param value - the new value for the field to be updated (number or pubkey)
* @returns - an instruction to update the vault config
*/
private updateUninitialisedVaultConfigIx(
admin: PublicKey,
vault: PublicKey,
mode: VaultConfigFieldKind,
value: string
): TransactionInstruction {
const updateVaultConfigAccs: UpdateVaultConfigAccounts = {
vaultAdminAuthority: admin,
vaultState: vault,
klendProgram: this._kaminoLendProgramId,
};
const updateVaultConfigArgs: UpdateVaultConfigArgs = {
entry: mode,
data: Buffer.from([0]),
};
if (isNaN(+value)) {
if (mode.kind === new VaultConfigField.Name().kind) {
const data = Array.from(this.encodeVaultName(value));
updateVaultConfigArgs.data = Buffer.from(data);
} else {
const data = new PublicKey(value);
updateVaultConfigArgs.data = data.toBuffer();
}
} else {
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64LE(BigInt(value.toString()));
updateVaultConfigArgs.data = buffer;
}
const updateVaultConfigIx = updateVaultConfig(
updateVaultConfigArgs,
updateVaultConfigAccs,
this._kaminoVaultProgramId
);
return updateVaultConfigIx;
}
/**
* 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
* @returns - an instruction to accept the ownership of the vault and a list of instructions to update the lookup table
*/
async acceptVaultOwnershipIxs(vault: KaminoVault): Promise<AcceptVaultOwnershipIxs> {
const vaultState: VaultState = await vault.getState(this.getConnection());
const acceptOwneshipAccounts: UpdateAdminAccounts = {
pendingAdmin: vaultState.pendingAdmin,
vaultState: vault.address,
};
const acceptVaultOwnershipIx = updateAdmin(acceptOwneshipAccounts, this._kaminoVaultProgramId);
// read the current LUT and create a new one for the new admin and backfill it
const accountsInExistentLUT = (await getAccountsInLUT(this.getConnection(), vaultState.vaultLookupTable)).filter(
(account) => !account.equals(vaultState.vaultAdminAuthority)
);
const LUTIxs: TransactionInstruction[] = [];
const [initNewLUTIx, newLUT] = initLookupTableIx(vaultState.pendingAdmin, await this.getConnection().getSlot());
const insertIntoLUTIxs = await this.insertIntoLookupTableIxs(
vaultState.pendingAdmin,
newLUT,
accountsInExistentLUT,
[]
);
LUTIxs.push(...insertIntoLUTIxs);
const updateVaultConfigIxs = await this.updateVaultConfigIxs(
vault,
new VaultConfigField.LookupTable(),
newLUT.toString(),
vaultState.pendingAdmin
);
LUTIxs.push(updateVaultConfigIxs.updateVaultConfigIx);
LUTIxs.push(...updateVaultConfigIxs.updateLUTIxs);
const acceptVaultOwnershipIxs: AcceptVaultOwnershipIxs = {
acceptVaultOwnershipIx,
initNewLUTIx,
updateLUTIxs: LUTIxs,
};
return acceptVaultOwnershipIxs;
}
/**
* 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
* @returns - an instruction to give up the specified pending fees
*/
async giveUpPendingFeesIx(vault: KaminoVault, maxAmountToGiveUp: Decimal): Promise<TransactionInstruction> {
const vaultState: VaultState = await vault.getState(this.getConnection());
const giveUpPendingFeesAccounts: GiveUpPendingFeesAccounts = {
vaultAdminAuthority: vaultState.vaultAdminAuthority,
vaultState: vault.address,
klendProgram: this._kaminoLendProgramId,
};
const maxAmountToGiveUpLamports = numberToLamportsDecimal(
maxAmountToGiveUp,
vaultState.tokenMintDecimals.toNumber()
);
const giveUpPendingFeesArgs: GiveUpPendingFeesArgs = {
maxAmountToGiveUp: new BN(maxAmountToGiveUpLamports.toString()),
};
return giveUpPendingFees(giveUpPendingFeesArgs, giveUpPendingFeesAccounts, this._kaminoVaultProgramId);
}
/**
* 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 [vaultReservesMap] - 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
* @returns - list of instructions to withdraw all pending fees, including the ATA creation instructions if needed
*/
async withdrawPendingFeesIxs(
vault: KaminoVault,
slot: number,
vaultReservesMap?: PubkeyHashMap<PublicKey, KaminoReserve>
): Promise<TransactionInstruction[]> {
const vaultState: VaultState = await vault.getState(this.getConnection());
const vaultReservesState = vaultReservesMap ? vaultReservesMap : await this.loadVaultReserves(vaultState);
const [{ ata: adminTokenAta, createAtaIx }] = createAtasIdempotent(vaultState.vaultAdminAuthority, [
{
mint: vaultState.tokenMint,
tokenProgram: TOKEN_PROGRAM_ID,
},
]);
const tokensToWithdraw = new Fraction(vaultState.pendingFeesSf).toDecimal();
let tokenLeftToWithdraw = tokensToWithdraw;
tokenLeftToWithdraw = tokenLeftToWithdraw.sub(new Decimal(vaultState.tokenAvailable.toString()));
const reservesToWithdraw: PublicKey[] = [];
if (tokenLeftToWithdraw.lte(0)) {
// Availabe enough to withdraw all - using first reserve as it does not matter
reservesToWithdraw.push(vaultState.vaultAllocationStrategy[0].reserve);
} else {
// Get decreasing order sorted available liquidity to withdraw from each reserve allocated to
const reserveAllocationAvailableLiquidityToWithdraw = await this.getReserveAllocationAvailableLiquidityToWithdraw(
vault,
slot,
vaultReservesState
);
// sort
const reserveAllocationAvailableLiquidityToWithdrawSorted = new PubkeyHashMap(
[...reserveAllocationAvailableLiquidityToWithdraw.entries()].sort((a, b) => b[1].sub(a[1]).toNumber())
);
reserveAllocationAvailableLiquidityToWithdrawSorted.forEach((availableLiquidityToWithdraw, key) => {
if (tokenLeftToWithdraw.gt(0)) {
reservesToWithdraw.push(key);
tokenLeftToWithdraw = tokenLeftToWithdraw.sub(availableLiquidityToWithdraw);
}
});
}
const reserveStates = await Reserve.fetchMultiple(
this.getConnection(),
reservesToWithdraw,
this._kaminoLendProgramId
);
const withdrawIxs: TransactionInstruction[] = await Promise.all(
reservesToWithdraw.map(async (reserve, index) => {
if (reserveStates[index] === null) {
throw new Error(`Reserve ${reserve.toBase58()} not found`);
}
const reserveState = reserveStates[index]!;
const marketAddress = reserveState.lendingMarket;
return this.withdrawPendingFeesIx(
vault,
vaultState,
marketAddress,
{ address: reserve, state: reserveState },
adminTokenAta
);
})
);
return [createAtaIx, ...withdrawIxs];
}
// async closeVaultIx(vault: KaminoVault): Promise<TransactionInstruction> {
// const vaultState: VaultState = await vault.getState(this.getConnection());
// const closeVaultAccounts: CloseVaultAccounts = {
// adminAuthority: vaultState.adminAuthority,
// vaultState: vault.address,
// };
// return closeVault(closeVaultAccounts, this._kaminoVaultProgramId);
// }
/**
* 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 depositIxs(
user: PublicKey,
vault: KaminoVault,
tokenAmount: Decimal,
vaultReservesMap?: PubkeyHashMap<PublicKey, KaminoReserve>,
farmState?: FarmState
): Promise<DepositIxs> {
const vaultState = await vault.getState(this.getConnection());
const tokenProgramID = vaultState.tokenProgram;
const userTokenAta = getAssociatedTokenAddress(vaultState.tokenMint, user, true, tokenProgramID);
const createAtasIxs: TransactionInstruction[] = [];
const closeAtasIxs: TransactionInstruction[] = [];
if (vaultState.tokenMint.equals(NATIVE_MINT)) {
const [{ ata: wsolAta, createAtaIx: createWsolAtaIxn }] = createAtasIdempotent(user, [
{
mint: NATIVE_MINT,
tokenProgram: TOKEN_PROGRAM_ID,
},
]);
createAtasIxs.push(createWsolAtaIxn);
const transferWsolIxs = getTransferWsolIxs(
user,
wsolAta,
numberToLamportsDecimal(tokenAmount, vaultState.tokenMintDecimals.toNumber()).ceil()
);
createAtasIxs.push(...transferWsolIxs);
}
const [{ ata: userSharesAta, createAtaIx: createSharesAtaIxs }] = createAtasIdempotent(user, [
{
mint: vaultState.sharesMint,
tokenProgram: TOKEN_PROGRAM_ID,
},
]);
createAtasIxs.push(createSharesAtaIxs);
const eventAuthority = getEventAuthorityPda(this._kaminoVaultProgramId);
const depoistAccounts: DepositAccounts = {
user: user,
vaultState: vault.address,
tokenVault: vaultState.tokenVault,
tokenMint: vaultState.tokenMint,
baseVaultAuthority: vaultState.baseVaultAuthority,
sharesMint: vaultState.sharesMint,
userTokenAta: userTokenAta,
userSharesAta: userSharesAta,
tokenProgram: tokenProgramID,
klendProgram: this._kaminoLendProgramId,
sharesTokenProgram: TOKEN_PROGRAM_ID,
eventAuthority: eventAuthority,
program: this._kaminoVaultProgramId,
};
const depositArgs: DepositArgs = {
maxAmount: new BN(
numberToLamportsDecimal(tokenAmount, vaultState.tokenMintDecimals.toNumber()).floor().toString()
),
};
const depositIx = deposit(depositArgs, depoistAccounts, this._kaminoVaultProgramId);
const vaultReserves = this.getVaultReserves(vaultState);
const vaultReservesState = vaultReservesMap ? vaultReservesMap : await this.loadVaultReserves(vaultState);
let vaultReservesAccountMetas: AccountMeta[] = [];
let vaultReservesLendingMarkets: AccountMeta[] = [];
vaultReserves.forEach((reserve) => {
const reserveState = vaultReservesState.get(reserve);
if (reserveState === undefined) {
throw new Error(`Reserve ${reserve.toBase58()} not found`);
}
vaultReservesAccountMetas = vaultReservesAccountMetas.concat([
{ pubkey: reserve, isSigner: false, isWritable: true },
]);
vaultReservesLendingMarkets = vaultReservesLendingMarkets.concat([
{ pubkey: reserveState.state.lendingMarket, isSigner: false, isWritable: false },
]);
});
depositIx.keys = depositIx.keys.concat(vaultReservesAccountMetas);
depositIx.keys = depositIx.keys.concat(vaultReservesLendingMarkets);
const depositIxs: DepositIxs = {
depositIxs: [...createAtasIxs, depositIx, ...closeAtasIxs],
stakeInFarmIfNeededIxs: [],
};
// if there is no farm, we can return the deposit instructions, otherwise include the stake ix in the response
if (!(await vault.hasFarm(this.getConnection()))) {
return depositIxs;
}
// if there is a farm, stake the shares
const stakeSharesIxs = await this.stakeSharesIxs(user, vault, undefined, farmState);
depositIxs.stakeInFarmIfNeededIxs = stakeSharesIxs;
return depositIxs;
}
/**
* 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: PublicKey,
vault: KaminoVault,
sharesAmount?: Decimal,
farmState?: FarmState
): Promise<TransactionInstruction[]> {
const vaultState = await vault.getState(this.getConnection());
let sharesToStakeLamports = new Decimal(U64_MAX);
if (sharesAmount) {
sharesToStakeLamports = numberToLamportsDecimal(sharesAmount, vaultState.sharesMintDecimals.toNumber());
}
// if tokens to be staked are 0 or vault has no farm there is no stake needed
if (sharesToStakeLamports.lte(0) || !vault.hasFarm(this.getConnection())) {
return [];
}
// returns the ix to create the farm state account if needed and the ix to stake the shares
return getFarmStakeIxs(this.getConnection(), user, sharesToStakeLamports, vaultState.vaultFarm, farmState);
}
/**
* This function will return a struct with the instructions to unstake from the farm if necessary and the instructions for 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 withdrawIxs(
user: PublicKey,
vault: KaminoVault,
shareAmountToWithdraw: Decimal,
slot: number,
vaultReservesMap?: PubkeyHashMap<PublicKey, KaminoReserve>,
farmState?: FarmState
): Promise<WithdrawIxs> {
const vaultState = await vault.getState(this.getConnection());
const kaminoVault = new KaminoVault(vault.address, vaultState, vault.programId);
const hasFarm = await vault.hasFarm(this.getConnection());
const withdrawIxs: WithdrawIxs = {
unstakeFromFarmIfNeededIxs: [],
withdrawIxs: [],
postWithdrawIxs: [],
};
// compute the total shares the user has (in ATA + in farm) and check if they want to withdraw everything or just a part
let userSharesAtaBalance = new Decimal(0);
const userSharesAta = getAssociatedTokenAddress(vaultState.sharesMint, user);
const userSharesAtaState = await this.getConnection().getAccountInfo(userSharesAta);
if (userSharesAtaState) {
const userSharesAtaBalanceInLamports = getTokenBalanceFromAccountInfoLamports(userSharesAtaState);
userSharesAtaBalance = userSharesAtaBalanceInLamports.div(
new Decimal(10).pow(vaultState.sharesMintDecimals.toString())
);
}
let userSharesInFarm = new Decimal(0);
if (hasFarm) {
userSharesInFarm = await getUserSharesInFarm(
this.getConnection(),
user,
vaultState.vaultFarm,
vaultState.sharesMintDecimals.toNumber()
);
}
let sharesToWithdraw = shareAmountToWithdraw;
const totalUserShares = userSharesAtaBalance.add(userSharesInFarm);
let withdrawAllShares = false;
if (sharesToWithdraw.gt(totalUserShares)) {
sharesToWithdraw = new Decimal(U64_MAX.toString()).div(
new Decimal(10).pow(vaultState.sharesMintDecimals.toString())
);
withdrawAllShares = true;
}
// if not enough shares in ATA unstake from farm
const sharesInAtaAreEnoughForWithdraw = sharesToWithdraw.lte(userSharesAtaBalance);
if (hasFarm && !sharesInAtaAreEnoughForWithdraw) {
// if we need to unstake we need to make sure share ata is created
const [{ createAtaIx }] = createAtasIdempotent(user, [
{
mint: vaultState.sharesMint,
tokenProgram: TOKEN_PROGRAM_ID,
},
]);
withdrawIxs.unstakeFromFarmIfNeededIxs.push(createAtaIx);
let shareLamportsToWithdraw = new Decimal(U64_MAX.toString());
if (!withdrawAllShares) {
const sharesToWithdrawFromFarm = sharesToWithdraw.sub(userSharesAtaBalance);
shareLamportsToWithdraw = collToLamportsDecimal(
sharesToWithdrawFromFarm,
vaultState.sharesMintDecimals.toNumber()
);
}
const unstakeAndWithdrawFromFarmIxs = await getFarmUnstakeAndWithdrawIxs(
this.getConnection(),
user,
shareLamportsToWithdraw,
vaultState.vaultFarm,
farmState
);
withdrawIxs.unstakeFromFarmIfNeededIxs.push(unstakeAndWithdrawFromFarmIxs.unstakeIx);
withdrawIxs.unstakeFromFarmIfNeededIxs.push(unstakeAndWithdrawFromFarmIxs.withdrawIx);
}
// if the vault has allocations withdraw otherwise wtihdraw from available ix
const vaultAllocation = vaultState.vaultAllocationStrategy.find(
(allocation) => !allocation.reserve.equals(PublicKey.default)
);
if (vaultAllocation) {
const withdrawFromVaultIxs = await this.withdrawWithReserveIxs(
user,
kaminoVault,
sharesToWithdraw,
totalUserShares,
slot,
vaultReservesMap
);
withdrawIxs.withdrawIxs = withdrawFromVaultIxs;
} else {
const withdrawFromVaultIxs = await this.withdrawFromAvailableIxs(user, kaminoVault, sharesToWithdraw);
withdrawIxs.withdrawIxs = withdrawFromVaultIxs;
}
// if the vault is for SOL return the ix to unwrap the SOL
if (vaultState.tokenMint.equals(NATIVE_MINT)) {
const userWsolAta = getAssociatedTokenAddress(NATIVE_MINT, user);
const unwrapIx = createCloseAccountInstruction(userWsolAta, user, user, [], TOKEN_PROGRAM_ID);
withdrawIxs.postWithdrawIxs.push(unwrapIx);
}
// if we burn all of user's shares close its shares ATA
const burnAllUserShares = sharesToWithdraw.gt(userSharesAtaBalance);
if (burnAllUserShares) {
const closeAtaIx = createCloseAccountInstruction(userSharesAta, user, user, [], TOKEN_PROGRAM_ID);
withdrawIxs.postWithdrawIxs.push(closeAtaIx);
}
return withdrawIxs;
}
private async withdrawFromAvailableIxs(
user: PublicKey,
vault: KaminoVault,
shareAmount: Decimal
): Promise<TransactionInstruction[]> {
const vaultState = await vault.getState(this.getConnection());
const kaminoVault = new KaminoVault(vault.address, vaultState, vault.programId);
const userSharesAta = getAssociatedTokenAddress(vaultState.sharesMint, user);
const [{ ata: userTokenAta, createAtaIx }] = createAtasIdempotent(user, [
{
mint: vaultState.tokenMint,
tokenProgram: vaultState.tokenProgram,
},
]);
const shareLamportsToWithdraw = collToLamportsDecimal(shareAmount, vaultState.sharesMintDecimals.toNumber());
const withdrawFromAvailableIxn = await this.withdrawFromAvailableIx(
user,
kaminoVault,
vaultState,
userSharesAta,
userTokenAta,
shareLamportsToWithdraw
);
return [createAtaIx, withdrawFromAvailableIxn];
}
private async withdrawWithReserveIxs(
user: PublicKey,
vault: KaminoVault,
shareAmount: Decimal,
allUserShares: Decimal,
slot: number,
vaultReservesMap?: PubkeyHashMap<PublicKey, KaminoReserve>
): Promise<TransactionInstruction[]> {
const vaultState = await vault.getState(this.getConnection());
const vaultReservesState = vaultReservesMap ? vaultReservesMap : await this.loadVaultReserves(vaultState);
const userSharesAta = getAssociatedTokenAddress(vaultState.sharesMint, user);
const [{ ata: userTokenAta, createAtaIx }] = createAtasIdempotent(user, [
{
mint: vaultState.tokenMint,
tokenProgram: vaultState.tokenProgram,
},
]);
const withdrawAllShares = shareAmount.gte(allUserShares);
const actualSharesToWithdraw = shareAmount.lte(allUserShares) ? shareAmount : allUserShares;
const shareLamportsToWithdraw = collToLamportsDecimal(
actualSharesToWithdraw,
vaultState.sharesMintDecimals.toNumber()
);
const tokensPerShare = await this.getTokensPerShareSingleVault(vault, slot);
const sharesPerToken = new Decimal(1).div(tokensPerShare);
const tokensToWithdraw = shareLamportsToWithdraw.mul(tokensPerShare);
let tokenLeftToWithdraw = tokensToWithdraw;
const availableTokens = new Decimal(vaultState.tokenAvailable.toString());
tokenLeftToWithdraw = tokenLeftToWithdraw.sub(availableTokens);
type ReserveWithTokensToWithdraw = { reserve: PublicKey; shares: Decimal };
const reserveWithSharesAmountToWithdraw: ReserveWithTokensToWithdraw[] = [];
let isFirstWithdraw = true;
if (tokenLeftToWithdraw.lte(0)) {
// Availabe enough to withdraw all - using the first existent reserve
const firstReserve = vaultState.vaultAllocationStrategy.find(
(reserve) => !reserve.reserve.equals(PublicKey.default)
);
if (withdrawAllShares) {
reserveWithSharesAmountToWithdraw.push({
reserve: firstReserve!.reserve,
shares: new Decimal(U64_MAX.toString()),
});
} else {
reserveWithSharesAmountToWithdraw.push({
reserve: firstReserve!.reserve,
shares: shareLamportsToWithdraw,
});
}
} else {
// Get decreasing order sorted available liquidity to withdraw from each reserve allocated to
const reserveAllocationAvailableLiquidityToWithdraw = await this.getReserveAllocationAvailableLiquidityToWithdraw(
vault,
slot,
vaultReservesState
);
// sort
const reserveAllocationAvailableLiquidityToWithdrawSorted = [
...reserveAllocationAvailableLiquidityToWithdraw.entries(),
].sort((a, b) => b[1].sub(a[1]).toNumber());
reserveAllocationAvailableLiquidityToWithdrawSorted.forEach(([key, availableLiquidityToWithdraw], _) => {
if (tokenLeftToWithdraw.gt(0)) {
let tokensToWithdrawFromReserve = Decimal.min(tokenLeftToWithdraw, availableLiquidityToWithdraw);
if (isFirstWithdraw) {
tokensToWithdrawFromReserve = tokensToWithdrawFromReserve.add(availableTokens);
isFirstWithdraw = false;
}
if (withdrawAllShares) {
reserveWithSharesAmountToWithdraw.push({ reserve: key, shares: new Decimal(U64_MAX.toString()) });
} else {
// round up to the nearest integer the shares to withdraw
const sharesToWit