Skip to content
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
65 changes: 60 additions & 5 deletions src/hooks/pool/useUserYield.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { ProtocolAction } from '@aave/contract-helpers';
import { FormatUserSummaryAndIncentivesResponse } from '@aave/math-utils';
import { BigNumber } from 'bignumber.js';
import memoize from 'micro-memoize';
import { MarketDataType } from 'src/ui-config/marketsConfig';

import { getMeritData } from '../useMeritIncentives';
import { useUserMeritIncentives } from '../useUserMeritIncentives';
import {
FormattedReservesAndIncentives,
usePoolsFormattedReserves,
} from './usePoolFormattedReserves';
import { useUserSummariesAndIncentives } from './useUserSummaryAndIncentives';
import { combineQueries, SimplifiedUseQueryResult } from './utils';

type UserMeritIncentivesData = {
currentAPR: {
actionsAPY: Record<string, number>;
};
} | null;

export interface UserYield {
earnedAPY: number;
debtAPY: number;
Expand All @@ -19,7 +28,9 @@ export interface UserYield {
const formatUserYield = memoize(
(
formattedPoolReserves: FormattedReservesAndIncentives[],
user: FormatUserSummaryAndIncentivesResponse
user: FormatUserSummaryAndIncentivesResponse,
userMeritIncentives?: UserMeritIncentivesData,
marketTitle?: string
) => {
const proportions = user.userReservesData.reduce(
(acc, value) => {
Expand All @@ -39,6 +50,23 @@ const formatUserYield = memoize(
);
});
}

// Add merit incentives for supply positions
if (userMeritIncentives?.currentAPR?.actionsAPY) {
const meritData = getMeritData(marketTitle || '', reserve.symbol);
if (meritData) {
meritData.forEach((merit) => {
if (merit.protocolAction === ProtocolAction.supply) {
const meritAPY = userMeritIncentives.currentAPR.actionsAPY[merit.action];
if (meritAPY) {
acc.positiveProportion = acc.positiveProportion.plus(
new BigNumber(meritAPY / 100).multipliedBy(value.underlyingBalanceUSD)
);
}
}
});
}
}
}
if (value.variableBorrowsUSD !== '0') {
acc.negativeProportion = acc.negativeProportion.plus(
Expand All @@ -51,6 +79,24 @@ const formatUserYield = memoize(
);
});
}

// Add merit incentives for borrow positions (reduces borrowing cost)
if (userMeritIncentives?.currentAPR?.actionsAPY) {
const meritData = getMeritData(marketTitle || '', reserve.symbol);
if (meritData) {
meritData.forEach((merit) => {
if (merit.protocolAction === ProtocolAction.borrow) {
const meritAPY = userMeritIncentives.currentAPR.actionsAPY[merit.action];
if (meritAPY) {
// For borrow positions, merit incentives reduce the effective borrow cost
acc.positiveProportion = acc.positiveProportion.plus(
new BigNumber(meritAPY / 100).multipliedBy(value.variableBorrowsUSD)
);
}
}
});
}
}
}
} else {
throw new Error('no possible to calculate net apy');
Expand Down Expand Up @@ -81,23 +127,32 @@ const formatUserYield = memoize(
);

export const useUserYields = (
marketsData: MarketDataType[]
marketsData: MarketDataType[],
userAddress?: string
): SimplifiedUseQueryResult<UserYield>[] => {
const poolsFormattedReservesQuery = usePoolsFormattedReserves(marketsData);
const userSummaryQuery = useUserSummariesAndIncentives(marketsData);
const userMeritIncentivesQuery = useUserMeritIncentives(userAddress);

return poolsFormattedReservesQuery.map((elem, index) => {
const selector = (
formattedPoolReserves: FormattedReservesAndIncentives[],
user: FormatUserSummaryAndIncentivesResponse
) => {
return formatUserYield(formattedPoolReserves, user);
// Get merit incentives data separately
const meritIncentives = userMeritIncentivesQuery.data;
return formatUserYield(
formattedPoolReserves,
user,
meritIncentives,
marketsData[index].market
);
};

return combineQueries([elem, userSummaryQuery[index]] as const, selector);
});
};

export const useUserYield = (marketData: MarketDataType) => {
return useUserYields([marketData])[0];
export const useUserYield = (marketData: MarketDataType, userAddress?: string) => {
return useUserYields([marketData], userAddress)[0];
};
30 changes: 30 additions & 0 deletions src/hooks/useEnhancedUserYield.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
import { useRootStore } from 'src/store/root';

import { useAppDataContext } from './app-data-provider/useAppDataProvider';
import { useUserYield } from './pool/useUserYield';

/**
* Enhanced user yield hook that includes merit incentives in Net APY calculation
* This provides a more comprehensive view of user returns including merit rewards
*/
export const useEnhancedUserYield = () => {
const { currentAccount } = useWeb3Context();
const currentMarketData = useRootStore((store) => store.currentMarketData);
const { user } = useAppDataContext();

const enhancedUserYield = useUserYield(currentMarketData, currentAccount);

const netAPYWithMerit = enhancedUserYield.data?.netAPY ?? user?.netAPY ?? 0;
const earnedAPYWithMerit = enhancedUserYield.data?.earnedAPY ?? 0;
const debtAPYWithMerit = enhancedUserYield.data?.debtAPY ?? 0;

return {
netAPY: netAPYWithMerit,
earnedAPY: earnedAPYWithMerit,
debtAPY: debtAPYWithMerit,
loading: enhancedUserYield.isPending,
error: enhancedUserYield.error,
hasEnhancedData: !!enhancedUserYield.data,
};
};
8 changes: 5 additions & 3 deletions src/hooks/useMeritIncentives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ export type MeritReserveIncentiveData = Omit<ReserveIncentiveResponse, 'incentiv
protocolAction?: ProtocolAction;
};

const getMeritData = (market: string, symbol: string): MeritReserveIncentiveData[] | undefined =>
MERIT_DATA_MAP[market]?.[symbol];
export const getMeritData = (
market: string,
symbol: string
): MeritReserveIncentiveData[] | undefined => MERIT_DATA_MAP[market]?.[symbol];

const antiLoopMessage =
'Borrowing of some assets or holding of some token may impact the amount of rewards you are eligible for. Please check the forum post for the full eligibility criteria.';
Expand Down Expand Up @@ -155,7 +157,7 @@ const StSLoopIncentiveProgramForumLink =
const baseIncentivesForumLink =
'https://governance.aave.com/t/arfc-base-incentive-campaign-funding/21983';

const MERIT_DATA_MAP: Record<string, Record<string, MeritReserveIncentiveData[]>> = {
export const MERIT_DATA_MAP: Record<string, Record<string, MeritReserveIncentiveData[]>> = {
[CustomMarket.proto_mainnet_v3]: {
GHO: [
{
Expand Down
60 changes: 60 additions & 0 deletions src/hooks/useUserMeritIncentives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useQuery } from '@tanstack/react-query';
import { convertAprToApy } from 'src/utils/utils';

import { MeritAction } from './useMeritIncentives';

type UserMeritIncentives = {
totalAPR: number;
actionsAPR: {
[key in MeritAction]: number | null | undefined;
};
};

type UserMeritIncentivesResponse = {
previousAPR: UserMeritIncentives | null;
currentAPR: UserMeritIncentives;
};

const url = 'https://apps.aavechan.com/api/merit/aprs';

export const useUserMeritIncentives = (userAddress?: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend to use only a hook for Merit and for each MeritAction key take the userAPR and if not available general APR

Copy link
Collaborator

Choose a reason for hiding this comment

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

@NandyBa how often is the user apr data updated, would it be common that we might not have the users data from the api?

Also just wanted to clarify that the user apr would include any boosters or penalties applied based on the campaign rules right?

return useQuery({
queryFn: async () => {
if (!userAddress) {
return null;
}

const response = await fetch(`${url}?user=${userAddress}`);
const data: UserMeritIncentivesResponse = await response.json();

return data;
},
queryKey: ['userMeritIncentives', userAddress],
staleTime: 1000 * 60 * 60, // 1 hour
enabled: !!userAddress,
select: (data) => {
if (!data) {
return null;
}

// Convert all APR values to APY using monthly compounding
const convertedActionsAPY: Record<string, number> = {};

Object.entries(data.currentAPR.actionsAPR).forEach(([action, apr]) => {
if (apr !== null && apr !== undefined) {
const aprDecimal = apr / 100; // Convert to decimal
const apy = convertAprToApy(aprDecimal);
convertedActionsAPY[action] = apy * 100; // Convert back to percentage
}
});

return {
...data,
currentAPR: {
...data.currentAPR,
actionsAPY: convertedActionsAPY, // Add converted APY values
},
};
},
});
};
4 changes: 3 additions & 1 deletion src/modules/dashboard/DashboardTopPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import { NoData } from '../../components/primitives/NoData';
import { TopInfoPanel } from '../../components/TopInfoPanel/TopInfoPanel';
import { TopInfoPanelItem } from '../../components/TopInfoPanel/TopInfoPanelItem';
import { useAppDataContext } from '../../hooks/app-data-provider/useAppDataProvider';
import { useEnhancedUserYield } from '../../hooks/useEnhancedUserYield';
import { LiquidationRiskParametresInfoModal } from './LiquidationRiskParametresModal/LiquidationRiskParametresModal';

export const DashboardTopPanel = () => {
const { user, reserves, loading } = useAppDataContext();
const { currentAccount } = useWeb3Context();
const { netAPY: enhancedNetAPY, hasEnhancedData } = useEnhancedUserYield();
const [open, setOpen] = useState(false);
const { openClaimRewards } = useModalContext();
const [
Expand Down Expand Up @@ -177,7 +179,7 @@ export const DashboardTopPanel = () => {
>
{currentAccount && user && Number(user.netWorthUSD) > 0 ? (
<FormattedNumber
value={user ? user.netAPY : 0}
value={hasEnhancedData ? enhancedNetAPY : user ? user.netAPY : 0}
variant={valueTypographyVariant}
visibleDecimals={2}
percent
Expand Down
Loading