@hubbleprotocol/farms-sdk
Version:
1,776 lines (1,594 loc) • 45.4 kB
text/typescript
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!,
};
}