UNPKG

@kamino-finance/farms-sdk

Version:
1,795 lines (1,594 loc) 73.2 kB
import { OraclePrices } from "@kamino-finance/scope-sdk/dist/@codegen/scope/accounts"; import { Address, address, Base58EncodedBytes, GetProgramAccountsDatasizeFilter, GetProgramAccountsMemcmpFilter, Instruction, none, Option, Rpc, Slot, SolanaRpcApi, some, TransactionSigner, UnixTimestamp, getBase64Encoder, } from "@solana/kit"; import Decimal from "decimal.js"; import { FarmState, GlobalConfig, UserState, fetchMaybeFarmState, fetchMaybeUserState, fetchMaybeGlobalConfig, fetchAllMaybeUserState, getFarmStateDecoder, getFarmStateSize, getUserStateDecoder, getUserStateSize, } from "./@codegen/farms/accounts"; import { FARMS_PROGRAM_ADDRESS } from "./@codegen/farms/programs"; import { FarmConfigOption, LockingMode, RewardInfo, RewardType, TimeUnit, } from "./@codegen/farms/types/index"; import { SECONDS_IN_A_DAY, SECONDS_IN_A_MONTH, SECONDS_IN_A_WEEK, SECONDS_IN_A_YEAR, } from "./consts"; import { FarmAndKey, FarmIncentives, IncentiveRewardStats, UserAndKey, UserFarm, } from "./models"; import { calculateCurrentRewardPerToken, calculatePendingRewards, checkIfAccountExists, collToLamportsDecimal, createKeypairRentExemptIx, decimalToBN, DEFAULT_PUBLIC_KEY, getFarmAuthorityPDA, getFarmVaultPDA, getRewardVaultPDA, getTreasuryAuthorityPDA, getTreasuryVaultPDA, getUserStatePDA, GlobalConfigFlagValueType, isValidPubkey, lamportsToCollDecimal, scaleDownWads, } from "./utils"; import { chunks } from "./utils/arrayUtils"; import { batchFetch } from "./utils/batch"; import * as farmOperations from "./utils/operations"; import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddress, } from "./utils/token"; import { GlobalConfigOption } from "./@codegen/farms/types"; import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; import { Connection } from "@solana/web3.js"; import { backOff, IBackOffOptions } from "exponential-backoff"; import { decompress } from "fzstd"; import BN from "bn.js"; import { getRewardsApyForStrategy } from "./utils"; import { U64_MAX } from "./utils/consts"; import { getAllFarmConfigsAndStates, IFarmResponse, ILogger, MarketWithReserves, noOpLogger, StrategyInfo, VaultInfo, } from "./utils/farms"; import { getScopePricesFromFarm } from "./utils/option"; import { SIZE_FARM_STATE, SIZE_GLOBAL_CONFIG } from "./accountSizes"; const ZERO_BN = 0n; const base64Encoder = getBase64Encoder(); export { SIZE_FARM_STATE, SIZE_GLOBAL_CONFIG }; export interface UserPointsBreakdown { totalPoints: Decimal; currentBoost: Decimal; currentPointsPerDay: Decimal; perPositionBoost: Map<Address, Decimal>; perPositionPointsPerDay: Map<Address, Decimal>; } export interface RewardCurvePoint { startTs: number; rps: number; } export type PendingWithdrawalCooldownUnit = "seconds" | "slots"; export interface PendingWithdrawalCooldownStatus { userStateAddress?: Address; farm: Address; stakeTokenMint: Address; hasPendingWithdrawal: boolean; pendingWithdrawalUnstakeScaled: BN; canWithdraw: boolean; currentTimeUnit: BN; unlockAt: BN; remaining: BN; unit: PendingWithdrawalCooldownUnit; } const SOLANA_API_RETRY: Partial<IBackOffOptions> = { maxDelay: 10 * 1000, numOfAttempts: 3, retry: (e: any, attemptNumber: number) => { // silent retry return true; }, }; export class Farms { private readonly _connection: Rpc<SolanaRpcApi>; private readonly _farmsProgramId: Address; constructor( connection: Rpc<SolanaRpcApi>, farmsProgramId: Address = FARMS_PROGRAM_ADDRESS, ) { this._connection = connection; this._farmsProgramId = farmsProgramId; } getConnection() { return this._connection; } getProgramID() { return this._farmsProgramId; } async getAllUserStatesForUser(user: Address): Promise<Array<UserAndKey>> { let filters: ( | GetProgramAccountsDatasizeFilter | GetProgramAccountsMemcmpFilter )[] = []; filters.push({ memcmp: { bytes: user.toString() as Base58EncodedBytes, offset: 48n, encoding: "base58", }, }); filters.push({ dataSize: BigInt(getUserStateSize()) }); const decoder = getUserStateDecoder(); return ( await this._connection .getProgramAccounts(this._farmsProgramId, { filters, encoding: "base64", }) .send() ).map((x) => { const userAndKey: UserAndKey = { userState: decoder.decode(base64Encoder.encode(x.account.data[0])), key: x.pubkey, }; return userAndKey; }); } async getUserStatesForUserAndFarm( user: Address, farm: Address, ): Promise<Array<UserAndKey>> { let filters: ( | GetProgramAccountsDatasizeFilter | GetProgramAccountsMemcmpFilter )[] = []; filters.push({ memcmp: { bytes: user.toString() as Base58EncodedBytes, offset: 48n, encoding: "base58", }, }); filters.push({ memcmp: { bytes: farm.toString() as Base58EncodedBytes, offset: 16n, encoding: "base58", }, }); filters.push({ dataSize: BigInt(getUserStateSize()) }); const decoder = getUserStateDecoder(); return ( await this._connection .getProgramAccounts(this._farmsProgramId, { filters, encoding: "base64", }) .send() ).map((x) => { const userAndKey: UserAndKey = { userState: decoder.decode(base64Encoder.encode(x.account.data[0])), key: x.pubkey, }; return userAndKey; }); } async getAllUserStates(): Promise<UserAndKey[]> { const decoder = getUserStateDecoder(); return ( await this._connection .getProgramAccounts(this._farmsProgramId, { filters: [{ dataSize: BigInt(getUserStateSize()) }], encoding: "base64", }) .send() ).map((x) => { const userAndKey: UserAndKey = { userState: decoder.decode(base64Encoder.encode(x.account.data[0])), key: x.pubkey, }; return userAndKey; }); } async getAllUserStatesWithFilter( isFarmDelegated: boolean, ): Promise<UserAndKey[]> { const decoder = getUserStateDecoder(); return ( await this._connection .getProgramAccounts(this._farmsProgramId, { filters: [ { dataSize: BigInt(getUserStateSize()) }, { memcmp: { offset: 80n, bytes: (isFarmDelegated ? "2" : "1") as Base58EncodedBytes, encoding: "base58", }, }, ], encoding: "base64", }) .send() ).map((x) => { const userAndKey: UserAndKey = { userState: decoder.decode(base64Encoder.encode(x.account.data[0])), key: x.pubkey, }; return userAndKey; }); } /** * Get all farms user states from an async generator filled with batches of max 100 user states each * @example * const userStateGenerator = farms.batchGetAllUserStates(); * for await (const userStates of userStateGenerator) { * console.log('got a batch of user states:', userStates.length); * } * @param isFarmDelegated - Optional filter to get only user states for farms that are delegated or not */ async *batchGetAllUserStates( isFarmDelegated?: boolean, ): AsyncGenerator<UserAndKey[], void, unknown> { // Get all farms first and then get user states for each farm let farms = await this.getAllFarmStates(); if (isFarmDelegated !== undefined) { farms = farms.filter( (farm) => Boolean(farm.farmState.isFarmDelegated) === isFarmDelegated, ); } for (const farm of farms) { const farmUserStates = await backOff( () => this.getAllUserStatesForFarm(farm.key), SOLANA_API_RETRY, ); if (farmUserStates.length > 0) { // Process in smaller batches to avoid memory issues for (const batch of chunks(farmUserStates, 100)) { yield batch; } } } } async getAllUserStatesForFarm(farm: Address): Promise<UserAndKey[]> { const decoder = getUserStateDecoder(); return ( ( await this._connection .getProgramAccounts(this._farmsProgramId, { filters: [ { dataSize: BigInt(getUserStateSize()) }, { memcmp: { offset: 8n + 8n, bytes: farm.toString() as Base58EncodedBytes, encoding: "base58", }, }, ], encoding: "base64+zstd", }) .send() ) // TODO: type properly when base64+zstd encoding type is available in @solana/kit .map((x: any) => { const compressedData = new Uint8Array( base64Encoder.encode(x.account.data[0]), ); const decompressedData = decompress(compressedData); const userAndKey: UserAndKey = { userState: decoder.decode(decompressedData), key: x.pubkey, }; return userAndKey; }) ); } async getFarmsForMint(mint: Address): Promise<Array<FarmAndKey>> { let filters: ( | GetProgramAccountsDatasizeFilter | GetProgramAccountsMemcmpFilter )[] = []; filters.push({ memcmp: { bytes: mint.toString() as Base58EncodedBytes, offset: 72n, encoding: "base58", }, }); filters.push({ dataSize: BigInt(getFarmStateSize()) }); const decoder = getFarmStateDecoder(); return ( await this._connection .getProgramAccounts(this._farmsProgramId, { filters, encoding: "base64", }) .send() ).map((x) => { const farmAndKey: FarmAndKey = { farmState: decoder.decode(base64Encoder.encode(x.account.data[0])), key: x.pubkey, }; return farmAndKey; }); } async getAllFarmStates(): Promise<FarmAndKey[]> { const decoder = getFarmStateDecoder(); return ( await this._connection .getProgramAccounts(this._farmsProgramId, { filters: [{ dataSize: BigInt(getFarmStateSize()) }], encoding: "base64", }) .send() ) .map((x) => { try { const farmAndKey: FarmAndKey = { farmState: decoder.decode(base64Encoder.encode(x.account.data[0])), key: x.pubkey, }; return farmAndKey; } catch (err) { return null; } }) .filter((x) => x !== null) as FarmAndKey[]; } /** * Get all farm configs and states categorized by type where possible (otehrwise standalone) * @param markets - Pre-fetched market data -> fetch via KaminoMarket.load() from klend-sdk * @param strategies - Pre-fetched strategy data -> fetch via Kamino.getAllStrategiesWithFilters() from kliquidity-sdk * @param vaults - Pre-fetched vault data -> fetch via KaminoManager.getAllVaults() from klend-sdk * @param logger - Optional logger for debugging */ async getAllConfigsAndStates({ markets, strategies, vaults, logger = noOpLogger, }: { markets: MarketWithReserves[]; strategies: StrategyInfo[]; vaults: VaultInfo[]; logger?: ILogger; }): Promise<{ collateralFarms: IFarmResponse[]; debtFarms: IFarmResponse[]; strategyFarms: IFarmResponse[]; earnVaultFarms: IFarmResponse[]; standaloneFarms: IFarmResponse[]; }> { const allFarms = await this.getAllFarmStates(); return getAllFarmConfigsAndStates({ allFarms, markets, strategies, vaults, logger, }); } async getAllFarmStatesByPubkeys(keys: Address[]): Promise<FarmAndKey[]> { const farmAndKeys: FarmAndKey[] = []; const farmStates = await batchFetch( keys, async (chunk) => await this.fetchMultipleFarmStatesWithCheckedSize(chunk), ); farmStates.forEach((farmState, index) => { if (farmState) { farmAndKeys.push({ farmState: farmState, key: keys[index] }); } }); return farmAndKeys; } async getStakedAmountForFarm(farm: Address): Promise<Decimal> { const farmAccount = await fetchMaybeFarmState(this._connection, farm); if (!farmAccount.exists) { throw Error("No Farm found"); } const farmState = farmAccount.data; return lamportsToCollDecimal( new Decimal(scaleDownWads(farmState.totalActiveStakeScaled)), Number(farmState.token.decimals), ); } async getStakedAmountForMintForFarm( _mint: Address, farm: Address, ): Promise<Decimal> { return this.getStakedAmountForFarm(farm); } async getStakedAmountForMint(mint: Address): Promise<Decimal> { const farms = await this.getFarmsForMint(mint); let totalStaked = new Decimal(0); for (let index = 0; index < farms.length; index++) { totalStaked = totalStaked.add( lamportsToCollDecimal( new Decimal(farms[index].farmState.totalStakedAmount.toString()), Number(farms[index].farmState.token.decimals), ), ); } return totalStaked; } async getLockupDurationAndExpiry( farm: Address, user: Address, timestampNow: number, ): Promise<{ lockupRemainingDuration: number; farmLockupOriginalDuration: number; farmLockupExpiry: number; }> { let userStateAddress = await getUserStatePDA( this._farmsProgramId, farm, user, ); let userStateAccount = await fetchMaybeUserState( this._connection, userStateAddress, ); let farmAccount = await fetchMaybeFarmState(this._connection, farm); if (!farmAccount.exists) { throw new Error("Error fetching farm state"); } const farmState = farmAccount.data; let lockingMode = Number(farmState.lockingMode); let lockingDuration = Number(farmState.lockingDuration); let penalty = Number(farmState.lockingEarlyWithdrawalPenaltyBps); if (penalty !== 0 && penalty !== 10000) { throw "Early withdrawal penalty is not supported yet"; } if (penalty > 10000) { throw "Early withdrawal penalty is too high"; } let lockingStart = 0; if (lockingMode == LockingMode.None) { return { farmLockupOriginalDuration: 0, farmLockupExpiry: 0, lockupRemainingDuration: 0, }; } if (lockingMode == LockingMode.WithExpiry) { // Locking starts globally for the entire farm lockingStart = Number(farmState.lockingStartTimestamp); } if (lockingMode == LockingMode.Continuous) { // Locking starts for each user individually at each stake // if the user has a state, else now if (!userStateAccount.exists) { lockingStart = timestampNow; } else { const userState = userStateAccount.data; lockingStart = Number(userState.lastStakeTs); } } const timestampBeginning = lockingStart; const timestampMaturity = lockingStart + lockingDuration; if (timestampNow >= timestampMaturity) { // Time has passed, no remaining return { farmLockupOriginalDuration: Number(farmState.lockingDuration), farmLockupExpiry: timestampMaturity, lockupRemainingDuration: 0, }; } if (timestampNow < timestampBeginning) { // Time has not started, no remaining return { farmLockupOriginalDuration: Number(farmState.lockingDuration), farmLockupExpiry: timestampMaturity, lockupRemainingDuration: 0, }; } const timeRemaining = timestampMaturity - timestampNow; const remainingLockedDurationSeconds = Math.max(timeRemaining, 0); return { farmLockupOriginalDuration: Number(farmState.lockingDuration), farmLockupExpiry: timestampMaturity, lockupRemainingDuration: remainingLockedDurationSeconds, }; } async getUserStateKeysForDelegatedFarm( user: Address, farm: Address, delegatees?: Address[], ): Promise<Array<UserAndKey>> { if (delegatees) { return this.getUserStateKeysForDelegatedFarmDeterministic( user, farm, delegatees, ); } const userStates = await this.getUserStatesForUserAndFarm(user, farm); const userStateKeysForFarm: UserAndKey[] = []; for (let index = 0; index < userStates.length; index++) { if (userStates[index].userState.farmState === farm) { userStateKeysForFarm.push(userStates[index]); } } if (userStateKeysForFarm.length === 0) { throw Error("No user state found for user " + user + " for farm " + farm); } else { return userStateKeysForFarm; } } async getUserStateKeysForDelegatedFarmDeterministic( user: Address, farm: Address, delegatees: Address[], ): Promise<Array<UserAndKey>> { const userStateKeysForFarm: UserAndKey[] = []; const userStateAddresses: Address[] = await Promise.all( delegatees.map(async (delegate) => { return await getUserStatePDA(this._farmsProgramId, farm, delegate); }), ); const userStateAccounts = await fetchAllMaybeUserState( this._connection, userStateAddresses, ); userStateAccounts.forEach((account, index) => { if (account.exists && account.data.farmState === farm) { userStateKeysForFarm.push({ key: userStateAddresses[index], userState: account.data, }); } }); if (userStateKeysForFarm.length === 0) { throw Error("No user state found for user " + user + " for farm " + farm); } else { return userStateKeysForFarm; } } async getOraclePrices(farmState: FarmState): Promise<OraclePrices | null> { let oraclePrices: OraclePrices | null = null; if (farmState.scopePrices !== DEFAULT_PUBLIC_KEY) { oraclePrices = await OraclePrices.fetch( this._connection, farmState.scopePrices, ); if (!oraclePrices) { throw new Error("Error fetching oracle prces"); } } return oraclePrices; } filterFarmsForStrategies( farmStates: FarmAndKey[], strategiesToInclude?: Set<Address>, ): FarmAndKey[] { if (strategiesToInclude) { return farmStates.filter((farmState) => strategiesToInclude.has(farmState.farmState.strategyId), ); } return farmStates; } filterFarmsForVaults( farmStates: FarmAndKey[], vaultsToInclude?: Set<Address>, ): FarmAndKey[] { if (vaultsToInclude) { return farmStates.filter((farmState) => vaultsToInclude.has(farmState.farmState.vaultId), ); } return farmStates; } async getFarmStatesFromUserStates( userStates: UserAndKey[], strategiesToInclude?: Set<Address>, vaultsToInclude?: Set<Address>, ): Promise<FarmAndKey[]> { const farmPks = new Set<Address>(); for (let i = 0; i < userStates.length; i++) { farmPks.add(userStates[i].userState.farmState); } const farmStates = await batchFetch( Array.from(farmPks), async (chunk) => await this.getAllFarmStatesByPubkeys(chunk), ); if (!farmStates) { throw new Error("Error fetching farms"); } let farmStatesFiltered = this.filterFarmsForStrategies( farmStates, strategiesToInclude, ); farmStatesFiltered = this.filterFarmsForVaults( farmStatesFiltered, vaultsToInclude, ); return farmStatesFiltered; } getUserPendingRewards( userState: UserState, farmState: FarmState, timestamp: Decimal, oraclePrices: OraclePrices | null, ): { userPendingRewardAmounts: Array<Decimal>; hasReward: boolean } { // calculate userState pending rewards const userPendingRewardAmounts: Array<Decimal> = []; let hasReward = false; for ( let indexReward = 0; indexReward < farmState.rewardInfos.length; indexReward++ ) { userPendingRewardAmounts[indexReward] = calculatePendingRewards( farmState, userState, indexReward, timestamp, oraclePrices, ); if (userPendingRewardAmounts[indexReward].gt(0)) { hasReward = true; } } return { userPendingRewardAmounts, hasReward }; } async getAllFarmsForUser( user: Address, timestamp: Decimal, strategiesToInclude?: Set<Address>, vaultsToInclude?: Set<Address>, ): Promise<Map<Address, UserFarm>> { const userStates = await this.getAllUserStatesForUser(user); const farmStatesFiltered = await this.getFarmStatesFromUserStates( userStates, strategiesToInclude, vaultsToInclude, ); if (farmStatesFiltered.length === 0) { // Return empty if no serializable farm states found return new Map<Address, UserFarm>(); } const userFarms = new Map<Address, UserFarm>(); for (let userState of userStates) { let farmState = farmStatesFiltered.find( (farmState) => farmState.key === userState.userState.farmState, ); if (!farmState) { // Skip farms that are not serializable anymore continue; } let oraclePrices = await this.getOraclePrices(farmState.farmState); const { userPendingRewardAmounts, hasReward } = this.getUserPendingRewards( userState.userState, farmState.farmState, timestamp, oraclePrices, ); // add new userFarm state if non empty (has rewards or stake) and not already present if (!userFarms.has(userState.userState.farmState)) { const userFarm: UserFarm = { userStateAddress: userState.key, farm: userState.userState.farmState, strategyId: farmState.farmState.strategyId, delegateAuthority: farmState.farmState.delegateAuthority, stakedToken: farmState.farmState.token.mint, userState: userState.userState, activeStakeByDelegatee: new Map<Address, Decimal>(), pendingDepositStakeByDelegatee: new Map<Address, Decimal>(), pendingWithdrawalUnstakeByDelegatee: new Map<Address, Decimal>(), pendingRewards: new Array(farmState.farmState.rewardInfos.length) .fill(undefined) .map(function (value, index) { return { rewardTokenMint: DEFAULT_PUBLIC_KEY, rewardTokenProgramId: farmState!.farmState.rewardInfos[index].token.tokenProgram, rewardType: farmState?.farmState.rewardInfos[index].rewardType || 0, cumulatedPendingRewards: new Decimal(0), pendingRewardsByDelegatee: new Map<Address, Decimal>(), }; }), }; if ( new Decimal(scaleDownWads(userState.userState.activeStakeScaled)).gt( 0, ) || hasReward ) { // active stake by delegatee userFarm.activeStakeByDelegatee.set( userState.userState.delegatee, lamportsToCollDecimal( new Decimal(scaleDownWads(userState.userState.activeStakeScaled)), Number(farmState.farmState.token.decimals), ), ); // pendingDepositStake by delegatee userFarm.pendingDepositStakeByDelegatee.set( userState.userState.delegatee, new Decimal( scaleDownWads(userState.userState.pendingDepositStakeScaled), ), ); // pendingWithdrawalUnstake by delegatee userFarm.pendingWithdrawalUnstakeByDelegatee.set( userState.userState.delegatee, new Decimal( scaleDownWads(userState.userState.pendingWithdrawalUnstakeScaled), ), ); // cumulating rewards for ( let indexReward = 0; indexReward < farmState.farmState.rewardInfos.length; indexReward++ ) { userFarm.pendingRewards[indexReward].rewardTokenMint = farmState.farmState.rewardInfos[indexReward].token.mint; userFarm.pendingRewards[indexReward].cumulatedPendingRewards = userFarm.pendingRewards[indexReward].cumulatedPendingRewards.add( userPendingRewardAmounts[indexReward], ); userFarm.pendingRewards[indexReward].pendingRewardsByDelegatee.set( userState.userState.delegatee, userPendingRewardAmounts[indexReward], ); } // set updated userFarm userFarms.set(userState.userState.farmState, userFarm); } else { // skip as we are not accounting for empty userFarms continue; } } } return userFarms; } async getRewardsAPYForStrategy(strategy: Address): Promise<FarmIncentives> { const farmIncentives = await getRewardsApyForStrategy( this.getConnection(), strategy, ); return farmIncentives; } async getAllFarmsForUserMultiState( user: Address, timestamp: Decimal, strategiesToInclude?: Set<Address>, vaultsToInclude?: Set<Address>, ): Promise<Map<Address, UserFarm[]>> { const userStates = await this.getAllUserStatesForUser(user); const farmStatesFiltered = await this.getFarmStatesFromUserStates( userStates, strategiesToInclude, vaultsToInclude, ); if (farmStatesFiltered.length === 0) { // Return empty if no serializable farm states found return new Map<Address, UserFarm[]>(); } const userFarmsByFarm = new Map<Address, UserFarm[]>(); for (let userState of userStates) { let farmState = farmStatesFiltered.find( (farmState) => farmState.key === userState.userState.farmState, ); if (!farmState) { // Skip farms that are not serializable anymore continue; } let oraclePrices = await this.getOraclePrices(farmState.farmState); const { userPendingRewardAmounts, hasReward } = this.getUserPendingRewards( userState.userState, farmState.farmState, timestamp, oraclePrices, ); // Only add if there's a reward or active stake if ( !new Decimal(scaleDownWads(userState.userState.activeStakeScaled)).gt( 0, ) && !hasReward ) { continue; } // Create a new UserFarm instance const userFarm: UserFarm = { userStateAddress: userState.key, farm: userState.userState.farmState, strategyId: farmState.farmState.strategyId, delegateAuthority: farmState.farmState.delegateAuthority, stakedToken: farmState.farmState.token.mint, userState: userState.userState, activeStakeByDelegatee: new Map<Address, Decimal>(), pendingDepositStakeByDelegatee: new Map<Address, Decimal>(), pendingWithdrawalUnstakeByDelegatee: new Map<Address, Decimal>(), pendingRewards: new Array(farmState.farmState.rewardInfos.length) .fill(undefined) .map(function (value, index) { return { rewardTokenMint: farmState!.farmState.rewardInfos[index].token.mint, rewardTokenProgramId: farmState!.farmState.rewardInfos[index].token.tokenProgram, rewardType: farmState?.farmState.rewardInfos[index].rewardType || 0, cumulatedPendingRewards: new Decimal(0), pendingRewardsByDelegatee: new Map<Address, Decimal>(), }; }), }; // active stake by delegatee userFarm.activeStakeByDelegatee.set( userState.userState.delegatee, lamportsToCollDecimal( new Decimal(scaleDownWads(userState.userState.activeStakeScaled)), Number(farmState.farmState.token.decimals), ), ); // pendingDepositStake by delegatee userFarm.pendingDepositStakeByDelegatee.set( userState.userState.delegatee, new Decimal( scaleDownWads(userState.userState.pendingDepositStakeScaled), ), ); // pendingWithdrawalUnstake by delegatee userFarm.pendingWithdrawalUnstakeByDelegatee.set( userState.userState.delegatee, new Decimal( scaleDownWads(userState.userState.pendingWithdrawalUnstakeScaled), ), ); // cumulating rewards for ( let indexReward = 0; indexReward < farmState.farmState.rewardInfos.length; indexReward++ ) { userFarm.pendingRewards[indexReward].rewardTokenMint = farmState.farmState.rewardInfos[indexReward].token.mint; userFarm.pendingRewards[indexReward].cumulatedPendingRewards = userPendingRewardAmounts[indexReward]; userFarm.pendingRewards[indexReward].pendingRewardsByDelegatee.set( userState.userState.delegatee, userPendingRewardAmounts[indexReward], ); } // Add the userFarm to the array for the corresponding farm if (!userFarmsByFarm.has(userState.userState.farmState)) { userFarmsByFarm.set(userState.userState.farmState, [userFarm]); } else { userFarmsByFarm.get(userState.userState.farmState)!.push(userFarm); } } return userFarmsByFarm; } async getUserStateKeyForUndelegatedFarm( user: Address, farmAddress: Address, ): Promise<UserAndKey> { const userStateAddress = await getUserStatePDA( this._farmsProgramId, farmAddress, user, ); const userStateAccount = await fetchMaybeUserState( this._connection, userStateAddress, ); if (!userStateAccount.exists) { throw new Error(`User state not found ${userStateAddress.toString()}`); } return { key: userStateAddress, userState: userStateAccount.data }; } async getCurrentTimeUnitForFarm(farmState: FarmState): Promise<BN> { // Use a finalized slot so cooldown status is conservative; processed slots can also lack block time. const slot = await this._connection .getSlot({ commitment: "finalized" }) .send(); if (farmState.timeUnit === TimeUnit.Slots) { return new BN(slot.toString()); } if (farmState.timeUnit === TimeUnit.Seconds) { const timestamp = await this._connection.getBlockTime(slot).send(); if (timestamp === null) { throw new Error( `Could not resolve block time for slot ${slot.toString()}`, ); } return new BN(timestamp.toString()); } throw new Error(`Unsupported farm time unit ${farmState.timeUnit}`); } async getPendingWithdrawalCooldownStatus( userStateAddress: Address, ): Promise<PendingWithdrawalCooldownStatus> { const userStateAccount = await fetchMaybeUserState( this._connection, userStateAddress, ); if (!userStateAccount.exists) { throw new Error(`User state not found ${userStateAddress.toString()}`); } const farmAccount = await fetchMaybeFarmState( this._connection, userStateAccount.data.farmState, ); if (!farmAccount.exists) { throw new Error( `Farm state not found ${userStateAccount.data.farmState.toString()}`, ); } const currentTimeUnit = await this.getCurrentTimeUnitForFarm( farmAccount.data, ); return calculatePendingWithdrawalCooldownStatus( farmAccount.data, userStateAccount.data, currentTimeUnit, userStateAddress, ); } async getPendingWithdrawalCooldownStatusForUser( user: Address, farm: Address | FarmAndKey, ): Promise<PendingWithdrawalCooldownStatus> { const farmAddress = typeof farm === "string" ? farm : farm.key; const userStateAddress = await getUserStatePDA( this._farmsProgramId, farmAddress, user, ); if (typeof farm === "string") { return this.getPendingWithdrawalCooldownStatus(userStateAddress); } const userStateAccount = await fetchMaybeUserState( this._connection, userStateAddress, ); if (!userStateAccount.exists) { throw new Error(`User state not found ${userStateAddress.toString()}`); } if (userStateAccount.data.farmState !== farmAddress) { throw new Error( `User state ${userStateAddress.toString()} belongs to farm ${userStateAccount.data.farmState.toString()}, not ${farmAddress.toString()}`, ); } if (farm.farmState.token.mint !== DEFAULT_PUBLIC_KEY) { const expectedFarmVault = await getFarmVaultPDA( this._farmsProgramId, farmAddress, farm.farmState.token.mint, ); if (farm.farmState.farmVault !== expectedFarmVault) { throw new Error( `Farm state data does not match farm address ${farmAddress.toString()}`, ); } } const currentTimeUnit = await this.getCurrentTimeUnitForFarm( farm.farmState, ); return calculatePendingWithdrawalCooldownStatus( farm.farmState, userStateAccount.data, currentTimeUnit, userStateAddress, ); } async getPendingWithdrawalCooldownStatusForWalletAndFarm( wallet: Address, farm: Address | FarmAndKey, ): Promise<PendingWithdrawalCooldownStatus> { return this.getPendingWithdrawalCooldownStatusForUser(wallet, farm); } async getUserTokensInUndelegatedFarm( user: Address, farm: Address, tokenDecimals: number, ): Promise<Decimal> { const userState = await this.getUserStateKeyForUndelegatedFarm(user, farm); return lamportsToCollDecimal( new Decimal(scaleDownWads(userState.userState.activeStakeScaled)), tokenDecimals, ); } async getUserForUndelegatedFarm( user: Address, farmAddress: Address, timestamp: Decimal, ): Promise<UserFarm> { const farmAccount = await fetchMaybeFarmState( this._connection, farmAddress, ); if (!farmAccount.exists) { throw new Error(`Farm not found ${farmAddress.toString()}`); } const farmState = farmAccount.data; const userStateAddress = await getUserStatePDA( this._farmsProgramId, farmAddress, user, ); const userStateAccount = await fetchMaybeUserState( this._connection, userStateAddress, ); if (!userStateAccount.exists) { throw new Error(`User state not found ${userStateAddress.toString()}`); } const userState = userStateAccount.data; const userFarm: UserFarm = { userStateAddress: userStateAddress, farm: farmAddress, userState, strategyId: farmState.strategyId, delegateAuthority: farmState.delegateAuthority, stakedToken: farmState.token.mint, activeStakeByDelegatee: new Map<Address, Decimal>(), pendingDepositStakeByDelegatee: new Map<Address, Decimal>(), pendingWithdrawalUnstakeByDelegatee: new Map<Address, Decimal>(), pendingRewards: new Array(farmState.rewardInfos.length) .fill(undefined) .map(function (value, index) { return { rewardTokenMint: DEFAULT_PUBLIC_KEY, rewardTokenProgramId: farmState?.rewardInfos[index].token.tokenProgram, rewardType: farmState?.rewardInfos[index].rewardType || 0, cumulatedPendingRewards: new Decimal(0), pendingRewardsByDelegatee: new Map<Address, Decimal>(), }; }), }; // active stake userFarm.activeStakeByDelegatee.set( user, lamportsToCollDecimal( new Decimal(scaleDownWads(userState.activeStakeScaled)), Number(farmState.token.decimals), ), ); // pendingDepositStake userFarm.pendingDepositStakeByDelegatee.set( user, new Decimal(scaleDownWads(userState.pendingDepositStakeScaled)), ); // pendingWithdrawalUnstake userFarm.pendingWithdrawalUnstakeByDelegatee.set( user, new Decimal(scaleDownWads(userState.pendingWithdrawalUnstakeScaled)), ); // get oraclePrices let oraclePrices: OraclePrices | null = null; if (farmState.scopePrices !== DEFAULT_PUBLIC_KEY) { oraclePrices = await OraclePrices.fetch( this._connection, farmState.scopePrices, ); if (!oraclePrices) { throw new Error("Error fetching oracle prices"); } } const userPendingRewardAmounts: Decimal[] = []; for ( let indexReward = 0; indexReward < farmState.rewardInfos.length; indexReward++ ) { // calculate pending rewards userPendingRewardAmounts[indexReward] = calculatePendingRewards( farmState, userState, indexReward, timestamp, oraclePrices, ); userFarm.pendingRewards[indexReward].rewardTokenMint = farmState.rewardInfos[indexReward].token.mint; userFarm.pendingRewards[indexReward].cumulatedPendingRewards = userPendingRewardAmounts[indexReward]; userFarm.pendingRewards[indexReward].pendingRewardsByDelegatee.set( user, userPendingRewardAmounts[indexReward], ); } return userFarm; } async createNewUserIx( authority: TransactionSigner, farm: Address, user: Address = authority.address, delegatee: Address = user, ): Promise<Instruction> { const userState = await getUserStatePDA(this._farmsProgramId, farm, user); const ix = farmOperations.initializeUser( farm, user, userState, authority, delegatee, ); return ix; } async stakeIx( user: TransactionSigner, farm: Address, amountLamports: Decimal, stakeTokenMint: Address, scopePrices: Option<Address>, ): Promise<Instruction> { const farmVault = await getFarmVaultPDA( this._farmsProgramId, farm, stakeTokenMint, ); const userStatePk = await getUserStatePDA( this._farmsProgramId, farm, user.address, ); const userTokenAta = await getAssociatedTokenAddress( user.address, stakeTokenMint, TOKEN_PROGRAM_ADDRESS, ); const ix = farmOperations.stake( user, userStatePk, userTokenAta, farm, farmVault, stakeTokenMint, scopePrices, decimalToBN(amountLamports), ); return ix; } async unstakeIx( user: TransactionSigner, farm: Address, amountLamports: Decimal, scopePrices: Option<Address>, ): Promise<Instruction> { const userStatePk = await getUserStatePDA( this._farmsProgramId, farm, user.address, ); const ix = farmOperations.unstake( user, userStatePk, farm, scopePrices, decimalToBN(amountLamports), ); return ix; } async withdrawUnstakedDepositIx( user: TransactionSigner, userState: Address, farmState: Address, stakeTokenMint: Address, ): Promise<Instruction> { const userTokenAta = await getAssociatedTokenAddress( user.address, stakeTokenMint, TOKEN_PROGRAM_ADDRESS, ); const farmVault = await getFarmVaultPDA( this._farmsProgramId, farmState, stakeTokenMint, ); const farmVaultsAuthority = await getFarmAuthorityPDA( this._farmsProgramId, farmState, ); return farmOperations.withdrawUnstakedDeposit( user, userState, farmState, userTokenAta, farmVault, farmVaultsAuthority, ); } async claimForUserForFarmRewardIx( user: TransactionSigner, farm: Address, rewardMint: Address, isDelegated: boolean, rewardIndex = -1, delegatees?: Address[], ): Promise<[[Address, Instruction][], Instruction[]]> { const ixns: Instruction[] = []; const ataIxns: [Address, Instruction][] = []; const userStatesAndKeys = isDelegated ? await this.getUserStateKeysForDelegatedFarm( user.address, farm, delegatees, ) : [await this.getUserStateKeyForUndelegatedFarm(user.address, farm)]; const farmAccount = await fetchMaybeFarmState(this._connection, farm); if (!farmAccount.exists) { throw new Error(`Farm not found ${farm.toString()}`); } const farmState = farmAccount.data; const treasuryVault = await getTreasuryVaultPDA( this._farmsProgramId, farmState.globalConfig, rewardMint, ); // find rewardIndex if not defined if (rewardIndex === -1) { rewardIndex = farmState.rewardInfos.findIndex( (r) => r.token.mint === rewardMint, ); } const rewardsTokenProgram = farmState.rewardInfos[rewardIndex].token.tokenProgram; const userRewardAta = await getAssociatedTokenAddress( user.address, rewardMint, rewardsTokenProgram, ); const ataExists = await checkIfAccountExists( this._connection, userRewardAta, ); if (!ataExists) { const [, ix] = await createAssociatedTokenAccountIdempotentInstruction( user, rewardMint, rewardsTokenProgram, user.address, userRewardAta, ); ataIxns.push([rewardMint, ix]); } for ( let userStateIndex = 0; userStateIndex < userStatesAndKeys.length; userStateIndex++ ) { const ix = farmOperations.harvestReward( user, userStatesAndKeys[userStateIndex].key, userRewardAta, farmState.globalConfig, treasuryVault, farm, rewardMint, farmState.rewardInfos[rewardIndex].rewardsVault, farmState.farmVaultsAuthority, getScopePricesFromFarm(farmState), rewardsTokenProgram, rewardIndex, ); ixns.push(ix); } return [ataIxns, ixns]; } async claimForUserForFarmAllRewardsIx( payer: TransactionSigner, user: Address, farm: Address, isDelegated: boolean, delegatees?: Address[], ): Promise<Array<Instruction>> { const farmAccount = await fetchMaybeFarmState(this._connection, farm); const userStatesAndKeys = isDelegated ? await this.getUserStateKeysForDelegatedFarm(user, farm, delegatees) : [await this.getUserStateKeyForUndelegatedFarm(user, farm)]; const ixs = new Array<Instruction>(); if (!farmAccount.exists) { throw new Error(`Farm not found ${farm.toString()}`); } const farmState = farmAccount.data; const timestampSeconds = Date.now() / 1000; for ( let userStateIndex = 0; userStateIndex < userStatesAndKeys.length; userStateIndex++ ) { for ( let rewardIndex = 0; rewardIndex < Number(farmState.numRewardTokens); rewardIndex++ ) { const rewardMint = farmState.rewardInfos[rewardIndex].token.mint; const rewardTokenProgram = farmState.rewardInfos[rewardIndex].token.tokenProgram; const rewardMinClaimDurationSeconds = Number( farmState.rewardInfos[rewardIndex].minClaimDurationSeconds, ); const lastClaimTsSeconds = Number( userStatesAndKeys[userStateIndex].userState.lastClaimTs[rewardIndex], ); if ( timestampSeconds - lastClaimTsSeconds < rewardMinClaimDurationSeconds ) { continue; } const userRewardAta = await getAssociatedTokenAddress( user, rewardMint, rewardTokenProgram, ); const treasuryVault = await getTreasuryVaultPDA( this._farmsProgramId, farmState.globalConfig, rewardMint, ); const ataExists = await checkIfAccountExists( this._connection, userRewardAta, ); if (!ataExists && payer.address === user) { const [, ix] = await createAssociatedTokenAccountIdempotentInstruction( payer, rewardMint, rewardTokenProgram, user, userRewardAta, ); ixs.push(ix); } ixs.push( farmOperations.harvestReward( payer, userStatesAndKeys[userStateIndex].key, userRewardAta, farmState.globalConfig, treasuryVault, farm, rewardMint, farmState.rewardInfos[rewardIndex].rewardsVault, farmState.farmVaultsAuthority, getScopePricesFromFarm(farmState), rewardTokenProgram, rewardIndex, ), ); } } return ixs; } async transferOwnershipIx( user: TransactionSigner, userState: Address, newUser: Address, ): Promise<Instruction> { const userStateAccount = await fetchMaybeUserState( this._connection, userState, ); if (!userStateAccount.exists) { throw new Error(`User state not found ${userState.toString()}`); } const userStateData = userStateAccount.data; const farmAccount = await fetchMaybeFarmState( this._connection, userStateData.farmState, ); if (!farmAccount.exists) { throw new Error( `Farm state not found ${userStateData.farmState.toString()}`, ); } const farmState = farmAccount.data; this.validateFarmStateForTransferOwnership(farmState); const newOwnerUserState = await getUserStatePDA( FARMS_PROGRAM_ADDRESS, userStateData.farmState, newUser, ); return farmOperations.transferOwnership( user, userState, newUser, userStateData.farmState, newOwnerUserState, getScopePricesFromFarm(farmState), ); } validateFarmStateForTransferOwnership(farmState: FarmState): void { if (Number(farmState.lockingMode) !== LockingMode.None) { throw new Error( "Transfer ownership is not allowed for farms with a locking mode", ); } if (farmState.isFarmDelegated) { throw new Error("Transfer ownership is not allowed for delegated farms"); } if (farmState.withdrawalCooldownPeriod > 0) { throw new Error( "Transfer ownership is not allowed for farms with a withdrawal cooldown period", ); } } async transferOwnershipAllUserStatesIx( user: TransactionSigner, newUser: Address, ): Promise<Array<Instruction>> { const userStates = await this.getAllUserStatesForUser(user.address); const farms = await this.getFarmStatesFromUserStates(userStates); const ixs = new Array<Instruction>(); for (let index = 0; index < userStates.length; index++) { const farmAddress = userStates[index].userState.farmState; const farmState = farms.find((farm) => farm.key === farmAddress); if (!farmState) { throw new Error( `Farm state not found for user state ${userStates[index].key}`, ); } this.validateFarmStateForTransferOwnership(farmState.farmState); const newOwnerUserState = await getUserStatePDA( FARMS_PROGRAM_ADDRESS, farmAddress, newUser, ); ixs[index] = farmOperations.transferOwnership( user, userStates[index].key, newUser, farmAddress, newOwnerUserState, getScopePricesFromFarm(farmState.farmState), ); } return ixs; } async createFarmIxs( admin: TransactionSigner, farm: TransactionSigner, globalConfig: Address, stakeTokenMint: Address, ): Promise<Instruction[]> { const farmVault = await getFarmVaultPDA( this._farmsProgramId, farm.address, stakeTokenMint, ); const farmVaultAuthority = await getFarmAuthorityPDA( this._farmsProgramId, farm.address, ); let ixs: Instruction[] = []; ixs.push( await createKeypairRentExemptIx( this.getConnection(), admin, farm, SIZE_FARM_STATE, this._farmsProgramId, ), ); ixs.push( farmOperations.initializeFarm( globalConfig, admin, farm.address, farmVault, farmVaultAuthority, stakeTokenMint, ), ); return ixs; } async createFarmDelegatedIx( admin: TransactionSigner, farm: TransactionSigner, globalConfig: Address, farmDelegate: TransactionSigner, ): Promise<Instruction[]> { const farmVaultAuthority = await getFarmAuthorityPDA( this._farmsProgramId, farm.address, ); let ixs: Instruction[] = []; ixs.push( await createKeypairRentExemptIx( this.getConnection(), admin, farm, SIZE_FARM_STATE, this._farmsProgramId, ), ); ixs.push( farmOperations.initializeFarmDelegated( globalConfig, admin, farm.address, farmVaultAuthority, farmDelegate, ), ); return ixs; } async addRewardToFarmIx( admin: TransactionSigner, globalConfig: Address, farm: Address, mint: Address, tokenProgram: Address, ): Promise<Instruction> { const globalConfigAccount = await fetchMaybeGlobalConfig( this._connection, globalConfig, ); if (!globalConfigAccount.exists) { throw new Error("Could not fetch global config"); } const globalConfigState = globalConfigAccount.data; const treasuryVault = await getTreasuryVaultPDA( this._farmsProgramId, globalConfig, mint, ); let farmVaultAuthority = await getFarmAuthorityPDA( this._farmsProgramId, farm, ); const rewardVault = await getRewardVaultPDA( this._farmsProgramId, farm, mint, ); const ix = farmOperations.initializeReward( globalConfig, globalConfigState.