UNPKG

@hubbleprotocol/farms-sdk

Version:
1,776 lines (1,594 loc) 45.4 kB
import { PublicKey, TransactionInstruction, Keypair, Transaction, TransactionSignature, Signer, VersionedTransaction, VersionedMessage, } from "@solana/web3.js"; import Decimal from "decimal.js"; import { Farms, printMultisigTx, printSimulateTx } from "../Farms"; import { FarmState } from "../rpc_client/accounts"; import { FarmConfigOption } from "../rpc_client/types"; import { createAddExtraComputeUnitFeeTransaction, initializeClient, } from "./utils"; import { Env, getFarmsProgramId, getMintDecimals, noopProfiledFunctionExecution, retryAsync, signSendAndConfirmRawTransactionWithRetry, } from "../utils"; import { RewardCurvePoint } from "../Farms"; import { FarmConfig } from "./download_all_farm_configs"; import fs from "fs"; import clc from "cli-color"; import { U64_MAX } from "@kamino-finance/klend-sdk"; import { RpsDecimals } from "../rpc_client/types/FarmConfigOption"; const microLamport = 10 ** 6; const computeUnits = 200_000; const microLamportsPrioritizationFee = microLamport / computeUnits; export async function upsertAllFarmConfigsCommand( targetPath: string, mode: string, priorityFeeMultiplier: number, ) { const admin = process.env.ADMIN; const rpc = process.env.RPC; const env = initializeClient( rpc!, admin!, getFarmsProgramId(rpc!), mode === "multisig", ); // Load files from given directory const farmConfigs = recursiveReadDirectoryOfConfigs(targetPath); const slot = await env.provider.connection.getSlot("finalized"); for (const [_index, farmConfig] of farmConfigs.entries()) { await upsertFarmConfig(env, farmConfig, mode, slot, priorityFeeMultiplier); } } async function upsertFarmConfig( env: Env, farmConfig: FarmConfig, mode: string, slot: number, priorityFeeMultiplier: number, ) { let ixs: TransactionInstruction[] = []; let signers: Keypair[] = [env.initialOwner]; const farmsClient = new Farms(env.provider.connection); const farmState = await FarmState.fetch( env.provider.connection, farmConfig.farmPubkey, ); if (farmState === null) { throw Error("Farm state does not exist"); } const topUpPayer = mode === "multisig" ? new PublicKey(process.env.MULTISIG!) : env.initialOwner.publicKey; const payer = farmState.farmAdmin; validateStakingTokenMint(farmConfig, farmState); // Set farm configurations based on differences between config and current farm state ixs.push( ...(await setDepositCapIxIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setLockingModeIxIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setLockingStartIxIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setLockingDurationIxIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setLockingEarlyWithdrawalPenaltyIxIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setStrategyIdIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setWithdrawAuthorityIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setSlashedAmountSpillAddressIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setPendingFarmAdminIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setScopePricesAccountIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setScopeOraclePriceIdIxIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setScopeOracleMaxAgeIxIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setDepositWarmupPeriodIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); ixs.push( ...(await setWithdrawCooldownPeriodIfNecessary( payer, farmsClient, farmConfig, farmState, mode, )), ); for (const [rewardIndex, _rewardInfoConfig] of farmConfig.rewards.entries()) { const rewardTokenProgram = farmState.rewardInfos[rewardIndex].token.tokenProgram; const rewardDecimals = farmState.rewardInfos[rewardIndex].token.decimals.toNumber(); // Initialize reward if required ixs.push( ...(await initRewardIfNecessary( payer, farmsClient, farmConfig, farmState, rewardIndex, mode, )), ); ixs.push( ...(await setRpsDecimalsIxIfNecessary( payer, farmsClient, farmConfig, farmState, rewardIndex, mode, )), ); ixs.push( ...(await setRewardTypeIxIfNecessary( payer, farmsClient, farmConfig, farmState, rewardIndex, mode, )), ); ixs.push( ...(await setRewardMinClaimDurationIxIfNecessary( payer, farmsClient, farmConfig, farmState, rewardIndex, mode, )), ); const { rewardCurve, rewardPerSecond } = getConfigRewardRps( farmState, farmConfig, rewardIndex, ); if (rewardPerSecond == -1 && !rewardCurve) { throw new Error("Must have either reward per second or reward curve"); } const setRewardCurveIxn = await setRewardCurveIfNecessary( env, payer, farmsClient, farmState, farmConfig, rewardCurve, rewardIndex, rewardDecimals, slot, mode, ); ixs.push(...setRewardCurveIxn); let setRewardRpsIxns: TransactionInstruction[] = []; if (setRewardCurveIxn.length == 0) { setRewardRpsIxns.push( ...(await setRewardPerSecondIfNecessary( payer, farmsClient, farmState, farmConfig, rewardPerSecond, rewardIndex, rewardDecimals, mode, )), ); } ixs.push(...setRewardRpsIxns); // Topup ixs.push( ...(await topupRewardAmountNecessary( env, topUpPayer, farmsClient, farmState, farmConfig, rewardIndex, rewardDecimals, rewardTokenProgram, slot, setRewardCurveIxn.length == 0 && setRewardRpsIxns.length == 0, mode, )), ); } // 5. Send or print txn const { blockhash } = await retryAsync(async () => noopProfiledFunctionExecution( env.provider.connection.getLatestBlockhash("confirmed"), ), ); let txn = new Transaction(); txn.recentBlockhash = blockhash; txn.feePayer = mode === "multisig" ? new PublicKey(process.env.MULTISIG!) : env.initialOwner.publicKey; if (ixs.length !== 0) { if (mode !== "multisig") { const priorityFeeIxn = createAddExtraComputeUnitFeeTransaction( computeUnits, microLamportsPrioritizationFee * priorityFeeMultiplier, ); txn.add(...priorityFeeIxn); } txn.add(...ixs); if (mode === "simulate") { await printSimulateTx(env.provider.connection, txn); } else if (mode === "multisig") { await printMultisigTx(txn); } else if (mode === "execute") { console.log("Sending."); const sig = await retryAsync(async () => noopProfiledFunctionExecution( sendAndConfirmTxn( env, new VersionedTransaction(txn.compileMessage()), signers, ), ), ); console.log("Signature", sig); } } mode !== "multisig" && ixs.length > 0 && console.log("-".repeat(150)); } async function sendAndConfirmTxn( env: Env, txn: VersionedTransaction, signers: Signer[], ): Promise<TransactionSignature> { const sig = await signSendAndConfirmRawTransactionWithRetry({ mainConnection: env.web3Client.sendConnection, extraConnections: env.web3Client.sendConnectionsExtra, tx: txn, signers, commitment: "confirmed", sendTransactionOptions: { maxRetries: 5, preflightCommitment: "confirmed", }, }); return sig; } function validateStakingTokenMint( farmConfig: FarmConfig, farmState: FarmState, ) { if (!farmState.token.mint.equals(farmConfig.stakingTokenMint)) { throw ( "Staking token mint mismatch Config: " + farmConfig.stakingTokenMint.toString() + " FarmState: " + farmState.token.mint.toString() ); } } async function setDepositCapIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let depositCapAmountIx = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.DepositCapAmount.kind]: "", }), farmConfig.depositCapAmount, 0, ); const currentDepositCapAmount = new Decimal( farmState.depositCapAmount.toString(), ).toNumber(); if (currentDepositCapAmount != farmConfig.depositCapAmount) { const valueStringBefore = "depositCapAmount before".padEnd(40) + ": " + currentDepositCapAmount; const valueStringAfter = "depositCapAmount after".padEnd(40) + ": " + farmConfig.depositCapAmount; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [depositCapAmountIx]; } return []; } async function setLockingModeIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setLockingModeIx = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.LockingMode.kind]: "", }), farmConfig.lockingMode, 0, ); const currentLockingMode = new Decimal( farmState.lockingMode.toString(), ).toNumber(); if (currentLockingMode != farmConfig.lockingMode) { const valueStringBefore = "lockingMode before".padEnd(40) + ": " + currentLockingMode; const valueStringAfter = "lockingMode after".padEnd(40) + ": " + farmConfig.lockingMode; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setLockingModeIx]; } return []; } async function setLockingStartIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setLockingStart = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.LockingStartTimestamp.kind]: "", }), farmConfig.lockingStart, 0, ); const currentLockingStartTimestamp = new Decimal( farmState.lockingStartTimestamp.toString(), ).toNumber(); if (currentLockingStartTimestamp != farmConfig.lockingStart) { const valueStringBefore = "lockingStartTimestamp before".padEnd(40) + ": " + currentLockingStartTimestamp; const valueStringAfter = "lockingStartTimestamp after".padEnd(40) + ": " + farmConfig.lockingStart; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setLockingStart]; } return []; } async function setLockingDurationIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setLockingDuration = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.LockingDuration.kind]: "", }), farmConfig.lockingDuration, 0, ); const currentLockingDuration = new Decimal( farmState.lockingDuration.toString(), ).toNumber(); if (currentLockingDuration != farmConfig.lockingDuration) { const valueStringBefore = "lockingDuration before".padEnd(40) + ": " + currentLockingDuration; const valueStringAfter = "lockingDuration after".padEnd(40) + ": " + farmConfig.lockingDuration; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setLockingDuration]; } return []; } async function setLockingEarlyWithdrawalPenaltyIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setLockingEarlyWithdrawalPenalty = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.LockingEarlyWithdrawalPenaltyBps.kind]: "", }), farmConfig.lockingEarlyWithdrawalPenaltyBps, 0, ); const currentLockingEarlyWithdrawalPenalty = new Decimal( farmState.lockingEarlyWithdrawalPenaltyBps.toString(), ).toNumber(); if ( currentLockingEarlyWithdrawalPenalty != farmConfig.lockingEarlyWithdrawalPenaltyBps ) { const valueStringBefore = "lockingEarlyWithdrawalPenaltyBps before".padEnd(40) + ": " + currentLockingEarlyWithdrawalPenalty; const valueStringAfter = "lockingEarlyWithdrawalPenaltyBps after".padEnd(40) + ": " + farmConfig.lockingEarlyWithdrawalPenaltyBps; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setLockingEarlyWithdrawalPenalty]; } return []; } async function setStrategyIdIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setStrategyId = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.UpdateStrategyId.kind]: "", }), farmConfig.strategyId, 0, ); if (!farmState.strategyId.equals(farmConfig.strategyId)) { const valueStringBefore = "strategyId before".padEnd(40) + ": " + farmState.strategyId.toString(); const valueStringAfter = "strategyId after".padEnd(40) + ": " + farmConfig.strategyId.toString(); printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setStrategyId]; } return []; } async function setWithdrawAuthorityIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setWithdrawAuthority = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.WithdrawAuthority.kind]: "", }), farmConfig.withdrawAuthority, 0, ); if (!farmState.withdrawAuthority.equals(farmConfig.withdrawAuthority)) { const valueStringBefore = "withdrawAuthority before".padEnd(40) + ": " + farmState.withdrawAuthority.toString(); const valueStringAfter = "withdrawAuthority after".padEnd(40) + ": " + farmConfig.withdrawAuthority.toString(); printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setWithdrawAuthority]; } return []; } async function setSlashedAmountSpillAddressIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setSlashedAmountSpillAddress = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.SlashedAmountSpillAddress.kind]: "", }), farmConfig.slashedAmountSpillAddress, 0, ); if ( !farmState.slashedAmountSpillAddress.equals( farmConfig.slashedAmountSpillAddress, ) ) { const valueStringBefore = "slashedAmountSpillAddress before".padEnd(40) + ": " + farmState.slashedAmountSpillAddress.toString(); const valueStringAfter = "slashedAmountSpillAddress after".padEnd(40) + ": " + farmConfig.slashedAmountSpillAddress.toString(); printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setSlashedAmountSpillAddress]; } return []; } async function setScopePricesAccountIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setScopePricesAccount = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.ScopePricesAccount.kind]: "", }), farmConfig.scopePrices, 0, ); if (!farmState.scopePrices.equals(farmConfig.scopePrices)) { const valueStringBefore = "scopePrices before".padEnd(40) + ": " + farmState.scopePrices.toString(); const valueStringAfter = "scopePrices after".padEnd(40) + ": " + farmConfig.scopePrices.toString(); printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setScopePricesAccount]; } return []; } async function setPendingFarmAdminIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setPendingFarmAdmin = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.UpdatePendingFarmAdmin.kind]: "", }), farmConfig.pendingFarmAdmin, 0, ); if (!farmState.pendingFarmAdmin.equals(farmConfig.pendingFarmAdmin)) { const valueStringBefore = "pendingFarmAdmin before".padEnd(40) + ": " + farmState.pendingFarmAdmin.toString(); const valueStringAfter = "pendingFarmAdmin after".padEnd(40) + ": " + farmConfig.pendingFarmAdmin.toString(); printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setPendingFarmAdmin]; } return []; } async function setScopeOraclePriceIdIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { const currentScopeOraclePriceId = new Decimal( farmState.scopeOraclePriceId.toString(), ).toNumber(); if ( currentScopeOraclePriceId != new Decimal(farmConfig.scopePriceOracleId).toNumber() ) { const valueStringBefore = "scopeOraclePriceId before".padEnd(40) + ": " + farmState.scopeOraclePriceId; const valueStringAfter = "scopeOraclePriceId after".padEnd(40) + ": " + farmConfig.scopePriceOracleId; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); let setScopeOraclePriceId = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.ScopeOraclePriceId.kind]: "", }), new Decimal(farmConfig.scopePriceOracleId).toNumber(), 0, ); return [setScopeOraclePriceId]; } return []; } async function setScopeOracleMaxAgeIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setScopeOracleMaxAge = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.ScopeOracleMaxAge.kind]: "", }), farmConfig.scopeOracleMaxAge, 0, ); const currentScopeOracleMaxAge = new Decimal( farmState.scopeOracleMaxAge.toString(), ).toNumber(); if (currentScopeOracleMaxAge != farmConfig.scopeOracleMaxAge) { const valueStringBefore = "scopeOracleMaxAge before".padEnd(40) + ": " + currentScopeOracleMaxAge; const valueStringAfter = "scopeOracleMaxAge after".padEnd(40) + ": " + farmConfig.lockingDuration; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setScopeOracleMaxAge]; } return []; } async function setDepositWarmupPeriodIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setDepositWarmupPeriodIx = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.DepositWarmupPeriod.kind]: "", }), farmConfig.depositWarmupPeriod, 0, ); const currentDepositWarmupPeriod = new Decimal( farmState.depositWarmupPeriod.toString(), ).toNumber(); if (currentDepositWarmupPeriod != farmConfig.depositWarmupPeriod) { const valueStringBefore = "depositWarmupPeriod before".padEnd(40) + ": " + currentDepositWarmupPeriod; const valueStringAfter = "depositWarmupPeriod after".padEnd(40) + ": " + farmConfig.depositWarmupPeriod; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setDepositWarmupPeriodIx]; } return []; } async function setWithdrawCooldownPeriodIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, mode: string, ): Promise<TransactionInstruction[]> { let setWithdrawCooldownPeriodIx = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, PublicKey.default, FarmConfigOption.fromDecoded({ [FarmConfigOption.WithdrawCooldownPeriod.kind]: "", }), farmConfig.withdrawCooldownPeriod, 0, ); const currentWithdrawCooldownPeriod = new Decimal( farmState.withdrawalCooldownPeriod.toString(), ).toNumber(); if (currentWithdrawCooldownPeriod != farmConfig.depositWarmupPeriod) { const valueStringBefore = "withdrawalCooldownPeriod before".padEnd(40) + ": " + currentWithdrawCooldownPeriod; const valueStringAfter = "withdrawalCooldownPeriod after".padEnd(40) + ": " + farmConfig.withdrawCooldownPeriod; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), ); return [setWithdrawCooldownPeriodIx]; } return []; } async function initRewardIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, rewardIndex: number, mode: string, ): Promise<TransactionInstruction[]> { if ( !farmState.rewardInfos[rewardIndex].token.mint.equals(PublicKey.default) && !farmConfig.rewards[rewardIndex]?.rewardTokenMint.equals( farmState.rewardInfos[rewardIndex].token.mint, ) ) { throw new Error( "Reward token mint already initialized and does not match config", ); } if ( !farmConfig.rewards[rewardIndex]?.rewardTokenMint.equals( farmState.rewardInfos[rewardIndex].token.mint, ) ) { const rewardMint = farmConfig.rewards[rewardIndex]?.rewardTokenMint!; const tokenProgram = (await farmsClient .getConnection() .getAccountInfo(rewardMint))!.owner; const initRewardIx = await farmsClient.addRewardToFarmIx( payer, farmState.globalConfig, farmConfig.farmPubkey, rewardMint, tokenProgram, ); mode !== "multisig" && console.log( "Change for farm: ", clc.yellow(farmConfig.farmPubkey.toString().padEnd(50)), ); mode !== "multisig" && console.log( clc.green("initialize new reward with mint".padEnd(40) + ": "), clc.yellow(rewardMint.toString()), "\n", ); return [initRewardIx]; } return []; } async function setRpsDecimalsIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, rewardIndex: number, mode: string, ): Promise<TransactionInstruction[]> { let setRpsDecimals = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, farmConfig.rewards[rewardIndex]?.rewardTokenMint!, FarmConfigOption.fromDecoded({ [FarmConfigOption.RpsDecimals.kind]: "", }), farmConfig.rewards[rewardIndex]?.rewardPerSecondDecimals!, rewardIndex, ); const currentRpsDecimals = new Decimal( farmState.rewardInfos[rewardIndex]?.rewardsPerSecondDecimals!.toString(), ).toNumber(); if ( currentRpsDecimals != farmConfig.rewards[rewardIndex]?.rewardPerSecondDecimals! ) { const valueStringBefore = "rpsDecimals before".padEnd(40) + ": " + currentRpsDecimals; const valueStringAfter = "rpsDecimals after".padEnd(40) + ": " + farmConfig.rewards[rewardIndex]?.rewardPerSecondDecimals!; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), rewardIndex, farmConfig.rewards[rewardIndex]?.rewardTokenMint!.toString(), ); return [setRpsDecimals]; } return []; } async function setRewardTypeIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, rewardIndex: number, mode: string, ): Promise<TransactionInstruction[]> { let configRewardType: number = 0; if (farmConfig.rewards[rewardIndex]?.rewardType === "Proportional") { configRewardType = 0; } else if (farmConfig.rewards[rewardIndex]?.rewardType === "Constant") { configRewardType = 1; } else { throw new Error( `Reward type invalid for farm ${farmConfig.farmPubkey.toString()}`, ); } let setRewardType = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, farmConfig.rewards[rewardIndex]?.rewardTokenMint!, FarmConfigOption.fromDecoded({ [FarmConfigOption.RewardType.kind]: "", }), configRewardType, rewardIndex, ); const currentRewardTypeString = farmState.rewardInfos[rewardIndex]?.rewardType === 0 ? "Proportional" : "Constant"; if (farmState.rewardInfos[rewardIndex]?.rewardType != configRewardType) { const valueStringBefore = "rewardType before".padEnd(40) + ": " + currentRewardTypeString; const valueStringAfter = "rewardType after".padEnd(40) + ": " + farmConfig.rewards[rewardIndex]?.rewardType; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), rewardIndex, farmConfig.rewards[rewardIndex]?.rewardTokenMint!.toString(), ); return [setRewardType]; } return []; } async function setRewardMinClaimDurationIxIfNecessary( payer: PublicKey, farmsClient: Farms, farmConfig: FarmConfig, farmState: FarmState, rewardIndex: number, mode: string, ): Promise<TransactionInstruction[]> { let setRewardMinClaimDuration = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, farmConfig.rewards[rewardIndex]?.rewardTokenMint!, FarmConfigOption.fromDecoded({ [FarmConfigOption.UpdateRewardMinClaimDuration.kind]: "", }), farmConfig.rewards[rewardIndex]?.minClaimDurationSeconds!, rewardIndex, ); const currentMinClaimDurationSeconds = new Decimal( farmState.rewardInfos[rewardIndex]?.minClaimDurationSeconds.toString(), ).toNumber(); if ( currentMinClaimDurationSeconds != farmConfig.rewards[rewardIndex]?.minClaimDurationSeconds! ) { const valueStringBefore = "minClaimDurationSeconds before".padEnd(40) + ": " + currentMinClaimDurationSeconds; const valueStringAfter = "minClaimDurationSeconds after".padEnd(40) + ": " + farmConfig.rewards[rewardIndex]?.minClaimDurationSeconds!; printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), rewardIndex, farmConfig.rewards[rewardIndex]?.rewardTokenMint!.toString(), ); return [setRewardMinClaimDuration]; } return []; } function getCurrentRewardInfo(farmState: FarmState, rewardIndex: number) { let rpsDecimalsCurrent = farmState?.rewardInfos[rewardIndex].rewardsPerSecondDecimals ?? 0; let rewardsAvailableLamports = 0; if (farmState) { let rewardInfo = farmState!.rewardInfos[rewardIndex]; rewardsAvailableLamports = new Decimal( rewardInfo.rewardsAvailable.toString(), ).toNumber(); } let rpsCurrent = farmState?.rewardInfos[ rewardIndex ].rewardScheduleCurve.points[0].rewardPerTimeUnit.toNumber() ?? 0; return { rpsDecimalsCurrent, rewardsAvailableLamports, rpsCurrent, }; } function getConfigRewardRps( farmState: FarmState, farmConfig: FarmConfig, rewardIndex: number, ) { const rewardCurve: RewardCurvePoint[] = []; farmConfig.rewards[rewardIndex]?.rewardCurve.forEach((point) => { rewardCurve.push({ startTs: point!.startTs, rps: point!.rps, }); }); const rewardToTopUpDurationSeconds = farmConfig.rewards[rewardIndex] ?.rewardToTopUpDurationDays ? farmConfig.rewards[rewardIndex]?.rewardToTopUpDurationDays! * 24 * 60 * 60 : 0; const rewardPerSecond = rewardToTopUpDurationSeconds == 0 ? -1 : Math.round( (farmConfig.rewards[rewardIndex]?.rewardToTopUp! / rewardToTopUpDurationSeconds) * Math.pow( 10, new Decimal( farmState.rewardInfos[rewardIndex].token.decimals.toString(), ).toNumber(), ), ); if (rewardCurve.length == 1 && rewardPerSecond != -1) { rewardCurve[0].rps = Number(rewardPerSecond.toFixed(0)); } return { rewardCurve, rewardPerSecond, }; } async function setRewardPerSecondIfNecessary( payer: PublicKey, farmsClient: Farms, farmState: FarmState, farmConfig: FarmConfig, rewardPerSecond: number, rewardIndex: number, rewardDecimals: number, mode: string, ): Promise<TransactionInstruction[]> { const { rewardsAvailableLamports } = getCurrentRewardInfo( farmState, rewardIndex, ); const rpsCurrent = farmState?.rewardInfos[ rewardIndex ].rewardScheduleCurve.points[0].rewardPerTimeUnit.toNumber() ?? 0; if (rpsCurrent != rewardPerSecond && rewardPerSecond != -1) { const valueStringBefore = "rewardPerSecond before".padEnd(40) + ": " + rpsCurrent; const valueStringAfter = "rewardPerSecond after".padEnd(40) + ": " + Math.round(rewardPerSecond); printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), rewardIndex, farmConfig.rewards[rewardIndex]?.rewardTokenMint!.toString(), ); const rpsDecimalsCurrent = farmConfig.rewards[rewardIndex]!.rewardPerSecondDecimals; if (rewardPerSecond) { estimateRewardDuration( rewardDecimals, farmConfig.rewards[rewardIndex]?.rewardToTopUp!, rewardsAvailableLamports, rpsCurrent / 10 ** rpsCurrent, rewardPerSecond / 10 ** farmConfig.rewards[rewardIndex]?.rewardPerSecondDecimals!, rpsDecimalsCurrent, mode, ); } let setRewardPerSecond = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, farmConfig.rewards[rewardIndex]?.rewardTokenMint!, FarmConfigOption.fromDecoded({ [FarmConfigOption.UpdateRewardRps.kind]: "", }), rewardPerSecond, rewardIndex, ); return [setRewardPerSecond]; } return []; } async function setRewardCurveIfNecessary( env: Env, payer: PublicKey, farmsClient: Farms, farmState: FarmState, farmConfig: FarmConfig, rewardCurve: RewardCurvePoint[], rewardIndex: number, rewardDecimals: number, slot: number, mode: string, ): Promise<TransactionInstruction[]> { const { rewardsAvailableLamports } = getCurrentRewardInfo( farmState, rewardIndex, ); const { currentRewardCurve, ts } = await getCurrentRewardCurveAndTs( env, farmState, rewardIndex, slot, ); let setRewardCurvePoint = await farmsClient.updateFarmConfigIx( payer, farmConfig.farmPubkey, farmConfig.rewards[rewardIndex]?.rewardTokenMint!, FarmConfigOption.fromDecoded({ [FarmConfigOption.UpdateRewardScheduleCurvePoints.kind]: "", }), rewardCurve, rewardIndex, ); let newCurve = false; if (rewardCurve.length > currentRewardCurve.length) { newCurve = true; } for ( let i = 0; i < Math.min(rewardCurve.length, currentRewardCurve.length); i++ ) { if ( (currentRewardCurve[i].startTs != rewardCurve[i].startTs || currentRewardCurve[i].rps != rewardCurve[i].rps) && !( rewardCurve[i].startTs == 0 && currentRewardCurve[i].startTs == Number(U64_MAX) ) ) { newCurve = true; break; } } if (newCurve) { const valueStringBefore = "rewardCurve before".padEnd(40) + ": " + JSON.stringify(currentRewardCurve); const valueStringAfter = "rewardCurve after".padEnd(40) + ": " + JSON.stringify(rewardCurve); printDiff( mode, valueStringBefore, valueStringAfter, farmConfig.farmPubkey.toString(), rewardIndex, farmConfig.rewards[rewardIndex]?.rewardTokenMint!.toString(), ); const rpsDecimalsCurrent = farmConfig.rewards[rewardIndex]!.rewardPerSecondDecimals; estimateRewardDurationRewardCurve( rewardDecimals, farmConfig.rewards[rewardIndex]?.rewardToTopUp!, rewardsAvailableLamports, currentRewardCurve, rewardCurve, rpsDecimalsCurrent, ts!, mode, ); return [setRewardCurvePoint]; } return []; } async function topupRewardAmountNecessary( env: Env, payer: PublicKey, farmsClient: Farms, farmState: FarmState, farmConfig: FarmConfig, rewardIndex: number, rewardDecimals: number, tokenProgram: PublicKey, slot: number, topUpOnly: boolean, mode: string, ): Promise<TransactionInstruction[]> { if (farmConfig.rewards[rewardIndex]?.rewardToTopUp! > 0) { let topupIx = await farmsClient.addRewardAmountToFarmIx( payer, farmConfig.farmPubkey, farmConfig.rewards[rewardIndex]?.rewardTokenMint!, new Decimal(farmConfig.rewards[rewardIndex]?.rewardToTopUp!), rewardIndex, rewardDecimals, tokenProgram, farmConfig.scopePrices.equals(PublicKey.default) ? undefined : farmConfig.scopePrices, ); const rewardIndexString = rewardIndex == -1 ? "" : " - rewardIndex[" + rewardIndex + "] : " + farmConfig.rewards[rewardIndex]?.rewardTokenMint!.toString(); const valueStringAfter = "reward to top up".padEnd(40) + ": " + farmConfig.rewards[rewardIndex]?.rewardToTopUp!; mode !== "multisig" && console.log( "Change for farm: ", clc.yellow( farmConfig.farmPubkey.toString().concat(rewardIndexString).padEnd(65), ), ); mode !== "multisig" && console.log(clc.green(valueStringAfter), "\n"); if (topUpOnly) { const { rewardsAvailableLamports } = getCurrentRewardInfo( farmState, rewardIndex, ); const { currentRewardCurve, ts } = await getCurrentRewardCurveAndTs( env, farmState, rewardIndex, slot, ); const rpsDecimalsCurrent = farmConfig.rewards[rewardIndex]!.rewardPerSecondDecimals; estimateRewardDurationRewardCurve( rewardDecimals, farmConfig.rewards[rewardIndex]?.rewardToTopUp!, rewardsAvailableLamports, currentRewardCurve, currentRewardCurve, rpsDecimalsCurrent, ts!, mode, ); } return [topupIx]; } return []; } export function estimateRewardDurationRewardCurve( rewardDecimals: number, rewardToTopUp: number, remainingLamports: number, rewardCurveCurrent: RewardCurvePoint[], newRewardCurve: RewardCurvePoint[], rpsDecimalsCurrent: number, ts: number, mode: string, ) { const rewardFactor = 10 ** rewardDecimals; /// Rewards available const newLamports = remainingLamports + rewardToTopUp * rewardFactor; // If we change the number of lamports available const numDaysBalanceRemaining = calculateDaysBalanceRemaining( remainingLamports, rewardCurveCurrent, rpsDecimalsCurrent, ts, ); const numDaysTopupRemaining = calculateDaysBalanceRemaining( newLamports, rewardCurveCurrent, rpsDecimalsCurrent, ts, ); mode !== "multisig" && console.log("Days with current balance:", numDaysBalanceRemaining); mode !== "multisig" && console.log("Days with new balance:", numDaysTopupRemaining); // If we change the number of rps const daysWithNewRps = calculateDaysBalanceRemaining( remainingLamports, newRewardCurve, rpsDecimalsCurrent, ts, ); const daysWithNewRpsAndTopup = calculateDaysBalanceRemaining( newLamports, newRewardCurve, rpsDecimalsCurrent, ts, ); mode !== "multisig" && console.log( "Days with current balance and new reward curve", daysWithNewRps, ); mode !== "multisig" && console.log( "Days with new balance and new reward curve", daysWithNewRpsAndTopup, "\n", ); return { remainingLamports, rewardFactor, }; } export function calculateDaysBalanceRemaining( lamports: number, rewardCurve: RewardCurvePoint[], rpsDecimalsCurrent: number, ts: number, ) { let numDays: number = 0; let remainingLamports: number = lamports; for (const [index, _curvePoint] of rewardCurve.entries()) { const rps = rewardCurve[index].rps / 10 ** rpsDecimalsCurrent; if (index == rewardCurve.length - 1) { return remainingLamports / rps / 86400; } if (remainingLamports > 0) { if ( ts > rewardCurve[index].startTs && ts < rewardCurve[index + 1].startTs ) { const secondsWithCurrentRps = remainingLamports / rps; if (ts + secondsWithCurrentRps <= rewardCurve[index + 1].startTs) { numDays += secondsWithCurrentRps / 86400; return numDays; // all remaining lamports elapsed } else { const actualSecondsAtCurrentRps = rewardCurve[index + 1].startTs - ts; numDays += actualSecondsAtCurrentRps / 86400; remainingLamports -= actualSecondsAtCurrentRps * rps; } } } } return numDays; } function parseFarmConfigFromFile(farmConfigFromFile: any): FarmConfig { const farmConfig: FarmConfig = { farmMetadata: { type: farmConfigFromFile.farmMetadata.type, reserve: farmConfigFromFile.reserve ? new PublicKey(farmConfigFromFile.reserve) : undefined, market: farmConfigFromFile.market ? new PublicKey(farmConfigFromFile.market) : undefined, strategy: farmConfigFromFile.strategy ? new PublicKey(farmConfigFromFile.strategy) : undefined, }, farmPubkey: new PublicKey(farmConfigFromFile.farmPubkey), stakingTokenMint: new PublicKey(farmConfigFromFile.stakingTokenMint), withdrawAuthority: new PublicKey(farmConfigFromFile.withdrawAuthority), globalConfig: new PublicKey(farmConfigFromFile.globalConfig), strategyId: new PublicKey(farmConfigFromFile.strategyId), depositCapAmount: farmConfigFromFile.depositCapAmount, rewards: farmConfigFromFile.rewards.map((reward) => { return { rewardTokenMint: new PublicKey(reward.rewardTokenMint), rewardType: reward.rewardType, rewardPerSecondDecimals: reward.rewardPerSecondDecimals, minClaimDurationSeconds: reward.minClaimDurationSeconds, rewardCurve: reward.rewardCurve, rewardAvailable: reward.rewardAvailable, rewardToTopUp: reward.rewardToTopUp, rewardToTopUpDurationDays: reward.rewardToTopUpDurationDays, }; }), pendingFarmAdmin: new PublicKey(farmConfigFromFile.pendingFarmAdmin), scopePrices: new PublicKey(farmConfigFromFile.scopePrices), scopePriceOracleId: farmConfigFromFile.scopePriceOracleId, scopeOracleMaxAge: farmConfigFromFile.scopeOracleMaxAge, lockingMode: farmConfigFromFile.lockingMode, lockingStart: farmConfigFromFile.lockingStart, lockingDuration: farmConfigFromFile.lockingDuration, lockingEarlyWithdrawalPenaltyBps: farmConfigFromFile.lockingEarlyWithdrawalPenaltyBps, depositWarmupPeriod: farmConfigFromFile.depositWarmupPeriod, withdrawCooldownPeriod: farmConfigFromFile.withdrawCooldownPeriod, slashedAmountSpillAddress: new PublicKey( farmConfigFromFile.slashedAmountSpillAddress, ), }; return farmConfig; } function recursiveReadDirectoryOfConfigs(targetPath: string): FarmConfig[] { const farmConfigs: FarmConfig[] = []; fs.readdirSync(targetPath, { withFileTypes: true }).forEach((file) => { if (file.isDirectory()) { farmConfigs.push( ...recursiveReadDirectoryOfConfigs(targetPath + "/" + file.name), ); } else { const farmConfigFromFile = JSON.parse( fs.readFileSync(targetPath + "/" + file.name, "utf8"), ); farmConfigs.push(parseFarmConfigFromFile(farmConfigFromFile)); } }); return farmConfigs; } function printDiff( mode: string, valueStringBefore: string, valueStringAfter: string, farmPubkeyString: string, rewardIndex: number = -1, rewardMint: string = "", ) { const rewardIndexString = rewardIndex == -1 ? "" : " - rewardIndex[" + rewardIndex + "] : " + rewardMint; mode !== "multisig" && console.log( "Change for farm: ", clc.yellow(farmPubkeyString.concat(rewardIndexString).padEnd(65)), ); mode !== "multisig" && console.log(clc.red(valueStringBefore)); mode !== "multisig" && console.log(clc.green(valueStringAfter), "\n"); } export function estimateRewardDuration( rewardDecimals: number, rewardToTopUp: number, remainingLamports: number, rpsCurrent: number, newRps: number, rpsDecimalsCurrent: number, mode: string, ) { rpsCurrent = rpsCurrent / 10 ** rpsDecimalsCurrent; newRps = newRps / 10 ** rpsDecimalsCurrent; const rewardFactor = 10 ** rewardDecimals; /// Rewards available const newLamports = remainingLamports + rewardToTopUp * rewardFactor; // If we change the number of lamports available const numDaysBalanceRemaining = remainingLamports / rpsCurrent / 86400; const numDaysTopupRemaining = newLamports / rpsCurrent / 86400; mode !== "multisig" && console.log("Days with current balance:", numDaysBalanceRemaining); mode !== "multisig" && console.log("Days with new balance:", numDaysTopupRemaining); // If we change the number of rps const daysWithNewRps = remainingLamports / newRps / 86400; const daysWithNewRpsAndTopup = newLamports / newRps / 86400; mode !== "multisig" && console.log("Days with current balance and new rps", daysWithNewRps); mode !== "multisig" && console.log( "Days with new balance and new rps", daysWithNewRpsAndTopup, "\n", ); return { remainingLamports, rewardFactor, }; } async function getCurrentRewardCurveAndTs( env: Env, farmState: FarmState, rewardIndex: number, slot: number, ): Promise<{ currentRewardCurve: RewardCurvePoint[]; ts: number }> { let currentRewardCurve: RewardCurvePoint[] = []; if (farmState) { for (const point of farmState.rewardInfos[rewardIndex].rewardScheduleCurve .points) { if ( point.tsStart.toString() === U64_MAX && new Decimal(point.rewardPerTimeUnit.toString()).toNumber() === 0 ) { continue; } currentRewardCurve.push({ startTs: new Decimal(point.tsStart.toString()).toNumber(), rps: new Decimal(point.rewardPerTimeUnit.toString()).toNumber(), }); } } let ts = await env.provider.connection.getBlockTime(slot); return { currentRewardCurve, ts: ts!, }; }