Skip to content

Commit e6ff7c1

Browse files
committed
feat: added support for multi incentives campaigns for a token
1 parent 58a5fea commit e6ff7c1

File tree

3 files changed

+209
-55
lines changed

3 files changed

+209
-55
lines changed

src/components/incentives/MerklIncentivesTooltipContent.tsx

Lines changed: 89 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -130,48 +130,98 @@ export const MerklIncentivesTooltipContent = ({
130130
</Row>
131131
)}
132132

133-
{/* Merit Incentives */}
134-
<Row
135-
height={32}
136-
caption={
137-
<Box
138-
sx={{
139-
display: 'flex',
140-
alignItems: 'center',
141-
mb: 0,
142-
}}
143-
>
144-
<TokenIcon
145-
aToken={merklIncentivesFormatted.aToken}
146-
symbol={merklIncentivesFormatted.tokenIconSymbol}
147-
sx={{ fontSize: '20px', mr: 1 }}
133+
{/* Merkl Incentives */}
134+
{merklIncentives.allOpportunities && merklIncentives.allOpportunities.length > 1 ? (
135+
<>
136+
{merklIncentives.allOpportunities.map((opportunity, index) => {
137+
const { tokenIconSymbol, symbol, aToken } = getSymbolMap({
138+
rewardTokenSymbol: opportunity.rewardToken.symbol,
139+
rewardTokenAddress: opportunity.rewardToken.address,
140+
incentiveAPR: opportunity.apy.toString(),
141+
});
142+
return (
143+
<Row
144+
key={index}
145+
height={32}
146+
caption={
147+
<Box
148+
sx={{
149+
display: 'flex',
150+
alignItems: 'center',
151+
mb: 0,
152+
}}
153+
>
154+
<TokenIcon
155+
symbol={tokenIconSymbol}
156+
aToken={aToken}
157+
sx={{ fontSize: '20px', mr: 1 }}
158+
/>
159+
<Typography variant={typographyVariant}>{symbol}</Typography>
160+
<Typography variant={typographyVariant} sx={{ ml: 0.5 }}>
161+
{merklIncentives.breakdown.isBorrow ? '(-)' : '(+)'}
162+
</Typography>
163+
</Box>
164+
}
165+
width="100%"
166+
>
167+
<Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
168+
<FormattedNumber
169+
value={
170+
merklIncentives.breakdown.isBorrow ? -opportunity.apy : opportunity.apy
171+
}
172+
percent
173+
variant={typographyVariant}
174+
/>
175+
<Typography variant={typographyVariant} sx={{ ml: 1 }}>
176+
<Trans>APY</Trans>
177+
</Typography>
178+
</Box>
179+
</Row>
180+
);
181+
})}
182+
</>
183+
) : (
184+
<Row
185+
height={32}
186+
caption={
187+
<Box
188+
sx={{
189+
display: 'flex',
190+
alignItems: 'center',
191+
mb: 0,
192+
}}
193+
>
194+
<TokenIcon
195+
aToken={merklIncentivesFormatted.aToken}
196+
symbol={merklIncentivesFormatted.tokenIconSymbol}
197+
sx={{ fontSize: '20px', mr: 1 }}
198+
/>
199+
<Typography variant={typographyVariant}>
200+
{merklIncentivesFormatted.symbol}
201+
</Typography>
202+
<Typography variant={typographyVariant} sx={{ ml: 0.5 }}>
203+
{merklIncentives.breakdown.isBorrow ? '(-)' : '(+)'}
204+
</Typography>
205+
</Box>
206+
}
207+
width="100%"
208+
>
209+
<Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
210+
<FormattedNumber
211+
value={
212+
merklIncentives.breakdown.isBorrow
213+
? -merklIncentives.breakdown.merklIncentivesAPR
214+
: merklIncentives.breakdown.merklIncentivesAPR
215+
}
216+
percent
217+
variant={typographyVariant}
148218
/>
149-
<Typography variant={typographyVariant}>
150-
{merklIncentivesFormatted.symbol}
151-
</Typography>
152-
<Typography variant={typographyVariant} sx={{ ml: 0.5 }}>
153-
{merklIncentives.breakdown.isBorrow ? '(-)' : '(+)'}
219+
<Typography variant={typographyVariant} sx={{ ml: 1 }}>
220+
<Trans>APY</Trans>
154221
</Typography>
155222
</Box>
156-
}
157-
width="100%"
158-
>
159-
<Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
160-
<FormattedNumber
161-
value={
162-
merklIncentives.breakdown.isBorrow
163-
? -merklIncentives.breakdown.merklIncentivesAPR
164-
: merklIncentives.breakdown.merklIncentivesAPR
165-
}
166-
percent
167-
variant={typographyVariant}
168-
/>
169-
<Typography variant={typographyVariant} sx={{ ml: 1 }}>
170-
<Trans>APY</Trans>
171-
</Typography>
172-
</Box>
173-
</Row>
174-
223+
</Row>
224+
)}
175225
{/* Total APY */}
176226
<Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'divider' }}>
177227
<Row

src/hooks/useMerklIncentives.ts

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ type ReserveIncentiveAdditionalData = {
8080
export type ExtendedReserveIncentiveResponse = ReserveIncentiveResponse &
8181
ReserveIncentiveAdditionalData & {
8282
breakdown: MerklIncentivesBreakdown;
83+
allOpportunities?: {
84+
name: string;
85+
apy: number;
86+
rewardToken: {
87+
address: string;
88+
symbol: string;
89+
icon: string;
90+
price: number;
91+
};
92+
}[];
8393
};
8494

8595
export type MerklIncentivesBreakdown = {
@@ -98,7 +108,80 @@ type WhitelistApiResponse = {
98108
whitelistedRewardTokens: string[];
99109
additionalIncentiveInfo: Record<string, ReserveIncentiveAdditionalData>;
100110
};
111+
// TODO: Remove mock before production
112+
const ethfiMockOpportunity: MerklOpportunity = {
113+
chainId: 9745,
114+
type: 'AAVE_SUPPLY',
115+
identifier: '0xaf1a7a488c8348b41d5860c04162af7d3d38a996' as Address,
116+
name: 'Lend weETH on Aave (ETHFI Bonus)',
117+
status: OpportunityStatus.LIVE,
118+
action: OpportunityAction.LEND,
119+
tvl: 1000000,
120+
apr: 5.0,
121+
dailyRewards: 500,
122+
tags: [],
123+
id: 'mock-ethfi-campaign-2',
124+
explorerAddress: '0xaf1a7a488c8348b41d5860c04162af7d3d38a996' as Address,
125+
tokens: [
126+
{
127+
id: '14178706307683891785',
128+
name: 'Aave Plasma weETH',
129+
chainId: 9745,
130+
address: '0xaf1a7a488c8348b41d5860c04162af7d3d38a996' as Address,
131+
decimals: 18,
132+
icon: '',
133+
verified: false,
134+
isTest: false,
135+
price: 4156.526571271317,
136+
symbol: 'aPlaweETH',
137+
},
138+
{
139+
id: '3885658325202072166',
140+
name: 'Wrapped eETH',
141+
chainId: 9745,
142+
address: '0xa3d68b74bf0528fdd07263c60d6488749044914b' as Address,
143+
decimals: 18,
144+
icon: 'https://storage.googleapis.com/merkl-static-assets/tokens/weETH.webp',
145+
verified: true,
146+
isTest: false,
147+
price: 4156.526571271317,
148+
symbol: 'weETH',
149+
},
150+
],
151+
rewardsRecord: {
152+
id: 'mock-ethfi-rewards-record',
153+
total: 500,
154+
timestamp: '1761125029',
155+
breakdowns: [
156+
{
157+
token: {
158+
id: 'ethfi-token-id',
159+
name: 'Ether.fi Governance Token',
160+
chainId: 9745,
161+
address: '0xfe0c30065b384f05761f15d0cc899d4f9f9cc0eb',
162+
decimals: 18,
163+
symbol: 'ETHFI',
164+
displaySymbol: 'ETHFI',
165+
icon: 'https://assets.coingecko.com/coins/images/35958/standard/etherfi.png',
166+
verified: true,
167+
isTest: false,
168+
type: 'TOKEN',
169+
isNative: false,
170+
price: 3.25,
171+
},
172+
amount: '153846153846153846153',
173+
value: 500,
174+
distributionType: 'DUTCH_AUCTION',
175+
id: 'mock-ethfi-breakdown-id',
176+
campaignId: 'mock-ethfi-campaign-id',
177+
dailyRewardsRecordId: 'mock-ethfi-daily-rewards-id',
178+
},
179+
],
180+
},
181+
};
101182

183+
// TODO: Remove mock before production
184+
const mockaddressWeETH = '0xfe0c30065b384f05761f15d0cc899d4f9f9cc0eb';
102185
const MERKL_ENDPOINT = 'https://api.merkl.xyz/v4/opportunities?mainProtocolId=aave'; // Merkl API
103186
const WHITELIST_ENDPOINT = 'https://apps.aavechan.com/api/aave/merkl/whitelist-token-list'; // Endpoint to fetch whitelisted tokens
104187
const checkOpportunityAction = (
@@ -121,7 +204,13 @@ const useWhitelistedTokens = () => {
121204
if (!response.ok) {
122205
throw new Error('Failed to fetch whitelisted tokens');
123206
}
124-
return response.json();
207+
const data = await response.json();
208+
209+
// TODO: Remove mock before production
210+
if (!data.whitelistedRewardTokens.includes(mockaddressWeETH.toLowerCase())) {
211+
data.whitelistedRewardTokens.push(mockaddressWeETH.toLowerCase());
212+
}
213+
return data;
125214
},
126215
queryKey: ['whitelistedTokens'],
127216
staleTime: 1000 * 60 * 5, // 5 minutes
@@ -148,6 +237,8 @@ export const useMerklIncentives = ({
148237
queryFn: async () => {
149238
const response = await fetch(`${MERKL_ENDPOINT}`);
150239
const merklOpportunities: MerklOpportunity[] = await response.json();
240+
// TODO: Remove mock before production
241+
merklOpportunities.push(ethfiMockOpportunity);
151242
return merklOpportunities;
152243
},
153244
queryKey: ['merklIncentives', market],
@@ -159,29 +250,22 @@ export const useMerklIncentives = ({
159250
opportunitiy.explorerAddress &&
160251
opportunitiy.explorerAddress.toLowerCase() === rewardedAsset.toLowerCase() &&
161252
protocolAction &&
162-
checkOpportunityAction(opportunitiy.action, protocolAction) &&
163-
opportunitiy.chainId === currentChainId
253+
checkOpportunityAction(opportunitiy.action, protocolAction)
254+
// disabled to allow cross-chain incentives e.g. ethfi on weETH
255+
// opportunitiy.chainId === currentChainId
164256
);
165257

166258
if (opportunities.length === 0) {
167259
return null;
168260
}
169261

170-
const opportunity = opportunities[0];
171-
172-
if (opportunity.status !== OpportunityStatus.LIVE) {
173-
return null;
174-
}
175-
176-
if (opportunity.apr <= 0) {
262+
const validOpportunities = opportunities.filter(
263+
(opp) => opp.status === OpportunityStatus.LIVE && opp.apr > 0
264+
);
265+
if (validOpportunities.length === 0) {
177266
return null;
178267
}
179268

180-
const merklIncentivesAPR = opportunity.apr / 100;
181-
const merklIncentivesAPY = convertAprToApy(merklIncentivesAPR);
182-
183-
const rewardToken = opportunity.rewardsRecord.breakdowns[0].token;
184-
185269
if (!whitelistData?.whitelistedRewardTokens) {
186270
return null;
187271
}
@@ -190,10 +274,24 @@ export const useMerklIncentives = ({
190274
whitelistData.whitelistedRewardTokens.map((token) => token.toLowerCase())
191275
);
192276

193-
if (!whitelistedTokensSet.has(rewardToken.address.toLowerCase())) {
277+
const whitelistedOpportunities = validOpportunities.filter((opp) => {
278+
const rewardToken = opp.rewardsRecord.breakdowns[0]?.token;
279+
return rewardToken && whitelistedTokensSet.has(rewardToken.address.toLowerCase());
280+
});
281+
282+
if (whitelistedOpportunities.length === 0) {
194283
return null;
195284
}
196285

286+
const totalMerklAPR = whitelistedOpportunities.reduce((sum, opp) => {
287+
return sum + opp.apr / 100;
288+
}, 0);
289+
290+
const merklIncentivesAPY = convertAprToApy(totalMerklAPR);
291+
292+
const primaryOpportunity = whitelistedOpportunities[0];
293+
const rewardToken = primaryOpportunity.rewardsRecord.breakdowns[0].token;
294+
197295
const protocolIncentivesAPR = protocolIncentives.reduce((sum, inc) => {
198296
return sum + (inc.incentiveAPR === 'Infinity' ? 0 : +inc.incentiveAPR);
199297
}, 0);
@@ -211,6 +309,11 @@ export const useMerklIncentives = ({
211309
rewardTokenAddress: rewardToken.address,
212310
rewardTokenSymbol: rewardToken.symbol,
213311
...incentiveAdditionalData,
312+
allOpportunities: whitelistedOpportunities.map((opp) => ({
313+
name: opp.name,
314+
apy: convertAprToApy(opp.apr / 100),
315+
rewardToken: opp.rewardsRecord.breakdowns[0].token,
316+
})),
214317
breakdown: {
215318
protocolAPY,
216319
protocolIncentivesAPR,

src/locales/en/messages.po

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3434,6 +3434,7 @@ msgstr "Selected supply assets"
34343434
#: src/components/incentives/MerklIncentivesTooltipContent.tsx
34353435
#: src/components/incentives/MerklIncentivesTooltipContent.tsx
34363436
#: src/components/incentives/MerklIncentivesTooltipContent.tsx
3437+
#: src/components/incentives/MerklIncentivesTooltipContent.tsx
34373438
#: src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsList.tsx
34383439
#: src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsList.tsx
34393440
#: src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItem.tsx

0 commit comments

Comments
 (0)