diff --git a/apps/web/src/components/Merkl/MerklSection.tsx b/apps/web/src/components/Merkl/MerklSection.tsx index 57a7586413cc1..5eabd9f5aa336 100644 --- a/apps/web/src/components/Merkl/MerklSection.tsx +++ b/apps/web/src/components/Merkl/MerklSection.tsx @@ -15,6 +15,9 @@ import { CurrencyLogo } from '@pancakeswap/widgets-internal' import { LightGreyCard } from 'components/Card' import { Currency, CurrencyAmount } from '@pancakeswap/swap-sdk-core' +import { useMemo } from 'react' +import { getMerklLink } from 'utils/getMerklLink' +import { ChainId } from '@pancakeswap/chains' import useMerkl from '../../hooks/useMerkl' function TextWarning({ tokenAmount }: { tokenAmount: CurrencyAmount }) { @@ -51,19 +54,23 @@ const LearnMoreLink = () => { export function MerklSection({ poolAddress, + chainId, notEnoughLiquidity, outRange, disabled, }: { + poolAddress?: `0x${string}` + chainId?: ChainId + notEnoughLiquidity: boolean outRange: boolean disabled: boolean - poolAddress: string | null - notEnoughLiquidity: boolean }) { const { t } = useTranslation() const { claimTokenReward, isClaiming, rewardsPerToken, hasMerkl } = useMerkl(poolAddress) + const merklLink = useMemo(() => getMerklLink({ chainId, lpAddress: poolAddress }), [chainId, poolAddress]) + if (!rewardsPerToken.length || (!hasMerkl && rewardsPerToken.every((r) => r.equalTo('0')))) return null return ( @@ -118,7 +125,7 @@ export function MerklSection({ external color="currentColor" style={{ display: 'inline-flex' }} - href="https://merkl.angle.money/?search=PancakeSwap&status=live%2Csoon" + href={merklLink ?? 'https://merkl.angle.money/?search=PancakeSwap&status=live%2Csoon'} > {t('here')} diff --git a/apps/web/src/components/Merkl/MerklTag.tsx b/apps/web/src/components/Merkl/MerklTag.tsx index 4f1ed21f44e85..e0b0ea2552469 100644 --- a/apps/web/src/components/Merkl/MerklTag.tsx +++ b/apps/web/src/components/Merkl/MerklTag.tsx @@ -2,7 +2,7 @@ import { useTranslation } from '@pancakeswap/localization' import { Tag } from '@pancakeswap/uikit' import { useMerklInfo } from 'hooks/useMerkl' -export function MerklTag({ poolAddress }: { poolAddress: string | null }) { +export function MerklTag({ poolAddress }: { poolAddress?: string }) { const { t } = useTranslation() const { hasMerkl } = useMerklInfo(poolAddress) @@ -15,7 +15,7 @@ export function MerklTag({ poolAddress }: { poolAddress: string | null }) { ) } -export function MerklRewardsTag({ poolAddress }: { poolAddress: string | null }) { +export function MerklRewardsTag({ poolAddress }: { poolAddress?: string }) { const { t } = useTranslation() const { hasMerkl } = useMerklInfo(poolAddress) diff --git a/apps/web/src/hooks/useMerkl.tsx b/apps/web/src/hooks/useMerkl.tsx index 00efb2c0d084a..39b8c46a58544 100644 --- a/apps/web/src/hooks/useMerkl.tsx +++ b/apps/web/src/hooks/useMerkl.tsx @@ -17,10 +17,11 @@ import { useAllLists } from 'state/lists/hooks' import { getContract } from 'utils/contractHelpers' import { Address } from 'viem' import { useWalletClient } from 'wagmi' +import { useMasterchefV3 } from 'hooks/useContract' export const MERKL_API_V2 = 'https://api.angle.money/v2/merkl' -export function useMerklInfo(poolAddress: string | null): { +export function useMerklInfo(poolAddress?: string): { rewardsPerToken: CurrencyAmount[] isPending: boolean transactionData: { @@ -34,31 +35,9 @@ export function useMerklInfo(poolAddress: string | null): { merklApr?: number } { const { account, chainId } = useAccountActiveChain() + const masterChefV3Address = useMasterchefV3()?.address as Address const lists = useAllLists() - const { data: merklApr } = useQuery({ - queryKey: ['merklAprData', chainId], - queryFn: async () => { - const resp = await fetch(`https://api.angle.money/v2/merkl?chainIds[]=${chainId}&AMMs[]=pancakeswapv3`) - if (resp.ok) { - const result = await resp.json() - return result - } - throw resp - }, - enabled: Boolean(chainId && poolAddress), - staleTime: FAST_INTERVAL, - retryDelay: (attemptIndex) => Math.min(2000 * 2 ** attemptIndex, 30000), - select: useCallback( - (data) => { - return data?.[chainId ?? 0]?.pools?.[poolAddress ?? '']?.aprs?.['Average APR (rewards / pool TVL)'] as - | number - | undefined - }, - [chainId, poolAddress], - ), - }) - const { data, isPending, refetch } = useQuery({ queryKey: [`fetchMerkl-${chainId}-${account || 'no-account'}`], queryFn: async () => { @@ -74,9 +53,7 @@ export function useMerklInfo(poolAddress: string | null): { if (!chainId || !merklDataV2[chainId]) return null - const { pools, transactionData } = merklDataV2[chainId] - - return { pools, transactionData } + return merklDataV2[chainId] }, enabled: Boolean(chainId && poolAddress), staleTime: FAST_INTERVAL, @@ -96,11 +73,28 @@ export function useMerklInfo(poolAddress: string | null): { const { pools, transactionData } = data - const hasLive = first( - Object.keys(pools) - .filter((poolId) => poolId === poolAddress && pools[poolId].meanAPR !== 0) - .map((poolId) => pools[poolId]), - ) + const hasLive = Object.keys(pools) + .filter((poolId) => poolId === poolAddress) + .some((poolId) => { + const pool = pools[poolId] + const hasMeanAPR = pool.meanAPR > 0 + + if (!hasMeanAPR) return false + + const hasLiveDistribution = pool.distributionData.some((distribution) => { + const { isLive, whitelist } = distribution + if (!isLive) return false + const whitelistValid = + !whitelist || + whitelist.length === 0 || + whitelist.includes(account) || + whitelist.includes(masterChefV3Address) + + return whitelistValid + }) + + return hasLiveDistribution + }) const merklPoolData = first( Object.keys(pools) @@ -148,16 +142,20 @@ export function useMerklInfo(poolAddress: string | null): { return CurrencyAmount.fromRawAmount(t, '0') }) + const merklApr = data?.[chainId ?? 0]?.pools?.[poolAddress ?? '']?.aprs?.['Average APR (rewards / pool TVL)'] as + | number + | undefined + return { ...rest, rewardsPerToken: rewardsPerToken.length ? rewardsPerToken : rewardCurrencies, refreshData: refetch, merklApr, } - }, [chainId, data, lists, refetch, merklApr, isPending, poolAddress]) + }, [chainId, data, lists, refetch, isPending, poolAddress, account, masterChefV3Address]) } -export default function useMerkl(poolAddress: string | null) { +export default function useMerkl(poolAddress?: string) { const { account, chainId } = useAccountActiveChain() const { data: signer } = useWalletClient() diff --git a/apps/web/src/pages/liquidity/[tokenId].tsx b/apps/web/src/pages/liquidity/[tokenId].tsx index f86d1c3f862cf..0beb4578057bc 100644 --- a/apps/web/src/pages/liquidity/[tokenId].tsx +++ b/apps/web/src/pages/liquidity/[tokenId].tsx @@ -232,7 +232,7 @@ export default function PoolPage() { return undefined }, [liquidity, pool, tickLower, tickUpper]) - const poolAddress = useMemo(() => pool && Pool.getAddress(pool.token0, pool.token1, pool.fee), [pool]) + const poolAddress = useMemo(() => (pool ? Pool.getAddress(pool.token0, pool.token1, pool.fee) : undefined), [pool]) const poolInfo = usePoolInfo({ poolAddress, chainId }) @@ -805,6 +805,7 @@ export default function PoolPage() { : false, )} poolAddress={poolAddress} + chainId={pool?.chainId} /> {positionDetails && currency0 && currency1 && ( diff --git a/apps/web/src/pages/liquidity/index.tsx b/apps/web/src/pages/liquidity/index.tsx index 10dce5ff6c2fb..f29934a90a699 100644 --- a/apps/web/src/pages/liquidity/index.tsx +++ b/apps/web/src/pages/liquidity/index.tsx @@ -148,6 +148,7 @@ export default function PoolListPage() { ( poolInfo?.chainId ?? chainId, diff --git a/apps/web/src/state/farmsV4/state/extendPools/hooks.ts b/apps/web/src/state/farmsV4/state/extendPools/hooks.ts index 6f864bb2c6a4e..6b09922f9eb3c 100644 --- a/apps/web/src/state/farmsV4/state/extendPools/hooks.ts +++ b/apps/web/src/state/farmsV4/state/extendPools/hooks.ts @@ -78,7 +78,7 @@ export const getPoolAddressByToken = memoize( const token0 = new Token(chainId, token0Address, 18, '') const token1 = new Token(chainId, token1Address, 18, '') if (!token0 || !token1) { - return null + return undefined } return computePoolAddress({ deployerAddress, @@ -95,7 +95,7 @@ export const usePoolInfo = ({ poolAddress, chainId, }: { - poolAddress: string | null + poolAddress: `0x${string}` | undefined chainId: number }): TPoolType | undefined | null => { const { data: poolInfo } = useQuery({ diff --git a/apps/web/src/state/farmsV4/state/poolApr/fetcher.ts b/apps/web/src/state/farmsV4/state/poolApr/fetcher.ts index 3687d38b4d01b..5bfa9391983ef 100644 --- a/apps/web/src/state/farmsV4/state/poolApr/fetcher.ts +++ b/apps/web/src/state/farmsV4/state/poolApr/fetcher.ts @@ -196,38 +196,32 @@ export const getV2PoolCakeApr = async ( } } -export const getMerklApr = async (chainId: number, signal?: AbortSignal) => { +export const getMerklApr = async (result: any, chainId: number) => { try { - // @todo @ChefJerry merkl api cannot accept multiple chainIds, we need to batch fetch - const resp = await fetch(`https://api.angle.money/v2/merkl?chainIds=${chainId}&AMMs=pancakeswapv3`, { signal }) - if (resp.ok) { - const result = await resp.json() - if (!result[chainId] || !result[chainId].pools) return {} - return Object.keys(result[chainId].pools).reduce((acc, poolId) => { - const key = `${chainId}:${safeGetAddress(poolId)}` - if (!result[chainId].pools[poolId].aprs || !Object.keys(result[chainId].pools[poolId].aprs).length) return acc - - const apr = result[chainId].pools[poolId].aprs?.['Average APR (rewards / pool TVL)'] ?? '0' - // eslint-disable-next-line no-param-reassign - acc[key] = apr / 100 - return acc - }, {} as MerklApr) - } - throw resp + if (!result[chainId] || !result[chainId].pools) return {} + return Object.keys(result[chainId].pools).reduce((acc, poolId) => { + const key = `${chainId}:${safeGetAddress(poolId)}` + if (!result[chainId].pools[poolId].aprs || !Object.keys(result[chainId].pools[poolId].aprs).length) return acc + + const apr = result[chainId].pools[poolId].aprs?.['Average APR (rewards / pool TVL)'] ?? '0' + // eslint-disable-next-line no-param-reassign + acc[key] = apr / 100 + return acc + }, {} as MerklApr) } catch (error) { - if (error instanceof Error) { - if (error.name === 'AbortError') { - throw error - } - } - console.error('Failed to fetch merkl apr', error) + console.error('Failed to process merkl apr', error) return {} } } export const getAllNetworkMerklApr = async (signal?: AbortSignal) => { - const aprs = await Promise.all(supportedChainIdV4.map((chainId) => getMerklApr(chainId, signal))) - return aprs.reduce((acc, apr) => Object.assign(acc, apr), {}) + const resp = await fetch(`https://api.angle.money/v2/merkl?AMMs=pancakeswapv3`, { signal }) + if (resp.ok) { + const result = await resp.json() + const aprs = await Promise.all(supportedChainIdV4.map((chainId) => getMerklApr(result, chainId))) + return aprs.reduce((acc, apr) => Object.assign(acc, apr), {}) + } + throw resp } const getV3PoolsCakeAprByChainId = async (pools: V3PoolInfo[], chainId: number, cakePrice: BigNumber) => { diff --git a/apps/web/src/state/farmsV4/state/poolApr/hooks.ts b/apps/web/src/state/farmsV4/state/poolApr/hooks.ts index 4e12c59bbea45..726ea48efe777 100644 --- a/apps/web/src/state/farmsV4/state/poolApr/hooks.ts +++ b/apps/web/src/state/farmsV4/state/poolApr/hooks.ts @@ -31,10 +31,15 @@ export const usePoolApr = ( const cakePrice = useCakePrice() const getMerklApr = useCallback(() => { if (Object.values(merklAprs).length === 0) { - return getAllNetworkMerklApr().then((aprs) => { - updateMerklApr(aprs) - return aprs[key!] ?? '0' - }) + return getAllNetworkMerklApr() + .then((aprs) => { + updateMerklApr(aprs) + return aprs[key!] ?? '0' + }) + .catch((error) => { + console.error('Error fetching Merkl APR:', error) + return '0' + }) } return merklAprs[key!] ?? '0' }, [key, merklAprs, updateMerklApr]) diff --git a/apps/web/src/views/AddLiquidity/components/LiquidityCardRow/index.tsx b/apps/web/src/views/AddLiquidity/components/LiquidityCardRow/index.tsx index 2ab23ab78b92d..8c8efeb8cc008 100644 --- a/apps/web/src/views/AddLiquidity/components/LiquidityCardRow/index.tsx +++ b/apps/web/src/views/AddLiquidity/components/LiquidityCardRow/index.tsx @@ -24,7 +24,7 @@ interface LiquidityCardRowProps { currency1?: Currency pairText: string | React.ReactElement feeAmount?: number - hasMerkl?: boolean + outOfRange?: boolean tokenId?: bigint tags: React.ReactElement subtitle: string @@ -41,12 +41,14 @@ export const LiquidityCardRow = ({ subtitle, tokenId, onSwitch, - hasMerkl, + outOfRange, }: LiquidityCardRowProps) => { const poolAddress = useMemo( () => - currency0 && currency1 && feeAmount ? Pool.getAddress(currency0.wrapped, currency1.wrapped, feeAmount) : null, - [currency0, currency1, feeAmount], + currency0 && currency1 && feeAmount && !outOfRange + ? Pool.getAddress(currency0.wrapped, currency1.wrapped, feeAmount) + : undefined, + [currency0, currency1, feeAmount, outOfRange], ) const content = ( @@ -65,7 +67,7 @@ export const LiquidityCardRow = ({ {new Percent(feeAmount, 1_000_000).toSignificant()}% )} - {!hasMerkl && } + {tags} diff --git a/apps/web/src/views/AddLiquidityV3/Modal.tsx b/apps/web/src/views/AddLiquidityV3/Modal.tsx index 4eeb1a628f1fc..41ae4d6bb380c 100644 --- a/apps/web/src/views/AddLiquidityV3/Modal.tsx +++ b/apps/web/src/views/AddLiquidityV3/Modal.tsx @@ -43,7 +43,9 @@ export function AddLiquidityV3Modal({ const poolAddress = useMemo( () => - currency0 && currency1 && feeAmount ? Pool.getAddress(currency0.wrapped, currency1.wrapped, feeAmount) : null, + currency0 && currency1 && feeAmount + ? Pool.getAddress(currency0.wrapped, currency1.wrapped, feeAmount) + : undefined, [currency0, currency1, feeAmount], ) diff --git a/apps/web/src/views/AddLiquidityV3/index.tsx b/apps/web/src/views/AddLiquidityV3/index.tsx index 5e869fe66e452..21f2bf0122192 100644 --- a/apps/web/src/views/AddLiquidityV3/index.tsx +++ b/apps/web/src/views/AddLiquidityV3/index.tsx @@ -398,7 +398,7 @@ export function AddLiquidityV3Layout({ () => baseCurrency?.wrapped && quoteCurrency?.wrapped && feeAmount ? Pool.getAddress(baseCurrency.wrapped, quoteCurrency.wrapped, feeAmount) - : null, + : undefined, [baseCurrency?.wrapped, feeAmount, quoteCurrency?.wrapped], ) diff --git a/apps/web/src/views/Farms/components/FarmCard/V3/FarmV3Card.tsx b/apps/web/src/views/Farms/components/FarmCard/V3/FarmV3Card.tsx index c3e1b6de2783a..4c6a94120fcf1 100644 --- a/apps/web/src/views/Farms/components/FarmCard/V3/FarmV3Card.tsx +++ b/apps/web/src/views/Farms/components/FarmCard/V3/FarmV3Card.tsx @@ -70,7 +70,7 @@ export const FarmV3Card: React.FC> = ({ f const { status: boostStatus } = useBoostStatus(farm.pid) const merklUserLink = useMerklUserLink() const merklLink = getMerklLink({ chainId, lpAddress }) - const { merklApr } = useMerklInfo(merklLink ? lpAddress : null) + const { merklApr } = useMerklInfo(merklLink ? lpAddress : undefined) const infoUrl = useMemo(() => { return chainId ? `/info/v3${multiChainPaths[chainId]}/pairs/${lpAddress}?chain=${CHAIN_QUERY_NAME[chainId]}` : '' }, [chainId, lpAddress]) diff --git a/apps/web/src/views/Farms/components/FarmTable/Actions/ActionPanel.tsx b/apps/web/src/views/Farms/components/FarmTable/Actions/ActionPanel.tsx index 019169bebe95d..c64bb8432dd5a 100644 --- a/apps/web/src/views/Farms/components/FarmTable/Actions/ActionPanel.tsx +++ b/apps/web/src/views/Farms/components/FarmTable/Actions/ActionPanel.tsx @@ -253,7 +253,7 @@ export const ActionPanelV3: FC = ({ ) const addLiquidityModal = useModalV2() - const { merklApr } = useMerklInfo(merklLink ? details.lpAddress : null) + const { merklApr } = useMerklInfo(merklLink ? details.lpAddress : undefined) return ( <> diff --git a/apps/web/src/views/Farms/components/FarmTable/Row.tsx b/apps/web/src/views/Farms/components/FarmTable/Row.tsx index 40418e08bbd09..d8bb725d25e21 100644 --- a/apps/web/src/views/Farms/components/FarmTable/Row.tsx +++ b/apps/web/src/views/Farms/components/FarmTable/Row.tsx @@ -146,7 +146,7 @@ const Row: React.FunctionComponent> const columnNames = useMemo(() => tableSchema.map((column) => column.name), [tableSchema]) const merklUserLink = useMerklUserLink() - const { merklApr } = useMerklInfo(farm?.merklLink ? props.details.lpAddress : null) + const { merklApr } = useMerklInfo(farm?.merklLink ? props.details.lpAddress : undefined) return ( <> diff --git a/apps/web/src/views/Migration/components/v3/Step4.tsx b/apps/web/src/views/Migration/components/v3/Step4.tsx index 794f8ad9d49bf..11ee33cc61228 100644 --- a/apps/web/src/views/Migration/components/v3/Step4.tsx +++ b/apps/web/src/views/Migration/components/v3/Step4.tsx @@ -92,6 +92,7 @@ export function Step4() { currency0={currencyQuote} currency1={currencyBase} tokenId={p.tokenId} + outOfRange={outOfRange} pairText={ !currencyQuote || !currencyBase ? ( {t('Loading')} diff --git a/apps/web/src/views/PoolDetail/hooks/usePoolInfo.ts b/apps/web/src/views/PoolDetail/hooks/usePoolInfo.ts index 3e8e2f2751233..1d967fcea5d0f 100644 --- a/apps/web/src/views/PoolDetail/hooks/usePoolInfo.ts +++ b/apps/web/src/views/PoolDetail/hooks/usePoolInfo.ts @@ -4,5 +4,5 @@ import { useRouterQuery } from './useRouterQuery' export const usePoolInfoByQuery = (): PoolInfo | undefined | null => { const { id, chainId } = useRouterQuery() - return usePoolInfo({ poolAddress: id, chainId }) + return usePoolInfo({ poolAddress: id as `0x${string}`, chainId }) } diff --git a/apps/web/src/views/universalFarms/components/PositionItem/PositionInfo.tsx b/apps/web/src/views/universalFarms/components/PositionItem/PositionInfo.tsx index 0e9ec5a9ca8e2..76f1f9c1921a2 100644 --- a/apps/web/src/views/universalFarms/components/PositionItem/PositionInfo.tsx +++ b/apps/web/src/views/universalFarms/components/PositionItem/PositionInfo.tsx @@ -11,6 +11,7 @@ import { PositionDetail, StableLPDetail, V2LPDetail } from 'state/farmsV4/state/ import { PoolInfo } from 'state/farmsV4/state/type' import styled from 'styled-components' import { useV2CakeEarning, useV3CakeEarning } from 'views/universalFarms/hooks/useCakeEarning' +import { MerklTag } from 'components/Merkl/MerklTag' import { PoolGlobalAprButton, V2PoolPositionAprButton, V3PoolPositionAprButton } from '../PoolAprButton' const displayTokenReserve = (amount?: CurrencyAmount) => { @@ -90,6 +91,7 @@ export const PositionInfo = memo( )} {protocol === Protocol.V3 && } + ) : ( @@ -104,10 +106,25 @@ export const PositionInfo = memo( )} {protocol === Protocol.V3 && } + ), - [feeTierBase, currency0, currency1, fee, isMobile, isTablet, isStaked, outOfRange, protocol, removed, t, tokenId], + [ + feeTierBase, + currency0, + currency1, + fee, + isMobile, + isTablet, + isStaked, + outOfRange, + protocol, + removed, + t, + tokenId, + pool, + ], ) return (