UNPKG

@hubbleprotocol/farms-sdk

Version:
1,884 lines (1,676 loc) 75 kB
import { AnchorProvider, BN, Idl, Program, Provider } from "@coral-xyz/anchor"; import FARMS_IDL from "./rpc_client/farms.json"; // @ts-ignore import { binary_to_base58 } from "base58-js"; import { Connection, GetProgramAccountsFilter, PublicKey, sendAndConfirmTransaction, Signer, Transaction, TransactionInstruction, TransactionSignature, } from "@solana/web3.js"; import { calculateCurrentRewardPerToken, calculateNewRewardToBeIssued, calculatePendingRewards, checkIfAccountExists, createAssociatedTokenAccountIdempotentInstruction, getReadOnlyWallet, scopePriceForFarm, SIZE_FARM_STATE, SIZE_GLOBAL_CONFIG, } from "./utils"; import { getAssociatedTokenAddress, getTreasuryVaultPDA, getUserStatePDA, collToLamportsDecimal, getFarmVaultPDA, getFarmAuthorityPDA, getRewardVaultPDA, lamportsToCollDecimal, getTreasuryAuthorityPDA, createKeypairRentExemptIx, scaleDownWads, createAddExtraComputeUnitsTransaction, } from "./utils"; import { UserState, UserStateFields } from "./rpc_client/accounts"; import { UserFarm } from "./models"; import { FarmState, FarmStateFields, GlobalConfig, } from "./rpc_client/accounts"; import * as farmOperations from "./utils/operations"; import Decimal from "decimal.js"; import { Keypair, VersionedTransaction } from "@solana/web3.js"; import { GlobalConfigOptionKind, FarmConfigOptionKind, TimeUnit, LockingMode, RewardType, RewardInfo, } from "./rpc_client/types/index"; import { FarmAndKey, UserAndKey } from "./models"; import { PROGRAM_ID } from "./rpc_client/programId"; import { OraclePrices } from "@hubbleprotocol/scope-sdk"; import { chunks } from "./utils/arrayUtils"; import { KaminoMarket, KaminoReserve, lamportsToNumberDecimal, ObligationTypeTag, Position, PubkeyHashMap, PublicKeySet, } from "@kamino-finance/klend-sdk"; import { ReserveFarmKind } from "@kamino-finance/klend-sdk/dist/idl_codegen/types"; import { createAddExtraComputeUnitFeeTransaction, unwrap, } from "./commands/utils"; import { getMintDecimals } from "@project-serum/serum/lib/market"; import { signSendAndConfirmRawTransactionWithRetry, Web3Client, } from "./utils/sendTransactionsUtils"; import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; export const farmsId = new PublicKey( "FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr", ); export interface UserPointsBreakdown { totalPoints: Decimal; currentBoost: Decimal; currentPointsPerDay: Decimal; perPositionBoost: PubkeyHashMap<PublicKey, Decimal>; perPositionPointsPerDay: PubkeyHashMap<PublicKey, Decimal>; } export interface RewardCurvePoint { startTs: number; rps: number; } export class Farms { private readonly _connection: Connection; private readonly _provider: Provider; private readonly _farmsProgram: Program; private readonly _farmsProgramId: PublicKey; constructor(connection: Connection) { this._connection = connection; this._provider = new AnchorProvider(connection, getReadOnlyWallet(), { commitment: connection.commitment, }); this._farmsProgramId = farmsId; this._farmsProgram = new Program( FARMS_IDL as Idl, this._farmsProgramId, this._provider, ); } getConnection() { return this._connection; } getProgramID() { return this._farmsProgramId; } getProgram() { return this._farmsProgram; } async getAllUserStatesForUser(user: PublicKey): Promise<Array<UserAndKey>> { let filters: GetProgramAccountsFilter[] = []; filters.push({ memcmp: { bytes: user.toBase58(), offset: 48, }, }); filters.push({ dataSize: UserState.layout.span + 8, }); const userStates = ( await this._farmsProgram.account.userState.all(filters) ).map((x) => { let res: UserAndKey = { userState: new UserState(x.account as unknown as UserStateFields), key: x.publicKey, }; return res; }); return userStates; } async getAllUserStates(): Promise<UserAndKey[]> { return ( await this._farmsProgram.account.userState.all([ { dataSize: UserState.layout.span + 8, }, ]) ).map((x) => { const userAndKey: UserAndKey = { userState: new UserState(x.account as unknown as UserStateFields), key: x.publicKey, }; return userAndKey; }); } async getAllUserStatesWithFilter( isFarmDelegated: boolean, ): Promise<UserAndKey[]> { return ( await this._farmsProgram.account.userState.all([ { dataSize: UserState.layout.span + 8, }, { memcmp: { bytes: isFarmDelegated ? "2" : "1", offset: 80 }, }, ]) ).map((x) => { const userAndKey: UserAndKey = { userState: new UserState(x.account as unknown as UserStateFields), key: x.publicKey, }; 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); * } */ async *batchGetAllUserStates(): AsyncGenerator<UserAndKey[], void, unknown> { const userStatePubkeys = await this._connection.getProgramAccounts( this._farmsProgramId, { filters: [ { dataSize: UserState.layout.span + 8, }, ], dataSlice: { offset: 0, length: 0, }, }, ); for (const batch of chunks( userStatePubkeys.map((x) => x.pubkey), 100, )) { const userStateAccounts = await this._connection.getMultipleAccountsInfo(batch); const userStateBatch: UserAndKey[] = []; for (let i = 0; i < userStateAccounts.length; i++) { const userState = userStateAccounts[i]; const pubkey = batch[i]; if (userState === null) { continue; } const userStateAccount = UserState.decode(userState.data); if (!userStateAccount) { throw Error( `Could not decode user state account ${pubkey.toString()}`, ); } userStateBatch.push({ key: pubkey, userState: userStateAccount }); } yield userStateBatch; } } async getAllUserStatesForFarm(farm: PublicKey): Promise<UserAndKey[]> { return ( await this._farmsProgram.account.userState.all([ { dataSize: UserState.layout.span + 8, }, { memcmp: { offset: 8 + 8, bytes: farm.toBase58(), }, }, ]) ).map((x) => { const userAndKey: UserAndKey = { userState: new UserState(x.account as unknown as UserStateFields), key: x.publicKey, }; return userAndKey; }); } async getFarmsForMint(mint: PublicKey): Promise<Array<FarmAndKey>> { let filters: GetProgramAccountsFilter[] = []; filters.push({ memcmp: { bytes: mint.toBase58(), offset: 72, }, }); filters.push({ dataSize: FarmState.layout.span + 8, }); const farms = (await this._farmsProgram.account.farmState.all(filters)).map( (x) => { let res: FarmAndKey = { farmState: new FarmState(x.account as unknown as FarmStateFields), key: x.publicKey, }; return res; }, ); return farms; } async getAllFarmStates(): Promise<FarmAndKey[]> { return ( await this._farmsProgram.account.farmState.all([ { dataSize: FarmState.layout.span + 8, }, ]) ).map((x): FarmAndKey => { const farmAndKey: FarmAndKey = { farmState: new FarmState(x.account as unknown as FarmStateFields), key: x.publicKey, }; return farmAndKey; }); } async getAllFarmStatesByPubkeys(keys: PublicKey[]): Promise<FarmAndKey[]> { const farmAndKeys: FarmAndKey[] = []; const farmStates = await this.fetchMultipleFarmStatesWithCheckedSize(keys); farmStates.forEach((farmState, index) => { if (farmState) { farmAndKeys.push({ farmState: farmState, key: keys[index], }); } }); return farmAndKeys; } async getStakedAmountForMintForFarm( mint: PublicKey, farm: PublicKey, ): Promise<Decimal> { const farms = await this.getFarmsForMint(mint); for (let index = 0; index < farms.length; index++) { if (farms[index].key.equals(farm)) { return lamportsToCollDecimal( new Decimal( scaleDownWads(farms[index].farmState.totalActiveStakeScaled), ), farms[index].farmState.token.decimals.toNumber(), ); } } throw Error("No Farm found"); } async getStakedAmountForMint(mint: PublicKey): 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: PublicKey, user: PublicKey, ): Promise<{ lockupRemainingDuration: number; farmLockupOriginalDuration: number; farmLockupExpiry: number; }> { let userStateAddress = 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; const slot = await this._connection.getSlot(); const timestampNow = (await this._connection.getBlockTime(slot))!; 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: PublicKey, farm: PublicKey, delegatees?: PublicKey[], ): Promise<Array<UserAndKey>> { if (delegatees) { return this.getUserStateKeysForDelegatedFarmDeterministic( user, farm, delegatees, ); } const userStates = await this.getAllUserStatesForUser(user); const userStateKeysForFarm: UserAndKey[] = []; for (let index = 0; index < userStates.length; index++) { if (userStates[index].userState.farmState.equals(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: PublicKey, farm: PublicKey, delegatees: PublicKey[], ): Promise<Array<UserAndKey>> { const userStateAddresses: PublicKey[] = []; const userStateKeysForFarm: UserAndKey[] = []; delegatees.forEach((delegatee) => { const userStateAddress = getUserStatePDA( this._farmsProgramId, farm, delegatee, ); userStateAddresses.push(userStateAddress); }); const userStates = await UserState.fetchMultiple( this._connection, userStateAddresses, ); userStates.forEach((userState, index) => { if (userState && userState.farmState.equals(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 getAllFarmsForUser( user: PublicKey, strategiesToInclude?: PublicKeySet<PublicKey>, ): Promise<PubkeyHashMap<PublicKey, UserFarm>> { const userStates = await this.getAllUserStatesForUser(user); const farmPks = new Array<PublicKey>(); for (let i = 0; i < userStates.length; i++) { farmPks[i] = userStates[i].userState.farmState; } const farmStates = await this.getAllFarmStatesByPubkeys(farmPks); if (!farmStates) { throw new Error("Error fetching farms"); } let farmStatesFiltered: FarmAndKey[] = []; if (strategiesToInclude) { farmStatesFiltered = farmStates.filter((farmStates) => { if (strategiesToInclude.contains(farmStates.farmState.strategyId)) { return true; } return false; }); } else { farmStatesFiltered = farmStates; } if (farmStatesFiltered.length === 0) { // Return empty if no serializable farm states found return new PubkeyHashMap<PublicKey, UserFarm>(); } const timestamp = new Decimal( (await this._connection.getBlockTime(await this._connection.getSlot()))!, ); const userFarms = new PubkeyHashMap<PublicKey, UserFarm>(); for (let userState of userStates) { const userPendingRewardAmounts: Decimal[] = []; let farmState = farmStatesFiltered.find((farmState) => farmState.key.equals(userState.userState.farmState), ); if (!farmState) { // Skip farms that are not serializable anymore continue; } let oraclePrices: OraclePrices | null = null; if (!farmState.farmState.scopePrices.equals(PublicKey.default)) { oraclePrices = await OraclePrices.fetch( this._connection, farmState.farmState.scopePrices, ); if (!oraclePrices) { throw new Error("Error fetching oracle prices"); } } let hasReward = false; // calculate userState pending rewards for ( let indexReward = 0; indexReward < farmState.farmState.rewardInfos.length; indexReward++ ) { userPendingRewardAmounts[indexReward] = calculatePendingRewards( farmState.farmState, userState.userState, indexReward, timestamp, oraclePrices, ); if (userPendingRewardAmounts[indexReward].gt(0)) { hasReward = true; } } // 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, activeStakeByDelegatee: new PubkeyHashMap<PublicKey, Decimal>(), pendingDepositStakeByDelegatee: new PubkeyHashMap< PublicKey, Decimal >(), pendingWithdrawalUnstakeByDelegatee: new PubkeyHashMap< PublicKey, Decimal >(), pendingRewards: new Array(farmState.farmState.rewardInfos.length) .fill(undefined) .map(function (value, index) { return { rewardTokenMint: new PublicKey(0), rewardTokenProgramId: farmState!.farmState.rewardInfos[index].token.tokenProgram, rewardType: farmState?.farmState.rewardInfos[index].rewardType || 0, cumulatedPendingRewards: new Decimal(0), pendingRewardsByDelegatee: new PubkeyHashMap< PublicKey, Decimal >(), }; }), }; if ( new Decimal(scaleDownWads(userState.userState.activeStakeScaled)).gt( 0, ) || hasReward ) { userFarms.set(userState.userState.farmState, userFarm); } else { // skip as we are not accounting for empty userFarms continue; } } // add new userFarm state if non empty (has rewards or stake) and not already present const refUserFarm = userFarms.get(userState.userState.farmState); if (!refUserFarm) { throw new Error("User farm state not loaded properly "); } const updatedUserFarm = { ...refUserFarm }; if ( updatedUserFarm.activeStakeByDelegatee.has( userState.userState.delegatee, ) ) { console.error( "Delegatee for user for farm already present. There should be only one delegatee for this user for this farm", ); continue; } // active stake by delegatee updatedUserFarm.activeStakeByDelegatee.set( userState.userState.delegatee, lamportsToCollDecimal( new Decimal(scaleDownWads(userState.userState.activeStakeScaled)), farmState.farmState.token.decimals.toNumber(), ), ); // pendingDepositStake by delegatee updatedUserFarm.pendingDepositStakeByDelegatee.set( userState.userState.delegatee, new Decimal( scaleDownWads(userState.userState.pendingDepositStakeScaled), ), ); // pendingWithdrawalUnstake by delegatee updatedUserFarm.pendingWithdrawalUnstakeByDelegatee.set( userState.userState.delegatee, new Decimal( scaleDownWads(userState.userState.pendingWithdrawalUnstakeScaled), ), ); // cummulating rewards for ( let indexReward = 0; indexReward < farmState.farmState.rewardInfos.length; indexReward++ ) { updatedUserFarm.pendingRewards[indexReward].rewardTokenMint = farmState.farmState.rewardInfos[indexReward].token.mint; updatedUserFarm.pendingRewards[indexReward].cumulatedPendingRewards = updatedUserFarm.pendingRewards[ indexReward ].cumulatedPendingRewards.add(userPendingRewardAmounts[indexReward]); updatedUserFarm.pendingRewards[ indexReward ].pendingRewardsByDelegatee.set( userState.userState.delegatee, userPendingRewardAmounts[indexReward], ); } // set updated userFarm userFarms.set(userState.userState.farmState, updatedUserFarm); } return userFarms; } async getUserStateKeyForUndelegatedFarm( user: PublicKey, farmAddress: PublicKey, ): Promise<UserAndKey> { const userStateAddress = 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 getUserForUndelegatedFarm( user: PublicKey, farmAddress: PublicKey, ): Promise<UserFarm> { const farmState = await FarmState.fetch(this._connection, farmAddress); if (!farmState) { throw new Error(`Farm not found ${farmAddress.toString()}`); } const userStateAddress = 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, strategyId: farmState.strategyId, delegateAuthority: farmState.delegateAuthority, stakedToken: farmState.token.mint, activeStakeByDelegatee: new PubkeyHashMap<PublicKey, Decimal>(), pendingDepositStakeByDelegatee: new PubkeyHashMap<PublicKey, Decimal>(), pendingWithdrawalUnstakeByDelegatee: new PubkeyHashMap< PublicKey, Decimal >(), pendingRewards: new Array(farmState.rewardInfos.length) .fill(undefined) .map(function (value, index) { return { rewardTokenMint: new PublicKey(0), rewardTokenProgramId: farmState?.rewardInfos[index].token.tokenProgram, rewardType: farmState?.rewardInfos[index].rewardType || 0, cumulatedPendingRewards: new Decimal(0), pendingRewardsByDelegatee: new PubkeyHashMap<PublicKey, 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 const timestamp = new Decimal( (await this._connection.getBlockTime(await this._connection.getSlot()))!, ); let oraclePrices: OraclePrices | null = null; if (!farmState.scopePrices.equals(PublicKey.default)) { 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 executeTransaction( ix: TransactionInstruction[], signer: Keypair, extraSigners: Signer[] = [], web3Client?: Web3Client, priorityFeeMultiplier: number = 0, ): Promise<TransactionSignature> { const microLamport = 10 ** 6; // 1 lamport const computeUnits = 200_000; const microLamportsPrioritizationFee = microLamport / computeUnits; const tx = new Transaction(); let { blockhash } = await this._connection.getLatestBlockhash(); if (priorityFeeMultiplier) { const priorityFeeIxn = createAddExtraComputeUnitFeeTransaction( computeUnits, microLamportsPrioritizationFee * priorityFeeMultiplier, ); tx.add(...priorityFeeIxn); } tx.recentBlockhash = blockhash; tx.feePayer = signer.publicKey; tx.add(...ix); let sig: TransactionSignature; if (web3Client) { sig = await signSendAndConfirmRawTransactionWithRetry({ mainConnection: web3Client.sendConnection, extraConnections: web3Client.sendConnectionsExtra, tx: new VersionedTransaction(tx.compileMessage()), signers: [signer, ...extraSigners], commitment: "confirmed", sendTransactionOptions: { skipPreflight: true, preflightCommitment: "confirmed", }, }); } else { sig = await sendAndConfirmTransaction( this._connection, tx, [signer, ...extraSigners], { skipPreflight: true, commitment: "confirmed" }, ); } return sig; } async createNewUserIx( user: PublicKey, farm: PublicKey, ): Promise<TransactionInstruction> { const userState = getUserStatePDA(this._farmsProgramId, farm, user); const ix = farmOperations.initializeUser(farm, user, userState); return ix; } async createNewUser( user: Keypair, farm: PublicKey, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.createNewUserIx(user.publicKey, farm); let sig = await this.executeTransaction( [ix], user, [], web3Client, priorityFeeMultiplier, ); const userState = getUserStatePDA( this._farmsProgramId, farm, user.publicKey, ); if (process.env.DEBUG === "true") { console.log("Initialize User: " + userState); console.log("Refresh Farm txn: " + sig.toString()); } return sig; } async stakeIx( user: PublicKey, farm: PublicKey, amountLamports: Decimal, stakeTokenMint: PublicKey, scopePrices: PublicKey, ): Promise<TransactionInstruction> { const farmVault = getFarmVaultPDA( this._farmsProgramId, farm, stakeTokenMint, ); const userStatePk = getUserStatePDA(this._farmsProgramId, farm, user); const userTokenAta = await getAssociatedTokenAddress( user, stakeTokenMint, TOKEN_PROGRAM_ID, ); const ix = farmOperations.stake( user, userStatePk, userTokenAta, farm, farmVault, stakeTokenMint, scopePrices, new BN(amountLamports.toString()), ); return ix; } async stake( user: Keypair, farm: PublicKey, amountLamports: Decimal, stakeTokenMint: PublicKey, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.stakeIx( user.publicKey, farm, amountLamports, stakeTokenMint, PROGRAM_ID, ); let increaseComputeIx = createAddExtraComputeUnitsTransaction( user.publicKey, 400_000, ); let sig = await this.executeTransaction( [increaseComputeIx, ix], user, [], web3Client, priorityFeeMultiplier, ); if (process.env.DEBUG === "true") { console.log("User " + " stake " + amountLamports); console.log("Stake txn: " + sig.toString()); } return sig; } async unstakeIx( user: PublicKey, farm: PublicKey, amountLamports: string, scopePrices: PublicKey, ): Promise<TransactionInstruction> { const userStatePk = getUserStatePDA(this._farmsProgramId, farm, user); const ix = farmOperations.unstake( user, userStatePk, farm, scopePrices, new BN(amountLamports), ); return ix; } async unstake( user: Keypair, farm: PublicKey, sharesAmount: string, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.unstakeIx( user.publicKey, farm, sharesAmount, PROGRAM_ID, ); let sig = await this.executeTransaction( [ix], user, [], web3Client, priorityFeeMultiplier, ); if (process.env.DEBUG === "true") { console.log("Unstake " + sharesAmount); console.log("Unstake txn: " + sig.toString()); } return sig; } async withdrawUnstakedDepositIx( user: PublicKey, userState: PublicKey, farmState: PublicKey, stakeTokenMint: PublicKey, ): Promise<TransactionInstruction> { const userTokenAta = await getAssociatedTokenAddress( user, stakeTokenMint, TOKEN_PROGRAM_ID, ); const farmVault = getFarmVaultPDA( this._farmsProgramId, farmState, stakeTokenMint, ); const farmVaultsAuthority = getFarmAuthorityPDA( this._farmsProgramId, farmState, ); const ix = farmOperations.withdrawUnstakedDeposit( user, userState, farmState, userTokenAta, farmVault, farmVaultsAuthority, ); return ix; } async withdrawUnstakedDeposit( user: Keypair, farmState: PublicKey, tokenMint: PublicKey, userState: PublicKey, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.withdrawUnstakedDepositIx( user.publicKey, userState, farmState, tokenMint, ); let sig = await this.executeTransaction( [ix], user, [], web3Client, priorityFeeMultiplier, ); if (process.env.DEBUG === "true") { console.log("User " + userState + " withdraw unstaked deposit "); console.log("Withdraw Unstaked Deposit txn: " + sig.toString()); } return sig; } async claimForUserForFarmRewardIx( user: PublicKey, farm: PublicKey, rewardMint: PublicKey, isDelegated: boolean, rewardIndex = -1, delegatees?: PublicKey[], ): Promise< [[PublicKey, TransactionInstruction][], TransactionInstruction[]] > { const ixns: TransactionInstruction[] = []; const ataIxns: [PublicKey, TransactionInstruction][] = []; const userStatesAndKeys = isDelegated ? await this.getUserStateKeysForDelegatedFarm(user, farm, delegatees) : [await this.getUserStateKeyForUndelegatedFarm(user, farm)]; const farmState = await FarmState.fetch(this._connection, farm); if (!farmState) { throw new Error(`Farm not found ${farm.toString()}`); } const treasuryVault = getTreasuryVaultPDA( this._farmsProgramId, farmState.globalConfig, rewardMint, ); // find rewardIndex if not defined if (rewardIndex === -1) { rewardIndex = farmState.rewardInfos.findIndex((r) => r.token.mint.equals(rewardMint), ); } const rewardsTokenProgram = farmState.rewardInfos[rewardIndex].token.tokenProgram; const userRewardAta = await getAssociatedTokenAddress( user, rewardMint, rewardsTokenProgram, ); const ataExists = await checkIfAccountExists( this._connection, userRewardAta, ); if (!ataExists) { const [, ix] = await createAssociatedTokenAccountIdempotentInstruction( user, rewardMint, user, rewardsTokenProgram, 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, farmState.scopePrices.equals(PublicKey.default) ? PROGRAM_ID : farmState.scopePrices, rewardsTokenProgram, rewardIndex, ); ixns.push(ix); } return [ataIxns, ixns]; } async claimForUserForFarmReward( user: Keypair, farm: PublicKey, rewardMint: PublicKey, isDelegated: boolean, rewardIndex = -1, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const [_ataIxns, ixns] = await this.claimForUserForFarmRewardIx( user.publicKey, farm, rewardMint, isDelegated, rewardIndex, ); let sig = await this.executeTransaction( ixns, user, [], web3Client, priorityFeeMultiplier, ); if (process.env.DEBUG === "true") { console.log("Harvest reward " + rewardIndex); console.log("HarvestReward txn: " + sig.toString()); } return sig; } async claimForUserForFarmAllRewardsIx( user: PublicKey, farm: PublicKey, isDelegated: boolean, delegatees?: PublicKey[], ): Promise<Array<TransactionInstruction>> { const farmState = await FarmState.fetch(this._connection, farm); const userStatesAndKeys = isDelegated ? await this.getUserStateKeysForDelegatedFarm(user, farm, delegatees) : [await this.getUserStateKeyForUndelegatedFarm(user, farm)]; const ixs = new Array<TransactionInstruction>(); // hardcoded as a hotfix for JTO release; // TODO: replace by proper fix const jitoFarm = new PublicKey( "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.equals(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, rewardMint, rewardTokenProgram, ); const treasuryVault = getTreasuryVaultPDA( this._farmsProgramId, farmState.globalConfig, rewardMint, ); const ataExists = await checkIfAccountExists( this._connection, userRewardAta, ); if (!ataExists) { const [, ix] = await createAssociatedTokenAccountIdempotentInstruction( user, rewardMint, user, rewardTokenProgram, userRewardAta, ); ixs.push(ix); } ixs.push( farmOperations.harvestReward( user, userStatesAndKeys[userStateIndex].key, userRewardAta, farmState.globalConfig, treasuryVault, farm, rewardMint, farmState.rewardInfos[rewardIndex].rewardsVault, farmState.farmVaultsAuthority, farmState.scopePrices.equals(PublicKey.default) ? PROGRAM_ID : farmState.scopePrices, rewardTokenProgram, rewardIndex, ), ); } } return ixs; } async claimForUserForFarmAllRewards( user: Keypair, farm: PublicKey, isDelegated: boolean, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<Array<TransactionSignature>> { const ixs = await this.claimForUserForFarmAllRewardsIx( user.publicKey, farm, isDelegated, ); const sigs = new Array<TransactionSignature>(); for (let i = 0; i < ixs.length; i++) { sigs[i] = await this.executeTransaction( [ixs[i]], user, [], web3Client, priorityFeeMultiplier, ); } return sigs; } async transferOwnershipIx( user: PublicKey, userState: PublicKey, newUser: PublicKey, ): Promise<TransactionInstruction> { const ix = farmOperations.transferOwnership(user, userState, newUser); return ix; } async transferOwnership( user: Keypair, userState: PublicKey, newUser: PublicKey, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.transferOwnershipIx( user.publicKey, userState, newUser, ); let sig = await this.executeTransaction( [ix], user, [], web3Client, priorityFeeMultiplier, ); if (process.env.DEBUG === "true") { console.log( "Transfer User " + userState + " ownership from " + user.publicKey + " to " + newUser, ); console.log("Transfer User Ownership txn: " + sig.toString()); } return sig; } async transferOwnershipAllUserStatesIx( user: PublicKey, newUser: PublicKey, ): Promise<Array<TransactionInstruction>> { const userStates = await this.getAllUserStatesForUser(user); const ixs = new Array<TransactionInstruction>(); for (let index = 0; index < userStates.length; index++) { ixs[index] = farmOperations.transferOwnership( user, userStates[index].key, newUser, ); } return ixs; } async transferOwnershipAllUserStates( user: Keypair, newUser: PublicKey, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<Array<TransactionSignature>> { const ixs = await this.transferOwnershipAllUserStatesIx( user.publicKey, newUser, ); const sigs = new Array<TransactionSignature>(); for (let i = 0; i < ixs.length; i++) { sigs[i] = await this.executeTransaction( [ixs[i]], user, [], web3Client, priorityFeeMultiplier, ); } return sigs; } async createFarmIx( admin: PublicKey, farm: Keypair, globalConfig: PublicKey, stakeTokenMint: PublicKey, ): Promise<TransactionInstruction[]> { const farmVault = getFarmVaultPDA( this._farmsProgramId, farm.publicKey, stakeTokenMint, ); const farmVaultAuthority = getFarmAuthorityPDA( this._farmsProgramId, farm.publicKey, ); let ixs: TransactionInstruction[] = []; ixs.push( await createKeypairRentExemptIx( this._provider.connection, admin, farm, SIZE_FARM_STATE, this._farmsProgramId, ), ); ixs.push( farmOperations.initializeFarm( globalConfig, admin, farm.publicKey, farmVault, farmVaultAuthority, stakeTokenMint, ), ); return ixs; } async createFarm( admin: Keypair, globalConfig: PublicKey, farm: Keypair, mint: PublicKey, mode: string = "execute", priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.createFarmIx( mode === "multisig" ? new PublicKey(process.env.MULTISIG!) : admin.publicKey, farm, globalConfig, mint, ); const log = "Initialize Farm: " + farm.toString(); return this.processTxn( admin, ix, mode, priorityFeeMultiplier, log, [farm], web3Client, ); } async addRewardToFarmIx( admin: PublicKey, globalConfig: PublicKey, farm: PublicKey, mint: PublicKey, tokenProgram: PublicKey, ): Promise<TransactionInstruction> { const globalConfigState = await GlobalConfig.fetch( this._connection, globalConfig, ); if (!globalConfigState) { throw new Error("Could not fetch global config"); } const treasuryVault = getTreasuryVaultPDA( this._farmsProgramId, globalConfig, mint, ); let farmVaultAuthority = getFarmAuthorityPDA(this._farmsProgramId, farm); const rewardVault = getRewardVaultPDA(this._farmsProgramId, farm, mint); const ix = farmOperations.initializeReward( globalConfig, globalConfigState.treasuryVaultsAuthority, treasuryVault, admin, farm, rewardVault, farmVaultAuthority, mint, tokenProgram, ); return ix; } async addRewardToFarm( admin: Keypair, globalConfig: PublicKey, farm: PublicKey, mint: PublicKey, tokenProgram: PublicKey, mode: string = "execute", priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.addRewardToFarmIx( mode === "multisig" ? new PublicKey(process.env.MULTISIG!) : admin.publicKey, globalConfig, farm, mint, tokenProgram, ); const log = "Initialize Reward: " + mint; return this.processTxn( admin, [ix], mode, priorityFeeMultiplier, log, [], web3Client, ); } async addRewardAmountToFarmIx( payer: PublicKey, farm: PublicKey, mint: PublicKey, amount: Decimal, rewardIndexOverride: number = -1, decimalsOverride: number = -1, tokenProgramOverride: PublicKey = TOKEN_PROGRAM_ID, scopePricesOverride: PublicKey = PROGRAM_ID, ): Promise<TransactionInstruction> { 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.toBase58()}`); } scopePrices = farmState.scopePrices.equals(PublicKey.default) ? PROGRAM_ID : farmState.scopePrices; for (let i = 0; farmState.rewardInfos.length; i++) { if (farmState.rewardInfos[i].token.mint.equals(mint)) { if ( !farmState.rewardInfos[i].token.tokenProgram.equals( PublicKey.default, ) ) { 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.toBase58()}`); } let amountLamports = new BN( collToLamportsDecimal(amount, decimals).floor().toString(), ); const payerRewardAta = await getAssociatedTokenAddress( payer, mint, tokenProgram, ); let rewardVault = getRewardVaultPDA(this._farmsProgramId, farm, mint); let farmVaultsAuthority = getFarmAuthorityPDA(this._farmsProgramId, farm); const ix = farmOperations.addReward( payer, farm, rewardVault, farmVaultsAuthority, payerRewardAta, mint, scopePrices, rewardIndex, tokenProgram, amountLamports, ); return ix; } async withdrawRewardAmountFromFarmIx( payer: PublicKey, farm: PublicKey, mint: PublicKey, amount: Decimal, rewardIndexOverride: number = -1, decimalsOverride: number = -1, tokenProgramOverride: PublicKey = TOKEN_PROGRAM_ID, scopePricesOverride: PublicKey = PROGRAM_ID, ): Promise<TransactionInstruction> { 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.toBase58()}`); } scopePrices = farmState.scopePrices.equals(PublicKey.default) ? PROGRAM_ID : farmState.scopePrices; for (let i = 0; farmState.rewardInfos.length; i++) { if (farmState.rewardInfos[i].token.mint.equals(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.toBase58()}`); } let amountLamports = new BN( collToLamportsDecimal(amount, decimals).floor().toString(), ); const payerRewardAta = await getAssociatedTokenAddress( payer, mint, tokenProgram, ); let rewardVault = getRewardVaultPDA(this._farmsProgramId, farm, mint); let farmVaultsAuthority = getFarmAuthorityPDA(this._farmsProgramId, farm); const ix = farmOperations.withdrawReward( payer, farm, mint, rewardVault, farmVaultsAuthority, payerRewardAta, scopePrices, tokenProgram, rewardIndex, amountLamports, ); return ix; } async addRewardAmountToFarm( payer: Keypair, farm: PublicKey, mint: PublicKey, amount: Decimal, mode: string, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.addRewardAmountToFarmIx( mode === "multisig" ? new PublicKey(process.env.MULTISIG!) : payer.publicKey, farm, mint, amount, ); const log = "Add Reward: " + mint + " amount: " + amount; return this.processTxn( payer, [ix], mode, priorityFeeMultiplier, log, [], web3Client, ); } async updateFarmConfigIx( admin: PublicKey, farm: PublicKey, mint: PublicKey, mode: FarmConfigOptionKind, value: number | PublicKey | number[] | RewardCurvePoint[], rewardIndexOverride: number = -1, scopePricesOverride: PublicKey = PROGRAM_ID, newFarm: boolean = false, ): Promise<TransactionInstruction> { 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.toBase58()}`); } if (!farmState.scopePrices.equals(PublicKey.default)) { scopePrices = farmState.scopePrices; } for (let i = 0; farmState.rewardInfos.length; i++) { if (farmState.rewardInfos[i].token.mint.equals(mint)) { rewardIndex = i; break; } } } const ix = farmOperations.updateFarmConfig( admin, farm, scopePrices, rewardIndex, mode, value, ); return ix; } async updateFarmConfig( admin: Keypair, farm: PublicKey, mint: PublicKey, updateMode: FarmConfigOptionKind, value: number | PublicKey, mode: string = "execute", priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const ix = await this.updateFarmConfigIx( mode === "multisig" ? new PublicKey(process.env.MULTISIG!) : admin.publicKey, farm, mint, updateMode, value, ); const log = "Update Reward: " + mint + " mode: " + updateMode.discriminator + " value: " + value; return this.processTxn( admin, [ix], mode, priorityFeeMultiplier, log, [], web3Client, ); } async refreshFarmIx( farm: PublicKey, scopePrices: PublicKey, ): Promise<TransactionInstruction> { const ix = farmOperations.refreshFarm(farm, scopePrices); return ix; } async refreshFarm( payer: Keypair, farm: PublicKey, priorityFeeMultiplier: number, web3Client?: Web3Client, ): Promise<TransactionSignature> { const farmState = await FarmState.fetch(this._connection, farm); if (!farmState) { throw new Error(`Could not fetch farm state ${farm.toBase58()}`); } const ix = await this.refreshFarmIx( farm, farmState.scopePrices.equals(PublicKey.default) ? PROGRAM_ID : farmState.scopePrices, ); let sig = await this.executeTransaction( [ix], payer, [], web3Client, priorityFeeMultiplier, ); if (process.env.DEBUG === "true") { console.log("Refresh Farm: " + farm); console.log("Refresh Farm txn: " + sig.toString()); } return sig; } async refreshUserIx( userState: PublicKey, farmState: PublicKey, scopePrices: Publ