From 114328fb5f5f563b9d8f6593a4df298ffa4d3bce Mon Sep 17 00:00:00 2001 From: Alejandro <95312462+AGMASO@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:05:32 +0200 Subject: [PATCH 01/36] refactor: sdk integrated in market page --- .../incentives/IncentivesTooltipContent.tsx | 5 + .../incentives/incentives.helper.ts | 76 ++ src/components/lists/ListMobileItem.tsx | 4 +- .../app-data-provider/useAppDataProvider.tsx | 67 +- src/hooks/app-data-provider/useMarketsData.ts | 38 + src/hooks/useMeritIncentives.ts | 50 +- src/hooks/useMerklIncentives.ts | 37 +- .../dashboard/lists/ListMobileItemWrapper.tsx | 3 + src/modules/markets/MarketAssetsList.tsx | 49 +- .../markets/MarketAssetsListContainer.tsx | 41 +- src/modules/markets/MarketAssetsListItem.tsx | 105 ++- .../markets/MarketAssetsListMobileItem.tsx | 86 +- src/modules/markets/MarketsTopPanel.tsx | 23 +- .../reserve-overview/TokenLinkDropdown.tsx | 2 +- src/ui-config/marketsConfig.tsx | 2 +- src/ui-config/marketsConfig_original.tsx | 803 ++++++++++++++++++ 16 files changed, 1258 insertions(+), 133 deletions(-) create mode 100644 src/components/incentives/incentives.helper.ts create mode 100644 src/hooks/app-data-provider/useMarketsData.ts create mode 100644 src/ui-config/marketsConfig_original.tsx diff --git a/src/components/incentives/IncentivesTooltipContent.tsx b/src/components/incentives/IncentivesTooltipContent.tsx index 6ba55667cf..7e1a68c24b 100644 --- a/src/components/incentives/IncentivesTooltipContent.tsx +++ b/src/components/incentives/IncentivesTooltipContent.tsx @@ -172,6 +172,11 @@ const IncentivesSymbolMap: { symbol: 'aSCR', aToken: true, }, + aPlaUSDe: { + tokenIconSymbol: 'USDe', + symbol: 'aUSDe', + aToken: true, + }, }; interface IncentivesTooltipContentProps { diff --git a/src/components/incentives/incentives.helper.ts b/src/components/incentives/incentives.helper.ts new file mode 100644 index 0000000000..d263564fbb --- /dev/null +++ b/src/components/incentives/incentives.helper.ts @@ -0,0 +1,76 @@ +import type { AaveBorrowIncentive, AaveSupplyIncentive, ReserveIncentive } from '@aave/graphql'; +import type { ReserveIncentiveResponse } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives'; + +const isAaveSupplyIncentive = (incentive: ReserveIncentive): incentive is AaveSupplyIncentive => { + return incentive.__typename === 'AaveSupplyIncentive'; +}; + +const isAaveBorrowIncentive = (incentive: ReserveIncentive): incentive is AaveBorrowIncentive => { + return incentive.__typename === 'AaveBorrowIncentive'; +}; + +// Guard combinado para incentivos de protocolo Aave +export const isAaveProtocolIncentive = (incentive: ReserveIncentive): boolean => { + return isAaveSupplyIncentive(incentive) || isAaveBorrowIncentive(incentive); +}; + +export const getIncentiveAPR = (incentive: ReserveIncentive): string => { + // Para AaveSupplyIncentive + if ('extraSupplyApr' in incentive && incentive.extraSupplyApr?.value) { + return incentive.extraSupplyApr.value.toString(); + } + + // Para AaveBorrowIncentive + if ('borrowAprDiscount' in incentive && incentive.borrowAprDiscount?.value) { + return incentive.borrowAprDiscount.value.toString(); + } + + // Fallback para estructura anterior (por compatibilidad) + if ('incentiveAPR' in incentive) { + return String(incentive.incentiveAPR); + } + + return '0'; +}; + +export const calculateProtocolIncentivesAPR = ( + incentives: ReserveIncentive[] | undefined +): number | 'Infinity' => { + return ( + incentives?.filter(isAaveProtocolIncentive)?.reduce((sum, inc) => { + const aprString = getIncentiveAPR(inc); + + if (aprString === 'Infinity' || sum === 'Infinity') { + return 'Infinity'; + } + + const aprValue = parseFloat(aprString); + + if (aprValue === Infinity || Number.isNaN(aprValue)) { + return sum; + } + + return sum + aprValue; + }, 0 as number | 'Infinity') || 0 + ); +}; + +export const mapAaveProtocolIncentives = ( + incentives: ReserveIncentive[] | undefined, + direction: 'supply' | 'borrow' +): ReserveIncentiveResponse[] => { + if (!incentives || incentives.length === 0) { + return []; + } + + const typedIncentives = + direction === 'supply' + ? incentives.filter(isAaveSupplyIncentive) + : incentives.filter(isAaveBorrowIncentive); + + return typedIncentives.map((incentive) => ({ + incentiveAPR: getIncentiveAPR(incentive), + rewardTokenAddress: incentive.rewardTokenAddress, + rewardTokenSymbol: incentive.rewardTokenSymbol, + })); +}; diff --git a/src/components/lists/ListMobileItem.tsx b/src/components/lists/ListMobileItem.tsx index 045e2f565c..c16f6102b8 100644 --- a/src/components/lists/ListMobileItem.tsx +++ b/src/components/lists/ListMobileItem.tsx @@ -20,6 +20,7 @@ interface ListMobileItemProps { showBorrowCapTooltips?: boolean; showDebtCeilingTooltips?: boolean; isIsolated: boolean; + onIconError?: () => void; } export const ListMobileItem = ({ @@ -35,6 +36,7 @@ export const ListMobileItem = ({ showBorrowCapTooltips = false, showDebtCeilingTooltips = false, isIsolated, + onIconError, }: ListMobileItemProps) => { const { supplyCap, borrowCap, debtCeiling } = useAssetCaps(); return ( @@ -59,7 +61,7 @@ export const ListMobileItem = ({ href={ROUTES.reserveOverview(underlyingAsset, currentMarket)} sx={{ display: 'inline-flex', alignItems: 'center' }} > - + {name} diff --git a/src/hooks/app-data-provider/useAppDataProvider.tsx b/src/hooks/app-data-provider/useAppDataProvider.tsx index 0c930901ab..3dc230854d 100644 --- a/src/hooks/app-data-provider/useAppDataProvider.tsx +++ b/src/hooks/app-data-provider/useAppDataProvider.tsx @@ -1,5 +1,7 @@ +import type { EmodeMarketCategory, Market, MarketUserState, Reserve } from '@aave/graphql'; import { UserReserveData } from '@aave/math-utils'; -import React, { PropsWithChildren, useContext } from 'react'; +import { client } from 'pages/_app.page'; +import React, { PropsWithChildren, useContext, useMemo } from 'react'; import { EmodeCategory } from 'src/helpers/types'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { useRootStore } from 'src/store/root'; @@ -16,6 +18,7 @@ import { import { usePoolReservesHumanized } from '../pool/usePoolReserves'; import { useUserPoolReservesHumanized } from '../pool/useUserPoolReserves'; import { FormattedUserReserves } from '../pool/useUserSummaryAndIncentives'; +import { useMarketsData } from './useMarketsData'; /** * removes the marketPrefix from a symbol @@ -40,9 +43,17 @@ export type ComputedUserReserveData = FormattedUserReserves; * @deprecated Use ExtendedFormattedUser type from useExtendedUserSummaryAndIncentives hook */ export type ExtendedFormattedUser = _ExtendedFormattedUser; - +export type ReserveWithId = Reserve & { id: string }; export interface AppDataContextType { loading: boolean; + /** SDK market snapshot */ + market?: Market; + totalBorrows?: number; + supplyReserves: ReserveWithId[]; + borrowReserves: ReserveWithId[]; + eModeCategories: EmodeMarketCategory[]; + userState?: MarketUserState; + /** Legacy fields (deprecated) kept temporarily for incremental migration */ reserves: ComputedReserveData[]; eModes: Record; user?: ExtendedFormattedUser; @@ -62,6 +73,47 @@ export const AppDataProvider: React.FC = ({ children }) => { const currentMarketData = useRootStore((state) => state.currentMarketData); + const { data, isPending } = useMarketsData({ + client, + marketData: currentMarketData, + account: currentAccount, + }); + + const marketAddress = currentMarketData.addresses.LENDING_POOL.toLowerCase(); + const marketsList = useMemo(() => { + if (!data) { + return [] as Market[]; + } + const withItems = (data as { items?: Market[] }).items; + if (Array.isArray(withItems)) { + return withItems; + } + if (Array.isArray(data)) { + return data as Market[]; + } + return [] as Market[]; + }, [data]); + + const sdkMarket = marketsList.find((item) => item.address.toLowerCase() === marketAddress); + console.log('sdkMarket', sdkMarket); + const totalBorrows = sdkMarket?.borrowReserves.reduce((acc, reserve) => { + const value = reserve.borrowInfo?.total?.usd ?? 0; + return acc + Number(value); + }, 0); + + const supplyReserves = (sdkMarket?.supplyReserves ?? []).map((reserve) => ({ + ...reserve, + id: `${sdkMarket?.address}-${reserve.underlyingToken.address}`, + })); + + const borrowReserves = (sdkMarket?.borrowReserves ?? []).map((reserve) => ({ + ...reserve, + id: `${sdkMarket?.address}-${reserve.underlyingToken.address}`, + })); + + const eModeCategories = sdkMarket?.eModeCategories ?? []; + const marketUserState = sdkMarket?.userState ?? undefined; + const { data: reservesData, isPending: reservesDataLoading } = usePoolReservesHumanized(currentMarketData); const { data: formattedPoolReserves, isPending: formattedPoolReservesLoading } = @@ -81,10 +133,19 @@ export const AppDataProvider: React.FC = ({ children }) => { const isReservesLoading = reservesDataLoading || formattedPoolReservesLoading; const isUserDataLoading = userReservesDataLoading || userSummaryLoading; + const loading = isPending || isReservesLoading || (!!currentAccount && isUserDataLoading); + return ( { + const userAddress = account ? evmAddress(account) : undefined; + const marketKey = [ + ...queryKeysFactory.market(marketData), + ...queryKeysFactory.user(userAddress ?? 'anonymous'), + ]; + + return useQuery({ + queryKey: marketKey, + enabled: !!client, + queryFn: async () => { + const response = await markets(client, { + chainIds: [chainId(marketData.chainId)], + user: userAddress, + suppliesOrderBy: { tokenName: OrderDirection.Asc }, + borrowsOrderBy: { tokenName: OrderDirection.Asc }, + }); + + if (response.isErr()) { + throw response.error; + } + + return response.value; + }, + }); +}; diff --git a/src/hooks/useMeritIncentives.ts b/src/hooks/useMeritIncentives.ts index 1d58be2f62..95d7d4b9ee 100644 --- a/src/hooks/useMeritIncentives.ts +++ b/src/hooks/useMeritIncentives.ts @@ -55,6 +55,7 @@ export enum MeritAction { AVALANCHE_SUPPLY_AUSD = 'avalanche-supply-ausd', AVALANCHE_SUPPLY_GHO = 'avalanche-supply-gho', AVALANCHE_BORROW_USDC = 'avalanche-borrow-usdc', + AVALANCHE_BORROW_EURC = 'avalanche-borrow-eurc', SONIC_SUPPLY_USDCE = 'sonic-supply-usdce', SONIC_SUPPLY_STS_BORROW_WS = 'sonic-supply-sts-borrow-ws', GNOSIS_BORROW_EURE = 'gnosis-borrow-eure', @@ -114,7 +115,8 @@ const antiLoopMessage = const antiLoopBorrowMessage = 'Supplying 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.'; - +const masivBorrowUsdcMessage = + 'Only new debt created since the campaign start will be rewarded. Supplying of some assets or holding of some token may impact the amount of rewards you are eligible for.'; const lbtcCbbtcCampaignMessage = 'You must supply LBTC and borrow cbBTC, while maintaining a health factor of 1.5 or below, in order to receive merit rewards. Please check the forum post for the full eligibility criteria.'; @@ -527,7 +529,7 @@ export const MERIT_DATA_MAP: Record 0) { + totalAmountIncentivesCampaigns.push(incentive.action); + } if (standardAPR == null) continue; if (totalMeritAPR === null) totalMeritAPR = 0; @@ -782,25 +798,33 @@ export const useMeritIncentives = ({ }, 0); const isBorrow = protocolAction === ProtocolAction.borrow; + const totalAPY = isBorrow ? protocolAPY - protocolIncentivesAPR - meritIncentivesAPY - (selfIncentivesAPY ?? 0) : protocolAPY + protocolIncentivesAPR + meritIncentivesAPY + (selfIncentivesAPY ?? 0); + let finalAction: MeritAction | undefined = undefined; + if (totalAmountIncentivesCampaigns.length >= 1) { + finalAction = totalAmountIncentivesCampaigns[0]; + } + + const actionMessages = incentives.reduce((acc, incentive) => { + acc[incentive.action] = { + customMessage: incentive.customMessage, + customForumLink: incentive.customForumLink, + }; + return acc; + }, {} as Record); + return { incentiveAPR: meritIncentivesAPY.toString(), rewardTokenAddress: incentives[0].rewardTokenAddress, rewardTokenSymbol: incentives[0].rewardTokenSymbol, - activeActions: incentives.map((incentive) => incentive.action), - actionMessages: incentives.reduce((acc, incentive) => { - acc[incentive.action] = { - customMessage: incentive.customMessage, - customForumLink: incentive.customForumLink, - }; - return acc; - }, {} as Record), - action: incentives[0].action, - customMessage: incentives[0].customMessage, - customForumLink: incentives[0].customForumLink, + activeActions: totalAmountIncentivesCampaigns, + actionMessages: actionMessages, + action: finalAction, + customMessage: finalAction ? actionMessages[finalAction]?.customMessage : undefined, + customForumLink: finalAction ? actionMessages[finalAction]?.customForumLink : undefined, variants: { selfAPY: selfIncentivesAPY }, breakdown: { diff --git a/src/hooks/useMerklIncentives.ts b/src/hooks/useMerklIncentives.ts index 77fb115f21..ba33429ea1 100644 --- a/src/hooks/useMerklIncentives.ts +++ b/src/hooks/useMerklIncentives.ts @@ -1,5 +1,6 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { ReserveIncentiveResponse } from '@aave/math-utils/dist/esm/formatters/incentive/calculate-reserve-incentives'; +import { AaveV3Plasma } from '@bgd-labs/aave-address-book'; import { useQuery } from '@tanstack/react-query'; import { useRootStore } from 'src/store/root'; import { convertAprToApy } from 'src/utils/utils'; @@ -99,7 +100,27 @@ type WhitelistApiResponse = { additionalIncentiveInfo: Record; }; -const hardcodedIncentives: Record = {}; +const hardcodedIncentives: Record = { + [AaveV3Plasma.ASSETS.USDe.A_TOKEN]: { + incentiveAPR: '0.12', + rewardTokenAddress: AaveV3Plasma.ASSETS.USDe.A_TOKEN, + rewardTokenSymbol: 'aPlaUSDe', + customMessage: + 'You must supply USDe and hold an equal or greater amount of sUSDe (by USD value) to receive the incentives. To be eligible, your assets supplied must be at least 2x your account equity, and you must not be borrowing any USDe. The rate provided to eligible users will change week by week, but will be roughly in line with the sUSDe rate for the forseeable future.', + breakdown: { + protocolAPY: 0, + protocolIncentivesAPR: 0, + merklIncentivesAPR: 0, + totalAPY: 0, + isBorrow: false, + breakdown: { + protocol: 0, + protocolIncentives: 0, + merklIncentives: 0, + }, + }, + }, +}; const MERKL_ENDPOINT = 'https://api.merkl.xyz/v4/opportunities?mainProtocolId=aave'; // Merkl API const WHITELIST_ENDPOINT = 'https://apps.aavechan.com/api/aave/merkl/whitelist-token-list'; // Endpoint to fetch whitelisted tokens @@ -158,7 +179,19 @@ export const useMerklIncentives = ({ const hardcodedIncentive = rewardedAsset ? hardcodedIncentives[rewardedAsset] : undefined; if (hardcodedIncentive) { - return hardcodedIncentive; + const protocolIncentivesAPR = protocolIncentives.reduce((sum, inc) => { + return sum + (inc.incentiveAPR === 'Infinity' ? 0 : +inc.incentiveAPR); + }, 0); + const merklIncentivesAPY = convertAprToApy(0.1); + return { + ...hardcodedIncentive, + breakdown: { + protocolAPY, + protocolIncentivesAPR, + merklIncentivesAPR: merklIncentivesAPY, + totalAPY: protocolAPY + protocolIncentivesAPR + merklIncentivesAPY, + } as MerklIncentivesBreakdown, + } as ExtendedReserveIncentiveResponse; } const opportunities = merklOpportunities.filter( diff --git a/src/modules/dashboard/lists/ListMobileItemWrapper.tsx b/src/modules/dashboard/lists/ListMobileItemWrapper.tsx index 11df134304..3b138559fb 100644 --- a/src/modules/dashboard/lists/ListMobileItemWrapper.tsx +++ b/src/modules/dashboard/lists/ListMobileItemWrapper.tsx @@ -32,6 +32,7 @@ interface ListMobileItemWrapperProps { showDebtCeilingTooltips?: boolean; isIsolated?: boolean; showExternalIncentivesTooltips?: ExternalIncentivesTooltipsConfig; + onIconError?: () => void; } export const ListMobileItemWrapper = ({ @@ -54,6 +55,7 @@ export const ListMobileItemWrapper = ({ spkAirdrop: false, kernelPoints: false, }, + onIconError, }: ListMobileItemWrapperProps) => { const WarningComponent: React.FC = () => { const showFrozenTooltip = frozen && symbol !== 'renFIL'; @@ -94,6 +96,7 @@ export const ListMobileItemWrapper = ({ showSupplyCapTooltips={showSupplyCapTooltips} showBorrowCapTooltips={showBorrowCapTooltips} showDebtCeilingTooltips={showDebtCeilingTooltips} + onIconError={onIconError} > {children} diff --git a/src/modules/markets/MarketAssetsList.tsx b/src/modules/markets/MarketAssetsList.tsx index 3b3e8ac07b..72e7ba80f8 100644 --- a/src/modules/markets/MarketAssetsList.tsx +++ b/src/modules/markets/MarketAssetsList.tsx @@ -5,8 +5,8 @@ import { VariableAPYTooltip } from 'src/components/infoTooltips/VariableAPYToolt import { ListColumn } from 'src/components/lists/ListColumn'; import { ListHeaderTitle } from 'src/components/lists/ListHeaderTitle'; import { ListHeaderWrapper } from 'src/components/lists/ListHeaderWrapper'; -import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider'; +import { ReserveWithId } from '../../hooks/app-data-provider/useAppDataProvider'; import { MarketAssetsListItem } from './MarketAssetsListItem'; import { MarketAssetsListItemLoader } from './MarketAssetsListItemLoader'; import { MarketAssetsListMobileItem } from './MarketAssetsListMobileItem'; @@ -15,19 +15,19 @@ import { MarketAssetsListMobileItemLoader } from './MarketAssetsListMobileItemLo const listHeaders = [ { title: Asset, - sortKey: 'symbol', + sortKey: 'underlyingToken.symbol', // Cambiado de 'symbol' }, { title: Total supplied, - sortKey: 'totalLiquidityUSD', + sortKey: 'size.usd', // Cambiado de 'totalLiquidityUSD' }, { title: Supply APY, - sortKey: 'supplyAPY', + sortKey: 'supplyInfo.apy.value', // Cambiado de 'supplyAPY' }, { title: Total borrowed, - sortKey: 'totalDebtUSD', + sortKey: 'borrowInfo.total.usd', // Cambiado de 'totalDebtUSD' }, { title: ( @@ -37,12 +37,12 @@ const listHeaders = [ variant="subheader2" /> ), - sortKey: 'variableBorrowAPY', + sortKey: 'borrowInfo.apy.value', }, ]; type MarketAssetsListProps = { - reserves: ComputedReserveData[]; + reserves: ReserveWithId[]; loading: boolean; }; @@ -50,21 +50,40 @@ export default function MarketAssetsList({ reserves, loading }: MarketAssetsList const isTableChangedToCards = useMediaQuery('(max-width:1125px)'); const [sortName, setSortName] = useState(''); const [sortDesc, setSortDesc] = useState(false); + const getValue = (obj: ReserveWithId, path: string): unknown => { + return path.split('.').reduce((current: unknown, key: string) => { + return current && typeof current === 'object' && key in current + ? (current as Record)[key] + : undefined; + }, obj); + }; if (sortDesc) { - if (sortName === 'symbol') { - reserves.sort((a, b) => (a.symbol.toUpperCase() < b.symbol.toUpperCase() ? -1 : 1)); + if (sortName === 'underlyingToken.symbol') { + reserves.sort((a, b) => + a.underlyingToken.symbol.toUpperCase() < b.underlyingToken.symbol.toUpperCase() ? -1 : 1 + ); } else { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - reserves.sort((a, b) => a[sortName] - b[sortName]); + reserves.sort((a, b) => { + const aValue = Number(getValue(a, sortName)) || 0; + const bValue = Number(getValue(b, sortName)) || 0; + return aValue - bValue; + }); } } else { - if (sortName === 'symbol') { - reserves.sort((a, b) => (b.symbol.toUpperCase() < a.symbol.toUpperCase() ? -1 : 1)); + if (sortName === 'underlyingToken.symbol') { + reserves.sort((a, b) => + b.underlyingToken.symbol.toUpperCase() < a.underlyingToken.symbol.toUpperCase() ? -1 : 1 + ); } else { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - reserves.sort((a, b) => b[sortName] - a[sortName]); + reserves.sort((a, b) => { + const aValue = Number(getValue(a, sortName)) || 0; + const bValue = Number(getValue(b, sortName)) || 0; + return bValue - aValue; + }); } } @@ -95,8 +114,8 @@ export default function MarketAssetsList({ reserves, loading }: MarketAssetsList {listHeaders.map((col) => ( { const { data, isLoading, error } = useCoingeckoCategories(); - const { reserves, loading } = useAppDataContext(); + const { supplyReserves, loading } = useAppDataContext(); + const [trackEvent, currentMarket, currentMarketData, currentNetworkConfig] = useRootStore( useShallow((store) => [ store.trackEvent, @@ -60,19 +59,19 @@ export const MarketAssetsListContainer = () => { const displayGhoBanner = shouldDisplayGhoBanner(currentMarket, searchTerm); - const filteredData = reserves + const filteredData = supplyReserves // Filter out any non-active reserves - .filter((res) => res.isActive) + .filter((res) => !res.isPaused) // Filter out any hidden assets - .filter((res) => !isAssetHidden(currentMarketData.market, res.underlyingAsset)) + .filter((res) => !isAssetHidden(currentMarketData.market, res.underlyingToken.address)) // filter out any that don't meet search term criteria .filter((res) => { if (!searchTerm) return true; const term = searchTerm.toLowerCase().trim(); return ( - res.symbol.toLowerCase().includes(term) || - res.name.toLowerCase().includes(term) || - res.underlyingAsset.toLowerCase().includes(term) + res.underlyingToken.symbol.toLowerCase().includes(term) || + res.underlyingToken.name.toLowerCase().includes(term) || + res.underlyingToken.address.toLowerCase().includes(term) ); }) // Filter by category @@ -81,27 +80,39 @@ export const MarketAssetsListContainer = () => { selectedCategories.length === 0 || selectedCategories.some((category) => isAssetInCategoryDynamic( - res.symbol, + res.underlyingToken.symbol, category, data?.stablecoinSymbols, data?.ethCorrelatedSymbols ) ) ) + // Add initial sorting by total supplied in USD descending + .sort((a, b) => { + const aValue = Number(a.size.usd) || 0; + const bValue = Number(b.size.usd) || 0; + return bValue - aValue; + }) // Transform the object for list to consume it .map((reserve) => ({ ...reserve, - ...(reserve.isWrappedBaseAsset - ? fetchIconSymbolAndName({ - symbol: currentNetworkConfig.baseAssetSymbol, - underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(), - }) + ...(reserve.acceptsNative + ? { + underlyingToken: { + ...reserve.underlyingToken, + name: currentNetworkConfig.baseAssetSymbol, // e.g., "Ethereum" + symbol: currentNetworkConfig.baseAssetSymbol, // e.g., "ETH" + imageUrl: currentNetworkConfig.baseAssetSymbol.toLowerCase(), // This might need adjustment based on your icon system + }, + } : {}), })); + // const marketFrozen = !reserves.some((reserve) => !reserve.isFrozen); // const showFrozenMarketWarning = // marketFrozen && ['Fantom', 'Ethereum AMM'].includes(currentMarketData.marketTitle); const unfrozenReserves = filteredData.filter((r) => !r.isFrozen && !r.isPaused); + const [showFrozenMarketsToggle, setShowFrozenMarketsToggle] = useState(false); const handleChange = () => { diff --git a/src/modules/markets/MarketAssetsListItem.tsx b/src/modules/markets/MarketAssetsListItem.tsx index 9fbf9a1ca9..ddde2b6364 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 { useState } from 'react'; import { KernelAirdropTooltip } from 'src/components/infoTooltips/KernelAirdropTooltip'; import { OffboardingTooltip } from 'src/components/infoTooltips/OffboardingToolTip'; import { RenFILToolTip } from 'src/components/infoTooltips/RenFILToolTip'; @@ -12,10 +13,12 @@ import { NoData } from 'src/components/primitives/NoData'; import { ReserveSubheader } from 'src/components/ReserveSubheader'; import { AssetsBeingOffboarded } from 'src/components/Warnings/OffboardingWarning'; import { useRootStore } from 'src/store/root'; +import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; import { MARKETS } from 'src/utils/events'; import { showExternalIncentivesTooltip } from 'src/utils/utils'; import { useShallow } from 'zustand/shallow'; +import { mapAaveProtocolIncentives } from '../../components/incentives/incentives.helper'; import { IncentivesCard } from '../../components/incentives/IncentivesCard'; import { AMPLToolTip } from '../../components/infoTooltips/AMPLToolTip'; import { ListColumn } from '../../components/lists/ListColumn'; @@ -23,25 +26,34 @@ 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 { ComputedReserveData } from '../../hooks/app-data-provider/useAppDataProvider'; +import { ReserveWithId } from '../../hooks/app-data-provider/useAppDataProvider'; -export const MarketAssetsListItem = ({ ...reserve }: ComputedReserveData) => { +export const MarketAssetsListItem = ({ ...reserve }: ReserveWithId) => { const router = useRouter(); const [trackEvent, currentMarket] = useRootStore( useShallow((store) => [store.trackEvent, store.currentMarket]) ); - - const offboardingDiscussion = AssetsBeingOffboarded[currentMarket]?.[reserve.symbol]; + const [useFetchIcon, setUseFetchIcon] = useState(false); + const offboardingDiscussion = + AssetsBeingOffboarded[currentMarket]?.[reserve.underlyingToken.symbol]; const externalIncentivesTooltipsSupplySide = showExternalIncentivesTooltip( - reserve.symbol, + reserve.underlyingToken.symbol, currentMarket, ProtocolAction.supply ); const externalIncentivesTooltipsBorrowSide = showExternalIncentivesTooltip( - reserve.symbol, + reserve.underlyingToken.symbol, currentMarket, ProtocolAction.borrow ); + const { iconSymbol } = fetchIconSymbolAndName({ + underlyingAsset: reserve.underlyingToken.address, + symbol: reserve.underlyingToken.symbol, + name: reserve.underlyingToken.name, + }); + + const supplyProtocolIncentives = mapAaveProtocolIncentives(reserve.incentives, 'supply'); + const borrowProtocolIncentives = mapAaveProtocolIncentives(reserve.incentives, 'borrow'); return ( { onClick={() => { trackEvent(MARKETS.DETAILS_NAVIGATION, { type: 'Row', - assetName: reserve.name, - asset: reserve.underlyingAsset, + assetName: reserve.underlyingToken.name, + asset: reserve.underlyingToken.address.toLowerCase(), market: currentMarket, }); - router.push(ROUTES.reserveOverview(reserve.underlyingAsset, currentMarket)); + router.push( + ROUTES.reserveOverview(reserve.underlyingToken.address.toLowerCase(), currentMarket) + ); }} sx={{ cursor: 'pointer' }} button - data-cy={`marketListItemListItem_${reserve.symbol.toUpperCase()}`} + data-cy={`marketListItemListItem_${reserve.underlyingToken.symbol.toUpperCase()}`} > - + {!useFetchIcon ? ( + { + setUseFetchIcon(true); + }} + /> + ) : ( + + )} - {reserve.name} + {reserve.underlyingToken.name} { }} > - {reserve.symbol} - {reserve.isIsolated && ( + {reserve.underlyingToken.symbol} + {reserve.isolationModeConfig?.canBeCollateral && ( @@ -82,22 +106,22 @@ export const MarketAssetsListItem = ({ ...reserve }: ComputedReserveData) => { - {reserve.symbol === 'AMPL' && } - {reserve.symbol === 'renFIL' && } + {reserve.underlyingToken.symbol === 'AMPL' && } + {reserve.underlyingToken.symbol === 'renFIL' && } {offboardingDiscussion && } - - + + { - {reserve.borrowingEnabled || Number(reserve.totalDebt) > 0 ? ( + {reserve.borrowInfo ? ( <> - {' '} - + {' '} + ) : ( @@ -125,10 +153,14 @@ export const MarketAssetsListItem = ({ ...reserve }: ComputedReserveData) => { 0 ? reserve.variableBorrowAPY : '-1'} - incentives={reserve.vIncentivesData || []} - address={reserve.variableDebtTokenAddress} - symbol={reserve.symbol} + value={ + Number(reserve.borrowInfo?.total.amount.value) > 0 + ? String(reserve.borrowInfo?.apy.value) + : '-1' + } + incentives={borrowProtocolIncentives} + address={reserve.vToken.address} + symbol={reserve.underlyingToken.symbol} variant="main16" symbolsVariant="secondary16" tooltip={ @@ -140,21 +172,24 @@ export const MarketAssetsListItem = ({ ...reserve }: ComputedReserveData) => { market={currentMarket} protocolAction={ProtocolAction.borrow} /> - {!reserve.borrowingEnabled && - Number(reserve.totalVariableDebt) > 0 && - !reserve.isFrozen && } + {reserve.borrowInfo?.borrowingState === 'DISABLED' && !reserve.isFrozen && ( + + )} - - - - ); -}; diff --git a/src/utils/dashboardSortUtils.ts b/src/utils/dashboardSortUtils.ts index 8414d8c3ae..01f81e25b1 100644 --- a/src/utils/dashboardSortUtils.ts +++ b/src/utils/dashboardSortUtils.ts @@ -3,10 +3,7 @@ import { BorrowAssetsItem } from 'src/modules/dashboard/lists/BorrowAssetsList/t import { SupplyAssetsItem } from 'src/modules/dashboard/lists/SupplyAssetsList/types'; // Sorting keys -import { - ComputedReserveData, - ComputedUserReserveData, -} from '../hooks/app-data-provider/useAppDataProvider'; +import { ReserveWithId } from '../hooks/app-data-provider/useAppDataProvider'; // Helpers export const DASHBOARD_LIST_COLUMN_WIDTHS = { @@ -17,16 +14,13 @@ export const DASHBOARD_LIST_COLUMN_WIDTHS = { // Note: Create a single type that works with all four dashboards list and all 8 list item components // Each list item may need a combination of a few types but not all, i.e. positions vs assets and supplied vs borrowed -type DashboardReserveData = ComputedUserReserveData & - ComputedReserveData & - BorrowAssetsItem & - SupplyAssetsItem; +type DashboardReserveData = ReserveWithId & BorrowAssetsItem & SupplyAssetsItem; export type DashboardReserve = DashboardReserveData & { // Additions borrowRateMode: InterestRate; // for the borrow positions list // Overrides - reserve: ComputedReserveData; + reserve: ReserveWithId; }; export const handleSortDashboardReserves = ( @@ -58,7 +52,7 @@ const handleSortDesc = ( } else { if (isBorrowedPosition) { positions.sort( - (a, b) => Number(b.reserve.variableBorrowAPY) - Number(a.reserve.variableBorrowAPY) + (a, b) => Number(b.reserve.borrowInfo?.apy.value) - Number(a.reserve.borrowInfo?.apy.value) ); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -84,7 +78,7 @@ const sortAsc = ( // Note because borrow positions have extra logic we need to have this if (isBorrowedPosition) { positions.sort( - (a, b) => Number(a.reserve.variableBorrowAPY) - Number(b.reserve.variableBorrowAPY) + (a, b) => Number(a.reserve.borrowInfo?.apy.value) - Number(b.reserve.borrowInfo?.apy.value) ); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -103,20 +97,33 @@ const handleSymbolSort = ( if (sortDesc) { if (sortPosition === 'position') { return positions.sort((a, b) => - a.reserve.symbol.toUpperCase() < b.reserve.symbol.toUpperCase() ? -1 : 1 + a.reserve.underlyingToken.symbol.toUpperCase() < + b.reserve.underlyingToken.symbol.toUpperCase() + ? -1 + : 1 ); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - return positions.sort((a, b) => (a.symbol.toUpperCase() < b.symbol.toUpperCase() ? -1 : 1)); + return positions.sort((a, b) => + a.reserve.underlyingToken.symbol.toUpperCase() < + b.reserve.underlyingToken.symbol.toUpperCase() + ? -1 + : 1 + ); } if (sortPosition === 'position') { return positions.sort((a, b) => - b.reserve.symbol.toUpperCase() < a.reserve.symbol.toUpperCase() ? -1 : 1 + b.reserve.underlyingToken.symbol.toUpperCase() < + a.reserve.underlyingToken.symbol.toUpperCase() + ? -1 + : 1 ); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - return positions.sort((a, b) => (b.symbol.toUpperCase() < a.symbol.toUpperCase() ? -1 : 1)); + return positions.sort((a, b) => + b.underlyingToken.symbol.toUpperCase() < a.underlyingToken.symbol.toUpperCase() ? -1 : 1 + ); }; From 9292a2fd60c5ca18c706ae929032a0f7fe056fb8 Mon Sep 17 00:00:00 2001 From: Alejandro <95312462+AGMASO@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:17:02 +0200 Subject: [PATCH 21/36] refactor: wip supplies positions listo, aun pequenos fallos --- .../app-data-provider/useAppDataProvider.tsx | 47 ++++- src/hooks/app-data-provider/useUserBorrows.ts | 39 ++++ .../app-data-provider/useUserSupplies.ts | 39 ++++ src/hooks/useWrappedTokens.tsx | 2 +- .../BorrowedPositionsList.tsx | 7 +- .../BorrowedPositionsListItem.tsx | 4 +- .../SuppliedPositionsList.tsx | 181 +++++++++++++----- .../SuppliedPositionsListItem.tsx | 92 +++++---- .../SuppliedPositionsListMobileItem.tsx | 98 +++++----- .../SupplyAssetsList/SupplyAssetsList.tsx | 2 + .../SupplyAssetsList/SupplyAssetsListItem.tsx | 4 +- src/utils/dashboardSortUtils.ts | 2 +- 12 files changed, 367 insertions(+), 150 deletions(-) create mode 100644 src/hooks/app-data-provider/useUserBorrows.ts create mode 100644 src/hooks/app-data-provider/useUserSupplies.ts diff --git a/src/hooks/app-data-provider/useAppDataProvider.tsx b/src/hooks/app-data-provider/useAppDataProvider.tsx index 4591e2c312..56de7825aa 100644 --- a/src/hooks/app-data-provider/useAppDataProvider.tsx +++ b/src/hooks/app-data-provider/useAppDataProvider.tsx @@ -1,4 +1,13 @@ -import type { EmodeMarketCategory, Market, MarketUserState, Reserve } from '@aave/graphql'; +import type { + EmodeMarketCategory, + Market, + MarketUserReserveBorrowPosition, + MarketUserReserveSupplyPosition, + MarketUserState, + PercentValue, + Reserve, + TokenAmount, +} from '@aave/graphql'; import { UserReserveData } from '@aave/math-utils'; import { client } from 'pages/_app.page'; import React, { PropsWithChildren, useContext } from 'react'; @@ -19,6 +28,8 @@ import { usePoolReservesHumanized } from '../pool/usePoolReserves'; import { useUserPoolReservesHumanized } from '../pool/useUserPoolReserves'; import { FormattedUserReserves } from '../pool/useUserSummaryAndIncentives'; import { useMarketsData } from './useMarketsData'; +import { useUserBorrows } from './useUserBorrows'; +import { useUserSupplies } from './useUserSupplies'; /** * removes the marketPrefix from a symbol @@ -43,7 +54,15 @@ export type ComputedUserReserveData = FormattedUserReserves; * @deprecated Use ExtendedFormattedUser type from useExtendedUserSummaryAndIncentives hook */ export type ExtendedFormattedUser = _ExtendedFormattedUser; -export type ReserveWithId = Reserve & { id: string }; +export type ReserveWithId = Reserve & { + id: string; + supplyAPY?: number; + underlyingBalance?: string; + usageAsCollateralEnabledOnUser?: boolean; + isCollateralPosition?: boolean; + apyPosition?: PercentValue; + balancePosition?: TokenAmount; +}; export interface AppDataContextType { loading: boolean; /** SDK market snapshot */ @@ -53,6 +72,8 @@ export interface AppDataContextType { borrowReserves: ReserveWithId[]; eModeCategories: EmodeMarketCategory[]; userState?: MarketUserState; + userSupplies?: MarketUserReserveSupplyPosition[]; + userBorrows?: MarketUserReserveBorrowPosition[]; /** Legacy fields (deprecated) kept temporarily for incremental migration */ reserves: ComputedReserveData[]; eModes: Record; @@ -79,6 +100,19 @@ export const AppDataProvider: React.FC = ({ children }) => { account: currentAccount, }); + const { data: userSuppliesData, isPending: userSuppliesLoading } = useUserSupplies({ + client, + marketData: currentMarketData, + account: currentAccount, + }); + + const { data: userBorrowsData, isPending: userBorrowsLoading } = useUserBorrows({ + client, + marketData: currentMarketData, + account: currentAccount, + }); + console.log('userSuppliesData', userSuppliesData); + console.log('userBorrowsData', userBorrowsData); const marketAddress = currentMarketData.addresses.LENDING_POOL.toLowerCase(); const sdkMarket = data?.find((item) => item.address.toLowerCase() === marketAddress); @@ -120,7 +154,12 @@ export const AppDataProvider: React.FC = ({ children }) => { const isReservesLoading = reservesDataLoading || formattedPoolReservesLoading; const isUserDataLoading = userReservesDataLoading || userSummaryLoading; - const loading = isPending || isReservesLoading || (!!currentAccount && isUserDataLoading); + const loading = + isPending || + userSuppliesLoading || + userBorrowsLoading || + isReservesLoading || + (!!currentAccount && isUserDataLoading); return ( = ({ children }) => { borrowReserves, eModeCategories, userState: marketUserState, + userSupplies: userSuppliesData, + userBorrows: userBorrowsData, // Legacy fields (to be removed once consumers migrate) reserves: formattedPoolReserves || [], eModes, diff --git a/src/hooks/app-data-provider/useUserBorrows.ts b/src/hooks/app-data-provider/useUserBorrows.ts new file mode 100644 index 0000000000..ee28718090 --- /dev/null +++ b/src/hooks/app-data-provider/useUserBorrows.ts @@ -0,0 +1,39 @@ +import { AaveClient, chainId, evmAddress, OrderDirection } from '@aave/client'; +import { userBorrows } from '@aave/client/actions'; +import { useQuery } from '@tanstack/react-query'; +import { MarketDataType } from 'src/ui-config/marketsConfig'; +import { queryKeysFactory } from 'src/ui-config/queries'; + +type UseUserBorrowsDataParams = { + client: AaveClient; + marketData: MarketDataType; + account?: string | null; +}; + +export const useUserBorrows = ({ client, marketData, account }: UseUserBorrowsDataParams) => { + const userAddress = account ? evmAddress(account) : undefined; + + return useQuery({ + queryKey: [ + ...queryKeysFactory.market(marketData), + ...queryKeysFactory.user(userAddress ?? 'anonymous'), + 'userBorrows', + ], + enabled: !!client && !!userAddress, + queryFn: async () => { + const response = await userBorrows(client, { + markets: [ + { + chainId: chainId(marketData.chainId), + address: evmAddress(marketData.addresses.LENDING_POOL), + }, + ], + user: userAddress!, + orderBy: { debt: OrderDirection.Asc }, + }); + + if (response.isErr()) throw response.error; + return response.value; + }, + }); +}; diff --git a/src/hooks/app-data-provider/useUserSupplies.ts b/src/hooks/app-data-provider/useUserSupplies.ts new file mode 100644 index 0000000000..0095be9fa1 --- /dev/null +++ b/src/hooks/app-data-provider/useUserSupplies.ts @@ -0,0 +1,39 @@ +import { AaveClient, chainId, evmAddress, OrderDirection } from '@aave/client'; +import { userSupplies } from '@aave/client/actions'; +import { useQuery } from '@tanstack/react-query'; +import { MarketDataType } from 'src/ui-config/marketsConfig'; +import { queryKeysFactory } from 'src/ui-config/queries'; + +type UseUserSuppliesDataParams = { + client: AaveClient; + marketData: MarketDataType; + account?: string | null; +}; + +export const useUserSupplies = ({ client, marketData, account }: UseUserSuppliesDataParams) => { + const userAddress = account ? evmAddress(account) : undefined; + + return useQuery({ + queryKey: [ + ...queryKeysFactory.market(marketData), + ...queryKeysFactory.user(userAddress ?? 'anonymous'), + 'userSupplies', + ], + enabled: !!client && !!userAddress, + queryFn: async () => { + const response = await userSupplies(client, { + markets: [ + { + chainId: chainId(marketData.chainId), + address: evmAddress(marketData.addresses.LENDING_POOL), + }, + ], + user: userAddress!, + orderBy: { balance: OrderDirection.Asc }, + }); + + if (response.isErr()) throw response.error; + return response.value; + }, + }); +}; diff --git a/src/hooks/useWrappedTokens.tsx b/src/hooks/useWrappedTokens.tsx index 58ff4f3be6..4e32e6d45a 100644 --- a/src/hooks/useWrappedTokens.tsx +++ b/src/hooks/useWrappedTokens.tsx @@ -76,6 +76,6 @@ export const useWrappedTokens = () => { tokenWrapperAddress: config.tokenWrapperContractAddress, }; }); - console.log('WrappedTokenReserves: ', wrappedTokenReserves); // ! debug + return wrappedTokenReserves; }; diff --git a/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsList.tsx b/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsList.tsx index 4ed68e3575..670e774d5e 100644 --- a/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsList.tsx +++ b/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsList.tsx @@ -6,7 +6,7 @@ import { useState } from 'react'; import { ListColumn } from 'src/components/lists/ListColumn'; import { ListHeaderTitle } from 'src/components/lists/ListHeaderTitle'; import { ListHeaderWrapper } from 'src/components/lists/ListHeaderWrapper'; -import { AssetCapsProvider } from 'src/hooks/useAssetCaps'; +import { AssetCapsProviderSDK } from 'src/hooks/useAssetCapsSDK'; import { useRootStore } from 'src/store/root'; import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; import { GENERAL } from 'src/utils/events'; @@ -50,6 +50,7 @@ const head = [ export const BorrowedPositionsList = () => { const { user, loading, eModes, reserves } = useAppDataContext(); + const [currentMarketData, currentNetworkConfig] = useRootStore( useShallow((store) => [store.currentMarketData, store.currentNetworkConfig]) ); @@ -198,12 +199,12 @@ export const BorrowedPositionsList = () => { <> {!downToXSM && } {sortedReserves.map((item) => ( - - + ))} ) : ( diff --git a/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItem.tsx b/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItem.tsx index 188c4d9371..16825fb6fd 100644 --- a/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItem.tsx +++ b/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItem.tsx @@ -4,7 +4,7 @@ import { Trans } from '@lingui/macro'; import { Box, Button, useMediaQuery, useTheme } from '@mui/material'; import { IncentivesCard } from 'src/components/incentives/IncentivesCard'; import { Row } from 'src/components/primitives/Row'; -import { useAssetCaps } from 'src/hooks/useAssetCaps'; +import { useAssetCapsSDK } from 'src/hooks/useAssetCapsSDK'; import { useModalContext } from 'src/hooks/useModal'; import { useRootStore } from 'src/store/root'; import { DashboardReserve } from 'src/utils/dashboardSortUtils'; @@ -29,7 +29,7 @@ export const BorrowedPositionsListItem = ({ item, disableEModeSwitch, }: BorrowedPositionsListItem) => { - const { borrowCap } = useAssetCaps(); + const { borrowCap } = useAssetCapsSDK(); const [currentMarket, currentMarketData] = useRootStore( useShallow((state) => [state.currentMarket, state.currentMarketData]) ); diff --git a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsList.tsx b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsList.tsx index ff8ef5deab..ae5ec26d62 100644 --- a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsList.tsx +++ b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsList.tsx @@ -1,11 +1,12 @@ import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers'; +import type { MarketUserReserveSupplyPosition } from '@aave/graphql'; import { Trans } from '@lingui/macro'; import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'; import { Fragment, useMemo, useState } from 'react'; import { ListColumn } from 'src/components/lists/ListColumn'; import { ListHeaderTitle } from 'src/components/lists/ListHeaderTitle'; import { ListHeaderWrapper } from 'src/components/lists/ListHeaderWrapper'; -import { AssetCapsProvider } from 'src/hooks/useAssetCaps'; +import { AssetCapsProviderSDK } from 'src/hooks/useAssetCapsSDK'; import { useRootStore } from 'src/store/root'; import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; import { DASHBOARD, GENERAL } from 'src/utils/events'; @@ -14,13 +15,15 @@ import { CollateralSwitchTooltip } from '../../../../components/infoTooltips/Col import { CollateralTooltip } from '../../../../components/infoTooltips/CollateralTooltip'; import { TotalSupplyAPYTooltip } from '../../../../components/infoTooltips/TotalSupplyAPYTooltip'; import { ListWrapper } from '../../../../components/lists/ListWrapper'; -import { useAppDataContext } from '../../../../hooks/app-data-provider/useAppDataProvider'; +import { + ReserveWithId, + useAppDataContext, +} from '../../../../hooks/app-data-provider/useAppDataProvider'; import { DASHBOARD_LIST_COLUMN_WIDTHS, DashboardReserve, handleSortDashboardReserves, } from '../../../../utils/dashboardSortUtils'; -import { amountToUsd } from '../../../../utils/utils'; import { ListTopInfoItem } from '../../../dashboard/lists/ListTopInfoItem'; import { DashboardContentNoData } from '../../DashboardContentNoData'; import { DashboardListTopPanel } from '../../DashboardListTopPanel'; @@ -63,7 +66,8 @@ const head = [ export const SMALL_BALANCE_THRESHOLD = 0.001; export const SuppliedPositionsList = () => { - const { user, loading, marketReferencePriceInUsd } = useAppDataContext(); + const { loading, supplyReserves, userState, userSupplies } = useAppDataContext(); + const userSupplyPositions = userSupplies ?? []; const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig); const currentMarketData = useRootStore((store) => store.currentMarketData); const theme = useTheme(); @@ -77,57 +81,132 @@ export const SuppliedPositionsList = () => { localStorage.getItem(localStorageName) === 'true' ); - const userHasSmallBalanceAssets = useMemo(() => { - return user?.userReservesData.some((userReserve) => { - if (userReserve.underlyingBalance === '0') return false; - - const balanceUSD = amountToUsd( - userReserve.underlyingBalance, - userReserve.reserve.formattedPriceInMarketReferenceCurrency, - marketReferencePriceInUsd - ); + const supplyReservesLookup = useMemo(() => { + const map = new Map(); + supplyReserves.forEach((reserve) => { + const address = reserve.underlyingToken.address?.toLowerCase(); + if (address) { + map.set(address, reserve); + } + }); + return map; + }, [supplyReserves]); - return Number(balanceUSD) <= SMALL_BALANCE_THRESHOLD; + const userHasSmallBalanceAssets = useMemo(() => { + return userSupplyPositions.some((position) => { + const balanceValue = Number(position.balance.amount.value ?? '0'); + if (balanceValue <= 0) { + return false; + } + const balanceUSD = Number(position.balance.usd ?? '0'); + return balanceUSD > 0 && balanceUSD <= SMALL_BALANCE_THRESHOLD; }); - }, [user?.userReservesData, marketReferencePriceInUsd]); + }, [userSupplyPositions]); const suppliedPositions = useMemo(() => { - return ( - user?.userReservesData - .filter( - (userReserve) => - userReserve.underlyingBalance !== '0' && - !isAssetHidden(currentMarketData.market, userReserve.reserve.underlyingAsset) - ) - .filter((userReserve) => { - if (userReserve.underlyingBalance === '0') return false; + if (!userSupplyPositions.length) { + return []; + } - if (!!isShowSmallBalanceAssets) return true; + return userSupplyPositions + .map((position: MarketUserReserveSupplyPosition) => { + const underlyingTokenAddress = position.currency.address.toLowerCase(); + const reserve = supplyReservesLookup.get(underlyingTokenAddress); - // Filter out dust amounts < $0.01 USD - const balanceUSD = amountToUsd( - userReserve.underlyingBalance, - userReserve.reserve.formattedPriceInMarketReferenceCurrency, - marketReferencePriceInUsd + if (!reserve) { + console.warn( + '[SuppliedPositionsList] Missing reserve snapshot for supplied position', + position.currency.symbol, + position.currency.address ); - return Number(balanceUSD) >= SMALL_BALANCE_THRESHOLD; - }) - .map((userReserve) => ({ - ...userReserve, - supplyAPY: userReserve.reserve.supplyAPY, // Note: added only for table sort - reserve: { - ...userReserve.reserve, - ...(userReserve.reserve.isWrappedBaseAsset - ? fetchIconSymbolAndName({ - symbol: currentNetworkConfig.baseAssetSymbol, - underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(), - }) - : {}), - }, - })) || [] + return null; + } + + if (isAssetHidden(currentMarketData.market, underlyingTokenAddress)) { + return null; + } + + if (position.balance.amount.value === '0') { + return null; + } + + const balanceUSD = Number(position.balance.usd ?? '0'); + if (!isShowSmallBalanceAssets && balanceUSD < SMALL_BALANCE_THRESHOLD) { + return null; + } + const isWrappedNative = reserve.acceptsNative !== null; + + const updatedReserve: ReserveWithId = { + ...reserve, + supplyAPY: Number(position.apy.value), + underlyingBalance: position.balance.usd, + usageAsCollateralEnabledOnUser: position.canBeCollateral, + isCollateralPosition: position.isCollateral, + apyPosition: position.apy, + balancePosition: position.balance, + }; + + if (isWrappedNative) { + const nativeData = fetchIconSymbolAndName({ + symbol: currentNetworkConfig.baseAssetSymbol, // ETH, MATIC, etc + underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(), + }); + + return [ + // Native token if isWrappedNative + { + ...updatedReserve, + symbol: nativeData.symbol, + iconSymbol: nativeData.iconSymbol, + name: nativeData.name, + underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(), + detailsAddress: position.currency.address, // Dirección real para funcionalidad + id: reserve.id + '_native', + reserve: updatedReserve, + }, + ]; + } + + return { + ...updatedReserve, + reserve: updatedReserve, + }; + }) + .flat() + .filter(Boolean); + }, [ + currentMarketData.market, + currentNetworkConfig.baseAssetSymbol, + isShowSmallBalanceAssets, + supplyReservesLookup, + userSupplyPositions, + ]); + //! debug + console.log('suppliedPositions', suppliedPositions); + + const userEarnedAPY = useMemo(() => { + const totalSupplyUSD = suppliedPositions.reduce( + (sum, position) => sum + Number(position?.balancePosition?.usd || '0'), + 0 ); - }, [isShowSmallBalanceAssets, user?.userReservesData, marketReferencePriceInUsd]); + // APY ponderado por balance USD + const weightedSupplyAPY = suppliedPositions.reduce((sum, position) => { + const balanceUSD = Number(position?.balancePosition?.usd || '0'); + const apy = Number(position?.apyPosition?.value || '0'); + return sum + balanceUSD * apy; + }, 0); + + // APY promedio ponderado + const earnedAPY = totalSupplyUSD > 0 ? weightedSupplyAPY / totalSupplyUSD : 0; + + return { earnedAPY, totalSupplyUSD }; + }, [suppliedPositions]); + + // ! debug + console.log('🔍 APY Breakdown:', { + earnedAPY: (userEarnedAPY.earnedAPY * 100).toFixed(2) + '%', + }); // Transform to the DashboardReserve schema so the sort utils can work with it const preSortedReserves = suppliedPositions as DashboardReserve[]; const sortedReserves = handleSortDashboardReserves( @@ -198,11 +277,11 @@ export const SuppliedPositionsList = () => { <> Balance} - value={user?.totalLiquidityUSD || 0} + value={userEarnedAPY.totalSupplyUSD || 0} /> APY} - value={user?.earnedAPY || 0} + value={userEarnedAPY.earnedAPY || 0} percent tooltip={ { /> Collateral} - value={user?.totalCollateralUSD || 0} + value={userState?.totalCollateralBase || 0} tooltip={ { {!downToXSM && } {sortedReserves.map((item) => ( - + {downToXSM ? ( ) : ( )} - + ))} diff --git a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListItem.tsx b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListItem.tsx index bc6c1e3ecb..78df210970 100644 --- a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListItem.tsx +++ b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListItem.tsx @@ -1,10 +1,10 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { Trans } from '@lingui/macro'; import { Button } from '@mui/material'; -import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; -import { useAssetCaps } from 'src/hooks/useAssetCaps'; +import { mapAaveProtocolIncentives } from 'src/components/incentives/incentives.helper'; import { useModalContext } from 'src/hooks/useModal'; import { useRootStore } from 'src/store/root'; +import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; import { DashboardReserve } from 'src/utils/dashboardSortUtils'; import { GENERAL } from 'src/utils/events'; import { showExternalIncentivesTooltip } from 'src/utils/utils'; @@ -20,80 +20,94 @@ import { ListValueColumn } from '../ListValueColumn'; export const SuppliedPositionsListItem = ({ reserve, - underlyingBalance, - underlyingBalanceUSD, usageAsCollateralEnabledOnUser, underlyingAsset, + symbol, + name, + iconSymbol, }: DashboardReserve) => { - const { user } = useAppDataContext(); - const { isIsolated, aIncentivesData, aTokenAddress, isFrozen, isActive, isPaused } = reserve; + const { isFrozen, isPaused } = reserve; const { openSupply, openWithdraw, openCollateralChange, openCollateralSwap } = useModalContext(); - const { debtCeiling } = useAssetCaps(); const [trackEvent, currentMarketData, currentMarket] = useRootStore( useShallow((store) => [store.trackEvent, store.currentMarketData, store.currentMarket]) ); const showSwitchButton = isFeatureEnabled.liquiditySwap(currentMarketData); - const canBeEnabledAsCollateral = user - ? !debtCeiling.isMaxed && - reserve.reserveLiquidationThreshold !== '0' && - ((!reserve.isIsolated && !user.isInIsolationMode) || - user.isolatedReserve?.underlyingAsset === reserve.underlyingAsset || - (reserve.isIsolated && user.totalCollateralMarketReferenceCurrency === '0')) - : false; + const canBeEnabledAsCollateral = reserve.usageAsCollateralEnabledOnUser; - const disableSwap = !isActive || isPaused || reserve.symbol == 'stETH'; - const disableWithdraw = !isActive || isPaused; - const disableSupply = !isActive || isFrozen || isPaused; + const disableSwap = isPaused || reserve.underlyingToken.symbol == 'stETH'; + const disableWithdraw = isPaused; + const disableSupply = isFrozen || isPaused; + const { iconSymbol: iconSymbolFetched } = fetchIconSymbolAndName({ + underlyingAsset: reserve.underlyingToken.address, + symbol: reserve.underlyingToken.symbol, + name: reserve.underlyingToken.name, + }); + + const displayIconSymbol = + iconSymbolFetched?.toLowerCase() !== reserve.underlyingToken.symbol.toLowerCase() + ? iconSymbolFetched + : reserve.underlyingToken.symbol; + const supplyProtocolIncentives = mapAaveProtocolIncentives(reserve.incentives, 'supply'); + + console.log( + 'symbol, iconSymbol, name, underlyingAsset, currentMarket, isFrozen', + symbol, + iconSymbol, + name, + underlyingAsset, + currentMarket, + isFrozen + ); return ( { openCollateralChange( underlyingAsset, currentMarket, - reserve.name, + reserve.underlyingToken.name, 'dashboard', usageAsCollateralEnabledOnUser ); @@ -113,7 +127,7 @@ export const SuppliedPositionsListItem = ({ trackEvent(GENERAL.OPEN_MODAL, { modal: 'Swap Collateral', market: currentMarket, - assetName: reserve.name, + assetName: reserve.underlyingToken.name, asset: underlyingAsset, }); openCollateralSwap(underlyingAsset); @@ -126,7 +140,9 @@ export const SuppliedPositionsListItem = ({ @@ -135,7 +151,7 @@ export const SuppliedPositionsListItem = ({ disabled={disableWithdraw} variant="outlined" onClick={() => { - openWithdraw(underlyingAsset, currentMarket, reserve.name, 'dashboard'); + openWithdraw(underlyingAsset, currentMarket, reserve.underlyingToken.name, 'dashboard'); }} > Withdraw diff --git a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListMobileItem.tsx b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListMobileItem.tsx index 210bbd454a..be0943a651 100644 --- a/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListMobileItem.tsx +++ b/src/modules/dashboard/lists/SuppliedPositionsList/SuppliedPositionsListMobileItem.tsx @@ -1,9 +1,9 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { Trans } from '@lingui/macro'; import { Box, Button } from '@mui/material'; -import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; -import { useAssetCaps } from 'src/hooks/useAssetCaps'; +import { mapAaveProtocolIncentives } from 'src/components/incentives/incentives.helper'; import { useRootStore } from 'src/store/root'; +import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; import { DashboardReserve } from 'src/utils/dashboardSortUtils'; import { showExternalIncentivesTooltip } from 'src/utils/utils'; import { useShallow } from 'zustand/shallow'; @@ -18,64 +18,60 @@ import { ListValueRow } from '../ListValueRow'; export const SuppliedPositionsListMobileItem = ({ reserve, - underlyingBalance, - underlyingBalanceUSD, usageAsCollateralEnabledOnUser, underlyingAsset, + symbol, + name, + iconSymbol, }: DashboardReserve) => { - const { user } = useAppDataContext(); const [currentMarketData, currentMarket] = useRootStore( useShallow((state) => [state.currentMarketData, state.currentMarket]) ); + const { openSupply, openCollateralSwap, openWithdraw, openCollateralChange } = useModalContext(); - const { debtCeiling } = useAssetCaps(); + const isSwapButton = isFeatureEnabled.liquiditySwap(currentMarketData); - const { - symbol, - iconSymbol, - name, - supplyAPY, - isIsolated, - aIncentivesData, - aTokenAddress, - isFrozen, - isActive, - isPaused, - } = reserve; - const canBeEnabledAsCollateral = user - ? !debtCeiling.isMaxed && - reserve.reserveLiquidationThreshold !== '0' && - ((!reserve.isIsolated && !user.isInIsolationMode) || - user.isolatedReserve?.underlyingAsset === reserve.underlyingAsset || - (reserve.isIsolated && user.totalCollateralMarketReferenceCurrency === '0')) - : false; + const { isFrozen, isPaused } = reserve; + + const { iconSymbol: iconSymbolFetched } = fetchIconSymbolAndName({ + underlyingAsset: reserve.underlyingToken.address, + symbol: reserve.underlyingToken.symbol, + name: reserve.underlyingToken.name, + }); - const disableSwap = !isActive || isPaused || reserve.symbol == 'stETH'; - const disableWithdraw = !isActive || isPaused; - const disableSupply = !isActive || isFrozen || isPaused; + const displayIconSymbol = + iconSymbolFetched?.toLowerCase() !== reserve.underlyingToken.symbol.toLowerCase() + ? iconSymbolFetched + : reserve.underlyingToken.symbol; + + const supplyProtocolIncentives = mapAaveProtocolIncentives(reserve.incentives, 'supply'); + const canBeEnabledAsCollateral = reserve.usageAsCollateralEnabledOnUser; + const disableSwap = isPaused || reserve.underlyingToken.symbol == 'stETH'; + const disableWithdraw = isPaused; + const disableSupply = isFrozen || isPaused; return ( Supply balance} - value={Number(underlyingBalance)} - subValue={Number(underlyingBalanceUSD)} - disabled={Number(underlyingBalance) === 0} + value={Number(reserve.balancePosition?.amount.value ?? 0)} + subValue={Number(reserve.balancePosition?.usd ?? 0)} + disabled={Number(reserve.balancePosition?.amount.value ?? 0) === 0} /> Used as collateral} - align={isIsolated ? 'flex-start' : 'center'} + align={reserve.userState?.isInIsolationMode ? 'flex-start' : 'center'} captionVariant="description" mb={2} > openCollateralChange( underlyingAsset, currentMarket, - reserve.name, + reserve.underlyingToken.name, 'dashboard', usageAsCollateralEnabledOnUser ) @@ -132,7 +128,9 @@ export const SuppliedPositionsListMobileItem = ({