@kamino-finance/farms-sdk
Version:
1,816 lines (1,615 loc) • 65.3 kB
text/typescript
import BN from "bn.js";
import {
Address,
address,
Base58EncodedBytes,
GetProgramAccountsDatasizeFilter,
GetProgramAccountsMemcmpFilter,
IInstruction,
none,
Option,
Rpc,
Slot,
SolanaRpcApi,
some,
TransactionSigner,
UnixTimestamp,
} from "@solana/kit";
import {
calculateCurrentRewardPerToken,
calculatePendingRewards,
checkIfAccountExists,
isValidPubkey,
collToLamportsDecimal,
createKeypairRentExemptIx,
DEFAULT_PUBLIC_KEY,
getFarmAuthorityPDA,
getFarmVaultPDA,
getRewardVaultPDA,
getTreasuryAuthorityPDA,
getTreasuryVaultPDA,
getUserStatePDA,
GlobalConfigFlagValueType,
lamportsToCollDecimal,
scaleDownWads,
SIZE_FARM_STATE,
SIZE_GLOBAL_CONFIG,
decimalToBN,
} from "./utils";
import {
FarmIncentives,
IncentiveRewardStats,
UserFarm,
UserAndKey,
FarmAndKey,
} from "./models";
import { FarmState, GlobalConfig, UserState } from "./@codegen/farms/accounts";
import * as farmOperations from "./utils/operations";
import Decimal from "decimal.js";
import {
FarmConfigOption,
FarmConfigOptionKind,
GlobalConfigOptionKind,
LockingMode,
RewardInfo,
RewardType,
TimeUnit,
} from "./@codegen/farms/types/index";
import { PROGRAM_ID } from "./@codegen/farms/programId";
import { OraclePrices } from "@kamino-finance/scope-sdk/dist/@codegen/scope/accounts";
import { chunks } from "./utils/arrayUtils";
import { batchFetch } from "./utils/batch";
import {
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddress,
} from "./utils/token";
import {
SECONDS_IN_A_DAY,
SECONDS_IN_A_MONTH,
SECONDS_IN_A_WEEK,
SECONDS_IN_A_YEAR,
} from "./consts";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
import { getScopePricesFromFarm } from "./utils/option";
import { getRewardsApyForStrategy } from "./utils/apy";
import { Connection } from "@solana/web3.js";
import { U64_MAX } from "./utils/consts";
import { decompress } from "fzstd";
import { backOff, IBackOffOptions } from "exponential-backoff";
import { ZERO_BN } from "@kamino-finance/kliquidity-sdk";
export interface UserPointsBreakdown {
totalPoints: Decimal;
currentBoost: Decimal;
currentPointsPerDay: Decimal;
perPositionBoost: Map<Address, Decimal>;
perPositionPointsPerDay: Map<Address, Decimal>;
}
export interface RewardCurvePoint {
startTs: number;
rps: number;
}
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 = PROGRAM_ID,
) {
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(UserState.layout.span + 8) });
return (
await this._connection
.getProgramAccounts(this._farmsProgramId, {
filters,
encoding: "base64",
})
.send()
).map((x) => {
const userAndKey: UserAndKey = {
userState: UserState.decode(Buffer.from(x.account.data[0], "base64")),
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(UserState.layout.span + 8) });
return (
await this._connection
.getProgramAccounts(this._farmsProgramId, {
filters,
encoding: "base64",
})
.send()
).map((x) => {
const userAndKey: UserAndKey = {
userState: UserState.decode(Buffer.from(x.account.data[0], "base64")),
key: x.pubkey,
};
return userAndKey;
});
}
async getAllUserStates(): Promise<UserAndKey[]> {
return (
await this._connection
.getProgramAccounts(this._farmsProgramId, {
filters: [{ dataSize: BigInt(UserState.layout.span + 8) }],
encoding: "base64",
})
.send()
).map((x) => {
const userAndKey: UserAndKey = {
userState: UserState.decode(Buffer.from(x.account.data[0], "base64")),
key: x.pubkey,
};
return userAndKey;
});
}
async getAllUserStatesWithFilter(
isFarmDelegated: boolean,
): Promise<UserAndKey[]> {
return (
await this._connection
.getProgramAccounts(this._farmsProgramId, {
filters: [
{ dataSize: BigInt(UserState.layout.span + 8) },
{
memcmp: {
offset: 80n,
bytes: (isFarmDelegated ? "2" : "1") as Base58EncodedBytes,
encoding: "base58",
},
},
],
encoding: "base64",
})
.send()
).map((x) => {
const userAndKey: UserAndKey = {
userState: UserState.decode(Buffer.from(x.account.data[0], "base64")),
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[]> {
return (
await this._connection
.getProgramAccounts(this._farmsProgramId, {
filters: [
{ dataSize: BigInt(UserState.layout.span + 8) },
{
memcmp: {
offset: 8n + 8n,
bytes: farm.toString() as Base58EncodedBytes,
encoding: "base58",
},
},
],
encoding: "base64+zstd",
})
.send()
).map((x) => {
const compressedData = Buffer.from(x.account.data[0], "base64");
const decompressedData = decompress(compressedData);
const userAndKey: UserAndKey = {
userState: UserState.decode(Buffer.from(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(FarmState.layout.span + 8) });
return (
await this._connection
.getProgramAccounts(this._farmsProgramId, {
filters,
encoding: "base64",
})
.send()
).map((x) => {
const farmAndKey: FarmAndKey = {
farmState: FarmState.decode(Buffer.from(x.account.data[0], "base64")),
key: x.pubkey,
};
return farmAndKey;
});
}
async getAllFarmStates(): Promise<FarmAndKey[]> {
return (
await this._connection
.getProgramAccounts(this._farmsProgramId, {
filters: [{ dataSize: BigInt(FarmState.layout.span + 8) }],
encoding: "base64",
})
.send()
)
.map((x) => {
try {
const farmAndKey: FarmAndKey = {
farmState: FarmState.decode(
Buffer.from(x.account.data[0], "base64"),
),
key: x.pubkey,
};
return farmAndKey;
} catch (err) {
return null;
}
})
.filter((x) => x !== null) as FarmAndKey[];
}
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 farmState = await FarmState.fetch(this._connection, farm);
if (!farmState) {
throw Error("No Farm found");
}
return lamportsToCollDecimal(
new Decimal(scaleDownWads(farmState.totalActiveStakeScaled)),
farmState.token.decimals.toNumber(),
);
}
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()),
farms[index].farmState.token.decimals.toNumber(),
),
);
}
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 userState = await UserState.fetch(this._connection, userStateAddress);
let farmState = await FarmState.fetch(this._connection, farm);
if (!farmState) {
throw new Error("Error fetching farm state");
}
let lockingMode = farmState?.lockingMode.toNumber();
let lockingDuration = farmState?.lockingDuration.toNumber();
let penalty = farmState.lockingEarlyWithdrawalPenaltyBps.toNumber();
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.discriminator) {
return {
farmLockupOriginalDuration: 0,
farmLockupExpiry: 0,
lockupRemainingDuration: 0,
};
}
if (lockingMode == LockingMode.WithExpiry.discriminator) {
// Locking starts globally for the entire farm
lockingStart = farmState?.lockingStartTimestamp.toNumber();
}
if (lockingMode == LockingMode.Continuous.discriminator) {
// Locking starts for each user individually at each stake
// if the user has a state, else now
if (userState === null) {
lockingStart = timestampNow;
} else {
if (!userState) {
throw new Error("Error fetching user state");
}
lockingStart = userState.lastStakeTs.toNumber();
}
}
const timestampBeginning = lockingStart;
const timestampMaturity = lockingStart + lockingDuration;
if (timestampNow >= timestampMaturity) {
// Time has passed, no remaining
return {
farmLockupOriginalDuration: farmState.lockingDuration.toNumber(),
farmLockupExpiry: timestampMaturity,
lockupRemainingDuration: 0,
};
}
if (timestampNow < timestampBeginning) {
// Time has not started, no remaining
return {
farmLockupOriginalDuration: farmState.lockingDuration.toNumber(),
farmLockupExpiry: timestampMaturity,
lockupRemainingDuration: 0,
};
}
const timeRemaining = timestampMaturity - timestampNow;
const remainingLockedDurationSeconds = Math.max(timeRemaining, 0);
return {
farmLockupOriginalDuration: farmState.lockingDuration.toNumber(),
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 userStates = await UserState.fetchMultiple(
this._connection,
userStateAddresses,
);
userStates.forEach((userState, index) => {
if (userState && userState.farmState === farm) {
userStateKeysForFarm.push({
key: userStateAddresses[index],
userState: userState,
});
}
});
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 farmStates;
}
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)),
farmState.farmState.token.decimals.toNumber(),
),
);
// 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,
rpcEndpoint: string,
): Promise<FarmIncentives> {
const legacyConnection = new Connection(rpcEndpoint);
const farmIncentives = await getRewardsApyForStrategy(
this.getConnection(),
legacyConnection,
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)),
farmState.farmState.token.decimals.toNumber(),
),
);
// 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 userState = await UserState.fetch(this._connection, userStateAddress);
if (!userState) {
throw new Error(`User state not found ${userStateAddress.toString()}`);
}
return { key: userStateAddress, userState: userState };
}
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 farmState = await FarmState.fetch(this._connection, farmAddress);
if (!farmState) {
throw new Error(`Farm not found ${farmAddress.toString()}`);
}
const userStateAddress = await getUserStatePDA(
this._farmsProgramId,
farmAddress,
user,
);
const userState = await UserState.fetch(this._connection, userStateAddress);
if (!userState) {
throw new Error(`User state not found ${userStateAddress.toString()}`);
}
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)),
farmState.token.decimals.toNumber(),
),
);
// 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<IInstruction> {
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<IInstruction> {
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<IInstruction> {
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<IInstruction> {
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, IInstruction][], IInstruction[]]> {
const ixns: IInstruction[] = [];
const ataIxns: [Address, IInstruction][] = [];
const userStatesAndKeys = isDelegated
? await this.getUserStateKeysForDelegatedFarm(
user.address,
farm,
delegatees,
)
: [await this.getUserStateKeyForUndelegatedFarm(user.address, farm)];
const farmState = await FarmState.fetch(this._connection, farm);
if (!farmState) {
throw new Error(`Farm not found ${farm.toString()}`);
}
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(
user: TransactionSigner,
farm: Address,
isDelegated: boolean,
delegatees?: Address[],
): Promise<Array<IInstruction>> {
const farmState = await FarmState.fetch(this._connection, farm);
const userStatesAndKeys = isDelegated
? await this.getUserStateKeysForDelegatedFarm(
user.address,
farm,
delegatees,
)
: [await this.getUserStateKeyForUndelegatedFarm(user.address, farm)];
const ixs = new Array<IInstruction>();
// hardcoded as a hotfix for JTO release;
// TODO: replace by proper fix
const jitoFarm = address("Cik985zLyHYdv5Hs73BUWUcMHMhgfBNwbcCYyvBjV2tt");
if (!farmState) {
throw new Error(`Farm not found ${farm.toString()}`);
}
for (
let userStateIndex = 0;
userStateIndex < userStatesAndKeys.length;
userStateIndex++
) {
for (
let rewardIndex = 0;
rewardIndex < farmState.numRewardTokens.toNumber();
rewardIndex++
) {
if (
jitoFarm !== farm &&
farmState.rewardInfos[rewardIndex].rewardType ==
RewardType.Constant.discriminator
) {
continue;
}
const rewardMint = farmState.rewardInfos[rewardIndex].token.mint;
const rewardTokenProgram =
farmState.rewardInfos[rewardIndex].token.tokenProgram;
const userRewardAta = await getAssociatedTokenAddress(
user.address,
rewardMint,
rewardTokenProgram,
);
const treasuryVault = await getTreasuryVaultPDA(
this._farmsProgramId,
farmState.globalConfig,
rewardMint,
);
const ataExists = await checkIfAccountExists(
this._connection,
userRewardAta,
);
if (!ataExists) {
const [, ix] =
await createAssociatedTokenAccountIdempotentInstruction(
user,
rewardMint,
rewardTokenProgram,
user.address,
userRewardAta,
);
ixs.push(ix);
}
ixs.push(
farmOperations.harvestReward(
user,
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<IInstruction> {
const userStateData = await UserState.fetch(
this._connection,
userState,
this._farmsProgramId,
);
if (!userStateData) {
throw new Error(`User state not found ${userState.toString()}`);
}
const farmState = await FarmState.fetch(
this._connection,
userStateData.farmState,
this._farmsProgramId,
);
if (!farmState) {
throw new Error(
`Farm state not found ${userStateData.farmState.toString()}`,
);
}
this.validateFarmStateForTransferOwnership(farmState);
const newOwnerUserState = await getUserStatePDA(
PROGRAM_ID,
userStateData.farmState,
newUser,
);
return farmOperations.transferOwnership(
user,
userState,
newUser,
userStateData.farmState,
newOwnerUserState,
getScopePricesFromFarm(farmState),
);
}
validateFarmStateForTransferOwnership(farmState: FarmState): void {
if (farmState.lockingMode.toNumber() !== LockingMode.None.discriminator) {
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<IInstruction>> {
const userStates = await this.getAllUserStatesForUser(user.address);
const farms = await this.getFarmStatesFromUserStates(userStates);
const ixs = new Array<IInstruction>();
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(
PROGRAM_ID,
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<IInstruction[]> {
const farmVault = await getFarmVaultPDA(
this._farmsProgramId,
farm.address,
stakeTokenMint,
);
const farmVaultAuthority = await getFarmAuthorityPDA(
this._farmsProgramId,
farm.address,
);
let ixs: IInstruction[] = [];
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<IInstruction[]> {
const farmVaultAuthority = await getFarmAuthorityPDA(
this._farmsProgramId,
farm.address,
);
let ixs: IInstruction[] = [];
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<IInstruction> {
const globalConfigState = await GlobalConfig.fetch(
this._connection,
globalConfig,
);
if (!globalConfigState) {
throw new Error("Could not fetch global config");
}
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.treasuryVaultsAuthority,
treasuryVault,
admin,
farm,
rewardVault,
farmVaultAuthority,
mint,
tokenProgram,
);
return ix;
}
async addRewardAmountToFarmIx(
payer: TransactionSigner,
farm: Address,
mint: Address,
amount: Decimal,
rewardIndexOverride: number = -1,
decimalsOverride: number = -1,
tokenProgramOverride: Address = TOKEN_PROGRAM_ADDRESS,
scopePricesOverride: Option<Address> = none(),
): Promise<IInstruction> {
let decimals = decimalsOverride;
let rewardIndex = rewardIndexOverride;
let scopePrices = scopePricesOverride;
let tokenProgram = tokenProgramOverride;
if (rewardIndex == -1) {
const farmState = await FarmState.fetch(this._connection, farm);
if (!farmState) {
throw new Error(`Could not fetch farm state ${farm}`);
}
scopePrices = getScopePricesFromFarm(farmState);
for (let i = 0; farmState.rewardInfos.length; i++) {
if (farmState.rewardInfos[i].token.mint === mint) {
if (
farmState.rewardInfos[i].token.tokenProgram !== DEFAULT_PUBLIC_KEY
) {
tokenProgram = farmState.rewardInfos[i].token.tokenProgram;
}
rewardIndex = i;
decimals = farmState.rewardInfos[i].token.decimals.toNumber();
break;
}
}
}
if (decimals == -1) {
throw new Error(`Could not find reward token ${mint}`);
}
let amountLamports = new BN(
collToLamportsDecimal(amount, decimals).floor().toString(),
);
const payerRewardAta = await getAssociatedTokenAddress(
payer.address,
mint,
tokenProgram,
);
let rewardVault = await getRewardVaultPDA(this._farmsProgramId, farm, mint);
let farmVaultsAuthority = await getFarmAuthorityPDA(
this._farmsProgramId,
farm,
);
const ix = farmOperations.addReward(
payer,
farm,
rewardVault,
farmVaultsAuthority,
payerRewardAta,
mint,
scopePrices,
rewardIndex,
tokenProgram,
amountLamports,
);
return ix;
}
async withdrawRewardAmountFromFarmIx(
payer: TransactionSigner,
farm: Address,
mint: Address,
amount: Decimal,
rewardIndexOverride: number = -1,
decimalsOverride: number = -1,
tokenProgramOverride: Address = TOKEN_PROGRAM_ADDRESS,
scopePricesOverride: Option<Address> = none(),
): Promise<IInstruction[]> {
let decimals = decimalsOverride;
let tokenProgram = tokenProgramOverride;
let rewardIndex = rewardIndexOverride;
let scopePrices = scopePricesOverride;
if (rewardIndex == -1) {
const farmState = await FarmState.fetch(this._connection, farm);
if (!farmState) {
throw new Error(`Could not fetch farm state ${farm}`);
}
scopePrices = getScopePricesFromFarm(farmState);
for (let i = 0; farmState.rewardInfos.length; i++) {
if (farmState.rewardInfos[i].token.mint === mint) {
rewardIndex = i;
decimals = farmState.rewardInfos[i].token.decimals.toNumber();
tokenProgram = farmState.rewardInfos[i].token.tokenProgram;
break;
}
}
}
if (decimals == -1) {
throw new Error(`Could not find reward token ${mint}`);
}
let amountLamports = new BN(
collToLamportsDecimal(amount, decimals).floor().toString(),
);
let rewardVault = await getRewardVaultPDA(this._farmsProgramId, farm, mint);
let farmVaultsAuthority = await getFarmAuthorityPDA(
this._farmsProgramId,
farm,
);
const [payerRewardAta, initAtaIdempotentIx] =
await createAssociatedTokenAccountIdempotentInstruction(
payer,
mint,
tokenProgram,
payer.address,
);
const ix = farmOperations.withdrawReward(
payer,
farm,
mint,
rewardVault,
farmVaultsAuthority,
payerRewardAta,
scopePrices,
tokenProgram,
rewardIndex,
amountLamports,
);
return [initAtaIdempotentIx, ix];
}
async updateFarmConfigIx(
admin: TransactionSigner,
farm: Address,
mint: Address,
mode: FarmConfigOptionKind,
value: number | Address | number[] | RewardCurvePoint[] | BN,
rewardIndexOverride: number = -1,
scopePricesOverride: Option<Address> = none(),
newFarm: boolean = false,
): Promise<IInstruction> {
let rewardIndex = rewardIndexOverride;
let scopePrices = scopePricesOverride;
if (rewardIndex == -1 && !newFarm) {
const farmState = await FarmState.fetch(this._connection, farm);
if (!farmState) {
throw new Error(`Could not fetch farm state ${farm}`);
}
if (farmState.scopePrices !== DEFAULT_PUBLIC_KEY) {
scopePrices = some(farmState.scopePrices);
}
for (let i = 0; farmState.rewardInfos.length; i++) {
if (farmState.rewardInfos[i].token.mint === mint) {
rewardIndex = i;
break;
}
}
}
const ix = farmOperations.updateFarmConfig(
admin,
farm,
scopePrices,
rewardIndex,
mode,
value,
);
return ix;
}
async refreshFarmIx(
farm: Address,
scopePrices: Option<Address>,
): Promise<IInstruction> {
return farmOperations.refreshFarm(farm, scopePrices);
}
async refreshUserIx(
userState: Address,
farmState: Address,
scopePrices: Option<Address>,
): Promise<IInstruction> {
return farmOperations.refreshUserState(userState, farmState, scopePrices);
}
async createGlobalConfigIxs(
admin: TransactionSigner,
globalConfig: TransactionSigner,
): Promise<IInstruction[]> {
let ixs: IInstruction[] = [];
ixs.push(
await createKeypairRentExemptIx(
this.getConnection(),
admin,
globalConfig,
SIZE_GLOBAL_CONFIG,
this._farmsProgramId,
),
);
const treasuryVaultAuthority = await getTreasuryAuthorityPDA(
this._farmsProgramId,
globalConfig.address,
);
ixs.push(
farmOperations.initializeGlobalConfig(
admin,
globalConfig.address,
treasuryVaultAuthority,
),
);
return ixs;
}
async updateGlobalConfigIx(
admin: TransactionSigner