Skip to content

Commit 5147f80

Browse files
MartinGbzNandyBa
andauthored
✨ Implement ZkSync Ignite incentives program (#2305)
Co-authored-by: Nandy Bâ <[email protected]>
1 parent 8b89cec commit 5147f80

22 files changed

+419
-133
lines changed
Lines changed: 1 addition & 0 deletions
Loading

src/components/incentives/IncentivesButton.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DotsHorizontalIcon } from '@heroicons/react/solid';
55
import { Box, SvgIcon, Typography } from '@mui/material';
66
import { useState } from 'react';
77
import { useMeritIncentives } from 'src/hooks/useMeritIncentives';
8+
import { useZkSyncIgniteIncentives } from 'src/hooks/useZkSyncIgniteIncentives';
89
import { useRootStore } from 'src/store/root';
910
import { DASHBOARD } from 'src/utils/mixPanelEvents';
1011

@@ -13,6 +14,7 @@ import { FormattedNumber } from '../primitives/FormattedNumber';
1314
import { TokenIcon } from '../primitives/TokenIcon';
1415
import { getSymbolMap, IncentivesTooltipContent } from './IncentivesTooltipContent';
1516
import { MeritIncentivesTooltipContent } from './MeritIncentivesTooltipContent';
17+
import { ZkSyncIgniteIncentivesTooltipContent } from './ZkSyncIgniteIncentivesTooltipContent';
1618

1719
interface IncentivesButtonProps {
1820
symbol: string;
@@ -61,6 +63,35 @@ export const MeritIncentivesButton = (params: {
6163
);
6264
};
6365

66+
export const ZkIgniteIncentivesButton = (params: {
67+
market: string;
68+
rewardedAsset?: string;
69+
protocolAction?: ProtocolAction;
70+
}) => {
71+
const [open, setOpen] = useState(false);
72+
const { data: zkSyncIgniteIncentives } = useZkSyncIgniteIncentives(params);
73+
74+
if (!zkSyncIgniteIncentives) {
75+
return null;
76+
}
77+
78+
return (
79+
<ContentWithTooltip
80+
tooltipContent={
81+
<ZkSyncIgniteIncentivesTooltipContent zkSyncIgniteIncentives={zkSyncIgniteIncentives} />
82+
}
83+
withoutHover
84+
setOpen={setOpen}
85+
open={open}
86+
>
87+
<Content
88+
incentives={[zkSyncIgniteIncentives]}
89+
incentivesNetAPR={+zkSyncIgniteIncentives.incentiveAPR}
90+
/>
91+
</ContentWithTooltip>
92+
);
93+
};
94+
6495
export const IncentivesButton = ({ incentives, symbol, displayBlank }: IncentivesButtonProps) => {
6596
const [open, setOpen] = useState(false);
6697

src/components/incentives/IncentivesCard.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,44 @@
1+
import { ProtocolAction } from '@aave/contract-helpers';
12
import { ReserveIncentiveResponse } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives';
2-
import { Box } from '@mui/material';
3+
import { Box, useMediaQuery } from '@mui/material';
34
import { ReactNode } from 'react';
45

56
import { FormattedNumber } from '../primitives/FormattedNumber';
67
import { NoData } from '../primitives/NoData';
7-
import { IncentivesButton } from './IncentivesButton';
8+
import {
9+
IncentivesButton,
10+
MeritIncentivesButton,
11+
ZkIgniteIncentivesButton,
12+
} from './IncentivesButton';
813

914
interface IncentivesCardProps {
1015
symbol: string;
1116
value: string | number;
1217
incentives?: ReserveIncentiveResponse[];
18+
address?: string;
1319
variant?: 'main14' | 'main16' | 'secondary14';
1420
symbolsVariant?: 'secondary14' | 'secondary16';
1521
align?: 'center' | 'flex-end';
1622
color?: string;
1723
tooltip?: ReactNode;
24+
market: string;
25+
protocolAction?: ProtocolAction;
1826
}
1927

2028
export const IncentivesCard = ({
2129
symbol,
2230
value,
2331
incentives,
32+
address,
2433
variant = 'secondary14',
2534
symbolsVariant,
2635
align,
2736
color,
2837
tooltip,
38+
market,
39+
protocolAction,
2940
}: IncentivesCardProps) => {
41+
const isTableChangedToCards = useMediaQuery('(max-width:1125px)');
3042
return (
3143
<Box
3244
sx={{
@@ -53,8 +65,27 @@ export const IncentivesCard = ({
5365
) : (
5466
<NoData variant={variant} color={color || 'text.secondary'} />
5567
)}
56-
57-
<IncentivesButton incentives={incentives} symbol={symbol} />
68+
<Box
69+
sx={
70+
isTableChangedToCards
71+
? { display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: '4px' }
72+
: {
73+
display: 'flex',
74+
justifyContent: 'center',
75+
gap: '4px',
76+
flexWrap: 'wrap',
77+
flex: '0 0 50%', // 2 items per row
78+
}
79+
}
80+
>
81+
<IncentivesButton incentives={incentives} symbol={symbol} />
82+
<MeritIncentivesButton symbol={symbol} market={market} protocolAction={protocolAction} />
83+
<ZkIgniteIncentivesButton
84+
market={market}
85+
rewardedAsset={address}
86+
protocolAction={protocolAction}
87+
/>
88+
</Box>
5889
</Box>
5990
);
6091
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Trans } from '@lingui/macro';
2+
import { Box, Typography } from '@mui/material';
3+
import { ExtendedReserveIncentiveResponse } from 'src/hooks/useMeritIncentives';
4+
5+
import { FormattedNumber } from '../primitives/FormattedNumber';
6+
import { Link } from '../primitives/Link';
7+
import { Row } from '../primitives/Row';
8+
import { TokenIcon } from '../primitives/TokenIcon';
9+
import { getSymbolMap } from './IncentivesTooltipContent';
10+
11+
export const ZkSyncIgniteIncentivesTooltipContent = ({
12+
zkSyncIgniteIncentives,
13+
}: {
14+
zkSyncIgniteIncentives: ExtendedReserveIncentiveResponse;
15+
}) => {
16+
const typographyVariant = 'secondary12';
17+
18+
const zkSyncIgniteIncentivesFormatted = getSymbolMap(zkSyncIgniteIncentives);
19+
20+
return (
21+
<Box
22+
sx={{
23+
display: 'flex',
24+
justifyContent: 'center',
25+
alignItems: 'start',
26+
flexDirection: 'column',
27+
}}
28+
>
29+
<img src={`/icons/other/zksync-ignite.svg`} width="100px" height="40px" alt="" />
30+
31+
<Typography variant="caption" color="text.primary" mb={3}>
32+
<Trans>Eligible for the ZKSync Ignite program.</Trans>
33+
</Typography>
34+
35+
<Typography variant="caption" color="text.secondary" mb={3}>
36+
<Trans>
37+
This is a program initiated and implemented by the decentralised ZKSync community. Aave
38+
Labs does not guarantee the program and accepts no liability.
39+
</Trans>{' '}
40+
<Link
41+
href={'https://zksyncignite.xyz/'}
42+
sx={{ textDecoration: 'underline' }}
43+
variant="caption"
44+
color="text.secondary"
45+
>
46+
Learn more
47+
</Link>
48+
</Typography>
49+
50+
<Typography variant="caption" color="text.secondary" mb={3}>
51+
<Trans>ZKSync Ignite Program rewards are claimed through the</Trans>{' '}
52+
<Link
53+
href="https://app.zksyncignite.xyz/users/"
54+
sx={{ textDecoration: 'underline' }}
55+
variant="caption"
56+
color="text.secondary"
57+
>
58+
official app
59+
</Link>
60+
{'.'}
61+
</Typography>
62+
{zkSyncIgniteIncentives.customMessage ? (
63+
<Typography variant="caption" color="text.strong" mb={3}>
64+
<Trans>{zkSyncIgniteIncentives.customMessage}</Trans>
65+
</Typography>
66+
) : null}
67+
68+
<Box sx={{ width: '100%' }}>
69+
<Row
70+
height={32}
71+
caption={
72+
<Box
73+
sx={{
74+
display: 'flex',
75+
alignItems: 'center',
76+
mb: 0,
77+
}}
78+
>
79+
<TokenIcon
80+
aToken={zkSyncIgniteIncentivesFormatted.aToken}
81+
symbol={zkSyncIgniteIncentivesFormatted.tokenIconSymbol}
82+
sx={{ fontSize: '20px', mr: 1 }}
83+
/>
84+
<Typography variant={typographyVariant}>
85+
{zkSyncIgniteIncentivesFormatted.symbol}
86+
</Typography>
87+
</Box>
88+
}
89+
width="100%"
90+
>
91+
<Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
92+
<FormattedNumber
93+
value={+zkSyncIgniteIncentivesFormatted.incentiveAPR}
94+
percent
95+
variant={typographyVariant}
96+
/>
97+
<Typography variant={typographyVariant} sx={{ ml: 1 }}>
98+
<Trans>APR</Trans>
99+
</Typography>
100+
</Box>
101+
</Row>
102+
</Box>
103+
</Box>
104+
);
105+
};
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { ProtocolAction } from '@aave/contract-helpers';
2+
import { ReserveIncentiveResponse } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives';
3+
import { AaveV3ZkSync } from '@bgd-labs/aave-address-book';
4+
import { useQuery } from '@tanstack/react-query';
5+
import { CustomMarket } from 'src/ui-config/marketsConfig';
6+
import { Address } from 'viem';
7+
8+
enum OpportunityAction {
9+
LEND = 'LEND',
10+
BORROW = 'BORROW',
11+
}
12+
13+
enum OpportunityStatus {
14+
LIVE = 'LIVE',
15+
PAST = 'PAST',
16+
UPCOMING = 'UPCOMING',
17+
}
18+
19+
type MerklOpportunity = {
20+
chainId: number;
21+
type: string;
22+
identifier: Address;
23+
name: string;
24+
status: OpportunityStatus;
25+
action: OpportunityAction;
26+
tvl: number;
27+
apr: number;
28+
dailyRewards: number;
29+
tags: [];
30+
id: string;
31+
tokens: [
32+
{
33+
id: string;
34+
name: string;
35+
chainId: number;
36+
address: Address;
37+
decimals: number;
38+
icon: string;
39+
verified: boolean;
40+
isTest: boolean;
41+
price: number;
42+
symbol: string;
43+
}
44+
];
45+
};
46+
47+
export type ExtendedReserveIncentiveResponse = ReserveIncentiveResponse & {
48+
customMessage: string;
49+
customForumLink: string;
50+
};
51+
52+
const url = 'https://api.merkl.xyz/v4/opportunities?tags=zksync&mainProtocolId=aave'; // Merkl API for ZK Ignite opportunities
53+
54+
const rewardToken = AaveV3ZkSync.ASSETS.ZK.UNDERLYING;
55+
const rewardTokenSymbol = 'ZK';
56+
57+
const checkOpportunityAction = (
58+
opportunityAction: OpportunityAction,
59+
protocolAction: ProtocolAction
60+
) => {
61+
switch (opportunityAction) {
62+
case OpportunityAction.LEND:
63+
return protocolAction === ProtocolAction.supply;
64+
case OpportunityAction.BORROW:
65+
return protocolAction === ProtocolAction.borrow;
66+
default:
67+
return false;
68+
}
69+
};
70+
71+
export const useZkSyncIgniteIncentives = ({
72+
market,
73+
rewardedAsset,
74+
protocolAction,
75+
}: {
76+
market: string;
77+
rewardedAsset?: string;
78+
protocolAction?: ProtocolAction;
79+
}) => {
80+
return useQuery({
81+
queryFn: async () => {
82+
if (market === CustomMarket.proto_zksync_v3) {
83+
const response = await fetch(url);
84+
const merklOpportunities: MerklOpportunity[] = await response.json();
85+
return merklOpportunities;
86+
} else {
87+
return [];
88+
}
89+
},
90+
queryKey: ['zkIgniteIncentives', market],
91+
staleTime: 1000 * 60 * 5,
92+
select: (merklOpportunities) => {
93+
const opportunities = merklOpportunities.filter(
94+
(opportunitiy) =>
95+
rewardedAsset &&
96+
opportunitiy.identifier.toLowerCase() === rewardedAsset.toLowerCase() &&
97+
protocolAction &&
98+
checkOpportunityAction(opportunitiy.action, protocolAction)
99+
);
100+
101+
if (opportunities.length === 0) {
102+
return null;
103+
}
104+
105+
const opportunity = opportunities[0];
106+
107+
if (opportunity.status !== OpportunityStatus.LIVE) {
108+
return null;
109+
}
110+
111+
const apr = opportunity.apr / 100;
112+
113+
return {
114+
incentiveAPR: apr.toString(),
115+
rewardTokenAddress: rewardToken,
116+
rewardTokenSymbol: rewardTokenSymbol,
117+
} as ExtendedReserveIncentiveResponse;
118+
},
119+
});
120+
};

src/locales/el/messages.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/locales/en/messages.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)