From ed487c19ce1f07264584a85ba86e4111cf246347 Mon Sep 17 00:00:00 2001 From: chyyynh Date: Tue, 2 Dec 2025 01:59:17 +0800 Subject: [PATCH 1/2] feat:add merkl reward --- app/rewards/components/RewardContent.tsx | 7 +- app/rewards/components/RewardTable.tsx | 100 ++++++++++-------- src/hooks/useRewards.ts | 123 ++++++++++++++++++++++- src/utils/types.ts | 2 +- 4 files changed, 184 insertions(+), 48 deletions(-) diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index fc713f2b..fa8e0c8a 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -75,7 +75,12 @@ export default function Rewards() { acc[key].total.claimable += BigInt(reward.amount.claimable_now); acc[key].total.pendingAmount += BigInt(reward.amount.claimable_next); acc[key].total.claimed += BigInt(reward.amount.claimed); - acc[key].programs.push(reward.type); + // Mark if this is a Merkl reward + if (reward.program_id === 'merkl') { + acc[key].programs.push('merkl'); + } else { + acc[key].programs.push(reward.type); + } } else if (reward.type === 'market-reward' || reward.type === 'vault-reward') { // go through all posible keys of rewad object: for_supply, for_borrow, for_collateral} diff --git a/app/rewards/components/RewardTable.tsx b/app/rewards/components/RewardTable.tsx index 88a88490..f5e39e25 100644 --- a/app/rewards/components/RewardTable.tsx +++ b/app/rewards/components/RewardTable.tsx @@ -112,6 +112,8 @@ export default function RewardTable({ d.asset.chain_id === tokenReward.asset.chain_id, ); + const isMerklReward = tokenReward.programs.includes('merkl'); + return ( @@ -206,47 +208,63 @@ export default function RewardTable({
- + {isMerklReward ? ( + + + + ) : ( + + )}
diff --git a/src/hooks/useRewards.ts b/src/hooks/useRewards.ts index d150fecc..ef7cc177 100644 --- a/src/hooks/useRewards.ts +++ b/src/hooks/useRewards.ts @@ -20,6 +20,114 @@ export type DistributionResponseType = { tx_data: string; }; +type MerklToken = { + address: string; + symbol: string; + decimals: number; + price?: number; +}; + +type MerklReward = { + distributionChainId: number; + root: string; + recipient: string; + amount: string; + claimed: string; + pending: string; + proofs: string[]; + token: MerklToken; +}; + +type MerklApiResponse = { + chain: { + id: number; + name: string; + }; + rewards: MerklReward[]; +}[]; + +async function fetchMerklRewards(userAddress: string): Promise { + try { + const rewardsList: RewardResponseType[] = []; + const chainIds = [1, 8453]; // Mainnet and Base + + for (const chainId of chainIds) { + const url = `https://api.merkl.xyz/v4/users/${userAddress}/rewards?chainId=${chainId}&reloadChainId=${chainId}&test=false&claimableOnly=false&breakdownPage=0&type=TOKEN`; + const response = await fetch(url); + + if (!response.ok) { + console.error( + `Merkl API error for chain ${chainId}:`, + response.status, + response.statusText, + ); + continue; + } + + const data = (await response.json()) as MerklApiResponse; + + if (!Array.isArray(data) || data.length === 0) { + console.warn(`No rewards data for chain ${chainId}`); + continue; + } + + for (const chainData of data) { + if (!chainData.rewards || chainData.rewards.length === 0) { + continue; + } + + const tokenAggregation: Record< + string, + { pending: bigint; amount: bigint; claimed: bigint } + > = {}; + + for (const reward of chainData.rewards) { + const tokenAddress = reward.token.address; + + if (!tokenAggregation[tokenAddress]) { + tokenAggregation[tokenAddress] = { + pending: 0n, + amount: 0n, + claimed: 0n, + }; + } + + const amount = BigInt(reward.amount || '0'); + const claimed = BigInt(reward.claimed || '0'); + const pending = amount > claimed ? amount - claimed : 0n; + + tokenAggregation[tokenAddress].pending += pending; + tokenAggregation[tokenAddress].amount += amount; + tokenAggregation[tokenAddress].claimed += claimed; + } + + for (const [tokenAddress, amounts] of Object.entries(tokenAggregation)) { + rewardsList.push({ + type: 'uniform-reward', + asset: { + id: `${tokenAddress}-${chainData.chain.id}`, + address: tokenAddress, + chain_id: chainData.chain.id, + }, + user: userAddress, + amount: { + total: amounts.amount.toString(), + claimable_now: amounts.pending.toString(), + claimable_next: '0', + claimed: amounts.claimed.toString(), + }, + program_id: 'merkl', + }); + } + } + } + return rewardsList; + } catch (error) { + console.error('Error fetching Merkl rewards:', error); + return []; + } +} + const useUserRewards = (user: string | undefined) => { const [loading, setLoading] = useState(true); const [rewards, setRewards] = useState([]); @@ -34,7 +142,7 @@ const useUserRewards = (user: string | undefined) => { try { setLoading(true); - const [totalRewardsRes, distributionRes] = await Promise.all([ + const [totalRewardsRes, distributionRes, merklRewards] = await Promise.all([ fetch(`${URLS.MORPHO_REWARDS_API}/users/${user}/rewards`, { method: 'GET', headers: { @@ -47,17 +155,22 @@ const useUserRewards = (user: string | undefined) => { 'Content-Type': 'application/json', }, }), + fetchMerklRewards(user), ]); - const newRewards = (await totalRewardsRes.json()).data as RewardResponseType[]; + const morphoRewards = (await totalRewardsRes.json()).data as RewardResponseType[]; const newDistributions = (await distributionRes.json()).data as DistributionResponseType[]; + // Combine Morpho and Merkl rewards + const combinedRewards = [ + ...(Array.isArray(morphoRewards) ? morphoRewards : []), + ...merklRewards, + ]; + if (Array.isArray(newDistributions)) { setDistributions(newDistributions); } - if (Array.isArray(newRewards)) { - setRewards(newRewards); - } + setRewards(combinedRewards); setError(null); } catch (err) { setError(err); diff --git a/src/utils/types.ts b/src/utils/types.ts index b4fe559e..4f2c51a4 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -189,7 +189,7 @@ export type AggregatedRewardType = { pendingAmount: bigint; claimed: bigint; }; - programs: ('vault-reward' | 'market-reward' | 'uniform-reward')[]; + programs: ('vault-reward' | 'market-reward' | 'uniform-reward' | 'merkl')[]; }; export type RebalanceAction = { From 786b11d7bfcd7401c91f2f5bb14419b05966f362 Mon Sep 17 00:00:00 2001 From: chyyynh Date: Tue, 2 Dec 2025 10:20:37 +0800 Subject: [PATCH 2/2] chore:using exist merkl type --- src/hooks/useRewards.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/hooks/useRewards.ts b/src/hooks/useRewards.ts index ef7cc177..f215009e 100644 --- a/src/hooks/useRewards.ts +++ b/src/hooks/useRewards.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { Address } from 'viem'; +import { MerklChain, MerklToken } from '@/utils/merklTypes'; import { RewardResponseType } from '@/utils/types'; import { URLS } from '@/utils/urls'; @@ -20,13 +21,6 @@ export type DistributionResponseType = { tx_data: string; }; -type MerklToken = { - address: string; - symbol: string; - decimals: number; - price?: number; -}; - type MerklReward = { distributionChainId: number; root: string; @@ -35,14 +29,11 @@ type MerklReward = { claimed: string; pending: string; proofs: string[]; - token: MerklToken; + token: Pick; }; type MerklApiResponse = { - chain: { - id: number; - name: string; - }; + chain: MerklChain; rewards: MerklReward[]; }[];