diff --git a/src/components/app/staking/AppStaking.tsx b/src/components/app/staking/AppStaking.tsx index b3504d72..383e3864 100644 --- a/src/components/app/staking/AppStaking.tsx +++ b/src/components/app/staking/AppStaking.tsx @@ -1,10 +1,51 @@ import { Card } from "@/components/ui"; -import { FC, useRef } from "react"; +import { FC, useEffect, useRef } from "react"; import { AppStakingPane } from "./AppStakingPane"; import { AppStakingDescription } from "./AppStakingDescription"; import { AppStakingPool } from "./AppStakingPool"; +import { useContractAddress } from "@/hooks/useContractAddress"; +import { useAccount, usePublicClient } from "wagmi"; +import { zeroAddress } from "viem"; +import { useReadLdyBalanceOf, useReadLdyDecimals } 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(); + const account = useAccount(); + const publicClient = usePublicClient(); + const ldySymbol = "LDY"; + const ldyTokenAddress = useContractAddress(ldySymbol); + + const { data: ldyBalance, queryKey: ldyBalanceQuery } = useReadLdyBalanceOf({ + args: [account.address || zeroAddress], + }); + + const { data: ldyDecimals } = useReadLdyDecimals(); + + const { + data: stakingAprInfo, + refetch: refetchStakingAPR, + isFetching: isFetchingAPR, + } = useGetStakingAprById(STAKING_APR_INFO_ID); + + // Refetch LdyBalance & APR from contract on network/wallet change + const queryKeys = [ldyBalanceQuery, [STAKING_APR_INFO_QUERY]]; + useEffect(() => { + queryKeys.forEach((k) => queryClient.invalidateQueries({ queryKey: k })); + }, [account.address, publicClient]); + + // Refetch stakingAPR 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); + }, [ldyBalance]); + return (
{ defaultGradient={true} className="w-full flex flex-col col-span-12 xl:col-span-6 gap-2 p-2" > - + { defaultGradient={false} className="w-full flex flex-col gap-8 col-span-12 before:bg-primary p-2" > - +
); diff --git a/src/components/app/staking/AppStakingPane.tsx b/src/components/app/staking/AppStakingPane.tsx index 376659fe..f56afe2d 100644 --- a/src/components/app/staking/AppStakingPane.tsx +++ b/src/components/app/staking/AppStakingPane.tsx @@ -1,52 +1,48 @@ import { ChangeEvent, FC, useEffect, useMemo, useRef, useState } from "react"; import { AllowanceTxButton, Amount, AmountInputWithLogo, Button, Spinner } from "@/components/ui"; -import { erc20Abi, formatUnits, parseUnits, zeroAddress } from "viem"; -import { UseSimulateContractReturnType, useAccount, useReadContract } from "wagmi"; +import { Address, formatUnits, parseUnits } from "viem"; +import { UseSimulateContractReturnType } from "wagmi"; import { useContractAddress } from "@/hooks/useContractAddress"; -import { useReadLTokenDecimals, useReadLdyDecimals, useSimulateLdyStakingStake } from "@/generated"; +import { useSimulateLdyStakingStake } from "@/generated"; import * as Slider from "@radix-ui/react-slider"; -import { useGetStakingAprById, useGetUserStakingsByAddress } from "@/services/graph"; -import { STAKING_APR_INFO_ID, StakeDurations } from "@/constants/staking"; +import { StakeDurations } from "@/constants/staking"; import { useAPYCalculation } from "@/hooks/useAPYCalculation"; +import { IStakingAPRInfo } from "@/services/graph/hooks/useStakingEvent"; -export const AppStakingPane: FC = () => { - const account = useAccount(); - const ldySymbol = "LDY"; - const ldyTokenAddress = useContractAddress(ldySymbol); +export const AppStakingPane: FC<{ + ldyTokenSymbol: string; + ldyTokenAddress?: Address; + ldyTokenBalance?: bigint; + ldyTokenDecimals?: number; + stakingAprInfo?: IStakingAPRInfo; +}> = ({ + ldyTokenSymbol = "LDY", + ldyTokenAddress, + ldyTokenBalance, + ldyTokenDecimals, + stakingAprInfo, +}) => { const ldyStakingAddress = useContractAddress("LDYStaking"); - const { data: ldyDecimals } = useReadLdyDecimals(); - const { data: ldyBalance, queryKey } = useReadContract({ - abi: erc20Abi, - functionName: "balanceOf", - address: ldyTokenAddress, - args: [account.address || zeroAddress], - }); const inputEl = useRef(null); const [depositedAmount, setDepositedAmount] = useState(0n); const [stakeOptionIndex, setStakeOptionIndex] = useState(0); const [hasUserInteracted, setHasUserInteracted] = useState(false); - const { - data: stakingAprInfo, - refetch: refetchStakingAPR, - isFetching: isFetchingAPR, - } = useGetStakingAprById(STAKING_APR_INFO_ID); - - // Reset everything whenever user ldy balance is changed. + // Reset everything on ldyBalance change. useEffect(() => { - refetchStakingAPR(); + // Reset input field setDepositedAmount(0n); setHasUserInteracted(false); if (inputEl && inputEl.current) { inputEl.current.value = "0"; } - }, [ldyBalance]); + }, [ldyTokenBalance]); // Calculate APY based on stakeIndex and stakingAprInfo. const APY = useMemo(() => { - if (stakingAprInfo && stakingAprInfo.stakingAPRInfo) { - return useAPYCalculation(stakingAprInfo.stakingAPRInfo.APR, true, stakeOptionIndex) + "%"; + if (stakingAprInfo) { + return useAPYCalculation(stakingAprInfo.APR, true, stakeOptionIndex) + "%"; } else { return "-%"; } @@ -60,11 +56,11 @@ export const AppStakingPane: FC = () => {
STAKE LDY TO GET REWARDS AND BENEFITS
) => { - setDepositedAmount(parseUnits(e.target.value, ldyDecimals!)); + setDepositedAmount(parseUnits(e.target.value, ldyTokenDecimals!)); if (hasUserInteracted === false) setHasUserInteracted(true); if (e.target.value === "") setHasUserInteracted(false); }} @@ -75,9 +71,12 @@ export const AppStakingPane: FC = () => { variant="outline" className="hover:bg-primary-fg" onClick={() => { - setDepositedAmount((ldyBalance! * 25n) / 100n); + setDepositedAmount((ldyTokenBalance! * 25n) / 100n); if (inputEl.current) - inputEl.current.value = formatUnits((ldyBalance! * 25n) / 100n, ldyDecimals!); + inputEl.current.value = formatUnits( + (ldyTokenBalance! * 25n) / 100n, + ldyTokenDecimals!, + ); }} > 25% @@ -87,9 +86,12 @@ export const AppStakingPane: FC = () => { variant="outline" className="hover:bg-primary-fg" onClick={() => { - setDepositedAmount((ldyBalance! * 50n) / 100n); + setDepositedAmount((ldyTokenBalance! * 50n) / 100n); if (inputEl.current) - inputEl.current.value = formatUnits((ldyBalance! * 50n) / 100n, ldyDecimals!); + inputEl.current.value = formatUnits( + (ldyTokenBalance! * 50n) / 100n, + ldyTokenDecimals!, + ); }} > 50% @@ -99,9 +101,12 @@ export const AppStakingPane: FC = () => { variant="outline" className="hover:bg-primary-fg" onClick={() => { - setDepositedAmount((ldyBalance! * 75n) / 100n); + setDepositedAmount((ldyTokenBalance! * 75n) / 100n); if (inputEl.current) - inputEl.current.value = formatUnits((ldyBalance! * 75n) / 100n, ldyDecimals!); + inputEl.current.value = formatUnits( + (ldyTokenBalance! * 75n) / 100n, + ldyTokenDecimals!, + ); }} > 75% @@ -111,8 +116,9 @@ export const AppStakingPane: FC = () => { variant="outline" className="hover:bg-primary-fg" onClick={() => { - setDepositedAmount(ldyBalance!); - if (inputEl.current) inputEl.current.value = formatUnits(ldyBalance!, ldyDecimals!); + setDepositedAmount(ldyTokenBalance!); + if (inputEl.current) + inputEl.current.value = formatUnits(ldyTokenBalance!, ldyTokenDecimals!); }} > MAX @@ -168,7 +174,8 @@ export const AppStakingPane: FC = () => {
-
{(isFetchingAPR && ) || APY}
+ {/*
{(isFetchingAPR && ) || APY}
*/} +
{APY}
APY
@@ -185,8 +192,8 @@ export const AppStakingPane: FC = () => { Deposit{" "} {" "} diff --git a/src/components/app/staking/AppStakingPool.tsx b/src/components/app/staking/AppStakingPool.tsx index 56c88ca9..7dc9eb55 100644 --- a/src/components/app/staking/AppStakingPool.tsx +++ b/src/components/app/staking/AppStakingPool.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from "react"; +import { FC, useEffect } from "react"; import { Carousel, @@ -7,35 +7,38 @@ import { CarouselNext, CarouselPrevious, } from "@/components/ui/Carousel"; -import { Button, Card } from "@/components/ui"; -import { useGetStakingAprById, useGetUserStakingsByAddress } from "@/services/graph"; -import { useAccount, useReadContract } from "wagmi"; -import { useContractAddress } from "@/hooks/useContractAddress"; +import { TxButton } from "@/components/ui"; +import { useGetUserStakingsByAddress } from "@/services/graph"; +import { useAccount, usePublicClient } from "wagmi"; import { formatUnits, zeroAddress } from "viem"; import { - useReadLdyDecimals, - useReadLdyStakingEarned, useReadLdyStakingGetEarnedUser, useReadLdyStakingGetUserStakes, - useReadLdyStakingRewardsDuration, + useSimulateLdyStakingGetReward, + useSimulateLdyStakingUnstake, } from "@/generated"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import relativeTime from "dayjs/plugin/relativeTime"; import utc from "dayjs/plugin/utc"; -import { OneMonth, STAKING_APR_INFO_ID } from "@/constants/staking"; +import { OneMonth } from "@/constants/staking"; import { useAPYCalculation } from "@/hooks/useAPYCalculation"; -import { useQueries } from "@tanstack/react-query"; +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"; dayjs.extend(localizedFormat); dayjs.extend(relativeTime); dayjs.extend(utc); -export const AppStakingPool: FC = () => { +export const AppStakingPool: FC<{ + ldyTokenDecimals?: number; + ldyTokenBalanceQuery?: QueryKey; + stakingAprInfo?: IStakingAPRInfo; +}> = ({ ldyTokenDecimals, ldyTokenBalanceQuery, stakingAprInfo }) => { + const queryClient = useQueryClient(); const account = useAccount(); - const ldyStakingAddress = useContractAddress("LDYStaking"); - const { data: ldyDecimals } = useReadLdyDecimals(); - - const [claimAmounts, setClaimAmounts] = useState([0n]); + const publicClient = usePublicClient(); // Fetch user staking info including earnedAmount from subgraph const { @@ -49,22 +52,35 @@ export const AppStakingPool: FC = () => { args: [account.address || zeroAddress], }); - // Fetch staking APR info from subgraph - const { - data: stakingAprInfo, - refetch: refetchStakingAPR, - isFetching: isFetchingAPR, - } = useGetStakingAprById(STAKING_APR_INFO_ID); - // Fetch claimable rewards array from ldyStaking Contract - const { data: earnedArray, queryKey: earnedArrayQuery } = useReadLdyStakingGetEarnedUser({ + const { data: rewardsArray, queryKey: rewardsArrayQuery } = useReadLdyStakingGetEarnedUser({ args: [account.address || zeroAddress], }); + // Refetch staking info, earned array from subgraph & contracts on wallet, network change + const queryKeys = [rewardsArrayQuery, getUserStakesQuery, [USER_STAKING_QUERY]]; + useEffect(() => { + queryKeys.forEach((k) => queryClient.invalidateQueries({ queryKey: k })); + }, [account.address, publicClient]); + + // Refetch staking info(earned info) on rewardsArray change + useEffect(() => { + // Refetch after 3 seconds due to subgraph latency + const timeoutId = setTimeout(() => { + queryClient.invalidateQueries({ queryKey: [USER_STAKING_QUERY] }); + }, 3000); + return () => clearTimeout(timeoutId); + }, [rewardsArray]); + return (
MY $LDY POOLS
- + {stakingPools && stakingPools.map((poolInfo, index) => ( @@ -75,7 +91,7 @@ export const AppStakingPool: FC = () => {
Staked Amount - {formatUnits(poolInfo.stakedAmount, ldyDecimals!)} + {formatUnits(poolInfo.stakedAmount, ldyTokenDecimals!)}
@@ -93,10 +109,12 @@ export const AppStakingPool: FC = () => {
Earned - {userStakingInfo + {userStakingInfo && + userStakingInfo.stakingUsers && + userStakingInfo.stakingUsers[index] ? formatUnits( BigInt(userStakingInfo.stakingUsers[index].earnedAmount), - ldyDecimals!, + ldyTokenDecimals!, ) : 0}{" "} Token @@ -106,11 +124,7 @@ export const AppStakingPool: FC = () => { APY {stakingAprInfo - ? useAPYCalculation( - stakingAprInfo.stakingAPRInfo.APR, - false, - Number(poolInfo.duration), - ) + ? useAPYCalculation(stakingAprInfo.APR, false, Number(poolInfo.duration)) : "-"} % @@ -122,16 +136,46 @@ export const AppStakingPool: FC = () => {
- +
- +
diff --git a/src/components/ui/AllowanceTxButton.tsx b/src/components/ui/AllowanceTxButton.tsx index 6336d2a4..74366eb6 100644 --- a/src/components/ui/AllowanceTxButton.tsx +++ b/src/components/ui/AllowanceTxButton.tsx @@ -75,6 +75,8 @@ export const AllowanceTxButton: FC = ({ address: token, args: [spender, amount], }); + + // Set hasEnoughAllowance when allowance or amount chanages useEffect(() => { preparation.refetch(); setHasEnoughAllowance(allowance !== undefined && allowance >= amount); diff --git a/src/components/ui/TxButton.tsx b/src/components/ui/TxButton.tsx index 9ef96c83..8df7d835 100644 --- a/src/components/ui/TxButton.tsx +++ b/src/components/ui/TxButton.tsx @@ -1,6 +1,6 @@ "use client"; import { FC, type ReactNode, useEffect, useState } from "react"; -import { Button } from "./Button"; +import { Button, ButtonSize, ButtonVariant } from "./Button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./Dialog"; import { Spinner } from "./Spinner"; import { Tooltip, TooltipTrigger, TooltipContent } from "./Tooltip"; @@ -51,7 +51,6 @@ export const TxButton: FC = ({ const account = useAccount(); const publicClient = usePublicClient(); const queryClient = useQueryClient(); - // Fix Safe math issue when no value is provided // if (preparation.data && preparation.data.request && !preparation.data.request.value) { // preparation.data.request.value = 0n; @@ -106,7 +105,7 @@ export const TxButton: FC = ({ } return ( <> -
+