From dd7252bbc94f1869ad435e19f39509c521925b85 Mon Sep 17 00:00:00 2001 From: painiteleo Date: Mon, 13 May 2024 17:42:57 +0800 Subject: [PATCH] fix: apy calculation --- package.json | 2 +- src/components/app/staking/AppStaking.tsx | 33 ++++++++---------- src/components/app/staking/AppStakingPane.tsx | 10 +++--- .../app/staking/AppStakingPoolPane.tsx | 8 ++--- .../app/staking/AppStakingPools.tsx | 17 ++++----- src/lib/getAPYCalculation.ts | 20 +++++++---- .../graph/hooks/useStakingEvent/index.ts | 2 +- src/services/graph/queries/staking.ts | 2 +- .../build/LTokenSignaler/LTokenSignaler.wasm | Bin 48298 -> 48334 bytes subgraph/build/schema.graphql | 2 +- subgraph/generated/schema.ts | 8 ++--- subgraph/helper.ts | 15 ++++---- subgraph/mapping.ts | 2 +- subgraph/schema.graphql | 2 +- 14 files changed, 64 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 4df55bd2..120bda33 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "node": ">=16.0.0" }, "scripts": { - "dev": "NODE_ENV=production next dev", + "dev": "next dev", "build": "prisma generate && next build", "start": "next start", "lint": "next lint", diff --git a/src/components/app/staking/AppStaking.tsx b/src/components/app/staking/AppStaking.tsx index bfbd8f17..293bb426 100644 --- a/src/components/app/staking/AppStaking.tsx +++ b/src/components/app/staking/AppStaking.tsx @@ -6,11 +6,12 @@ import { AppStakingPools } from "./AppStakingPools"; import { useContractAddress } from "@/hooks/useContractAddress"; import { useAccount, usePublicClient } from "wagmi"; import { zeroAddress } from "viem"; -import { useReadLdyBalanceOf, useReadLdyDecimals } from "@/generated"; +import { + useReadLdyBalanceOf, + useReadLdyDecimals, + useReadLdyStakingRewardPerTokenStored, +} from "@/generated"; import { useQueryClient } from "@tanstack/react-query"; -import { useGetStakingAprById } from "@/services/graph"; -import { STAKING_APR_INFO_ID } from "@/constants/staking"; -import { STAKING_APR_INFO_QUERY } from "@/services/graph/queries"; export const AppStaking: FC = () => { const queryClient = useQueryClient(); @@ -25,25 +26,18 @@ export const AppStaking: FC = () => { const { data: ldyDecimals } = useReadLdyDecimals(); - const { - data: stakingAprInfo, - refetch: refetchStakingAPR, - isFetching: isFetchingAPR, - } = useGetStakingAprById(STAKING_APR_INFO_ID); + const { data: rewardPerToken, queryKey: rewardPerTokenQuery } = + useReadLdyStakingRewardPerTokenStored(); - // Refetch LdyBalance & APR from contract on network/wallet change - const queryKeys = [ldyBalanceQuery, [STAKING_APR_INFO_QUERY]]; + // Refetch LdyBalance & RewardPerToken from contract on network/wallet change + const queryKeys = [ldyBalanceQuery, rewardPerTokenQuery]; useEffect(() => { queryKeys.forEach((k) => queryClient.invalidateQueries({ queryKey: k })); }, [account.address, publicClient]); - // Refetch stakingAPR on ldyBalance change. + // Refetch rewardPerToken on ldyBalance change. useEffect(() => { - // Refetch after 3 seconds due to subgraph latency - const timeoutId = setTimeout(() => { - queryClient.invalidateQueries({ queryKey: [STAKING_APR_INFO_QUERY] }); - }, 3000); - return () => clearTimeout(timeoutId); + queryClient.invalidateQueries({ queryKey: rewardPerTokenQuery }); }, [ldyBalance]); return ( @@ -58,7 +52,7 @@ export const AppStaking: FC = () => { ldyTokenAddress={ldyTokenAddress || zeroAddress} ldyTokenBalance={ldyBalance || 0n} ldyTokenDecimals={ldyDecimals || 18} - stakingAprInfo={stakingAprInfo ? stakingAprInfo.stakingAPRInfo || undefined : undefined} + rewardPerToken={rewardPerToken || 0n} /> { > diff --git a/src/components/app/staking/AppStakingPane.tsx b/src/components/app/staking/AppStakingPane.tsx index c9872e4d..a7c17505 100644 --- a/src/components/app/staking/AppStakingPane.tsx +++ b/src/components/app/staking/AppStakingPane.tsx @@ -7,20 +7,19 @@ import { useSimulateLdyStakingStake } from "@/generated"; import * as Slider from "@radix-ui/react-slider"; import { StakeDurations } from "@/constants/staking"; import { getAPYCalculation } from "@/lib/getAPYCalculation"; -import { IStakingAPRInfo } from "@/services/graph/hooks/useStakingEvent"; export const AppStakingPane: FC<{ ldyTokenSymbol: string; ldyTokenAddress: Address; ldyTokenBalance: bigint; ldyTokenDecimals: number; - stakingAprInfo: IStakingAPRInfo | undefined; + rewardPerToken: bigint; }> = ({ ldyTokenSymbol = "LDY", ldyTokenAddress, ldyTokenBalance, ldyTokenDecimals, - stakingAprInfo, + rewardPerToken, }) => { const ldyStakingAddress = useContractAddress("LDYStaking"); @@ -42,9 +41,10 @@ export const AppStakingPane: FC<{ // Calculate APY based on stakeIndex and stakingAprInfo. const APY = useMemo(() => { return ( - getAPYCalculation(stakingAprInfo ? stakingAprInfo.APR : "0", true, stakeOptionIndex) + "%" + getAPYCalculation(formatUnits(rewardPerToken, ldyTokenDecimals!), true, stakeOptionIndex) + + "%" ); - }, [stakeOptionIndex, stakingAprInfo]); + }, [stakeOptionIndex, rewardPerToken]); const preparation = useSimulateLdyStakingStake({ args: [depositedAmount, stakeOptionIndex], diff --git a/src/components/app/staking/AppStakingPoolPane.tsx b/src/components/app/staking/AppStakingPoolPane.tsx index 0417343e..2e00f9d4 100644 --- a/src/components/app/staking/AppStakingPoolPane.tsx +++ b/src/components/app/staking/AppStakingPoolPane.tsx @@ -11,7 +11,7 @@ import utc from "dayjs/plugin/utc"; import { OneMonth } from "@/constants/staking"; import { getAPYCalculation } from "@/lib/getAPYCalculation"; import { QueryKey } from "@tanstack/react-query"; -import { IStakingAPRInfo, IUserStakingInfo } from "@/services/graph/hooks/useStakingEvent"; +import { IUserStakingInfo } from "@/services/graph/hooks/useStakingEvent"; dayjs.extend(localizedFormat); dayjs.extend(relativeTime); dayjs.extend(utc); @@ -30,7 +30,7 @@ export const AppStakingPoolPane: FC<{ ldyTokenDecimals: number; userStakingInfo: IUserStakingInfo | undefined; rewardsArray: readonly bigint[] | undefined; - stakingAprInfo: IStakingAPRInfo | undefined; + rewardPerToken: bigint; getUserStakesQuery?: QueryKey; ldyTokenBalanceQuery?: QueryKey; rewardsArrayQuery?: QueryKey; @@ -40,7 +40,7 @@ export const AppStakingPoolPane: FC<{ ldyTokenDecimals, userStakingInfo, rewardsArray, - stakingAprInfo, + rewardPerToken, getUserStakesQuery, ldyTokenBalanceQuery, rewardsArrayQuery, @@ -80,7 +80,7 @@ export const AppStakingPoolPane: FC<{ APY {getAPYCalculation( - stakingAprInfo ? stakingAprInfo.APR : "0", + formatUnits(rewardPerToken, ldyTokenDecimals!), false, Number(poolInfo.duration), )} diff --git a/src/components/app/staking/AppStakingPools.tsx b/src/components/app/staking/AppStakingPools.tsx index 6926e197..0ef39826 100644 --- a/src/components/app/staking/AppStakingPools.tsx +++ b/src/components/app/staking/AppStakingPools.tsx @@ -8,19 +8,19 @@ import { } from "@/components/ui/Carousel"; import { useGetUserStakingsByAddress } from "@/services/graph"; import { useAccount, usePublicClient } from "wagmi"; -import { zeroAddress } from "viem"; +import { formatUnits, zeroAddress } from "viem"; import { useReadLdyStakingGetEarnedUser, useReadLdyStakingGetUserStakes } from "@/generated"; import { QueryKey, useQueryClient } from "@tanstack/react-query"; import { twMerge } from "tailwind-merge"; import { USER_STAKING_QUERY } from "@/services/graph/queries"; -import { IStakingAPRInfo } from "@/services/graph/hooks/useStakingEvent"; import { AppStakingPoolPane } from "./AppStakingPoolPane"; export const AppStakingPools: FC<{ ldyTokenDecimals: number; + ldyTokenBalance: bigint; ldyTokenBalanceQuery: QueryKey; - stakingAprInfo: IStakingAPRInfo | undefined; -}> = ({ ldyTokenDecimals, ldyTokenBalanceQuery, stakingAprInfo }) => { + rewardPerToken: bigint; +}> = ({ ldyTokenDecimals, ldyTokenBalance, ldyTokenBalanceQuery, rewardPerToken }) => { const queryClient = useQueryClient(); const account = useAccount(); const publicClient = usePublicClient(); @@ -42,11 +42,11 @@ export const AppStakingPools: FC<{ args: [account.address || zeroAddress], }); - // Refetch staking info, earned array from subgraph & contracts on wallet, network change - const queryKeys = [rewardsArrayQuery, getUserStakesQuery, [USER_STAKING_QUERY]]; + // Refetch staking info, earned array from contracts on wallet, network change + const queryKeys = [rewardsArrayQuery, getUserStakesQuery]; useEffect(() => { queryKeys.forEach((k) => queryClient.invalidateQueries({ queryKey: k })); - }, [account.address, publicClient]); + }, [account.address, publicClient, ldyTokenBalance]); // Refetch staking info(earned info) on rewardsArray change useEffect(() => { @@ -82,7 +82,8 @@ export const AppStakingPools: FC<{ : undefined } rewardsArray={rewardsArray ? rewardsArray : undefined} - stakingAprInfo={stakingAprInfo} + // stakingAprInfo={stakingAprInfo} + rewardPerToken={rewardPerToken} getUserStakesQuery={getUserStakesQuery} ldyTokenBalanceQuery={ldyTokenBalanceQuery} rewardsArrayQuery={rewardsArrayQuery} diff --git a/src/lib/getAPYCalculation.ts b/src/lib/getAPYCalculation.ts index 5807ea2c..bcb0fbff 100644 --- a/src/lib/getAPYCalculation.ts +++ b/src/lib/getAPYCalculation.ts @@ -1,17 +1,23 @@ import { StakeDurations, OneMonth } from "@/constants/staking"; export const getAPYCalculation = ( - apr: string, + interestRate: string, useStakeIndex: boolean = true, stakeDuration: number, ) => { - // -------- APY Formula ----------- // - // APY(%) = (((1 + r/n )^n) – 1)*100 - // r: APR(annual interest rate) - // n: Number of compound periods + // -------- APR and APY Formula ----------- // + // R: Interest rate(reward per token) + // APR(%) = R * stakeDuration(ie. 1 month in sec) / 365 days(in sec) * 100 + // N: Number of compounds + // APY(%) = (((1 + R/N )^N) – 1)*100 - const N = useStakeIndex ? StakeDurations[stakeDuration] * OneMonth : stakeDuration; - const R = Number(apr); + const OneYear = 12 * OneMonth; + const Duration = useStakeIndex ? StakeDurations[stakeDuration] * OneMonth : stakeDuration; + + const R = Number(interestRate); + const APR = (((R * Duration) / OneYear) * 100).toFixed(2); + console.log("APR: ", APR); + const N = Duration / OneYear; const APY = (Math.pow(1 + R / N, N) - 1) * 100; return APY.toFixed(2); }; diff --git a/src/services/graph/hooks/useStakingEvent/index.ts b/src/services/graph/hooks/useStakingEvent/index.ts index 538242c5..4173b602 100644 --- a/src/services/graph/hooks/useStakingEvent/index.ts +++ b/src/services/graph/hooks/useStakingEvent/index.ts @@ -13,7 +13,7 @@ export interface IUserStakingInfo { export interface IStakingAPRInfo { rewardPerSec: string; totalStaked: string; - APR: string; + interestRate: string; } export const useGetUserStakingsByAddress = ( diff --git a/src/services/graph/queries/staking.ts b/src/services/graph/queries/staking.ts index 03d64d68..ccfc9bf9 100644 --- a/src/services/graph/queries/staking.ts +++ b/src/services/graph/queries/staking.ts @@ -17,7 +17,7 @@ export const STAKING_APR_INFO_QUERY = gql` stakingAPRInfo(id: $stakeAprId) { rewardPerSec totalStaked - APR + interestRate id } } diff --git a/subgraph/build/LTokenSignaler/LTokenSignaler.wasm b/subgraph/build/LTokenSignaler/LTokenSignaler.wasm index 8c9c52ff2dc6e0ebebddc3cc5f7fdd70816e3789..1e79b45800e97cd5c3f932d72b5880ff057a3935 100644 GIT binary patch delta 9664 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 delta 9750 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