Skip to content

feat: juice apys #2550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 169 additions & 47 deletions src/components/incentives/IncentivesButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ProtocolAction } from '@aave/contract-helpers';
import { valueToBigNumber } from '@aave/math-utils';
import { ReserveIncentiveResponse } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives';
import { DotsHorizontalIcon } from '@heroicons/react/solid';
import { Box, SvgIcon, Typography } from '@mui/material';
import { Box, Typography } from '@mui/material';
import { useState } from 'react';
import { useEthenaIncentives } from 'src/hooks/useEthenaIncentives';
import { useEtherfiIncentives } from 'src/hooks/useEtherfiIncentives';
Expand All @@ -14,18 +13,100 @@ import { DASHBOARD } from 'src/utils/events';

import { ContentWithTooltip } from '../ContentWithTooltip';
import { FormattedNumber } from '../primitives/FormattedNumber';
import { TokenIcon } from '../primitives/TokenIcon';
import { EthenaAirdropTooltipContent } from './EthenaIncentivesTooltipContent';
import { EtherFiAirdropTooltipContent } from './EtherfiIncentivesTooltipContent';
import { getSymbolMap, IncentivesTooltipContent } from './IncentivesTooltipContent';
import { IncentivesTooltipContent } from './IncentivesTooltipContent';
import { MeritIncentivesTooltipContent } from './MeritIncentivesTooltipContent';
import { MerklIncentivesTooltipContent } from './MerklIncentivesTooltipContent';
import { SonicAirdropTooltipContent } from './SonicIncentivesTooltipContent';

const INFINITY = 'Infinity';

export type IconProps = {
className?: string;
width?: string | number;
height?: string | number;
viewBox?: string;
style?: React.CSSProperties;
color?: string;
};

export type IconWrapperProps = {
children?: React.ReactNode;
} & IconProps;

export const Icon = ({
className = '',
width = '24px',
height = '24px',
viewBox = '0 0 20 20',
children,
color,
...props
}: IconWrapperProps) => {
return (
<svg
width={width}
height={height}
viewBox={viewBox}
data-color={color}
fill="none"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
className={className}
style={{
display: 'flex',
flexShrink: 0,
...props.style,
}}
{...props}
>
{children}
</svg>
);
};

interface IncentivesButtonProps {
symbol: string;
incentives?: ReserveIncentiveResponse[];
displayBlank?: boolean;
market?: string;
protocolAction?: ProtocolAction;
protocolAPY?: number;
address?: string;
}

export function IncentivesIcon(props: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
{...props}
viewBox="0 0 16 16"
className={props.className}
style={{
display: 'flex',
flexShrink: 0,
...props.style,
}}
>
<circle
cx="7.2"
cy="7.2"
r="7.2"
stroke="#9391F7"
strokeWidth="1.5"
transform="matrix(1 0 0 -1 .8 15.2)"
/>
<path
stroke="#BCBBFF"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="m4.557 8.082.891 1.132a1 1 0 0 0 1.591-.026l1.75-2.376a1 1 0 0 1 1.591-.026l1.064 1.35"
/>
</svg>
);
}

const BlankIncentives = () => {
Expand All @@ -49,6 +130,8 @@ export const MeritIncentivesButton = (params: {
symbol: string;
market: string;
protocolAction?: ProtocolAction;
protocolAPY?: number;
protocolIncentives?: ReserveIncentiveResponse[];
}) => {
const [open, setOpen] = useState(false);
const { data: meritIncentives } = useMeritIncentives(params);
Expand All @@ -57,14 +140,17 @@ export const MeritIncentivesButton = (params: {
return null;
}

// Show only merit incentives APR
const displayValue = +meritIncentives.incentiveAPR;

return (
<ContentWithTooltip
tooltipContent={<MeritIncentivesTooltipContent meritIncentives={meritIncentives} />}
withoutHover
setOpen={setOpen}
open={open}
>
<Content incentives={[meritIncentives]} incentivesNetAPR={+meritIncentives.incentiveAPR} />
<Content incentives={[meritIncentives]} incentivesNetAPR={displayValue} />
</ContentWithTooltip>
);
};
Expand All @@ -73,6 +159,8 @@ export const MerklIncentivesButton = (params: {
market: string;
rewardedAsset?: string;
protocolAction?: ProtocolAction;
protocolAPY?: number;
protocolIncentives?: ReserveIncentiveResponse[];
}) => {
const [open, setOpen] = useState(false);
const { data: merklIncentives } = useMerklIncentives(params);
Expand Down Expand Up @@ -158,7 +246,15 @@ export const SonicIncentivesButton = ({ rewardedAsset }: { rewardedAsset?: strin
);
};

export const IncentivesButton = ({ incentives, symbol, displayBlank }: IncentivesButtonProps) => {
export const IncentivesButton = ({
incentives,
symbol,
displayBlank,
market,
protocolAction,
protocolAPY,
address,
}: IncentivesButtonProps) => {
const [open, setOpen] = useState(false);

if (!(incentives && incentives.length > 0)) {
Expand All @@ -169,18 +265,16 @@ export const IncentivesButton = ({ incentives, symbol, displayBlank }: Incentive
}
}

const isIncentivesInfinity = incentives.some(
(incentive) => incentive.incentiveAPR === 'Infinity'
);
const isIncentivesInfinity = incentives.some((incentive) => incentive.incentiveAPR === INFINITY);
const incentivesAPRSum = isIncentivesInfinity
? 'Infinity'
? INFINITY
: incentives.reduce((aIncentive, bIncentive) => aIncentive + +bIncentive.incentiveAPR, 0);

const incentivesNetAPR = isIncentivesInfinity
? 'Infinity'
: incentivesAPRSum !== 'Infinity'
? INFINITY
: incentivesAPRSum !== INFINITY
? valueToBigNumber(incentivesAPRSum || 0).toNumber()
: 'Infinity';
: INFINITY;

return (
<ContentWithTooltip
Expand All @@ -189,6 +283,10 @@ export const IncentivesButton = ({ incentives, symbol, displayBlank }: Incentive
incentives={incentives}
incentivesNetAPR={incentivesNetAPR}
symbol={symbol}
market={market}
protocolAction={protocolAction}
protocolAPY={protocolAPY}
address={address}
/>
}
withoutHover
Expand All @@ -211,7 +309,7 @@ const Content = ({
plus,
}: {
incentives: ReserveIncentiveResponse[];
incentivesNetAPR: number | 'Infinity';
incentivesNetAPR: number | typeof INFINITY;
displayBlank?: boolean;
plus?: boolean;
}) => {
Expand All @@ -235,48 +333,72 @@ const Content = ({
}

const incentivesButtonValue = () => {
if (incentivesNetAPR !== 'Infinity' && incentivesNetAPR < 10000) {
return (
<FormattedNumber
value={incentivesNetAPR}
percent
variant="secondary12"
color="text.secondary"
/>
);
} else if (incentivesNetAPR !== 'Infinity' && incentivesNetAPR > 9999) {
return (
<FormattedNumber
value={incentivesNetAPR}
percent
compact
variant="secondary12"
color="text.secondary"
/>
);
} else if (incentivesNetAPR === 'Infinity') {
return (
<Typography variant="main12" color="text.secondary">
</Typography>
);
// NOTE: For GHO incentives, we want to show the formatted number given its on sGHO page only

const hasGhoIncentives = incentives.some(
(incentive) =>
incentive.rewardTokenSymbol?.toLowerCase().includes('gho') ||
incentive.rewardTokenSymbol?.toLowerCase().includes('agho') ||
incentive.rewardTokenSymbol?.toLowerCase().includes('abasgho')
);

if (hasGhoIncentives) {
if (incentivesNetAPR !== INFINITY && incentivesNetAPR < 10000) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<FormattedNumber
value={incentivesNetAPR}
percent
variant="secondary12"
color="text.secondary"
/>
<IncentivesIcon width="16" height="16" />
</Box>
);
} else if (incentivesNetAPR !== INFINITY && incentivesNetAPR > 9999) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<FormattedNumber
value={incentivesNetAPR}
percent
compact
variant="secondary12"
color="text.secondary"
/>
<IncentivesIcon width="16" height="16" />
</Box>
);
} else if (incentivesNetAPR === INFINITY) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="main12" color="text.secondary">
</Typography>
<IncentivesIcon width="16" height="16" />
</Box>
);
}
}

// Default behavior: show icon for non-GHO incentives
return (
<>
<IncentivesIcon width="16" height="16" />
</>
);
};

const iconSize = 12;
// const iconSize = 12;

return (
<Box
sx={(theme) => ({
p: { xs: '0 4px', xsm: '2px 4px' },
border: `1px solid ${open ? theme.palette.action.disabled : theme.palette.divider}`,
sx={() => ({
borderRadius: '4px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'opacity 0.2s ease',
bgcolor: open ? 'action.hover' : 'transparent',
'&:hover': {
bgcolor: 'action.hover',
borderColor: 'action.disabled',
Expand All @@ -288,10 +410,10 @@ const Content = ({
setOpen(!open);
}}
>
<Box sx={{ mr: 2 }}>
<Box sx={{ p: 0.5 }}>
{plus ? '+' : ''} {incentivesButtonValue()}
</Box>
<Box sx={{ display: 'inline-flex' }}>
{/* <Box sx={{ display: 'inline-flex' }}>
<>
{incentives.length < 5 ? (
<>
Expand Down Expand Up @@ -333,7 +455,7 @@ const Content = ({
</>
)}
</>
</Box>
</Box> */}
</Box>
);
};
Expand Down
Loading
Loading