@hubbleprotocol/farms-sdk
Version:
321 lines (295 loc) • 10.3 kB
text/typescript
import { getFarmsProgramId, lamportsToCollDecimal } from "../utils";
import { initializeClient } from "./utils";
import { Farms } from "../Farms";
import * as fs from "fs";
import { PublicKey } from "@solana/web3.js";
import {
getMarketsFromApi,
KaminoMarket,
PublicKeySet,
U64_MAX,
} from "@kamino-finance/klend-sdk";
import { FarmState } from "../rpc_client/accounts";
import Decimal from "decimal.js";
import { RewardType } from "../rpc_client/types";
import { Kamino } from "@kamino-finance/kliquidity-sdk";
export async function downloadAllFarmConfigs(targetPath: string) {
const admin = process.env.ADMIN!;
const rpc = process.env.RPC!;
const klendProgramId = new PublicKey(process.env.KLEND_PROGRAM_ID!);
const lendingMarkets = await getMarketsFromApiData(klendProgramId);
const env = initializeClient(rpc, admin, getFarmsProgramId(rpc), false);
const farmClient = new Farms(env.provider.connection);
const allFarms = await farmClient.getAllFarmStates();
const fetchedFarmsForStratsAndReserves = new PublicKeySet<PublicKey>([]);
// Download for lending based on API markets - all reserve farms
for (const market of lendingMarkets) {
const kaminoMarket = await KaminoMarket.load(
env.provider.connection,
market.key,
450,
klendProgramId,
);
if (!kaminoMarket) {
throw new Error("Kamino market not found");
}
fs.mkdirSync(`${targetPath}/lending/${market.marketName}`, {
recursive: true,
});
for (const reserve of kaminoMarket.reserves.values()) {
if (!reserve.state.farmCollateral.equals(PublicKey.default)) {
fetchedFarmsForStratsAndReserves.add(reserve.state.farmCollateral);
const farmStateCollateral = await FarmState.fetch(
env.provider.connection,
reserve.state.farmCollateral,
);
if (farmStateCollateral) {
const farmConfig = getFarmConfigType(
reserve.state.farmCollateral,
farmStateCollateral,
{
type: "reserve",
reserve: reserve.address,
market: market.key,
strategy: undefined,
},
);
fs.writeFileSync(
`${targetPath}/lending/${market.marketName}/${
reserve!.symbol
}-Collateral.json`,
JSON.stringify(farmConfig, null, 2),
);
} else {
console.log(
"Could not fetch farm",
reserve.state.farmCollateral.toBase58(),
);
}
}
if (!reserve.state.farmDebt.equals(PublicKey.default)) {
fetchedFarmsForStratsAndReserves.add(reserve.state.farmDebt);
const farmStateDebt = await FarmState.fetch(
env.provider.connection,
reserve.state.farmDebt,
);
if (farmStateDebt) {
const farmConfig = getFarmConfigType(
reserve.state.farmDebt,
farmStateDebt,
{
type: "reserve",
reserve: reserve.address,
market: market.key,
strategy: undefined,
},
);
fs.writeFileSync(
`${targetPath}/lending/${market.marketName}/${
reserve!.symbol
}-Debt.json`,
JSON.stringify(farmConfig, null, 2),
);
} else {
console.log(
"Could not fetch farm",
reserve.state.farmDebt.toBase58(),
);
}
}
}
}
// Download for yvaults all strategy farms
const kamino = new Kamino("mainnet-beta", env.provider.connection);
let strategies = await kamino.getAllStrategiesWithFilters({
strategyCreationStatus: "LIVE",
});
fs.mkdirSync(`${targetPath}/yvaults/`, { recursive: true });
for (const strategy of strategies) {
const farmAddress = strategy!.strategy.farm;
if (!farmAddress.equals(PublicKey.default)) {
fetchedFarmsForStratsAndReserves.add(farmAddress);
const farmState = await FarmState.fetch(
env.provider.connection,
farmAddress,
);
if (farmState) {
const farmConfig = getFarmConfigType(farmAddress, farmState, {
type: "strategy",
reserve: undefined,
market: undefined,
strategy: strategy.address,
});
// in case strategy is not set on farm side, we override value so we set on next upsert
farmConfig.strategyId = strategy.address;
fs.writeFileSync(
`${targetPath}/yvaults/${strategy.address.toBase58()}.json`,
JSON.stringify(farmConfig, null, 2),
);
} else {
console.log("Could not fetch farm", farmAddress.toBase58());
}
}
}
// Download all standalone farms
fs.mkdirSync(`${targetPath}/standaloneFarms/`, { recursive: true });
for (const farmAndKey of allFarms) {
// skip farms already downloaded as part of reserves or strategies
if (fetchedFarmsForStratsAndReserves.contains(farmAndKey.key)) {
continue;
}
const farmConfig = getFarmConfigType(farmAndKey.key, farmAndKey.farmState, {
type: "standalone",
reserve: undefined,
market: undefined,
strategy: undefined,
});
fs.writeFileSync(
`${targetPath}/standaloneFarms/${farmAndKey.key.toBase58()}.json`,
JSON.stringify(farmConfig, null, 2),
);
}
}
export async function getMarketsFromApiData(
programId: PublicKey,
): Promise<{ marketName: string; key: PublicKey }[]> {
const markets: { marketName: string; key: PublicKey }[] = [];
await getMarketsFromApi(programId, "API").then(function (response) {
for (const marketData of response) {
markets.push({
marketName: marketData.description.replace(" ", "-"),
key: new PublicKey(marketData.lendingMarket),
});
}
});
return markets;
}
export type FarmConfig = {
farmMetadata: FarmMetadata;
farmPubkey: PublicKey;
stakingTokenMint: PublicKey;
withdrawAuthority: PublicKey;
globalConfig: PublicKey;
strategyId: PublicKey;
depositCapAmount: number;
rewards: Array<
| {
rewardTokenMint: PublicKey;
rewardType: string;
rewardPerSecondDecimals: number;
minClaimDurationSeconds: number;
rewardCurve: Array<
| {
startTs: number;
rps: number;
}
| undefined
>;
rewardAvailable: number;
rewardToTopUp: number;
rewardToTopUpDurationDays: number;
}
| undefined
>;
pendingFarmAdmin: PublicKey;
scopePrices: PublicKey;
scopePriceOracleId: string;
scopeOracleMaxAge: number;
lockingMode: number;
lockingStart: number;
lockingDuration: number;
lockingEarlyWithdrawalPenaltyBps: number;
depositWarmupPeriod: number;
withdrawCooldownPeriod: number;
slashedAmountSpillAddress: PublicKey;
};
export type FarmMetadata = {
type: string; // strategy or reserve
reserve: PublicKey | undefined;
market: PublicKey | undefined;
strategy: PublicKey | undefined;
};
function getRewardType(rewardTypeNumber: number): string {
switch (rewardTypeNumber) {
case RewardType.Proportional.discriminator:
return RewardType.Proportional.kind;
case RewardType.Constant.discriminator:
return RewardType.Constant.kind;
default:
throw new Error(`Invalid reward type: ${rewardTypeNumber}`);
}
}
export function getFarmConfigType(
farmKey: PublicKey,
farmState: FarmState,
farmMetadata: FarmMetadata,
): FarmConfig {
return {
farmMetadata,
farmPubkey: farmKey,
stakingTokenMint: farmState.token.mint,
withdrawAuthority: farmState.withdrawAuthority,
globalConfig: farmState.globalConfig,
strategyId: farmState.strategyId, // reserve farm
depositCapAmount: new Decimal(
farmState.depositCapAmount.toString(),
).toNumber(),
rewards: farmState.rewardInfos
.map((rewardInfo) => {
if (!rewardInfo.token.mint.equals(PublicKey.default)) {
return {
rewardTokenMint: rewardInfo.token.mint,
rewardType: getRewardType(rewardInfo.rewardType),
rewardPerSecondDecimals: rewardInfo.rewardsPerSecondDecimals,
minClaimDurationSeconds: new Decimal(
rewardInfo.minClaimDurationSeconds.toString(),
).toNumber(),
rewardCurve: rewardInfo.rewardScheduleCurve.points
.map((point) => {
if (
new Decimal(point.rewardPerTimeUnit.toString()).toNumber() !==
0 ||
point.tsStart.toString() !== U64_MAX
) {
return {
startTs: new Decimal(point.tsStart.toString()).toNumber(),
rps: new Decimal(
point.rewardPerTimeUnit.toString(),
).toNumber(),
};
}
})
.filter((point) => point !== undefined),
rewardAvailable: lamportsToCollDecimal(
new Decimal(rewardInfo.rewardsAvailable.toString()),
rewardInfo.token.decimals.toNumber(),
)
.floor()
.toNumber(),
rewardToTopUp: 0,
rewardToTopUpDurationDays: 0,
};
}
})
.filter((rewardInfoConfig) => rewardInfoConfig !== undefined),
pendingFarmAdmin: farmState.pendingFarmAdmin,
scopePrices: farmState.scopePrices,
scopePriceOracleId: farmState.scopeOraclePriceId.toString(),
scopeOracleMaxAge: new Decimal(
farmState.scopeOracleMaxAge.toString(),
).toNumber(),
lockingMode: new Decimal(farmState.lockingMode.toString()).toNumber(),
lockingStart: new Decimal(
farmState.lockingStartTimestamp.toString(),
).toNumber(),
lockingDuration: new Decimal(
farmState.lockingDuration.toString(),
).toNumber(),
lockingEarlyWithdrawalPenaltyBps: new Decimal(
farmState.lockingEarlyWithdrawalPenaltyBps.toString(),
).toNumber(),
depositWarmupPeriod: farmState.depositWarmupPeriod,
withdrawCooldownPeriod: farmState.withdrawalCooldownPeriod,
slashedAmountSpillAddress: farmState.slashedAmountSpillAddress,
};
}