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 8c9c52ff..1e79b458 100644 Binary files a/subgraph/build/LTokenSignaler/LTokenSignaler.wasm and b/subgraph/build/LTokenSignaler/LTokenSignaler.wasm differ diff --git a/subgraph/build/schema.graphql b/subgraph/build/schema.graphql index 71284343..50df301e 100644 --- a/subgraph/build/schema.graphql +++ b/subgraph/build/schema.graphql @@ -75,5 +75,5 @@ type StakingAPRInfo @entity { id: ID! # Static ID, "StakingAPRInfo" rewardPerSec: BigInt! totalStaked: BigInt! - APR: BigDecimal! + interestRate: BigDecimal! # rewardPerSec/totalStaked } diff --git a/subgraph/generated/schema.ts b/subgraph/generated/schema.ts index a1206136..cbd3868b 100644 --- a/subgraph/generated/schema.ts +++ b/subgraph/generated/schema.ts @@ -773,8 +773,8 @@ export class StakingAPRInfo extends Entity { this.set("totalStaked", Value.fromBigInt(value)); } - get APR(): BigDecimal { - let value = this.get("APR"); + get interestRate(): BigDecimal { + let value = this.get("interestRate"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { @@ -782,8 +782,8 @@ export class StakingAPRInfo extends Entity { } } - set APR(value: BigDecimal) { - this.set("APR", Value.fromBigDecimal(value)); + set interestRate(value: BigDecimal) { + this.set("interestRate", Value.fromBigDecimal(value)); } } diff --git a/subgraph/helper.ts b/subgraph/helper.ts index 0f5033fe..c9fb612e 100644 --- a/subgraph/helper.ts +++ b/subgraph/helper.ts @@ -2,7 +2,10 @@ import { BigDecimal, BigInt } from "@graphprotocol/graph-ts"; import { StakingAPRInfo } from "./generated/schema"; export const STAKING_APR_INFO_ID = "STAKING_APR_INFO_ID"; -export const SECONDS_PER_YEAR = BigInt.fromI32(365 * 24 * 60 * 60); + +export function bigDecimalExp18(): BigDecimal { + return BigDecimal.fromString("1000000000000000000"); +} export function getOrCreateStakingAPRInfo(): StakingAPRInfo { let stakingAprInfo = StakingAPRInfo.load(STAKING_APR_INFO_ID); @@ -10,7 +13,7 @@ export function getOrCreateStakingAPRInfo(): StakingAPRInfo { stakingAprInfo = new StakingAPRInfo(STAKING_APR_INFO_ID); stakingAprInfo.rewardPerSec = BigInt.fromI32(0); stakingAprInfo.totalStaked = BigInt.fromI32(0); - stakingAprInfo.APR = BigDecimal.fromString("0"); + stakingAprInfo.interestRate = BigDecimal.fromString("0"); } return stakingAprInfo; } @@ -18,11 +21,11 @@ export function getOrCreateStakingAPRInfo(): StakingAPRInfo { export function updateStakingAPRInfo(): void { const stakingAPRInfo = getOrCreateStakingAPRInfo(); if (stakingAPRInfo.totalStaked == BigInt.fromI32(0)) { - stakingAPRInfo.APR = BigDecimal.fromString("0"); + stakingAPRInfo.interestRate = BigDecimal.fromString("0"); } else { - stakingAPRInfo.APR = new BigDecimal(stakingAPRInfo.rewardPerSec) - .times(new BigDecimal(SECONDS_PER_YEAR)) - .div(new BigDecimal(stakingAPRInfo.totalStaked)); + stakingAPRInfo.interestRate = new BigDecimal(stakingAPRInfo.rewardPerSec) + .times(bigDecimalExp18()) + .div(stakingAPRInfo.totalStaked.toBigDecimal()); } stakingAPRInfo.save(); } diff --git a/subgraph/mapping.ts b/subgraph/mapping.ts index d5d9261c..bf14ee15 100644 --- a/subgraph/mapping.ts +++ b/subgraph/mapping.ts @@ -25,7 +25,7 @@ import { import { Address, BigDecimal, BigInt } from "@graphprotocol/graph-ts"; import { LTokenSignalEvent } from "./generated/LTokenSignaler/LTokenSignaler"; import { store } from "@graphprotocol/graph-ts"; -import { SECONDS_PER_YEAR, getOrCreateStakingAPRInfo, updateStakingAPRInfo } from "./helper"; +import { getOrCreateStakingAPRInfo, updateStakingAPRInfo } from "./helper"; export function handleSignaledLToken(event: LTokenSignalEvent): void { // Start indexing the signaled LToken diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index 71284343..50df301e 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -75,5 +75,5 @@ type StakingAPRInfo @entity { id: ID! # Static ID, "StakingAPRInfo" rewardPerSec: BigInt! totalStaked: BigInt! - APR: BigDecimal! + interestRate: BigDecimal! # rewardPerSec/totalStaked }