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 @@ -51,3 +51,4 @@ package-lock.json

# Sentry Config File
.env.sentry-build-plugin
tsconfig.tsbuildinfo
1 change: 1 addition & 0 deletions src/components/incentives/IncentivesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface IncentivesCardProps {
symbol: string;
value: string | number;
incentives?: ReserveIncentiveResponse[];
/** aToken / vToken address (legacy; hook resolves underlying internally). */
address?: string;
variant?: 'main14' | 'main16' | 'secondary14';
symbolsVariant?: 'secondary14' | 'secondary16';
Expand Down
10 changes: 10 additions & 0 deletions src/components/incentives/IncentivesTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ const IncentivesSymbolMap: {
symbol: 'aUSDm',
aToken: true,
},
aCelUSDT: {
tokenIconSymbol: 'USDT',
symbol: 'aUSDT',
aToken: true,
},
aGnoEURe: {
tokenIconSymbol: 'EURe',
symbol: 'aEURe',
Expand Down Expand Up @@ -233,6 +238,11 @@ const IncentivesSymbolMap: {
symbol: 'aGHO',
aToken: true,
},
aInkWlUSDe: {
tokenIconSymbol: 'USDe',
symbol: 'aUSDe',
aToken: true,
},
};

interface IncentivesTooltipContentProps {
Expand Down
33 changes: 19 additions & 14 deletions src/components/incentives/MeritIncentivesTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,19 @@ interface CampaignConfig {
}

const isCeloAction = (action: MeritAction): boolean => {
return [
MeritAction.CELO_SUPPLY_CELO,
MeritAction.CELO_SUPPLY_USDT,
MeritAction.CELO_SUPPLY_USDC,
MeritAction.CELO_SUPPLY_WETH,
MeritAction.CELO_SUPPLY_MULTIPLE_BORROW_USDT,
MeritAction.CELO_BORROW_CELO,
MeritAction.CELO_BORROW_USDT,
MeritAction.CELO_BORROW_USDC,
MeritAction.CELO_BORROW_WETH,
].includes(action);
return (
[
MeritAction.CELO_SUPPLY_CELO,
MeritAction.CELO_SUPPLY_USDT,
MeritAction.CELO_SUPPLY_USDC,
MeritAction.CELO_SUPPLY_WETH,
MeritAction.CELO_SUPPLY_MULTIPLE_BORROW_USDT,
MeritAction.CELO_BORROW_CELO,
MeritAction.CELO_BORROW_USDT,
MeritAction.CELO_BORROW_USDC,
MeritAction.CELO_BORROW_WETH,
] as string[]
).includes(action);
};

const selfCampaignConfig: Map<MeritAction, { limit: string; token: string }> = new Map([
Expand Down Expand Up @@ -122,12 +124,15 @@ export const MeritIncentivesTooltipContent = ({
};
const meritIncentivesFormatted = getSymbolMap(meritIncentives);
const isCombinedMeritIncentives: boolean = meritIncentives.activeActions.length > 1;
const campaignConfig = getCampaignConfig(meritIncentives.action);
const selfConfig = selfCampaignConfig.get(meritIncentives.action);
// `action` is now optional (backend-driven). Fall back to an empty string
// so the switch/lookup helpers match their STANDARD branch.
const primaryAction = meritIncentives.action ?? '';
const campaignConfig = getCampaignConfig(primaryAction);
const selfConfig = selfCampaignConfig.get(primaryAction);

const remainingCustomMessage = getRemainingMessagesWhenCombined(
meritIncentives.activeActions,
meritIncentives.action,
primaryAction,
isCombinedMeritIncentives,
meritIncentives.actionMessages
);
Expand Down
52 changes: 4 additions & 48 deletions src/components/incentives/MerklIncentivesTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,55 +185,11 @@ export const MerklIncentivesTooltipContent = ({
</Typography>
</Box>
</Row>
) : merklIncentives.rewardsTokensMappedApys &&
merklIncentives.rewardsTokensMappedApys.length > 1 ? (
<>
{merklIncentives.rewardsTokensMappedApys.map((reward, index) => {
const { tokenIconSymbol, symbol, aToken } = getSymbolMap({
rewardTokenSymbol: reward.token.symbol,
rewardTokenAddress: reward.token.address,
incentiveAPR: reward.apy.toString(),
});
return (
<Row
key={index}
height={32}
caption={
<Box
sx={{
display: 'flex',
alignItems: 'center',
mb: 0,
}}
>
<TokenIcon
symbol={tokenIconSymbol}
aToken={aToken}
sx={{ fontSize: '20px', mr: 1 }}
/>
<Typography variant={typographyVariant}>{symbol}</Typography>
<Typography variant={typographyVariant} sx={{ ml: 0.5 }}>
{merklIncentives.breakdown.isBorrow ? '(-)' : '(+)'}
</Typography>
</Box>
}
width="100%"
>
<Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
<FormattedNumber
value={merklIncentives.breakdown.isBorrow ? -reward.apy : reward.apy}
percent
variant={typographyVariant}
/>
<Typography variant={typographyVariant} sx={{ ml: 1 }}>
<Trans>APY</Trans>
</Typography>
</Box>
</Row>
);
})}
</>
) : (
// Note: legacy multi-reward-token rendering (`rewardsTokensMappedApys`)
// is gone. The V3 backend returns one `MerklSupply/Borrow`
// variant per reserve per direction with a single `payoutToken`,
// so the single-row render below covers all live campaigns.
<Row
height={32}
caption={
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChainId, Stake } from '@aave/contract-helpers';
import { ChainId, ProtocolAction, Stake } from '@aave/contract-helpers';
import { normalize, valueToBigNumber } from '@aave/math-utils';
import { Trans } from '@lingui/macro';
import { Typography } from '@mui/material';
Expand Down Expand Up @@ -38,6 +38,7 @@ export const SavingsGhoModalDepositContent = () => {
const { data: meritIncentives } = useMeritIncentives({
symbol: 'GHO',
market: currentMarketData.market,
protocolAction: ProtocolAction.stake,
});
const [_amount, setAmount] = useState('');
const amountRef = useRef<string>();
Expand Down
7 changes: 6 additions & 1 deletion src/hooks/app-data-provider/useAppDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@ export const AppDataProvider: React.FC<PropsWithChildren> = ({ children }) => {

const marketAddress = currentMarketData.addresses.LENDING_POOL.toLowerCase();

const sdkMarket = data?.find((item) => item.address.toLowerCase() === marketAddress);
// react-query's structural sharing can replace our Market[] with a
// structurally-similar plain object on refetch when it encounters
// non-POJO values (e.g. bigint-ish strings wrapped by the SDK). Guard
// before calling Array.prototype methods.
const marketsList = Array.isArray(data) ? data : [];
const sdkMarket = marketsList.find((item) => item.address.toLowerCase() === marketAddress);

const totalBorrows = sdkMarket?.borrowReserves.reduce((acc, reserve) => {
const value = reserve.borrowInfo?.total?.usd ?? 0;
Expand Down
116 changes: 116 additions & 0 deletions src/hooks/pool/usePoolsMerits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Per-market Merit APR lookup for the net-APY calculation in
* `useUserYield`.
*
* Reads directly from the SDK's `markets()` query (same react-query cache
* as `useAppDataProvider`'s `useMarketsData`), extracts each reserve's
* active `MeritSupply/Borrow/Conditional` incentive, and keys by
* underlying address. The backend already evaluates `userEligible` when
* the user address is passed, so we only credit APR for reserves the user
* is actually eligible for — same behaviour as the legacy aavechan
* per-user fetch.
*
* No new GraphQL query: the shared cache means calling this hook
* alongside the main AppDataProvider fetch is a cache hit.
*/
import { chainId as sdkChainId, evmAddress, OrderDirection } from '@aave/client';
import { markets } from '@aave/client/actions';
import { useQueries } from '@tanstack/react-query';
import { client } from 'pages/_app.page';
import { MarketDataType } from 'src/ui-config/marketsConfig';
import { queryKeysFactory } from 'src/ui-config/queries';

/**
* Map of `lowercase(underlyingAddress) -> {supplyApr, borrowApr}`. Backed
* by a plain Record because react-query's default `structuralSharing`
* deep-merges fetched data against the previous value, and `Map` instances
* don't round-trip through that merge — they come back as plain objects on
* refetch and `.get()` blows up at the consumer.
*/
export type MeritAprByUnderlying = Record<string, { supplyApr: number; borrowApr: number }>;

const EMPTY_MAP: MeritAprByUnderlying = Object.freeze({});

type Incentive = {
__typename?: string;
userEligible?: boolean | null;
extraSupplyApr?: { formatted: string } | null;
borrowAprDiscount?: { formatted: string } | null;
extraApr?: { formatted: string } | null;
};

const parseApr = (value?: { formatted: string } | null): number => {
if (!value) return 0;
const n = parseFloat(value.formatted);
return Number.isFinite(n) && n > 0 ? n : 0;
};

/**
* Per-market query that resolves the SDK's `markets()` response and builds
* a `Map<underlyingAddress, {supplyApr, borrowApr}>` of eligible Merit
* APRs for the user. Entries are only present when the user passes the
* backend's eligibility criteria for that reserve; missing keys mean "no
* Merit contribution for this position".
*/
export const usePoolsMerits = (marketsData: MarketDataType[], userAddress?: string | null) => {
const userAddr = userAddress ? evmAddress(userAddress) : undefined;

return useQueries({
queries: marketsData.map((marketData) => ({
queryKey: [
...queryKeysFactory.market(marketData),
...queryKeysFactory.user(userAddr ?? 'anonymous'),
],
enabled: !!client,
queryFn: async (): Promise<MeritAprByUnderlying> => {
const response = await markets(client, {
chainIds: [sdkChainId(marketData.chainId)],
user: userAddr,
suppliesOrderBy: { tokenName: OrderDirection.Asc },
borrowsOrderBy: { tokenName: OrderDirection.Asc },
});
if (response.isErr()) throw response.error;

// `markets()` returns every market on the chain (Core, Lido,
// EtherFi, Horizon, …). Keep only the one this query is keyed on,
// otherwise identical underlyings across pools would get merged
// and `useUserYield` would credit incentives from the wrong pool.
const targetPool = marketData.addresses.LENDING_POOL?.toLowerCase();
const scopedMarkets = targetPool
? response.value.filter((m) => m.address?.toLowerCase() === targetPool)
: response.value;

const result: MeritAprByUnderlying = {};
for (const sdkMarket of scopedMarkets) {
const allReserves = [
...(sdkMarket.supplyReserves ?? []),
...(sdkMarket.borrowReserves ?? []),
];
for (const r of allReserves) {
const underlying = r.underlyingToken.address.toLowerCase();
const existing = result[underlying] ?? { supplyApr: 0, borrowApr: 0 };
const incentives: Incentive[] = (r.incentives ?? []) as Incentive[];
for (const inc of incentives) {
if (!inc.userEligible) continue;
if (inc.__typename === 'MeritSupplyIncentive') {
existing.supplyApr += parseApr(inc.extraSupplyApr);
} else if (inc.__typename === 'MeritBorrowIncentive') {
existing.borrowApr += parseApr(inc.borrowAprDiscount);
} else if (inc.__typename === 'MeritBorrowAndSupplyIncentiveCondition') {
// Conditional reward: paid to both sides when the user
// holds the specified collateral + debt simultaneously.
const apr = parseApr(inc.extraApr);
existing.supplyApr += apr;
existing.borrowApr += apr;
}
}
result[underlying] = existing;
}
}
return result;
},
})),
});
};

export const emptyMeritMap = (): MeritAprByUnderlying => EMPTY_MAP;
Loading
Loading