From 17566faf2b4d23761ccd970abf1f6a116dc4893b Mon Sep 17 00:00:00 2001 From: Alejandro <95312462+AGMASO@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:14:32 +0100 Subject: [PATCH 1/3] feat: create hook to isolate displayapy calculation --- src/hooks/useIncentivizedApy.ts | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/hooks/useIncentivizedApy.ts diff --git a/src/hooks/useIncentivizedApy.ts b/src/hooks/useIncentivizedApy.ts new file mode 100644 index 0000000000..8f01f4616b --- /dev/null +++ b/src/hooks/useIncentivizedApy.ts @@ -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 }; +}; From eb403fdfda89fb2ca2b42ec163fdfbccb1d56e03 Mon Sep 17 00:00:00 2001 From: Alejandro <95312462+AGMASO@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:15:33 +0100 Subject: [PATCH 2/3] chore: style --- .../transactions/Switch/cowprotocol/cowprotocol.constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/transactions/Switch/cowprotocol/cowprotocol.constants.ts b/src/components/transactions/Switch/cowprotocol/cowprotocol.constants.ts index 72b1d935c6..03941a23ea 100644 --- a/src/components/transactions/Switch/cowprotocol/cowprotocol.constants.ts +++ b/src/components/transactions/Switch/cowprotocol/cowprotocol.constants.ts @@ -5,7 +5,7 @@ export const COW_UNSUPPORTED_ASSETS: Partial< Record>> > = { [ModalType.CollateralSwap]: { - [SupportedChainId.POLYGON]: "ALL", // Waiting for better solvers support + [SupportedChainId.POLYGON]: 'ALL', // Waiting for better solvers support [SupportedChainId.AVALANCHE]: [ '0x8eb270e296023e9d92081fdf967ddd7878724424'.toLowerCase(), // AVaMAI not supported '0x078f358208685046a11c85e8ad32895ded33a249'.toLowerCase(), // aVaWBTC not supported From 5aa8385ed64a0f0f76523297e750162a3aa12382 Mon Sep 17 00:00:00 2001 From: Alejandro <95312462+AGMASO@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:13:44 +0100 Subject: [PATCH 3/3] feat: added filtering for apys accounting extra incentives on market page --- .gitignore | 1 + src/components/incentives/IncentivesCard.tsx | 83 +++++++++++-------- src/modules/markets/MarketAssetsList.tsx | 68 +++++++++++++-- src/modules/markets/MarketAssetsListItem.tsx | 38 +++++++++ .../markets/MarketAssetsListMobileItem.tsx | 39 +++++++++ 5 files changed, 188 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index 4aa049068b..3e4e76a541 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ yarn-error.log* package-lock.json .eslintcache +.yarn-cache # IDE specific .idea diff --git a/src/components/incentives/IncentivesCard.tsx b/src/components/incentives/IncentivesCard.tsx index 706bcf999e..52512f6481 100644 --- a/src/components/incentives/IncentivesCard.tsx +++ b/src/components/incentives/IncentivesCard.tsx @@ -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'; @@ -32,6 +32,7 @@ interface IncentivesCardProps { protocolAction?: ProtocolAction; align?: 'center' | 'flex-end'; inlineIncentives?: boolean; + displayAPY?: number | 'Infinity'; } export const IncentivesCard = ({ @@ -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, @@ -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; + + 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; + }, [ + displayAPY, + incentives, + meritIncentives, + merklIncentives, + merklPointsIncentives, + protocolAPY, + protocolAction, + ]); const isSghoPage = typeof router?.asPath === 'string' && router.asPath.toLowerCase().startsWith('/sgho'); @@ -156,14 +167,14 @@ export const IncentivesCard = ({ > {value.toString() !== '-1' ? ( - {displayAPY === 'Infinity' ? ( + {computedDisplayAPY === '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; borrowProtocolIncentives: ReturnType; + 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({}); + + const handleApyChange = useCallback((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; @@ -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': @@ -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: @@ -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) { diff --git a/src/modules/markets/MarketAssetsListItem.tsx b/src/modules/markets/MarketAssetsListItem.tsx index fc1e626acd..45a5616b12 100644 --- a/src/modules/markets/MarketAssetsListItem.tsx +++ b/src/modules/markets/MarketAssetsListItem.tsx @@ -2,6 +2,7 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { Trans } from '@lingui/macro'; import { Box, Button, Typography } from '@mui/material'; import { useRouter } from 'next/router'; +import { useEffect } from 'react'; import { KernelAirdropTooltip } from 'src/components/infoTooltips/KernelAirdropTooltip'; import { OffboardingTooltip } from 'src/components/infoTooltips/OffboardingToolTip'; import { RenFILToolTip } from 'src/components/infoTooltips/RenFILToolTip'; @@ -24,6 +25,7 @@ import { ListItem } from '../../components/lists/ListItem'; import { FormattedNumber } from '../../components/primitives/FormattedNumber'; import { Link, ROUTES } from '../../components/primitives/Link'; import { TokenIcon } from '../../components/primitives/TokenIcon'; +import { useIncentivizedApy } from '../../hooks/useIncentivizedApy'; import { ReserveWithProtocolIncentives } from './MarketAssetsList'; export const MarketAssetsListItem = ({ ...reserve }: ReserveWithProtocolIncentives) => { @@ -31,6 +33,7 @@ export const MarketAssetsListItem = ({ ...reserve }: ReserveWithProtocolIncentiv const [trackEvent, currentMarket] = useRootStore( useShallow((store) => [store.trackEvent, store.currentMarket]) ); + const { onApyChange, id } = reserve; const offboardingDiscussion = AssetsBeingOffboarded[currentMarket]?.[reserve.underlyingToken.symbol]; const externalIncentivesTooltipsSupplySide = showExternalIncentivesTooltip( @@ -49,6 +52,39 @@ export const MarketAssetsListItem = ({ ...reserve }: ReserveWithProtocolIncentiv name: reserve.underlyingToken.name, }); + const supplyIncentivizedApy = useIncentivizedApy({ + symbol: reserve.underlyingToken.symbol, + market: currentMarket, + rewardedAsset: reserve.aToken.address, + protocolAction: ProtocolAction.supply, + protocolAPY: reserve.supplyInfo.apy.value, + protocolIncentives: reserve.supplyProtocolIncentives, + }); + const shouldShowBorrow = + !!reserve.borrowInfo && Number(reserve.borrowInfo.total.amount.value) > 0; + + const borrowIncentivizedApy = useIncentivizedApy({ + symbol: reserve.underlyingToken.symbol, + market: currentMarket, + rewardedAsset: shouldShowBorrow ? reserve.vToken.address : '', + protocolAction: ProtocolAction.borrow, + protocolAPY: reserve.borrowInfo?.apy.value ?? 0, + protocolIncentives: reserve.borrowProtocolIncentives, + }); + + const supplyDisplayApy = supplyIncentivizedApy.displayAPY; + const borrowDisplayApy = borrowIncentivizedApy?.displayAPY; + + useEffect(() => { + if (supplyDisplayApy === undefined) return; + onApyChange(id, 'supply', supplyDisplayApy); + }, [id, onApyChange, supplyDisplayApy]); + + useEffect(() => { + if (!borrowIncentivizedApy || borrowDisplayApy === undefined) return; + onApyChange(id, 'borrow', borrowDisplayApy); + }, [id, onApyChange, borrowDisplayApy]); + const displayIconSymbol = iconSymbol?.toLowerCase() !== reserve.underlyingToken.symbol.toLowerCase() ? iconSymbol @@ -122,6 +158,7 @@ export const MarketAssetsListItem = ({ ...reserve }: ReserveWithProtocolIncentiv } market={currentMarket} protocolAction={ProtocolAction.supply} + displayAPY={supplyDisplayApy} /> @@ -160,6 +197,7 @@ export const MarketAssetsListItem = ({ ...reserve }: ReserveWithProtocolIncentiv } market={currentMarket} protocolAction={ProtocolAction.borrow} + displayAPY={borrowDisplayApy} /> {reserve.borrowInfo?.borrowingState === 'DISABLED' && !reserve.isFrozen && diff --git a/src/modules/markets/MarketAssetsListMobileItem.tsx b/src/modules/markets/MarketAssetsListMobileItem.tsx index 77822f5f95..f94affc6df 100644 --- a/src/modules/markets/MarketAssetsListMobileItem.tsx +++ b/src/modules/markets/MarketAssetsListMobileItem.tsx @@ -1,6 +1,7 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { Trans } from '@lingui/macro'; import { Box, Button, Divider } from '@mui/material'; +import { useEffect } from 'react'; import { KernelAirdropTooltip } from 'src/components/infoTooltips/KernelAirdropTooltip'; import { SpkAirdropTooltip } from 'src/components/infoTooltips/SpkAirdropTooltip'; import { SuperFestTooltip } from 'src/components/infoTooltips/SuperFestTooltip'; @@ -17,6 +18,7 @@ import { IncentivesCard } from '../../components/incentives/IncentivesCard'; import { FormattedNumber } from '../../components/primitives/FormattedNumber'; import { Link, ROUTES } from '../../components/primitives/Link'; import { Row } from '../../components/primitives/Row'; +import { useIncentivizedApy } from '../../hooks/useIncentivizedApy'; import { ListMobileItemWrapper } from '../dashboard/lists/ListMobileItemWrapper'; import { ReserveWithProtocolIncentives } from './MarketAssetsList'; @@ -24,6 +26,7 @@ export const MarketAssetsListMobileItem = ({ ...reserve }: ReserveWithProtocolIn const [trackEvent, currentMarket] = useRootStore( useShallow((store) => [store.trackEvent, store.currentMarket]) ); + const { onApyChange, id } = reserve; const externalIncentivesTooltipsSupplySide = showExternalIncentivesTooltip( reserve.underlyingToken.symbol, @@ -46,6 +49,40 @@ export const MarketAssetsListMobileItem = ({ ...reserve }: ReserveWithProtocolIn ? iconSymbol : reserve.underlyingToken.symbol; + const supplyIncentivizedApy = useIncentivizedApy({ + symbol: reserve.underlyingToken.symbol, + market: currentMarket, + rewardedAsset: reserve.aToken.address, + protocolAction: ProtocolAction.supply, + protocolAPY: reserve.supplyInfo.apy.value, + protocolIncentives: reserve.supplyProtocolIncentives, + }); + + const shouldShowBorrow = + !!reserve.borrowInfo && Number(reserve.borrowInfo.total.amount.value) > 0; + + const borrowIncentivizedApy = useIncentivizedApy({ + symbol: reserve.underlyingToken.symbol, + market: currentMarket, + rewardedAsset: shouldShowBorrow ? reserve.vToken.address : '', + protocolAction: ProtocolAction.borrow, + protocolAPY: reserve.borrowInfo?.apy.value ?? 0, + protocolIncentives: reserve.borrowProtocolIncentives, + }); + + const supplyDisplayApy = supplyIncentivizedApy.displayAPY; + const borrowDisplayApy = borrowIncentivizedApy?.displayAPY; + + useEffect(() => { + if (supplyDisplayApy === undefined) return; + onApyChange(id, 'supply', supplyDisplayApy); + }, [id, onApyChange, supplyDisplayApy]); + + useEffect(() => { + if (!borrowIncentivizedApy || borrowDisplayApy === undefined) return; + onApyChange(id, 'borrow', borrowDisplayApy); + }, [id, onApyChange, borrowDisplayApy, borrowIncentivizedApy]); + return ( @@ -157,6 +195,7 @@ export const MarketAssetsListMobileItem = ({ ...reserve }: ReserveWithProtocolIn } market={currentMarket} protocolAction={ProtocolAction.borrow} + displayAPY={borrowDisplayApy} /> {reserve.borrowInfo?.borrowingState === 'DISABLED' && !reserve.isFrozen &&