From 3be7687d33f0e87f63424dbcc835d59035522f25 Mon Sep 17 00:00:00 2001 From: McSam Date: Thu, 21 Dec 2023 15:59:55 +0800 Subject: [PATCH] feat: optimized getClaimableRewards --- ts-client/package.json | 3 +- ts-client/pnpm-lock.yaml | 5 +- ts-client/src/farm.ts | 226 ++++++++++++++++++++----------- ts-client/src/tests/farm.test.ts | 29 +--- ts-client/src/types.ts | 17 +-- ts-client/src/utils.ts | 16 ++- 6 files changed, 179 insertions(+), 117 deletions(-) diff --git a/ts-client/package.json b/ts-client/package.json index fdda69d..1d3a6a4 100644 --- a/ts-client/package.json +++ b/ts-client/package.json @@ -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", @@ -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": { diff --git a/ts-client/pnpm-lock.yaml b/ts-client/pnpm-lock.yaml index 313cb6f..71798ad 100644 --- a/ts-client/pnpm-lock.yaml +++ b/ts-client/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@solana/web3.js': specifier: ~1.78.3 version: 1.78.3 + buffer-layout: + specifier: ^1.2.2 + version: 1.2.2 node-fetch: specifier: ^2.6.12 version: 2.6.12 @@ -1975,7 +1978,7 @@ packages: dependencies: '@solana/buffer-layout': 4.0.1 '@solana/buffer-layout-utils': 0.2.0 - '@solana/web3.js': 1.77.3 + '@solana/web3.js': 1.78.3 start-server-and-test: 1.15.4 transitivePeerDependencies: - bufferutil diff --git a/ts-client/src/farm.ts b/ts-client/src/farm.ts index 46a789b..f4a2a0a 100644 --- a/ts-client/src/farm.ts +++ b/ts-client/src/farm.ts @@ -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, @@ -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, @@ -66,18 +73,6 @@ const getAllPoolState = async ( return poolStates; }; -const getAllUserState = async ( - farmMints: Array, - program: FarmProgram -) => { - const poolStates = (await chunkedFetchMultipleUserAccount( - program, - farmMints - )) as Array; - - return poolStates; -}; - const MAX_CLAIM_ALL_ALLOWED = 2; export class PoolFarmImpl { @@ -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); @@ -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, + 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 + >((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) + ), + }; } diff --git a/ts-client/src/tests/farm.test.ts b/ts-client/src/tests/farm.test.ts index d75aaf7..de6013d 100644 --- a/ts-client/src/tests/farm.test.ts +++ b/ts-client/src/tests/farm.test.ts @@ -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"; @@ -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) => @@ -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()) - }); - - }); diff --git a/ts-client/src/types.ts b/ts-client/src/types.ts index b9ba677..20a4526 100644 --- a/ts-client/src/types.ts +++ b/ts-client/src/types.ts @@ -45,17 +45,18 @@ export type FarmProgram = Program; export type PoolState = IdlAccounts["pool"]; export type UserState = IdlAccounts["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; +} diff --git a/ts-client/src/utils.ts b/ts-client/src/utils.ts index 7a032c2..304daa3 100644 --- a/ts-client/src/utils.ts +++ b/ts-client/src/utils.ts @@ -3,7 +3,9 @@ import { Cluster, Connection, LAMPORTS_PER_SOL, + ParsedAccountData, PublicKey, + SYSVAR_CLOCK_PUBKEY, TransactionInstruction, } from "@solana/web3.js"; import { @@ -20,7 +22,7 @@ import { FARMING_API_ENDPOINT, FARM_PROGRAM_ID, } from "./constant"; -import { PoolInfo } from "./types"; +import { ParsedClockState, PoolInfo } from "./types"; export const getFarmProgram = (connection: Connection) => { const provider = new AnchorProvider( @@ -126,3 +128,15 @@ export const airDropSol = async ( throw error; } }; + +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; +};