Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ yarn-error.log*
package-lock.json

.eslintcache
.yarn-cache

# IDE specific
.idea
Expand Down
83 changes: 47 additions & 36 deletions src/components/incentives/IncentivesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ProtocolAction } from '@aave/contract-helpers';
import { ReserveIncentiveResponse } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives';
import { Box, Typography } from '@mui/material';
import { useRouter } from 'next/router';
import { ReactNode } from 'react';
import { ReactNode, useMemo } from 'react';
import { ENABLE_SELF_CAMPAIGN, useMeritIncentives } from 'src/hooks/useMeritIncentives';
import { useMerklIncentives } from 'src/hooks/useMerklIncentives';
import { useMerklPointsIncentives } from 'src/hooks/useMerklPointsIncentives';
Expand Down Expand Up @@ -32,6 +32,7 @@ interface IncentivesCardProps {
protocolAction?: ProtocolAction;
align?: 'center' | 'flex-end';
inlineIncentives?: boolean;
displayAPY?: number | 'Infinity';
}

export const IncentivesCard = ({
Expand All @@ -47,21 +48,10 @@ export const IncentivesCard = ({
market,
protocolAction,
inlineIncentives = false,
displayAPY,
}: IncentivesCardProps) => {
const router = useRouter();
const protocolAPY = typeof value === 'string' ? parseFloat(value) : value;

const protocolIncentivesAPR =
incentives?.reduce((sum, inc) => {
if (inc.incentiveAPR === 'Infinity' || sum === 'Infinity') {
return 'Infinity';
}
return sum + +inc.incentiveAPR;
}, 0 as number | 'Infinity') || 0;

const protocolIncentivesAPY = convertAprToApy(
protocolIncentivesAPR === 'Infinity' ? 0 : protocolIncentivesAPR
);
const { data: meritIncentives } = useMeritIncentives({
symbol,
market,
Expand All @@ -86,27 +76,48 @@ export const IncentivesCard = ({
protocolIncentives: incentives || [],
});

const meritIncentivesAPR = meritIncentives?.breakdown?.meritIncentivesAPR || 0;

// TODO: This is a one-off for the Self campaign.
// Remove once the Self incentives are finished.
const selfAPY = ENABLE_SELF_CAMPAIGN ? meritIncentives?.variants?.selfAPY ?? 0 : 0;
const totalMeritAPY = meritIncentivesAPR + selfAPY;

const merklIncentivesAPR = merklPointsIncentives?.breakdown?.points
? merklPointsIncentives.breakdown.merklIncentivesAPR || 0
: merklIncentives?.breakdown?.merklIncentivesAPR || 0;

const isBorrow = protocolAction === ProtocolAction.borrow;

// If any incentive is infinite, the total should be infinite
const hasInfiniteIncentives = protocolIncentivesAPR === 'Infinity';

const displayAPY = hasInfiniteIncentives
? 'Infinity'
: isBorrow
? protocolAPY - (protocolIncentivesAPY as number) - totalMeritAPY - merklIncentivesAPR
: protocolAPY + (protocolIncentivesAPY as number) + totalMeritAPY + merklIncentivesAPR;
const computedDisplayAPY = useMemo(() => {
if (displayAPY !== undefined) {
return displayAPY;
}

const protocolIncentivesAPR =
incentives?.reduce((sum, inc) => {
if (inc.incentiveAPR === 'Infinity' || sum === 'Infinity') {
return 'Infinity';
}
return sum + +inc.incentiveAPR;
}, 0 as number | 'Infinity') || 0;

const protocolIncentivesAPY = convertAprToApy(
protocolIncentivesAPR === 'Infinity' ? 0 : protocolIncentivesAPR
);

const meritIncentivesAPR = meritIncentives?.breakdown?.meritIncentivesAPR || 0;
const selfAPY = ENABLE_SELF_CAMPAIGN ? meritIncentives?.variants?.selfAPY ?? 0 : 0;
const totalMeritAPY = meritIncentivesAPR + selfAPY;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is APR + APY okay?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, meritIncentivesAPR has been kept with this name (probably to avoid conflicts between files), but it actually represents the APY.
Screenshot 2025-12-02 at 10 34 38


const merklIncentivesAPR = merklPointsIncentives?.breakdown?.points
? merklPointsIncentives.breakdown.merklIncentivesAPR || 0
: merklIncentives?.breakdown?.merklIncentivesAPR || 0;

const isBorrow = protocolAction === ProtocolAction.borrow;
const hasInfiniteIncentives = protocolIncentivesAPR === 'Infinity';

return hasInfiniteIncentives
? 'Infinity'
: isBorrow
? protocolAPY - (protocolIncentivesAPY as number) - totalMeritAPY - merklIncentivesAPR
: protocolAPY + (protocolIncentivesAPY as number) + totalMeritAPY + merklIncentivesAPR;
Comment on lines +107 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can remove this one because we have something similar in useIncentivizedApy.ts?

}, [
displayAPY,
incentives,
meritIncentives,
merklIncentives,
merklPointsIncentives,
protocolAPY,
protocolAction,
]);

const isSghoPage =
typeof router?.asPath === 'string' && router.asPath.toLowerCase().startsWith('/sgho');
Expand Down Expand Up @@ -156,14 +167,14 @@ export const IncentivesCard = ({
>
{value.toString() !== '-1' ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{displayAPY === 'Infinity' ? (
{computedDisplayAPY === 'Infinity' ? (
<Typography variant={variant} color={color || 'text.secondary'}>
∞ %
</Typography>
) : (
<FormattedNumber
data-cy={`apy`}
value={displayAPY}
value={computedDisplayAPY}
percent
variant={variant}
symbolsVariant={symbolsVariant}
Expand Down
92 changes: 92 additions & 0 deletions src/hooks/useIncentivizedApy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ProtocolAction } from '@aave/contract-helpers';
import { ReserveIncentiveResponse } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives';
import { ENABLE_SELF_CAMPAIGN, useMeritIncentives } from 'src/hooks/useMeritIncentives';
import { convertAprToApy } from 'src/utils/utils';

import { useMerklIncentives } from './useMerklIncentives';
import { useMerklPointsIncentives } from './useMerklPointsIncentives';

interface IncentivizedApyParams {
symbol: string;
market: string;
rewardedAsset: string;
protocolAction?: ProtocolAction;
protocolAPY: number | string;
protocolIncentives?: ReserveIncentiveResponse[];
}
type UseIncentivizedApyResult = {
displayAPY: number | 'Infinity';
hasInfiniteIncentives: boolean;
isLoading: boolean;
};
export const useIncentivizedApy = ({
symbol,
market,
rewardedAsset: address,
protocolAction,
protocolAPY: value,
protocolIncentives: incentives = [],
}: IncentivizedApyParams): UseIncentivizedApyResult => {
const protocolAPY = typeof value === 'string' ? parseFloat(value) : value;

const protocolIncentivesAPR =
incentives?.reduce((sum, inc) => {
if (inc.incentiveAPR === 'Infinity' || sum === 'Infinity') {
return 'Infinity';
}
return sum + +inc.incentiveAPR;
}, 0 as number | 'Infinity') || 0;

const protocolIncentivesAPY = convertAprToApy(
protocolIncentivesAPR === 'Infinity' ? 0 : protocolIncentivesAPR
);
const { data: meritIncentives, isLoading: meritLoading } = useMeritIncentives({
symbol,
market,
protocolAction,
protocolAPY,
protocolIncentives: incentives || [],
});

const { data: merklIncentives, isLoading: merklLoading } = useMerklIncentives({
market,
rewardedAsset: address,
protocolAction,
protocolAPY,
protocolIncentives: incentives || [],
});

const { data: merklPointsIncentives, isLoading: merklPointsLoading } = useMerklPointsIncentives({
market,
rewardedAsset: address,
protocolAction,
protocolAPY,
protocolIncentives: incentives || [],
});

const isLoading = meritLoading || merklLoading || merklPointsLoading;

const meritIncentivesAPR = meritIncentives?.breakdown?.meritIncentivesAPR || 0;

// TODO: This is a one-off for the Self campaign.
// Remove once the Self incentives are finished.
const selfAPY = ENABLE_SELF_CAMPAIGN ? meritIncentives?.variants?.selfAPY ?? 0 : 0;
const totalMeritAPY = meritIncentivesAPR + selfAPY;

const merklIncentivesAPR = merklPointsIncentives?.breakdown?.points
? merklPointsIncentives.breakdown.merklIncentivesAPR || 0
: merklIncentives?.breakdown?.merklIncentivesAPR || 0;

const isBorrow = protocolAction === ProtocolAction.borrow;

// If any incentive is infinite, the total should be infinite
const hasInfiniteIncentives = protocolIncentivesAPR === 'Infinity';

const displayAPY = hasInfiniteIncentives
? 'Infinity'
: isBorrow
? protocolAPY - (protocolIncentivesAPY as number) - totalMeritAPY - merklIncentivesAPR
: protocolAPY + (protocolIncentivesAPY as number) + totalMeritAPY + merklIncentivesAPR;

return { displayAPY, hasInfiniteIncentives, isLoading };
};
68 changes: 63 additions & 5 deletions src/modules/markets/MarketAssetsList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro';
import { useMediaQuery } from '@mui/material';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { mapAaveProtocolIncentives } from 'src/components/incentives/incentives.helper';
import { VariableAPYTooltip } from 'src/components/infoTooltips/VariableAPYTooltip';
import { ListColumn } from 'src/components/lists/ListColumn';
Expand Down Expand Up @@ -46,15 +46,60 @@ type MarketAssetsListProps = {
reserves: ReserveWithId[];
loading: boolean;
};

export type IncentivizedApySide = 'supply' | 'borrow';
export type IncentivizedApyState = Record<
string,
Partial<Record<IncentivizedApySide, number | 'Infinity'>>
>;
export type ApyUpdateHandler = (
reserveId: string,
side: IncentivizedApySide,
value: number | 'Infinity'
) => void;

const getComparableApy = (
storedValue: number | 'Infinity' | undefined,
fallback: number
): number => {
if (storedValue === 'Infinity') {
return Number.POSITIVE_INFINITY;
}
if (typeof storedValue === 'number' && !Number.isNaN(storedValue)) {
return storedValue;
}
return fallback;
};

export type ReserveWithProtocolIncentives = ReserveWithId & {
supplyProtocolIncentives: ReturnType<typeof mapAaveProtocolIncentives>;
borrowProtocolIncentives: ReturnType<typeof mapAaveProtocolIncentives>;
onApyChange: ApyUpdateHandler;
};

export default function MarketAssetsList({ reserves, loading }: MarketAssetsListProps) {
const isTableChangedToCards = useMediaQuery('(max-width:1125px)');
const [sortName, setSortName] = useState('');
const [sortDesc, setSortDesc] = useState(false);
const [incentivizedApys, setIncentivizedApys] = useState<IncentivizedApyState>({});

const handleApyChange = useCallback<ApyUpdateHandler>((reserveId, side, value) => {
setIncentivizedApys((prev) => {
const current = prev[reserveId]?.[side];
if (current === value) {
return prev;
}

return {
...prev,
[reserveId]: {
...prev[reserveId],
[side]: value,
},
};
});
}, []);

const sortedReserves = [...reserves].sort((a, b) => {
if (!sortName) return 0;

Expand All @@ -76,8 +121,14 @@ export default function MarketAssetsList({ reserves, loading }: MarketAssetsList
break;

case 'supplyInfo.apy.value':
aValue = Number(a.supplyInfo.apy.value) || 0;
bValue = Number(b.supplyInfo.apy.value) || 0;
aValue = getComparableApy(
incentivizedApys[a.id]?.supply,
Number(a.supplyInfo.apy.value) || 0
);
bValue = getComparableApy(
incentivizedApys[b.id]?.supply,
Number(b.supplyInfo.apy.value) || 0
);
break;

case 'borrowInfo.total.usd':
Expand All @@ -86,8 +137,14 @@ export default function MarketAssetsList({ reserves, loading }: MarketAssetsList
break;

case 'borrowInfo.apy.value':
aValue = Number(a.borrowInfo?.apy.value) || 0;
bValue = Number(b.borrowInfo?.apy.value) || 0;
aValue = getComparableApy(
incentivizedApys[a.id]?.borrow,
Number(a.borrowInfo?.apy.value) || 0
);
bValue = getComparableApy(
incentivizedApys[b.id]?.borrow,
Number(b.borrowInfo?.apy.value) || 0
);
break;

default:
Expand All @@ -102,6 +159,7 @@ export default function MarketAssetsList({ reserves, loading }: MarketAssetsList
...reserve,
supplyProtocolIncentives: mapAaveProtocolIncentives(reserve.incentives, 'supply'),
borrowProtocolIncentives: mapAaveProtocolIncentives(reserve.incentives, 'borrow'),
onApyChange: handleApyChange,
}));
// Show loading state when loading
if (loading) {
Expand Down
Loading
Loading