Skip to content

Commit

Permalink
feat: Support v2 pools merkl rewards (#11099)
Browse files Browse the repository at this point in the history
<!--
Before opening a pull request, please read the [contributing
guidelines](https://github.com/pancakeswap/pancake-frontend/blob/develop/CONTRIBUTING.md)
first
-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on integrating the `Merkl` feature into the application,
enhancing the `MerklSection` component, and updating API endpoints for
fetching `Merkl` data. It also refines the logic for calculating APR and
improves the structure of the `Merkl` configuration.

### Detailed summary
- Updated `AprTooltipContent` to simplify the `expired` condition.
- Enhanced `MerklSection` with additional props.
- Integrated `MerklSection` into `PoolV2Page` and adjusted rendering in
`liquidity/[tokenId].tsx`.
- Removed unnecessary `expired` logic in `PoolAprButton`.
- Modified `merklPools.json` to include new `Merkl` pool addresses.
- Updated API endpoint from `angle.money` to `merkl.xyz` for fetching
APR data.
- Refined logic in `fetcher.ts` for retrieving `Merkl` APR.
- Adjusted `useMerkl` hook to support new API structure and handle user
rewards.
- Enhanced error handling and data parsing in `useMerklInfo`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your
question}`

<!-- end pr-codex -->
  • Loading branch information
memoyil authored Dec 30, 2024
1 parent 4bf9e36 commit ab900ed
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 127 deletions.
3 changes: 2 additions & 1 deletion apps/web/src/components/Merkl/MerklSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function MerklSection({
}: {
poolAddress?: `0x${string}`
chainId?: ChainId
tokenId?: bigint
notEnoughLiquidity: boolean
outRange: boolean
disabled: boolean
Expand All @@ -74,7 +75,7 @@ export function MerklSection({
if (!rewardsPerToken.length || (!hasMerkl && rewardsPerToken.every((r) => r.equalTo('0')))) return null

return (
<Column justifyContent="space-between" gap="8px" width="100%" ml={['0px', '0px', '16px', '16px']} mt="24px">
<Column justifyContent="space-between" gap="8px" width="100%">
<AutoRow justifyContent="space-between">
<Text fontSize="12px" color="secondary" bold textTransform="uppercase">
{t('Merkl Rewards')}
Expand Down
25 changes: 15 additions & 10 deletions apps/web/src/config/constants/merklPools.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
[
{
"chainId": 56,
"address": "0xeD000AB362Ef11E962658Fc04c1A7D667a647213",
"link": "https://merkl.angle.money/bnb%20smart%20chain/pool/1/0xeD000AB362Ef11E962658Fc04c1A7D667a647213"
},
{
"chainId": 56,
"address": "0xd5a79aB649E0a5F20e995026d034a0bF28B8aACa",
"link": "https://merkl.angle.money/bnb%20smart%20chain/pool/2/0xd5a79aB649E0a5F20e995026d034a0bF28B8aACa"
},
{
"chainId": 42161,
"address": "0xE4BfcC208f3447cc5D2f5CCB40C52778d4bE2004",
"link": "https://merkl.angle.money/arbitrum/pool/2/0xE4BfcC208f3447cc5D2f5CCB40C52778d4bE2004"
},
{
"chainId": 1,
"address": "0x7B94A5622035207d3f527d236d47B7714Ee0acBa",
Expand All @@ -9,19 +24,9 @@
"address": "0x6db0f81Db2C3B2A85a802d511577d8522D0D8C14",
"link": "https://merkl.angle.money/ethereum/pool/2/0x6db0f81Db2C3B2A85a802d511577d8522D0D8C14"
},
{
"chainId": 56,
"address": "0xd5a79aB649E0a5F20e995026d034a0bF28B8aACa",
"link": "https://merkl.angle.money/bnb%20smart%20chain/pool/2/0xd5a79aB649E0a5F20e995026d034a0bF28B8aACa"
},
{
"chainId": 56,
"address": "0x9d84f1d12FdC6c977BF451e70689F45107b79b77",
"link": "https://merkl.angle.money/bnb%20smart%20chain/pool/2/0x9d84f1d12FdC6c977BF451e70689F45107b79b77"
},
{
"chainId": 42161,
"address": "0xE4BfcC208f3447cc5D2f5CCB40C52778d4bE2004",
"link": "https://merkl.angle.money/arbitrum/pool/2/0xE4BfcC208f3447cc5D2f5CCB40C52778d4bE2004"
}
]
142 changes: 100 additions & 42 deletions apps/web/src/hooks/useMerkl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { DISTRIBUTOR_ADDRESSES } from 'config/merkl'
import useAccountActiveChain from 'hooks/useAccountActiveChain'
import { useCallWithGasPrice } from 'hooks/useCallWithGasPrice'
import useCatchTxError from 'hooks/useCatchTxError'
import first from 'lodash/first'
import uniq from 'lodash/uniq'
import { useCallback, useMemo } from 'react'
import { useAllLists } from 'state/lists/hooks'
Expand All @@ -19,8 +18,9 @@ import { Address } from 'viem'
import { useWalletClient } from 'wagmi'
import { useMasterchefV3 } from 'hooks/useContract'
import { isAddressEqual } from 'utils'
import { useCurrentBlockTimestamp as useBlockTimestamp } from 'state/block/hooks'

export const MERKL_API_V2 = 'https://api.angle.money/v2/merkl'
export const MERKL_API_V4 = 'https://api.merkl.xyz/v4'

export function useMerklInfo(poolAddress?: string): {
rewardsPerToken: CurrencyAmount<Currency>[]
Expand All @@ -36,33 +36,75 @@ export function useMerklInfo(poolAddress?: string): {
merklApr?: number
} {
const { account, chainId } = useAccountActiveChain()
const currentTimestamp = useBlockTimestamp()
const masterChefV3Address = useMasterchefV3()?.address as Address
const lists = useAllLists()

const { data, isPending, refetch } = useQuery({
queryKey: [`fetchMerkl-${chainId}-${account || 'no-account'}`],
queryKey: [`fetchMerkl-${chainId}`],
queryFn: async () => {
const responsev2 = await fetch(
`${MERKL_API_V2}?chainIds[]=${chainId}${account ? `&user=${account}` : ''}&AMMs[]=pancakeswapv3`,
if (!chainId) return undefined

const responsev4 = await fetch(
`${MERKL_API_V4}/opportunities?chainId=${chainId}&test=false&items=1000&action=POOL,HOLD`,
)

if (!responsev2.ok) {
throw responsev2
if (!responsev4.ok) {
throw responsev4
}

const merklDataV2 = await responsev2.json()
const merklDataV4 = await responsev4.json()

const opportunities = merklDataV4?.filter(
(opportunity) =>
opportunity?.tokens?.[0]?.symbol?.toLowerCase().startsWith('Cake-LP') ||
opportunity?.protocol?.id?.toLowerCase().startsWith('pancakeswap'),
)

if (!chainId || !merklDataV2[chainId]) return null
if (!opportunities || !opportunities.length) return undefined

const pools = await Promise.all(
opportunities.map(async (opportunity) => {
const responseCampaignV4 = await fetch(`${MERKL_API_V4}/opportunities/${opportunity.id}/campaigns`)
if (!responseCampaignV4.ok) {
throw responseCampaignV4
}
const campaignV4 = await responseCampaignV4.json()
return { ...opportunity, campaigns: campaignV4?.campaigns }
}),
)

return merklDataV2[chainId]
return { pools }
},
enabled: Boolean(chainId && poolAddress),
staleTime: FAST_INTERVAL,
retryDelay: (attemptIndex) => Math.min(2000 * 2 ** attemptIndex, 30000),
})

const { data: userData } = useQuery({
queryKey: [`fetchMerkl-${chainId}-${account}`],
queryFn: async () => {
if (!chainId) return undefined

const responsev4 = await fetch(`${MERKL_API_V4}/users/${account}/rewards?chainId=${chainId}`)

if (!responsev4.ok) {
throw responsev4
}

const merklDataV4 = await responsev4.json()

if (!merklDataV4) return undefined

return merklDataV4?.[0] || {}
},
enabled: Boolean(data && chainId && account && poolAddress),
staleTime: FAST_INTERVAL,
retryDelay: (attemptIndex) => Math.min(2000 * 2 ** attemptIndex, 30000),
})

return useMemo(() => {
if (!data)
if (!data || !currentTimestamp)
return {
rewardsPerToken: [],
rewardTokenAddresses: [],
Expand All @@ -72,53 +114,69 @@ export function useMerklInfo(poolAddress?: string): {
isPending,
}

const { pools, transactionData } = data
const { pools } = data

const hasLive = Object.keys(pools)
.filter((poolId) => poolId === poolAddress)
.some((poolId) => {
const pool = pools[poolId]
const hasMeanAPR = pool.meanAPR > 0
const hasLive = pools.some((pool) => {
const hasMeanAPR = pool.status === 'LIVE' && pool.apr > 0

if (!hasMeanAPR) return false
if (!hasMeanAPR) return false

const hasLiveDistribution = pool.distributionData.some((distribution) => {
const { isLive, whitelist } = distribution
const hasLiveDistribution = Boolean(
pool.campaigns?.some((campaign) => {
const { startTimestamp, endTimestamp, whitelist, blacklist } = campaign
const startTimestampNumber = Number(startTimestamp)
const endTimestampNumber = Number(endTimestamp)
const isLive = startTimestampNumber <= currentTimestamp && currentTimestamp <= endTimestampNumber
if (!isLive) return false
const whitelistValid =
!whitelist ||
whitelist.length === 0 ||
whitelist.includes(account) ||
whitelist.includes(masterChefV3Address)

return whitelistValid
})
const blacklistValid =
!blacklist ||
blacklist.length === 0 ||
!blacklist.includes(account) ||
!blacklist.includes(masterChefV3Address)

return hasLiveDistribution
})
return whitelistValid && blacklistValid
}),
)

return hasLiveDistribution
})

const merklPoolData = first(
Object.keys(pools)
.filter((poolId) => poolId === poolAddress)
.map((poolId) => pools[poolId]),
)
const rewardsPerTokenObject = userData?.rewards?.filter((reward) => {
const { amount, claimed } = reward || {}
const unclaimed = BigInt(amount || 0) - BigInt(claimed || 0)
return unclaimed > 0
})

const rewardsPerTokenObject = merklPoolData?.rewardsPerToken
const transactionData = rewardsPerTokenObject?.reduce((acc, reward) => {
// eslint-disable-next-line no-param-reassign
acc[reward?.token?.address] = { proof: reward?.proofs, claim: reward?.amount }
return acc
}, {})

const rewardResult = {
hasMerkl: Boolean(hasLive),
rewardsPerToken: rewardsPerTokenObject
? Object.keys(rewardsPerTokenObject)
.map((tokenAddress) => {
const tokenInfo = rewardsPerTokenObject[tokenAddress]

const token = new Token(chainId as number, tokenAddress as Address, tokenInfo.decimals, tokenInfo.symbol)

return CurrencyAmount.fromRawAmount(token, tokenInfo.unclaimedUnformatted)
? rewardsPerTokenObject
.map((tokenInfo) => {
const {
amount,
claimed,
token: { address, decimals, symbol },
} = tokenInfo

const token = new Token(chainId as number, address as Address, decimals, symbol)
const unclaimed = BigInt(amount || 0) - BigInt(claimed || 0)
return CurrencyAmount.fromRawAmount(token, unclaimed)
})
.filter(Boolean)
: [],
rewardTokenAddresses: uniq(merklPoolData?.distributionData?.map((d) => d.token)),
rewardTokenAddresses: uniq(rewardsPerTokenObject?.map((tokenInfo) => tokenInfo?.token?.address)),
transactionData,
isPending,
}
Expand All @@ -143,7 +201,7 @@ export function useMerklInfo(poolAddress?: string): {
return CurrencyAmount.fromRawAmount(t, '0')
})

const merklApr = data?.[chainId ?? 0]?.pools?.[poolAddress ?? '']?.aprs?.['Average APR (rewards / pool TVL)'] as
const merklApr = data?.pools?.find((pool) => isAddressEqual(pool.identifier, poolAddress))?.apr as
| number
| undefined

Expand All @@ -153,7 +211,7 @@ export function useMerklInfo(poolAddress?: string): {
refreshData: refetch,
merklApr,
}
}, [chainId, data, lists, refetch, isPending, poolAddress, account, masterChefV3Address])
}, [chainId, data, lists, refetch, isPending, poolAddress, account, masterChefV3Address, currentTimestamp, userData])
}

export default function useMerkl(poolAddress?: string) {
Expand Down Expand Up @@ -192,8 +250,8 @@ export default function useMerkl(poolAddress?: string) {
})
.filter(Boolean) as string[]

const claims = tokens.map((txnData) => transactionData[txnData].claim)
const proofs = tokens.map((txnData) => transactionData[txnData].proof)
const claims = tokens.map((token) => transactionData[token].claim)
const proofs = tokens.map((token) => transactionData[token].proof)

const receipt = await fetchWithCatchTxError(() => {
return callWithGasPrice(distributorContract, 'claim', [
Expand Down
37 changes: 19 additions & 18 deletions apps/web/src/pages/liquidity/[tokenId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -789,24 +789,25 @@ export default function PoolPage() {
tickAtLimit={tickAtLimit}
/>
</Box>

<MerklSection
disabled={!isOwnNFT}
outRange={!inRange}
notEnoughLiquidity={Boolean(
fiatValueOfLiquidity
? fiatValueOfLiquidity.lessThan(
// NOTE: if Liquidity is lessage 20$, can't participate in Merkl
new Fraction(
BigInt(20) * fiatValueOfLiquidity.decimalScale * fiatValueOfLiquidity.denominator,
fiatValueOfLiquidity?.denominator,
),
)
: false,
)}
poolAddress={poolAddress}
chainId={pool?.chainId}
/>
<Flex ml={['0px', '0px', '16px', '16px']} mt="24px">
<MerklSection
disabled={!isOwnNFT}
outRange={!inRange}
notEnoughLiquidity={Boolean(
fiatValueOfLiquidity
? fiatValueOfLiquidity.lessThan(
// NOTE: if Liquidity is lessage 20$, can't participate in Merkl
new Fraction(
BigInt(20) * fiatValueOfLiquidity.decimalScale * fiatValueOfLiquidity.denominator,
fiatValueOfLiquidity?.denominator,
),
)
: false,
)}
poolAddress={poolAddress}
chainId={pool?.chainId}
/>
</Flex>
</Flex>
{positionDetails && currency0 && currency1 && (
<PositionHistory
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/pages/v2/pair/[[...currency]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { formatFiatNumber } from '@pancakeswap/utils/formatFiatNumber'
import { useTotalPriceUSD } from 'hooks/useTotalPriceUSD'
import { useLPApr } from 'state/swap/useLPApr'
import { formatAmount } from 'utils/formatInfoNumbers'
import { MerklSection } from 'components/Merkl/MerklSection'

export const BodyWrapper = styled(Card)`
border-radius: 24px;
Expand Down Expand Up @@ -234,6 +235,15 @@ export default function PoolV2Page() {
</LightGreyCard>
</Box>
</Flex>
<Flex width="100%">
<MerklSection
disabled={!pair || !positionDetails}
notEnoughLiquidity={totalUSDValue < 20}
poolAddress={pair?.liquidityToken?.address}
chainId={chainId}
outRange={false}
/>
</Flex>
<Flex
flexDirection={isMobile ? 'column' : 'row'}
justifyContent={isPoolStaked ? 'space-between' : 'flex-end'}
Expand Down
25 changes: 17 additions & 8 deletions apps/web/src/state/farmsV4/state/poolApr/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,13 @@ export const getV2PoolCakeApr = async (

export const getMerklApr = async (result: any, chainId: number) => {
try {
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 opportunities = result?.filter((opportunity) => opportunity?.chainId === chainId)
if (!opportunities || opportunities?.length === 0) return {}
return opportunities.reduce((acc, opportunity) => {
const key = `${chainId}:${safeGetAddress(opportunity.identifier)}`

const apr = result[chainId].pools[poolId].aprs?.['Average APR (rewards / pool TVL)'] ?? '0'
// eslint-disable-next-line no-param-reassign
acc[key] = apr / 100
acc[key] = (opportunity.apr ?? 0) / 100
return acc
}, {} as MerklApr)
} catch (error) {
Expand All @@ -217,10 +216,20 @@ export const getMerklApr = async (result: any, chainId: number) => {
}

export const getAllNetworkMerklApr = async (signal?: AbortSignal) => {
const resp = await fetch(`https://api.angle.money/v2/merkl`, { signal })
const resp = await fetch(
`https://api.merkl.xyz/v4/opportunities/?chainId=${supportedChainIdV4.join(
',',
)}&test=false&status=LIVE&items=1000&action=POOL,HOLD`,
{ signal },
)
if (resp.ok) {
const result = await resp.json()
const aprs = await Promise.all(supportedChainIdV4.map((chainId) => getMerklApr(result, chainId)))
const pancakeResult = result?.filter(
(opportunity) =>
opportunity?.tokens?.[0].name?.toLowerCase().startsWith('pancake') ||
opportunity?.protocol?.id?.toLowerCase().startsWith('pancakeswap'),
)
const aprs = await Promise.all(supportedChainIdV4.map((chainId) => getMerklApr(pancakeResult, chainId)))
return aprs.reduce((acc, apr) => Object.assign(acc, apr), {})
}
throw resp
Expand Down
Loading

0 comments on commit ab900ed

Please sign in to comment.