Skip to content

Commit

Permalink
feat: optimized getClaimableRewards
Browse files Browse the repository at this point in the history
  • Loading branch information
McSam94 committed Dec 21, 2023
1 parent 0ca54cb commit 3be7687
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 117 deletions.
3 changes: 2 additions & 1 deletion ts-client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mercurial-finance/farming-sdk",
"version": "1.0.14",
"version": "1.0.15",
"description": "",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down Expand Up @@ -29,6 +29,7 @@
"@solana/spl-token": "^0.1.8",
"@solana/spl-token-registry": "^0.2.4574",
"@solana/web3.js": "~1.78.3",
"buffer-layout": "^1.2.2",
"node-fetch": "^2.6.12"
},
"publishConfig": {
Expand Down
5 changes: 4 additions & 1 deletion ts-client/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

226 changes: 147 additions & 79 deletions ts-client/src/farm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ import {
ComputeBudgetProgram,
Connection,
PublicKey,
SYSVAR_CLOCK_PUBKEY,
Transaction,
TransactionInstruction,
SYSVAR_CLOCK_PUBKEY,
ParsedAccountData,
} from "@solana/web3.js";
import * as BufferLayout from "buffer-layout";

import { FarmProgram, Opt, PoolState, UserState, ParsedClockState } from "./types";
import {
ClockState,
FarmProgram,
Opt,
ParsedClockState,
PoolState,
UserState,
} from "./types";
import {
chunks,
getFarmInfo,
Expand All @@ -20,7 +27,7 @@ import {
parseLogs,
} from "./utils";
import { FARM_PROGRAM_ID, SIMULATION_USER } from "./constant";

import { chunkedGetMultipleAccountInfos } from "@mercurial-finance/dynamic-amm-sdk/dist/cjs/src/amm/utils";

const chunkedFetchMultipleUserAccount = async (
program: FarmProgram,
Expand Down Expand Up @@ -66,18 +73,6 @@ const getAllPoolState = async (
return poolStates;
};

const getAllUserState = async (
farmMints: Array<PublicKey>,
program: FarmProgram
) => {
const poolStates = (await chunkedFetchMultipleUserAccount(
program,
farmMints
)) as Array<UserState>;

return poolStates;
};

const MAX_CLAIM_ALL_ALLOWED = 2;

export class PoolFarmImpl {
Expand Down Expand Up @@ -366,25 +361,25 @@ export class PoolFarmImpl {
await Promise.all(
isDual
? [
getOrCreateATAInstruction(
this.poolState.rewardAMint,
owner,
this.program.provider.connection
),
getOrCreateATAInstruction(
this.poolState.rewardBMint,
owner,
this.program.provider.connection
),
]
getOrCreateATAInstruction(
this.poolState.rewardAMint,
owner,
this.program.provider.connection
),
getOrCreateATAInstruction(
this.poolState.rewardBMint,
owner,
this.program.provider.connection
),
]
: [
getOrCreateATAInstruction(
this.poolState.rewardAMint,
owner,
this.program.provider.connection
),
[undefined, undefined],
]
getOrCreateATAInstruction(
this.poolState.rewardAMint,
owner,
this.program.provider.connection
),
[undefined, undefined],
]
);
userRewardAIx && preInstructions.push(userRewardAIx);
userRewardBIx && preInstructions.push(userRewardBIx);
Expand Down Expand Up @@ -416,69 +411,142 @@ export class PoolFarmImpl {
}).add(claimTx);
}

async getClaimableReward(owner: PublicKey) {
if (!this.eventParser) throw "EventParser not found";
// async getClaimableReward(owner: PublicKey) {
// if (!this.eventParser) throw "EventParser not found";

// const claimMethodBuilder = await this.claimMethodBuilder(owner);

// const claimTransaction = await claimMethodBuilder.transaction();

const claimMethodBuilder = await this.claimMethodBuilder(owner);
// if (!claimTransaction) return;

const claimTransaction = await claimMethodBuilder.transaction();
// const blockhash = (
// await this.program.provider.connection.getLatestBlockhash("finalized")
// ).blockhash;
// const claimTx = new Transaction({
// recentBlockhash: blockhash,
// feePayer: SIMULATION_USER,
// });
// claimTransaction && claimTx.add(claimTransaction);

if (!claimTransaction) return;
// const tx = await this.program.provider.connection.simulateTransaction(
// claimTx
// );

const blockhash = (
await this.program.provider.connection.getLatestBlockhash("finalized")
).blockhash;
const claimTx = new Transaction({
recentBlockhash: blockhash,
feePayer: SIMULATION_USER,
// const simulatedReward = (await parseLogs(
// this.eventParser,
// tx?.value?.logs ?? []
// )) as { amountA: BN; amountB: BN };

// return simulatedReward;
// }

static async getClaimableRewards(
owner: PublicKey,
farmMints: Array<PublicKey>,
connection: Connection
) {
const { program } = getFarmProgram(connection);

const usersPda = farmMints.map((mint) => {
const [userStakingAddress] = PublicKey.findProgramAddressSync(
[owner.toBuffer(), mint.toBuffer()],
FARM_PROGRAM_ID
);

return userStakingAddress;
});
claimTransaction && claimTx.add(claimTransaction);

const tx = await this.program.provider.connection.simulateTransaction(
claimTx
const accountsToFetched = [SYSVAR_CLOCK_PUBKEY, ...farmMints, ...usersPda];
const accounts = await chunkedGetMultipleAccountInfos(
connection,
accountsToFetched
);

const simulatedReward = (await parseLogs(
this.eventParser,
tx?.value?.logs ?? []
)) as { amountA: BN; amountB: BN };
const [clockAccountInfo, ...restAccounts] = accounts;
const ClockLayout = BufferLayout.struct([
BufferLayout.blob(8, "slot"),
BufferLayout.blob(8, "epochStartTimestamp"),
BufferLayout.blob(8, "epoch"),
BufferLayout.blob(8, "leaderScheduleEpoch"),
BufferLayout.blob(8, "unixTimestamp"),
]);
const clockState = clockAccountInfo?.data
? (ClockLayout.decode(clockAccountInfo.data) as ClockState)
: undefined;
if (!clockState) throw new Error("Clock state not found");
const onChainTime = clockState.unixTimestamp;

const poolStatesMap = new Map();
for (let i = 0; i < farmMints.length; i++) {
const farmMint = farmMints[i];
const poolAccount = restAccounts[i];
const userPdaAccount = restAccounts[i + farmMints.length];

const poolState = poolAccount?.data
? (program.coder.accounts.decode("pool", poolAccount.data) as PoolState)
: undefined;
const userState = userPdaAccount?.data
? (program.coder.accounts.decode(
"user",
userPdaAccount.data
) as UserState)
: undefined;
if (!poolState) throw new Error("Pool state not found");

poolStatesMap.set(farmMint.toBase58(), {
poolState,
userState,
});
}

return simulatedReward;
return Array.from(poolStatesMap.entries()).reduce<
Map<string, { rewardA: BN; rewardB: BN }>
>((accValue, [farmMint, { poolState, userState }]) => {
const rewardDurationEnd = poolState.rewardDurationEnd.toNumber();
const lastTimeRewardApplicable =
onChainTime < rewardDurationEnd ? onChainTime : rewardDurationEnd;
const { a, b } = rewardPerToken(poolState, lastTimeRewardApplicable);

const rewardA = userState
? userState.balanceStaked
.mul(a.sub(userState.rewardAPerTokenComplete))
.div(new BN(1_000_000_000))
.add(userState.rewardAPerTokenPending)
: new BN(0);
const rewardB = userState
? userState.balanceStaked
.mul(b.sub(userState.rewardBPerTokenComplete))
.div(new BN(1_000_000_000))
.add(userState.rewardBPerTokenPending)
: new BN(0);
accValue.set(farmMint, {
rewardA,
rewardB,
});

return accValue;
}, new Map());
}
}

export const getOnchainTime = async (connection: Connection) => {
const parsedClock = await connection.getParsedAccountInfo(SYSVAR_CLOCK_PUBKEY);

const parsedClockAccount = (parsedClock.value!.data as ParsedAccountData).parsed as ParsedClockState;

const currentTime = parsedClockAccount.info.unixTimestamp;
return currentTime;
};

function rewardPerToken(pool: PoolState, lastTimeRewardApplicable: number) {
let totalStake = pool.totalStaked;
if (totalStake.isZero()) {
return {
a: pool.rewardAPerTokenStored,
b: pool.rewardBPerTokenStored,
}
};
}
let timePeriod = new BN(lastTimeRewardApplicable - pool.lastUpdateTime.toNumber());
let timePeriod = new BN(
lastTimeRewardApplicable - pool.lastUpdateTime.toNumber()
);
return {
a: pool.rewardAPerTokenStored.add(timePeriod.mul(pool.rewardARateU128).div(totalStake)),
b: pool.rewardAPerTokenStored.add(timePeriod.mul(pool.rewardARateU128).div(totalStake))
}
}

export function getClaimableRewardSync(onchainTIme: number, userState: UserState, poolState: PoolState) {
// update reward
let rewardDurationEnd = poolState.rewardDurationEnd.toNumber();
let lastTimeRewardApplicable = ((onchainTIme < rewardDurationEnd) ? onchainTIme : rewardDurationEnd);
let { a, b } = rewardPerToken(poolState, lastTimeRewardApplicable);

return {
a: (userState.balanceStaked.mul(a.sub(userState.rewardAPerTokenComplete)).div(new BN(1_000_000_000))).add(userState.rewardAPerTokenPending),
b: (userState.balanceStaked.mul(b.sub(userState.rewardBPerTokenComplete)).div(new BN(1_000_000_000))).add(userState.rewardBPerTokenPending),
}
a: pool.rewardAPerTokenStored.add(
timePeriod.mul(pool.rewardARateU128).div(totalStake)
),
b: pool.rewardAPerTokenStored.add(
timePeriod.mul(pool.rewardARateU128).div(totalStake)
),
};
}
29 changes: 2 additions & 27 deletions ts-client/src/tests/farm.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Cluster, Connection, Keypair, PublicKey } from "@solana/web3.js";
import AmmImpl from "@mercurial-finance/dynamic-amm-sdk";
import { PoolFarmImpl, getOnchainTime, getClaimableRewardSync } from "../farm";
import { PoolFarmImpl } from "../farm";
import { AnchorProvider, BN, Wallet } from "@coral-xyz/anchor";
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import { airDropSol, getFarmProgram } from "../utils";
Expand Down Expand Up @@ -41,7 +41,7 @@ describe("Interact with devnet farm", () => {
let lpBalance: BN;
let stakedBalance: BN;
beforeAll(async () => {
await airDropSol(DEVNET.connection, mockWallet.publicKey).catch(() => { });
await airDropSol(DEVNET.connection, mockWallet.publicKey).catch(() => {});

const USDT = DEVNET_COIN.find(
(token) =>
Expand Down Expand Up @@ -150,29 +150,4 @@ describe("Interact with mainnet farm", () => {
"9dGX6N3FLAVfKmvtkwHA9MVGsvEqGKnLFDQQFbw5dprr"
);
});


test("Get claimable reward", async () => {
let onchainTIme = await getOnchainTime(provider.connection);
const { program } = getFarmProgram(provider.connection);

const poolAdrr = new PublicKey(
"29DQB5C97HgJg5EKQ2EtnvSk28sS93WkgmnaXErB7HtT"
);
const poolState = await program.account.pool.fetchNullable(poolAdrr);
const owner = new PublicKey(
"BULRqL3U2jPgwvz6HYCyBVq9BMtK94Y1Nz98KQop23aD"
)
const [userPda] = PublicKey.findProgramAddressSync(
[owner.toBuffer(), poolAdrr.toBuffer()],
program.programId
);

const userState = await program.account.user.fetchNullable(userPda);

const { a, b } = getClaimableRewardSync(onchainTIme, userState, poolState);
console.log(a.toNumber(), b.toNumber())
});


});
17 changes: 9 additions & 8 deletions ts-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,18 @@ export type FarmProgram = Program<Farming>;
export type PoolState = IdlAccounts<Farming>["pool"];
export type UserState = IdlAccounts<Farming>["user"];


/** Utils */
export interface ParsedClockState {
info: {
epoch: number;
epochStartTimestamp: number;
leaderScheduleEpoch: number;
slot: number;
unixTimestamp: number;
};
info: ClockState;
type: string;
program: string;
space: number;
}

export interface ClockState {
epoch: number;
epochStartTimestamp: number;
leaderScheduleEpoch: number;
slot: number;
unixTimestamp: number;
}
Loading

0 comments on commit 3be7687

Please sign in to comment.