From 2b05d2b41d0256af79aa11051e8032a189c010da Mon Sep 17 00:00:00 2001 From: Daryna Shpankova Date: Tue, 24 Jun 2025 14:07:47 +0200 Subject: [PATCH] add lock staking --- .../src/components/PositionListItem/index.tsx | 4 +- .../PositionListItem/rewardLabel.tsx | 15 +- apps/haust-dex/src/constants/addresses.ts | 2 +- apps/haust-dex/src/hooks/Tokens.ts | 26 +- apps/haust-dex/src/hooks/useV3Incentive.ts | 4 +- .../src/hooks/useV3StakingRewardInfo.ts | 7 +- .../haust-dex/src/pages/Pool/PositionPage.tsx | 4 +- apps/haust-dex/src/pages/Pool/RewardInfo.tsx | 69 +- .../sdks/v3-periphery/UniswapV3Staker.json | 975 +----------------- 9 files changed, 105 insertions(+), 1001 deletions(-) diff --git a/apps/haust-dex/src/components/PositionListItem/index.tsx b/apps/haust-dex/src/components/PositionListItem/index.tsx index da26c41a566..e99076bfb38 100644 --- a/apps/haust-dex/src/components/PositionListItem/index.tsx +++ b/apps/haust-dex/src/components/PositionListItem/index.tsx @@ -307,8 +307,8 @@ export default function PositionListItem({ }: PositionListItemProps) { const { chainId } = useWeb3React() const theme = useTheme() - const token0 = useToken(token0Address) - const token1 = useToken(token1Address) + const token0 = useToken(token0Address, true) + const token1 = useToken(token1Address, true) const {incentiveEvents, loading: incentivesLoading} = useV3Incentive() const currency0 = token0 ? unwrappedToken(token0) : undefined diff --git a/apps/haust-dex/src/components/PositionListItem/rewardLabel.tsx b/apps/haust-dex/src/components/PositionListItem/rewardLabel.tsx index f1fd00862aa..295b23b567b 100644 --- a/apps/haust-dex/src/components/PositionListItem/rewardLabel.tsx +++ b/apps/haust-dex/src/components/PositionListItem/rewardLabel.tsx @@ -22,16 +22,17 @@ const StakingLabel = styled(ThemedText.UtilityBadge)` interface PositionListItemProps { incentive: { rewardToken: string; - pool: string; - startTime: number; - endTime: number; - reward: string; + pool: string; + startTime: number; + endTime: number; + reward: string; + isLocked?: boolean; } tokenId: BigNumber } const formatTime = (totalSeconds: number): string => { - if (totalSeconds <= 0) return 'Staking ended' + if (totalSeconds <= 0) return 'Staking Rewards Available' const days = Math.floor(totalSeconds / 86400) const hours = Math.floor((totalSeconds % 86400) / 3600) @@ -50,7 +51,7 @@ export default function RewardLabel({ incentive, tokenId, }: PositionListItemProps) { - const {rewardInfo, globalLock} = useV3StakingRewardInfo( + const {rewardInfo} = useV3StakingRewardInfo( incentive, tokenId.toString() ) @@ -76,7 +77,7 @@ export default function RewardLabel({ return null } - if (rewardInfo && globalLock) { + if (rewardInfo && incentive?.isLocked) { return ( {timeRemaining} diff --git a/apps/haust-dex/src/constants/addresses.ts b/apps/haust-dex/src/constants/addresses.ts index 799dd5a4c58..0c1452fad5d 100644 --- a/apps/haust-dex/src/constants/addresses.ts +++ b/apps/haust-dex/src/constants/addresses.ts @@ -22,7 +22,7 @@ const HAUST_TESTNET_V3_MIGRATOR_ADDRESSES = '0xA0E3f176012B18170581F9AE142723303 const HAUST_TESTNET_MULTICALL_ADDRESS = '0x7646AE4D47281Eb5b641E5ee8959Ac78781c8567' const HAUST_TESTNET_QUOTER_ADDRESSES = '0x27cd86dd99c73c7F93Ca4407EFd17d7108fBDFA2' const HAUST_TESTNET_NONFUNGIBLE_POSITION_MANAGER_ADDRESSES = '0x32E7EeC6be44Ff17e30237dF15eB8B3541138C94' -const HAUST_TESTNET_UNISWAP_V3_STAKER_ADDRESSES = '0x27572122ffF50bFB8fc1F649d59b01899c6a2fe0' +const HAUST_TESTNET_UNISWAP_V3_STAKER_ADDRESSES = '0xfc0430ce0777f4ef86dcb263efc8de60fbc76528' const HAUST_TESTNET_TICK_LENS_ADDRESSES = '0x423043f8C5F9e23BC1fd9f97977458f2Fd606B1f' const HAUST_TESTNET_POOL_DEPLOYER = '0xbBf50bcbd1439385637FAE13bF1Be84F04C7366b' const HAUST_TESTNET_UNIVERSAL_ROUTER_ADDRESS = '0xE71Af6EEd736bA513CbD19Ebc82D1Cf943b80b63' diff --git a/apps/haust-dex/src/hooks/Tokens.ts b/apps/haust-dex/src/hooks/Tokens.ts index 8cc1a94d53c..1ae3bb2db0b 100644 --- a/apps/haust-dex/src/hooks/Tokens.ts +++ b/apps/haust-dex/src/hooks/Tokens.ts @@ -48,23 +48,38 @@ export function useAllTokensMultichain(): TokenAddressMap { } // Returns all tokens from the default list + user added tokens -export function useDefaultActiveTokens(): { [address: string]: Token } { +export function useDefaultActiveTokens(forPools?: boolean): { + [address: string]: Token; +} { const defaultListTokens = useCombinedActiveList(); const tokensFromMap = useTokensFromMap(defaultListTokens); const userAddedTokens = useUserAddedTokens(); return useMemo(() => { + // First filter out MYR tokens from tokensFromMap + const filteredTokensFromMap = forPools + ? tokensFromMap + : Object.entries(tokensFromMap).reduce((acc, [address, token]) => { + if (token.symbol !== "MYR") { + acc[address] = token; + } + return acc; + }, {} as { [address: string]: Token }); + return ( userAddedTokens // reduce into all ALL_TOKENS filtered by the current chain .reduce<{ [address: string]: Token }>( (tokenMap, token) => { - tokenMap[token.address] = token; + // Only add tokens that are not MYR + if (token.symbol !== "MYR" || !forPools) { + tokenMap[token.address] = token; + } return tokenMap; }, // must make a copy because reduce modifies the map, and we do not // want to make a copy in every iteration - { ...tokensFromMap } + { ...filteredTokensFromMap } ) ); }, [tokensFromMap, userAddedTokens]); @@ -203,9 +218,10 @@ export function useIsUserAddedTokenOnChain( // null if loading or null was passed // otherwise returns the token export function useToken( - tokenAddress?: string | null + tokenAddress?: string | null, + forPools?: boolean ): Token | null | undefined { - const tokens = useDefaultActiveTokens(); + const tokens = useDefaultActiveTokens(forPools); return useTokenFromMapOrNetwork(tokens, tokenAddress); } diff --git a/apps/haust-dex/src/hooks/useV3Incentive.ts b/apps/haust-dex/src/hooks/useV3Incentive.ts index 6076151beda..b61ac0daf65 100644 --- a/apps/haust-dex/src/hooks/useV3Incentive.ts +++ b/apps/haust-dex/src/hooks/useV3Incentive.ts @@ -15,6 +15,7 @@ interface IncentiveCreatedEvent { endTime: number; reward: string; tokenIds: number[]; + isLocked?: boolean; } export function useV3Incentive(): UseV3IncentivesCreatedResults { @@ -31,7 +32,7 @@ export function useV3Incentive(): UseV3IncentivesCreatedResults { const incentiveEvents = useMemo(() => { if (!activeIncentives?.[0]?.[0]) return []; return activeIncentives[0].map((incentiveData: any[]) => { - const [incentive, tokenIdsData] = incentiveData; + const [incentive, tokenIdsData, isLocked] = incentiveData; return { rewardToken: incentive[0], pool: incentive[1], @@ -41,6 +42,7 @@ export function useV3Incentive(): UseV3IncentivesCreatedResults { tokenIds: tokenIdsData.map( (tokenId: { _hex: string; _isBigNumber: boolean }) => Number(tokenId) ), + isLocked, }; }); }, [activeIncentives]); diff --git a/apps/haust-dex/src/hooks/useV3StakingRewardInfo.ts b/apps/haust-dex/src/hooks/useV3StakingRewardInfo.ts index 352dce8779f..17ad02aa927 100644 --- a/apps/haust-dex/src/hooks/useV3StakingRewardInfo.ts +++ b/apps/haust-dex/src/hooks/useV3StakingRewardInfo.ts @@ -44,9 +44,6 @@ export function useV3StakingRewardInfo( } ); - const { loading: globalLockLoading, result: globalLock } = - useSingleCallResult(staker, "globalLock"); - const rewardInfo = useMemo(() => { if (!activeIncentives) return undefined; @@ -56,8 +53,8 @@ export function useV3StakingRewardInfo( }, [activeIncentives]); return { - loading: loading || globalLockLoading, + loading, rewardInfo, - globalLock: globalLock?.[0] ?? false, + globalLock: true, }; } diff --git a/apps/haust-dex/src/pages/Pool/PositionPage.tsx b/apps/haust-dex/src/pages/Pool/PositionPage.tsx index c15ddd27b73..140be8bd47d 100644 --- a/apps/haust-dex/src/pages/Pool/PositionPage.tsx +++ b/apps/haust-dex/src/pages/Pool/PositionPage.tsx @@ -868,7 +868,7 @@ function PositionPageContent() { > Stake Position - ) : ( + ) : !incentive?.isLocked ? ( Unstake position - )} + ) : null} )} diff --git a/apps/haust-dex/src/pages/Pool/RewardInfo.tsx b/apps/haust-dex/src/pages/Pool/RewardInfo.tsx index 2b977af4123..701b05a7e02 100644 --- a/apps/haust-dex/src/pages/Pool/RewardInfo.tsx +++ b/apps/haust-dex/src/pages/Pool/RewardInfo.tsx @@ -3,15 +3,26 @@ import { DarkCard, LightCard } from 'components/Card' import { AutoColumn } from 'components/Column' import CurrencyLogo from 'components/Logo/CurrencyLogo' import { RowBetween, RowFixed } from 'components/Row' -import { useV3StakingRewardInfo, V3StakingRewardInfo } from 'hooks/useV3StakingRewardInfo' +import { useUSDPrice } from 'hooks/useUSDPrice' +import { useV3StakingRewardInfo } from 'hooks/useV3StakingRewardInfo' import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' +import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import styled from 'styled-components/macro' import { ThemedText } from 'theme' import { SmallButtonPrimary } from '../../components/Button' -import { useUSDPrice } from 'hooks/useUSDPrice' -import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' +import { colors } from 'theme/colors' + +interface V3StakingRewardInfo { + rewardToken: string + pool: string + startTime: number + endTime: number + reward: string + isLocked?: boolean +} // responsive text // disable the warning because we don't use the end prop, we just want to filter it out @@ -32,7 +43,29 @@ export const ResponsiveRow = styled(RowBetween)` } ` -export function RewardInfo({tokenId, stakedInfo}: {tokenId: string, stakedInfo: V3StakingRewardInfo}) { +const formatTime = (totalSeconds: number): string => { + if (totalSeconds <= 0) return 'Claim rewards' + + const days = Math.floor(totalSeconds / 86400) + const hours = Math.floor((totalSeconds % 86400) / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = Math.floor(totalSeconds % 60) + + const pad = (num: number): string => num.toString().padStart(2, '0') + + if (days > 0) { + return `Rewards available in ${days}d ${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s` + } + return `Rewards available in ${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s` +} + +export function RewardInfo({ + tokenId, + stakedInfo, +}: { + tokenId: string, + stakedInfo: V3StakingRewardInfo, +}) { const { rewardInfo: rewardAmount } = useV3StakingRewardInfo( stakedInfo!, tokenId! @@ -43,8 +76,26 @@ export function RewardInfo({tokenId, stakedInfo}: {tokenId: string, stakedInfo: rewardAmount } + const [timeRemaining, setTimeRemaining] = useState('') + + useEffect(() => { + const updateTimeRemaining = () => { + const now = Math.floor(Date.now() / 1000) + const endTime = stakedInfo.endTime + const secondsRemaining = endTime - now + + setTimeRemaining(formatTime(secondsRemaining)) + } + + updateTimeRemaining() + const interval = setInterval(updateTimeRemaining, 1000) + + return () => clearInterval(interval) + }, [stakedInfo.endTime]) + const parsedRewardAmount = tryParseCurrencyAmount(rewardAmount?.reward.toString(), rewardToken) const fiatValue = useUSDPrice(parsedRewardAmount) + return ( @@ -89,6 +140,16 @@ export function RewardInfo({tokenId, stakedInfo}: {tokenId: string, stakedInfo: > Claim rewards + ) : stakedInfo?.isLocked && stakedInfo?.endTime > Math.floor(Date.now() / 1000) ? ( + + {timeRemaining} + ) : (