test-raydium-sdk-v2
Version:
An SDK for building applications on top of Raydium.
338 lines (301 loc) • 11.6 kB
text/typescript
import { Connection, PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import { GetMultipleAccountsInfoConfig, getMultipleAccountsInfoWithCustomFlags } from "@/common/accountInfo";
import { parseBigNumberish } from "@/common/bignumber";
import { createLogger } from "@/common/logger";
import { findProgramAddress, ProgramAddress } from "@/common/txTool/txUtils";
import { DateParam, isDateAfter, isDateBefore } from "@/common/date";
import { jsonInfo2PoolKeys } from "@/common/utility";
import { RewardInfoV6 } from "@/api/type";
import { splAccountLayout } from "../account/layout";
import { SplAccount } from "../account/types";
import { FARM_VERSION_TO_LEDGER_LAYOUT, FARM_VERSION_TO_STATE_LAYOUT, poolTypeV6 } from "./config";
import { FarmLedger, FarmLedgerLayout, FarmState, FarmStateLayout } from "./layout";
import { FarmRewardInfo, FarmRewardInfoConfig } from "./type";
import { VoterRegistrar, Voter } from "./layout";
const logger = createLogger("Raydium.farm.util");
interface AssociatedLedgerPoolAccount {
programId: PublicKey;
poolId: PublicKey;
mint: PublicKey;
type: "lpVault" | "rewardVault";
}
export function getAssociatedLedgerPoolAccount({
programId,
poolId,
mint,
type,
}: AssociatedLedgerPoolAccount): PublicKey {
const { publicKey } = findProgramAddress(
[
poolId.toBuffer(),
mint.toBuffer(),
Buffer.from(
type === "lpVault" ? "lp_vault_associated_seed" : type === "rewardVault" ? "reward_vault_associated_seed" : "",
"utf-8",
),
],
programId,
);
return publicKey;
}
export function getAssociatedLedgerAccount({
programId,
poolId,
owner,
version,
}: {
programId: PublicKey;
poolId: PublicKey;
owner: PublicKey;
version: 6 | 5 | 3;
}): PublicKey {
const { publicKey } = findProgramAddress(
[
poolId.toBuffer(),
owner.toBuffer(),
Buffer.from(version === 6 ? "farmer_info_associated_seed" : "staker_info_v2_associated_seed", "utf-8"),
],
programId,
);
return publicKey;
}
export const getAssociatedAuthority = ({
programId,
poolId,
}: {
programId: PublicKey;
poolId: PublicKey;
}): ProgramAddress => findProgramAddress([poolId.toBuffer()], programId);
export function farmRewardInfoToConfig(data: FarmRewardInfo): FarmRewardInfoConfig {
return {
isSet: new BN(1),
rewardPerSecond: parseBigNumberish(data.perSecond),
rewardOpenTime: parseBigNumberish(data.openTime),
rewardEndTime: parseBigNumberish(data.endTime),
rewardType: parseBigNumberish(poolTypeV6[data.rewardType]),
};
}
export function calFarmRewardAmount(data: Pick<RewardInfoV6, "openTime" | "endTime"> & { perSecond: string }): BN {
return parseBigNumberish(data.endTime).sub(parseBigNumberish(data.openTime)).mul(parseBigNumberish(data.perSecond));
}
export function getFarmLedgerLayout(version: number): FarmLedgerLayout | undefined {
const ledgerLayout = FARM_VERSION_TO_LEDGER_LAYOUT[version];
if (!ledgerLayout) logger.logWithError("invalid version", version);
return ledgerLayout;
}
export function getFarmStateLayout(version: number): FarmStateLayout | undefined {
const stateLayout = FARM_VERSION_TO_STATE_LAYOUT[version];
if (!stateLayout) logger.logWithError("invalid version", version);
return stateLayout;
}
export function updateFarmPoolInfo(
poolInfo: FarmState,
lpVault: SplAccount,
slot: number,
chainTime: number,
): FarmState {
if (poolInfo.version === 3 || poolInfo.version === 5) {
if (poolInfo.lastSlot.gte(new BN(slot))) return poolInfo;
const spread = new BN(slot).sub(poolInfo.lastSlot);
poolInfo.lastSlot = new BN(slot);
for (const itemRewardInfo of poolInfo.rewardInfos) {
if (lpVault.amount.eq(new BN(0))) continue;
const reward = itemRewardInfo.perSlotReward.mul(spread);
itemRewardInfo.perShareReward = itemRewardInfo.perShareReward.add(
reward.mul(new BN(10).pow(new BN(poolInfo.version === 3 ? 9 : 15))).div(lpVault.amount),
);
itemRewardInfo.totalReward = itemRewardInfo.totalReward.add(reward);
}
} else if (poolInfo.version === 6) {
for (const itemRewardInfo of poolInfo.rewardInfos) {
if (itemRewardInfo.rewardState.eq(new BN(0))) continue;
const updateTime = BN.min(new BN(chainTime), itemRewardInfo.rewardEndTime);
if (itemRewardInfo.rewardOpenTime.gte(updateTime)) continue;
const spread = updateTime.sub(itemRewardInfo.rewardLastUpdateTime);
let reward = spread.mul(itemRewardInfo.rewardPerSecond);
const leftReward = itemRewardInfo.totalReward.sub(itemRewardInfo.totalRewardEmissioned);
if (leftReward.lt(reward)) {
reward = leftReward;
itemRewardInfo.rewardLastUpdateTime = itemRewardInfo.rewardLastUpdateTime.add(
leftReward.div(itemRewardInfo.rewardPerSecond),
);
} else {
itemRewardInfo.rewardLastUpdateTime = updateTime;
}
if (lpVault.amount.eq(new BN(0))) continue;
itemRewardInfo.accRewardPerShare = itemRewardInfo.accRewardPerShare.add(
reward.mul(poolInfo.rewardMultiplier).div(lpVault.amount),
);
itemRewardInfo.totalRewardEmissioned = itemRewardInfo.totalRewardEmissioned.add(reward);
}
}
return poolInfo;
}
interface FarmPoolsInfo {
[id: string]: {
state: FarmState;
lpVault: SplAccount;
ledger?: FarmLedger;
wrapped?: { pendingRewards: BN[] };
};
}
export interface FarmFetchMultipleInfoParams {
connection: Connection;
farmPools: any[];
owner?: PublicKey;
config?: GetMultipleAccountsInfoConfig;
chainTime: number;
}
export async function fetchMultipleFarmInfoAndUpdate({
connection,
farmPools,
owner,
config,
chainTime,
}: FarmFetchMultipleInfoParams): Promise<FarmPoolsInfo> {
let hasNotV6Pool = false;
let hasV6Pool = false;
const tenBN = new BN(10);
const publicKeys: {
pubkey: PublicKey;
version: number;
key: "state" | "lpVault" | "ledger";
poolId: PublicKey;
}[] = [];
for (const poolInfo of farmPools) {
const pool = jsonInfo2PoolKeys(poolInfo);
if (pool.version === 6) hasV6Pool = true;
else hasNotV6Pool = true;
publicKeys.push(
{
pubkey: pool.id,
version: pool.version,
key: "state",
poolId: pool.id,
},
{
pubkey: pool.lpVault,
version: pool.version,
key: "lpVault",
poolId: pool.id,
},
);
if (owner) {
publicKeys.push({
pubkey: getAssociatedLedgerAccount({
programId: pool.programId,
poolId: pool.id,
owner,
version: poolInfo.version as 6 | 5 | 3,
}),
version: pool.version,
key: "ledger",
poolId: pool.id,
});
}
}
const poolsInfo: FarmPoolsInfo = {};
const accountsInfo = await getMultipleAccountsInfoWithCustomFlags(connection, publicKeys, config);
for (const { pubkey, version, key, poolId, accountInfo } of accountsInfo) {
const _poolId = poolId.toBase58();
poolsInfo[_poolId] = { ...poolsInfo[_poolId] };
if (key === "state") {
const stateLayout = getFarmStateLayout(version);
if (!accountInfo || !accountInfo.data || accountInfo.data.length !== stateLayout!.span)
logger.logWithError(`invalid farm state account info, pools.id, ${pubkey}`);
poolsInfo[_poolId].state = stateLayout!.decode(accountInfo!.data);
} else if (key === "lpVault") {
if (!accountInfo || !accountInfo.data || accountInfo.data.length !== splAccountLayout.span)
logger.logWithError(`invalid farm lp vault account info, pools.lpVault, ${pubkey}`);
poolsInfo[_poolId].lpVault = splAccountLayout.decode(accountInfo!.data);
} else if (key === "ledger") {
const legerLayout = getFarmLedgerLayout(version)!;
if (accountInfo && accountInfo.data) {
if (accountInfo.data.length !== legerLayout.span)
logger.logWithError(`invalid farm ledger account info, ledger, ${pubkey}`);
poolsInfo[_poolId].ledger = legerLayout.decode(accountInfo.data);
}
}
}
const slot = hasV6Pool || hasNotV6Pool ? await connection.getSlot() : 0;
for (const poolId of Object.keys(poolsInfo)) {
if (poolsInfo[poolId] === undefined) continue;
poolsInfo[poolId].state = updateFarmPoolInfo(poolsInfo[poolId].state, poolsInfo[poolId].lpVault, slot, chainTime);
}
for (const [poolId, { state, ledger }] of Object.entries(poolsInfo)) {
if (ledger) {
const multiplier =
state.version === 6
? state.rewardMultiplier
: state.rewardInfos.length === 1
? tenBN.pow(new BN(9))
: tenBN.pow(new BN(15));
const pendingRewards = state.rewardInfos.map((rewardInfo, index) => {
const rewardDebt = ledger.rewardDebts[index];
const pendingReward = ledger.deposited
.mul(state.version === 6 ? rewardInfo.accRewardPerShare : rewardInfo.perShareReward)
.div(multiplier)
.sub(rewardDebt);
return pendingReward;
});
poolsInfo[poolId].wrapped = {
...poolsInfo[poolId].wrapped,
pendingRewards,
};
}
}
return poolsInfo;
}
/** deprecated */
export function judgeFarmType(
info: any,
currentTime: DateParam = Date.now(),
): "closed pool" | "normal fusion pool" | "dual fusion pool" | undefined | "upcoming pool" {
if (info.version === 6) {
const rewardInfos = info.state.rewardInfos;
if (rewardInfos.every(({ rewardOpenTime }) => isDateBefore(currentTime, rewardOpenTime.toNumber(), { unit: "s" })))
return "upcoming pool";
if (rewardInfos.every(({ rewardEndTime }) => isDateAfter(currentTime, rewardEndTime.toNumber(), { unit: "s" })))
return "closed pool";
} else {
const perSlotRewards = info.state.rewardInfos.map(({ perSlotReward }) => perSlotReward);
if (perSlotRewards.length === 2) {
// v5
if (String(perSlotRewards[0]) === "0" && String(perSlotRewards[1]) !== "0") {
return "normal fusion pool"; // reward xxx token
}
if (String(perSlotRewards[0]) !== "0" && String(perSlotRewards[1]) !== "0") {
return "dual fusion pool"; // reward ray and xxx token
}
if (String(perSlotRewards[0]) === "0" && String(perSlotRewards[1]) === "0") {
return "closed pool";
}
} else if (perSlotRewards.length === 1) {
// v3
if (String(perSlotRewards[0]) === "0") {
return "closed pool";
}
}
}
}
export async function getDepositEntryIndex(
connection: Connection,
registrar: PublicKey,
voter: PublicKey,
voterMint: PublicKey,
): Promise<{ index: number; isInit: boolean }> {
const registrarAccountData = await connection.getAccountInfo(registrar);
if (registrarAccountData === null) throw Error("registrar info check error");
const registrarData = VoterRegistrar.decode(registrarAccountData.data);
const votingMintConfigIndex = registrarData.votingMints.findIndex((i) => i.mint.equals(voterMint));
if (votingMintConfigIndex === -1) throw Error("find voter mint error");
const voterAccountData = await connection.getAccountInfo(voter);
if (voterAccountData === null) return { index: votingMintConfigIndex, isInit: false }; // throw Error('voter info check error')
const voterData = Voter.decode(voterAccountData.data);
const depositEntryIndex = voterData.deposits.findIndex(
(i) => i.isUsed && i.votingMintConfigIdx === votingMintConfigIndex,
);
if (depositEntryIndex === -1) return { index: votingMintConfigIndex, isInit: false };
else return { index: depositEntryIndex, isInit: true };
}