Skip to content

Commit

Permalink
feat: Support v2 pools merkl rewards
Browse files Browse the repository at this point in the history
  • Loading branch information
memoyil committed Dec 28, 2024
1 parent c811004 commit 84261b5
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 127 deletions.
6 changes: 4 additions & 2 deletions apps/web/src/components/Merkl/MerklSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,29 @@ const LearnMoreLink = () => {

export function MerklSection({
poolAddress,
tokenId,
chainId,
notEnoughLiquidity,
outRange,
disabled,
}: {
poolAddress?: `0x${string}`
chainId?: ChainId
tokenId?: bigint
notEnoughLiquidity: boolean
outRange: boolean
disabled: boolean
}) {
const { t } = useTranslation()

const { claimTokenReward, isClaiming, rewardsPerToken, hasMerkl } = useMerkl(poolAddress)
const { claimTokenReward, isClaiming, rewardsPerToken, hasMerkl } = useMerkl(poolAddress, tokenId)

const merklLink = useMemo(() => getMerklLink({ chainId, lpAddress: poolAddress }), [chainId, poolAddress])

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"
}
]
166 changes: 117 additions & 49 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,10 +18,15 @@ 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): {
export function useMerklInfo(
poolAddress?: string,
tokenId?: bigint,
): {
rewardsPerToken: CurrencyAmount<Currency>[]
isPending: boolean
transactionData: {
Expand All @@ -36,33 +40,68 @@ 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`,
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()

if (!chainId || !merklDataV2[chainId]) return null
const opportunities = merklDataV4?.filter(
(opportunity) =>
opportunity?.tokens?.[0].name?.toLowerCase().startsWith('pancake') ||
opportunity?.protocol?.id?.toLowerCase().startsWith('pancakeswap'),
)

if (!chainId || !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`)
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 () => {
const responsev4 = await fetch(`${MERKL_API_V4}/users/${account}/rewards?chainId=${chainId}`)

if (!responsev4.ok) {
throw responsev4
}

const merklDataV4 = await responsev4.json()

if (!chainId || !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 +111,72 @@ 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
if (!isLive) return false
const whitelistValid =
!whitelist ||
whitelist.length === 0 ||
whitelist.includes(account) ||
whitelist.includes(masterChefV3Address)
const hasLiveDistribution = 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 whitelistValid && blacklistValid
})

return hasLiveDistribution
})

return hasLiveDistribution
const rewardsPerTokenObject = userData?.rewards
?.map((reward) => {
const breakdowns = reward?.breakdowns.filter((breakdown) =>
tokenId ? breakdown?.reason.includes(tokenId) : breakdown?.reason.includes(poolAddress),
)
if (breakdowns?.length === 0) return undefined
return { ...reward, breakdowns }
})
.filter(Boolean)

const merklPoolData = first(
Object.keys(pools)
.filter((poolId) => poolId === poolAddress)
.map((poolId) => pools[poolId]),
)
const transactionData = rewardsPerTokenObject?.reduce((acc, reward) => {
const { amount, claimed } = reward.breakdowns[0]
const unclaimed = BigInt(amount) - BigInt(claimed)

const rewardsPerTokenObject = merklPoolData?.rewardsPerToken
// eslint-disable-next-line no-param-reassign
acc[reward?.token?.address] = { proof: reward.proof, claim: unclaimed }
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 {
breakdowns,
token: { address, decimals, symbol },
} = tokenInfo

const { amount, claimed } = breakdowns[0]

const token = new Token(chainId as number, address as Address, decimals, symbol)
const unclaimed = BigInt(amount) - BigInt(claimed)
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,25 +201,35 @@ export function useMerklInfo(poolAddress?: string): {
return CurrencyAmount.fromRawAmount(t, '0')
})

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

return {
...rest,
rewardsPerToken: rewardsPerToken.length ? rewardsPerToken : rewardCurrencies,
refreshData: refetch,
merklApr,
}
}, [chainId, data, lists, refetch, isPending, poolAddress, account, masterChefV3Address])
}, [
chainId,
data,
lists,
refetch,
isPending,
poolAddress,
account,
masterChefV3Address,
currentTimestamp,
tokenId,
userData,
])
}

export default function useMerkl(poolAddress?: string) {
export default function useMerkl(poolAddress?: string, tokenId?: bigint) {
const { account, chainId } = useAccountActiveChain()

const { data: signer } = useWalletClient()

const { transactionData, rewardsPerToken, refreshData, hasMerkl } = useMerklInfo(poolAddress)
const { transactionData, rewardsPerToken, refreshData, hasMerkl } = useMerklInfo(poolAddress, tokenId)

const { callWithGasPrice } = useCallWithGasPrice()
const { fetchWithCatchTxError, loading: isTxPending } = useCatchTxError()
Expand Down
38 changes: 20 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,26 @@ 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}
tokenId={tokenId}
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
Loading

0 comments on commit 84261b5

Please sign in to comment.