diff --git a/cypress/e2e/4-gho-ethereum/gho-modal.gho-v3.cy.ts b/cypress/e2e/4-gho-ethereum/gho-modal.gho-v3.cy.ts index 2cb6a2e31d..36d2601cbb 100644 --- a/cypress/e2e/4-gho-ethereum/gho-modal.gho-v3.cy.ts +++ b/cypress/e2e/4-gho-ethereum/gho-modal.gho-v3.cy.ts @@ -18,7 +18,7 @@ const testData = { let minApy: number; let maxApy: number; -//skip while borrow limit + describe.skip(`GHO MODAL APY TESTING`, () => { configEnvWithTenderlyAEthereumV3Fork({ v3: true, diff --git a/package.json b/package.json index 2a8a80fded..373dff0bf3 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "graphql-request": "^6.1.0", "gray-matter": "^4.0.3", "immer": "^9.0.15", + "micro-memoize": "^4.1.2", "mixpanel-browser": "^2.45.0", "next": "12.1.1", "react": "latest", diff --git a/pages/_app.page.tsx b/pages/_app.page.tsx index 3ff66aac38..61c00794de 100644 --- a/pages/_app.page.tsx +++ b/pages/_app.page.tsx @@ -145,10 +145,10 @@ export default function MyApp(props: MyAppProps) { - - - - + + + + {getLayout()} @@ -165,10 +165,10 @@ export default function MyApp(props: MyAppProps) { - - - - + + + + diff --git a/src/components/transactions/Borrow/BorrowActions.tsx b/src/components/transactions/Borrow/BorrowActions.tsx index d18fdc665a..87e422611c 100644 --- a/src/components/transactions/Borrow/BorrowActions.tsx +++ b/src/components/transactions/Borrow/BorrowActions.tsx @@ -68,7 +68,7 @@ export const BorrowActions = React.memo( setLoadingTxns, setApprovalTxState, } = useModalContext(); - const { refetchPoolData, refetchIncentiveData, refetchGhoData } = useBackgroundDataProvider(); + const { refetchPoolData, refetchIncentiveData } = useBackgroundDataProvider(); const { sendTx } = useWeb3Context(); const [requiresApproval, setRequiresApproval] = useState(false); const [approvedAmount, setApprovedAmount] = useState(); @@ -135,9 +135,9 @@ export const BorrowActions = React.memo( }); queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); + queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho }); refetchPoolData && refetchPoolData(); refetchIncentiveData && refetchIncentiveData(); - refetchGhoData && refetchGhoData(); } catch (error) { const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false); setTxError(parsedError); diff --git a/src/components/transactions/Borrow/BorrowModal.tsx b/src/components/transactions/Borrow/BorrowModal.tsx index 7cf8417401..0eecc03263 100644 --- a/src/components/transactions/Borrow/BorrowModal.tsx +++ b/src/components/transactions/Borrow/BorrowModal.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal'; import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; import { useRootStore } from 'src/store/root'; +import { displayGho } from 'src/utils/ghoUtilities'; import { GENERAL } from 'src/utils/mixPanelEvents'; import { BasicModal } from '../../primitives/BasicModal'; @@ -18,7 +19,7 @@ export const BorrowModal = () => { const { currentMarket } = useProtocolDataContext(); const [borrowUnWrapped, setBorrowUnWrapped] = useState(true); - const [trackEvent, displayGho] = useRootStore((store) => [store.trackEvent, store.displayGho]); + const [trackEvent] = useRootStore((store) => [store.trackEvent]); const handleBorrowUnwrapped = (borrowUnWrapped: boolean) => { trackEvent(GENERAL.OPEN_MODAL, { diff --git a/src/components/transactions/Borrow/GhoBorrowModalContent.tsx b/src/components/transactions/Borrow/GhoBorrowModalContent.tsx index 65f41600fd..81e427dfd1 100644 --- a/src/components/transactions/Borrow/GhoBorrowModalContent.tsx +++ b/src/components/transactions/Borrow/GhoBorrowModalContent.tsx @@ -21,13 +21,14 @@ import { Row } from 'src/components/primitives/Row'; import { StyledTxModalToggleButton } from 'src/components/StyledToggleButton'; import { StyledTxModalToggleGroup } from 'src/components/StyledToggleButtonGroup'; import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; +import { useGhoPoolReserve } from 'src/hooks/pool/useGhoPoolReserve'; +import { useUserGhoPoolReserve } from 'src/hooks/pool/useUserGhoPoolReserve'; import { useAssetCaps } from 'src/hooks/useAssetCaps'; import { useModalContext } from 'src/hooks/useModal'; -import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; import { useRootStore } from 'src/store/root'; import { CustomMarket } from 'src/ui-config/marketsConfig'; import { getMaxGhoMintAmount } from 'src/utils/getMaxAmountAvailableToBorrow'; -import { weightedAverageAPY } from 'src/utils/ghoUtilities'; +import { ghoUserQualifiesForDiscount, weightedAverageAPY } from 'src/utils/ghoUtilities'; import { roundToTokenDecimals } from 'src/utils/utils'; import { CapType } from '../../caps/helper'; @@ -113,13 +114,14 @@ export const GhoBorrowModalContent = ({ symbol, }: ModalWrapperProps) => { const { mainTxState: borrowTxState, gasLimit, txError, close: closeModal } = useModalContext(); + const currentMarketData = useRootStore((state) => state.currentMarketData); + const currentMarket = useRootStore((state) => state.currentMarket); const { user, marketReferencePriceInUsd, ghoReserveData, ghoUserData, ghoLoadingData } = useAppDataContext(); - const ghoUserQualifiesForDiscount = useRootStore((state) => state.ghoUserQualifiesForDiscount); + const { data: _ghoUserData } = useUserGhoPoolReserve(currentMarketData); + const { data: _ghoReserveData } = useGhoPoolReserve(currentMarketData); const { borrowCap } = useAssetCaps(); - const { currentMarket: customMarket } = useProtocolDataContext(); - const [interestRateMode, setInterestRateMode] = useState(InterestRate.Variable); const [amount, setAmount] = useState(''); const [riskCheckboxAccepted, setRiskCheckboxAccepted] = useState(false); @@ -128,7 +130,10 @@ export const GhoBorrowModalContent = ({ // Check if user can borrow at a discount const hasGhoBorrowPositions = ghoUserData.userGhoBorrowBalance > 0; const userStakedAaveBalance: number = ghoUserData.userDiscountTokenBalance; - const discountAvailable = ghoUserQualifiesForDiscount(amount); + const discountAvailable = + _ghoUserData && _ghoReserveData + ? ghoUserQualifiesForDiscount(_ghoReserveData, _ghoUserData, amount) + : false; // amount calculations let maxAmountToBorrow = getMaxGhoMintAmount(user, poolReserve); @@ -298,7 +303,7 @@ export const GhoBorrowModalContent = ({ discountAvailable={discountAvailable} userDiscountTokenBalance={ghoUserData.userDiscountTokenBalance} underlyingAsset={underlyingAsset} - customMarket={customMarket} + customMarket={currentMarket} currentBorrowAPY={currentBorrowAPY} futureBorrowAPY={futureBorrowAPY} onDetailsClick={() => closeModal()} diff --git a/src/components/transactions/DebtSwitch/DebtSwitchActions.tsx b/src/components/transactions/DebtSwitch/DebtSwitchActions.tsx index 30238e8fe3..16fa231f66 100644 --- a/src/components/transactions/DebtSwitch/DebtSwitchActions.tsx +++ b/src/components/transactions/DebtSwitch/DebtSwitchActions.tsx @@ -92,7 +92,7 @@ export const DebtSwitchActions = ({ setApprovalTxState, } = useModalContext(); const { sendTx, signTxData } = useWeb3Context(); - const { refetchPoolData, refetchIncentiveData, refetchGhoData } = useBackgroundDataProvider(); + const { refetchPoolData, refetchIncentiveData } = useBackgroundDataProvider(); const [requiresApproval, setRequiresApproval] = useState(false); const [approvedAmount, setApprovedAmount] = useState(); const [useSignature, setUseSignature] = useState(false); @@ -202,7 +202,7 @@ export const DebtSwitchActions = ({ }); queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); - refetchGhoData && refetchGhoData(); + queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho }); refetchPoolData && refetchPoolData(); refetchIncentiveData && refetchIncentiveData(); } catch (error) { diff --git a/src/components/transactions/DebtSwitch/DebtSwitchModalContent.tsx b/src/components/transactions/DebtSwitch/DebtSwitchModalContent.tsx index c1e452b498..a012f729ed 100644 --- a/src/components/transactions/DebtSwitch/DebtSwitchModalContent.tsx +++ b/src/components/transactions/DebtSwitch/DebtSwitchModalContent.tsx @@ -16,14 +16,19 @@ import { Warning } from 'src/components/primitives/Warning'; import { Asset, AssetInput } from 'src/components/transactions/AssetInput'; import { TxModalDetails } from 'src/components/transactions/FlowCommons/TxModalDetails'; import { useDebtSwitch } from 'src/hooks/paraswap/useDebtSwitch'; +import { useGhoPoolReserve } from 'src/hooks/pool/useGhoPoolReserve'; +import { useUserGhoPoolReserve } from 'src/hooks/pool/useUserGhoPoolReserve'; import { useModalContext } from 'src/hooks/useModal'; -import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { ListSlippageButton } from 'src/modules/dashboard/lists/SlippageList'; import { useRootStore } from 'src/store/root'; import { CustomMarket } from 'src/ui-config/marketsConfig'; import { assetCanBeBorrowedByUser } from 'src/utils/getMaxAmountAvailableToBorrow'; -import { weightedAverageAPY } from 'src/utils/ghoUtilities'; +import { + displayGho, + ghoUserQualifiesForDiscount, + weightedAverageAPY, +} from 'src/utils/ghoUtilities'; import { ComputedUserReserveData, @@ -67,18 +72,16 @@ export const DebtSwitchModalContent = ({ isWrongNetwork, currentRateMode, }: ModalWrapperProps & { currentRateMode: InterestRate }) => { - const { reserves, user, ghoReserveData, ghoUserData } = useAppDataContext(); - const { currentChainId, currentNetworkConfig } = useProtocolDataContext(); + const { reserves, user, ghoReserveData, ghoUserData, ghoUserLoadingData } = useAppDataContext(); + const currentChainId = useRootStore((store) => store.currentChainId); + const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig); const { currentAccount } = useWeb3Context(); const { gasLimit, mainTxState, txError, setTxError } = useModalContext(); - const [displayGho, currentMarket, ghoUserDataFetched, ghoUserQualifiesForDiscount] = useRootStore( - (state) => [ - state.displayGho, - state.currentMarket, - state.ghoUserDataFetched, - state.ghoUserQualifiesForDiscount, - ] - ); + + const currentMarket = useRootStore((store) => store.currentMarket); + const currentMarketData = useRootStore((store) => store.currentMarketData); + const { data: _ghoUserData } = useUserGhoPoolReserve(currentMarketData); + const { data: _ghoReserveData } = useGhoPoolReserve(currentMarketData); let switchTargets = reserves .filter( @@ -232,10 +235,13 @@ export const DebtSwitchModalContent = ({ ghoUserData.userGhoAvailableToBorrowAtDiscount, ghoReserveData.ghoBorrowAPYWithMaxDiscount ); - const ghoApyRange: [number, number] | undefined = ghoUserDataFetched + const ghoApyRange: [number, number] | undefined = !ghoUserLoadingData ? [userCurrentBorrowApy, userBorrowApyAfterMaxSwitchTo] : undefined; - qualifiesForDiscount = ghoUserQualifiesForDiscount(maxAmountToSwitch); + qualifiesForDiscount = + _ghoUserData && _ghoReserveData + ? ghoUserQualifiesForDiscount(_ghoReserveData, _ghoUserData, maxAmountToSwitch) + : false; ghoTargetData = { qualifiesForDiscount, ghoApyRange, diff --git a/src/components/transactions/Repay/RepayActions.tsx b/src/components/transactions/Repay/RepayActions.tsx index 612824cd4f..ab9979156f 100644 --- a/src/components/transactions/Repay/RepayActions.tsx +++ b/src/components/transactions/Repay/RepayActions.tsx @@ -69,7 +69,7 @@ export const RepayActions = ({ store.currentMarketData, ]); const { sendTx } = useWeb3Context(); - const { refetchGhoData, refetchIncentiveData, refetchPoolData } = useBackgroundDataProvider(); + const { refetchIncentiveData, refetchPoolData } = useBackgroundDataProvider(); const [signatureParams, setSignatureParams] = useState(); const { approvalTxState, @@ -202,9 +202,9 @@ export const RepayActions = ({ }); queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); + queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho }); refetchPoolData && refetchPoolData(); refetchIncentiveData && refetchIncentiveData(); - refetchGhoData && refetchGhoData(); } catch (error) { const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false); setTxError(parsedError); diff --git a/src/components/transactions/Repay/RepayModalContent.tsx b/src/components/transactions/Repay/RepayModalContent.tsx index 81f942b0c9..ecf055da04 100644 --- a/src/components/transactions/Repay/RepayModalContent.tsx +++ b/src/components/transactions/Repay/RepayModalContent.tsx @@ -17,6 +17,7 @@ import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvide import { useModalContext } from 'src/hooks/useModal'; import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; import { useRootStore } from 'src/store/root'; +import { displayGho } from 'src/utils/ghoUtilities'; import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig'; import { Asset, AssetInput } from '../AssetInput'; @@ -47,9 +48,8 @@ export const RepayModalContent = ({ const { marketReferencePriceInUsd, user } = useAppDataContext(); const { currentChainId, currentMarketData, currentMarket } = useProtocolDataContext(); - const [minRemainingBaseTokenBalance, displayGho] = useRootStore((store) => [ + const [minRemainingBaseTokenBalance] = useRootStore((store) => [ store.poolComputed.minRemainingBaseTokenBalance, - store.displayGho, ]); // states diff --git a/src/components/transactions/Supply/SupplyActions.tsx b/src/components/transactions/Supply/SupplyActions.tsx index 725a63ee96..70f700abdb 100644 --- a/src/components/transactions/Supply/SupplyActions.tsx +++ b/src/components/transactions/Supply/SupplyActions.tsx @@ -68,7 +68,7 @@ export const SupplyActions = React.memo( setGasLimit, setTxError, } = useModalContext(); - const { refetchPoolData, refetchIncentiveData, refetchGhoData } = useBackgroundDataProvider(); + const { refetchPoolData, refetchIncentiveData } = useBackgroundDataProvider(); const permitAvailable = tryPermit({ reserveAddress: poolAddress, isWrappedBaseAsset: isWrappedBaseAsset, @@ -187,7 +187,6 @@ export const SupplyActions = React.memo( queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); refetchPoolData && refetchPoolData(); refetchIncentiveData && refetchIncentiveData(); - refetchGhoData && refetchGhoData(); } catch (error) { const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false); setTxError(parsedError); diff --git a/src/components/transactions/Swap/SwapModalContent.tsx b/src/components/transactions/Swap/SwapModalContent.tsx index f53de5431a..0faa2785e1 100644 --- a/src/components/transactions/Swap/SwapModalContent.tsx +++ b/src/components/transactions/Swap/SwapModalContent.tsx @@ -15,8 +15,8 @@ import { useModalContext } from 'src/hooks/useModal'; import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { ListSlippageButton } from 'src/modules/dashboard/lists/SlippageList'; -import { useRootStore } from 'src/store/root'; import { remainingCap } from 'src/utils/getMaxAmountAvailableToSupply'; +import { displayGho } from 'src/utils/ghoUtilities'; import { calculateHFAfterSwap } from 'src/utils/hfUtils'; import { amountToUsd } from 'src/utils/utils'; @@ -44,10 +44,9 @@ export const SwapModalContent = ({ const { currentChainId, currentMarket, currentNetworkConfig } = useProtocolDataContext(); const { currentAccount } = useWeb3Context(); const { gasLimit, mainTxState: supplyTxState, txError } = useModalContext(); - const isGho = useRootStore((store) => store.displayGho); const swapTargets = reserves - .filter((r) => !isGho({ symbol: r.symbol, currentMarket })) + .filter((r) => !displayGho({ symbol: r.symbol, currentMarket })) .filter((r) => r.underlyingAsset !== poolReserve.underlyingAsset && !r.isFrozen) .map((reserve) => ({ address: reserve.underlyingAsset, diff --git a/src/components/transactions/Withdraw/WithdrawAndSwitchActions.tsx b/src/components/transactions/Withdraw/WithdrawAndSwitchActions.tsx index 4ab4090ed2..1a969ba911 100644 --- a/src/components/transactions/Withdraw/WithdrawAndSwitchActions.tsx +++ b/src/components/transactions/Withdraw/WithdrawAndSwitchActions.tsx @@ -97,7 +97,7 @@ export const WithdrawAndSwitchActions = ({ const [approvedAmount, setApprovedAmount] = useState(undefined); const [signatureParams, setSignatureParams] = useState(); - const { refetchPoolData, refetchIncentiveData, refetchGhoData } = useBackgroundDataProvider(); + const { refetchPoolData, refetchIncentiveData } = useBackgroundDataProvider(); const requiresApproval = useMemo(() => { if ( @@ -130,7 +130,7 @@ export const WithdrawAndSwitchActions = ({ const response = await sendTx(txDataWithGasEstimation); await response.wait(1); queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); - refetchGhoData && refetchGhoData(); + queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho }); refetchPoolData && refetchPoolData(); refetchIncentiveData && refetchIncentiveData(); setMainTxState({ diff --git a/src/helpers/useTransactionHandler.tsx b/src/helpers/useTransactionHandler.tsx index 938d9b4632..cba037ec57 100644 --- a/src/helpers/useTransactionHandler.tsx +++ b/src/helpers/useTransactionHandler.tsx @@ -59,7 +59,7 @@ export const useTransactionHandler = ({ setTxError, } = useModalContext(); const { signTxData, sendTx, getTxError } = useWeb3Context(); - const { refetchPoolData, refetchIncentiveData, refetchGhoData } = useBackgroundDataProvider(); + const { refetchPoolData, refetchIncentiveData } = useBackgroundDataProvider(); const [signatures, setSignatures] = useState([]); const [signatureDeadline, setSignatureDeadline] = useState(); @@ -125,9 +125,9 @@ export const useTransactionHandler = ({ }); queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool }); + queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho }); queryClient.invalidateQueries({ queryKey: queryKeysFactory.staking }); refetchPoolData && refetchPoolData(); - refetchGhoData && refetchGhoData(); refetchIncentiveData && refetchIncentiveData(); } catch (e) { // TODO: what to do with this error? diff --git a/src/hooks/app-data-provider/BackgroundDataProvider.tsx b/src/hooks/app-data-provider/BackgroundDataProvider.tsx index e18c1ad213..26e1f5ff6d 100644 --- a/src/hooks/app-data-provider/BackgroundDataProvider.tsx +++ b/src/hooks/app-data-provider/BackgroundDataProvider.tsx @@ -1,12 +1,7 @@ import React, { useContext } from 'react'; -import { - useGhoDataSubscription, - useIncentiveDataSubscription, - usePoolDataSubscription, -} from 'src/store/root'; +import { useIncentiveDataSubscription, usePoolDataSubscription } from 'src/store/root'; interface BackgroundDataProviderContextType { - refetchGhoData: () => Promise; refetchIncentiveData?: () => Promise; refetchPoolData?: () => Promise | Promise; } @@ -23,11 +18,8 @@ const BackgroundDataProviderContext = React.createContext { const refetchPoolData = usePoolDataSubscription(); const refetchIncentiveData = useIncentiveDataSubscription(); - const refetchGhoData = useGhoDataSubscription(); return ( - + {children} ); diff --git a/src/hooks/app-data-provider/useAppDataProvider.tsx b/src/hooks/app-data-provider/useAppDataProvider.tsx index e8fa96093d..7fcaae4790 100644 --- a/src/hooks/app-data-provider/useAppDataProvider.tsx +++ b/src/hooks/app-data-provider/useAppDataProvider.tsx @@ -1,8 +1,6 @@ import { ReserveDataHumanized } from '@aave/contract-helpers'; import { ComputedUserReserve, - formatGhoReserveData, - formatGhoUserData, formatReservesAndIncentives, FormattedGhoReserveData, FormattedGhoUserData, @@ -17,7 +15,7 @@ import React, { useContext } from 'react'; import { EmodeCategory } from 'src/helpers/types'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { useRootStore } from 'src/store/root'; -import { GHO_SUPPORTED_MARKETS, GHO_SYMBOL, weightedAverageAPY } from 'src/utils/ghoUtilities'; +import { displayGho, GHO_SUPPORTED_MARKETS, weightedAverageAPY } from 'src/utils/ghoUtilities'; import { reserveSortFn, @@ -29,6 +27,8 @@ import { selectFormattedReserves, selectUserSummaryAndIncentives, } from '../../store/poolSelectors'; +import { useGhoPoolFormattedReserve } from '../pool/useGhoPoolFormattedReserve'; +import { useUserGhoPoolFormattedReserve } from '../pool/useUserGhoPoolFormattedReserve'; import { useCurrentTimestamp } from '../useCurrentTimestamp'; import { useProtocolDataContext } from '../useProtocolDataContext'; @@ -74,6 +74,7 @@ export interface AppDataContextType { ghoReserveData: FormattedGhoReserveData; ghoUserData: FormattedGhoUserData; ghoLoadingData: boolean; + ghoUserLoadingData: boolean; } const AppDataContext = React.createContext({} as AppDataContextType); @@ -92,52 +93,53 @@ export const AppDataProvider: React.FC = ({ children }) => { userReserves, userEmodeCategoryId, eModes, - ghoReserveData, - ghoUserData, - ghoReserveDataFetched, formattedPoolReserves, userSummary, - displayGho, ] = useRootStore((state) => [ selectCurrentReserves(state), selectCurrentBaseCurrencyData(state), selectCurrentUserReserves(state), selectCurrentUserEmodeCategoryId(state), selectEmodes(state), - state.ghoReserveData, - state.ghoUserData, - state.ghoReserveDataFetched, selectFormattedReserves(state, currentTimestamp), selectUserSummaryAndIncentives(state, currentTimestamp), - state.displayGho, ]); - const formattedGhoReserveData: FormattedGhoReserveData = formatGhoReserveData({ - ghoReserveData, - }); - const formattedGhoUserData: FormattedGhoUserData = formatGhoUserData({ - ghoReserveData, - ghoUserData, - currentTimestamp, - }); + const currentMarketData = useRootStore((state) => state.currentMarketData); + + const { data: formattedGhoUserData, isLoading: isGhoUserDataLoading } = + useUserGhoPoolFormattedReserve(currentMarketData); + const { data: formattedGhoReserveData, isLoading: ghoReserveDataLoading } = + useGhoPoolFormattedReserve(currentMarketData); + + const formattedGhoReserveDataWithDefault = formattedGhoReserveData || { + aaveFacilitatorRemainingCapacity: 0, + aaveFacilitatorMintedPercent: 0, + aaveFacilitatorBucketLevel: 0, + aaveFacilitatorBucketMaxCapacity: 0, + ghoBorrowAPYWithMaxDiscount: 0, + ghoBaseVariableBorrowRate: 0, + ghoVariableBorrowAPY: 0, + ghoDiscountedPerToken: 0, + ghoDiscountRate: 0, + ghoMinDebtTokenBalanceForDiscount: 0, + ghoMinDiscountTokenBalanceForDiscount: 0, + }; + + const formattedGhoUserDataWithDefault = formattedGhoUserData || { + userGhoDiscountPercent: 0, + userDiscountTokenBalance: 0, + userGhoBorrowBalance: 0, + userDiscountedGhoInterest: 0, + userGhoAvailableToBorrowAtDiscount: 0, + }; - let ghoBorrowCap = '0'; - let aaveFacilitatorRemainingCapacity = Math.max( - formattedGhoReserveData.aaveFacilitatorRemainingCapacity - 0.000001, - 0 - ); let user = userSummary; // Factor discounted GHO interest into cumulative user fields if (GHO_SUPPORTED_MARKETS.includes(currentMarket)) { - ghoBorrowCap = reserves.find((r) => r.symbol === GHO_SYMBOL)?.borrowCap || '0'; - - if (ghoBorrowCap && ghoBorrowCap !== '0') { - aaveFacilitatorRemainingCapacity = Number(ghoBorrowCap); - } - - if (formattedGhoUserData.userDiscountedGhoInterest > 0) { + if (formattedGhoUserDataWithDefault.userDiscountedGhoInterest > 0) { const userSummaryWithDiscount = formatUserSummaryWithDiscount({ - userGhoDiscountedInterest: formattedGhoUserData.userDiscountedGhoInterest, + userGhoDiscountedInterest: formattedGhoUserDataWithDefault.userDiscountedGhoInterest, user, marketReferenceCurrencyPriceUSD: Number( formatUnits(baseCurrencyData.marketReferenceCurrencyPriceInUsd, USD_DECIMALS) @@ -173,21 +175,21 @@ export const AppDataProvider: React.FC = ({ children }) => { // TODO: Export to unified helper function if (displayGho({ symbol: reserve.symbol, currentMarket: currentMarket })) { const borrowRateAfterDiscount = weightedAverageAPY( - formattedGhoReserveData.ghoVariableBorrowAPY, - formattedGhoUserData.userGhoBorrowBalance, - formattedGhoUserData.userGhoAvailableToBorrowAtDiscount, - formattedGhoReserveData.ghoBorrowAPYWithMaxDiscount + formattedGhoReserveDataWithDefault.ghoVariableBorrowAPY, + formattedGhoUserDataWithDefault.userGhoBorrowBalance, + formattedGhoUserDataWithDefault.userGhoAvailableToBorrowAtDiscount, + formattedGhoReserveDataWithDefault.ghoBorrowAPYWithMaxDiscount ); acc.negativeProportion = acc.negativeProportion.plus( new BigNumber(borrowRateAfterDiscount).multipliedBy( - formattedGhoUserData.userGhoBorrowBalance + formattedGhoUserDataWithDefault.userGhoBorrowBalance ) ); if (reserve.vIncentivesData) { reserve.vIncentivesData.forEach((incentive) => { acc.positiveProportion = acc.positiveProportion.plus( new BigNumber(incentive.incentiveAPR).multipliedBy( - formattedGhoUserData.userGhoBorrowBalance + formattedGhoUserDataWithDefault.userGhoBorrowBalance ) ); }); @@ -267,12 +269,10 @@ export const AppDataProvider: React.FC = ({ children }) => { // TODO: we should consider removing this from the context and use zustand instead. If we had a selector that would return the formatted gho data, I think that // would work out pretty well. We could even extend that pattern for the other reserves, and migrate towards the global store instead of the app data provider. // ghoLoadingData for now is just propagated through to reduce changes to other components. - ghoReserveData: { - ...formattedGhoReserveData, - aaveFacilitatorRemainingCapacity, - }, - ghoUserData: formattedGhoUserData, - ghoLoadingData: !ghoReserveDataFetched, + ghoReserveData: formattedGhoReserveDataWithDefault, + ghoUserData: formattedGhoUserDataWithDefault, + ghoLoadingData: ghoReserveDataLoading, + ghoUserLoadingData: !!currentAccount && isGhoUserDataLoading, }} > {children} diff --git a/src/hooks/pool/useGhoPoolFormattedReserve.ts b/src/hooks/pool/useGhoPoolFormattedReserve.ts new file mode 100644 index 0000000000..09e93190d3 --- /dev/null +++ b/src/hooks/pool/useGhoPoolFormattedReserve.ts @@ -0,0 +1,39 @@ +import { ReservesDataHumanized } from '@aave/contract-helpers'; +import { formatGhoReserveData, GhoReserveData } from '@aave/math-utils'; +import { MarketDataType } from 'src/ui-config/marketsConfig'; +import { GHO_SYMBOL } from 'src/utils/ghoUtilities'; + +import { useGhoPoolsReserve } from './useGhoPoolReserve'; +import { usePoolsReservesHumanized } from './usePoolReserves'; +import { combineQueries } from './utils'; + +const selector = (ghoReserveData: GhoReserveData, reservesData: ReservesDataHumanized) => { + const formattedGhoReserveData = formatGhoReserveData({ ghoReserveData }); + let aaveFacilitatorRemainingCapacity = Math.max( + formattedGhoReserveData.aaveFacilitatorRemainingCapacity - 0.000001, + 0 + ); + const ghoReserveBorrowCap = reservesData.reservesData.find( + (elem) => elem.symbol === GHO_SYMBOL + )?.borrowCap; + if (ghoReserveBorrowCap && ghoReserveBorrowCap !== '0') { + aaveFacilitatorRemainingCapacity = Number(ghoReserveBorrowCap); + } + return { + ...formattedGhoReserveData, + aaveFacilitatorRemainingCapacity, + }; +}; + +export const useGhoPoolsFormattedReserve = (marketsData: MarketDataType[]) => { + const ghoReservesQueries = useGhoPoolsReserve(marketsData); + const reservesQueries = usePoolsReservesHumanized(marketsData); + + return ghoReservesQueries.map((elem, index) => { + return combineQueries([elem, reservesQueries[index]] as const, selector); + }); +}; + +export const useGhoPoolFormattedReserve = (marketData: MarketDataType) => { + return useGhoPoolsFormattedReserve([marketData])[0]; +}; diff --git a/src/hooks/pool/useGhoPoolReserve.ts b/src/hooks/pool/useGhoPoolReserve.ts new file mode 100644 index 0000000000..defaf090f1 --- /dev/null +++ b/src/hooks/pool/useGhoPoolReserve.ts @@ -0,0 +1,33 @@ +import { GhoReserveData } from '@aave/math-utils'; +import { useQueries, UseQueryOptions } from '@tanstack/react-query'; +import { MarketDataType } from 'src/ui-config/marketsConfig'; +import { POLLING_INTERVAL, queryKeysFactory } from 'src/ui-config/queries'; +import { useSharedDependencies } from 'src/ui-config/SharedDependenciesProvider'; + +import { HookOpts } from '../commonTypes'; + +export const useGhoPoolsReserve = ( + marketsData: MarketDataType[], + opts?: HookOpts +) => { + const { uiGhoService } = useSharedDependencies(); + return useQueries({ + queries: marketsData.map( + (marketData) => + ({ + queryKey: queryKeysFactory.ghoReserveData(marketData), + queryFn: () => uiGhoService.getGhoReserveData(marketData), + enabled: !!marketData.addresses.GHO_TOKEN_ADDRESS, + refetchInterval: POLLING_INTERVAL, + ...opts, + } as UseQueryOptions) + ), + }); +}; + +export const useGhoPoolReserve = ( + marketData: MarketDataType, + opts?: HookOpts +) => { + return useGhoPoolsReserve([marketData], opts)[0]; +}; diff --git a/src/hooks/pool/useUserGhoPoolFormattedReserve.ts b/src/hooks/pool/useUserGhoPoolFormattedReserve.ts new file mode 100644 index 0000000000..0cbf05b52d --- /dev/null +++ b/src/hooks/pool/useUserGhoPoolFormattedReserve.ts @@ -0,0 +1,28 @@ +import { formatGhoUserData, GhoReserveData, GhoUserData } from '@aave/math-utils'; +import dayjs from 'dayjs'; +import { MarketDataType } from 'src/ui-config/marketsConfig'; + +import { useGhoPoolsReserve } from './useGhoPoolReserve'; +import { useUserGhoPoolsReserve } from './useUserGhoPoolReserve'; +import { combineQueries } from './utils'; + +const selector = (ghoReserveData: GhoReserveData, ghoUserData: GhoUserData) => { + return formatGhoUserData({ + ghoReserveData, + ghoUserData, + currentTimestamp: dayjs().unix(), + }); +}; + +export const useUserGhoPoolsFormattedReserve = (marketsData: MarketDataType[]) => { + const ghoReserveQuery = useGhoPoolsReserve(marketsData); + const userGhoReservesQuery = useUserGhoPoolsReserve(marketsData); + + return ghoReserveQuery.map((elem, index) => { + return combineQueries([elem, userGhoReservesQuery[index]] as const, selector); + }); +}; + +export const useUserGhoPoolFormattedReserve = (marketData: MarketDataType) => { + return useUserGhoPoolsFormattedReserve([marketData])[0]; +}; diff --git a/src/hooks/pool/useUserGhoPoolReserve.ts b/src/hooks/pool/useUserGhoPoolReserve.ts new file mode 100644 index 0000000000..b550363577 --- /dev/null +++ b/src/hooks/pool/useUserGhoPoolReserve.ts @@ -0,0 +1,35 @@ +import { GhoUserData } from '@aave/math-utils'; +import { useQueries, UseQueryOptions } from '@tanstack/react-query'; +import { useRootStore } from 'src/store/root'; +import { MarketDataType } from 'src/ui-config/marketsConfig'; +import { POLLING_INTERVAL, queryKeysFactory } from 'src/ui-config/queries'; +import { useSharedDependencies } from 'src/ui-config/SharedDependenciesProvider'; + +import { HookOpts } from '../commonTypes'; + +export const useUserGhoPoolsReserve = ( + marketsData: MarketDataType[], + opts?: HookOpts +) => { + const { uiGhoService } = useSharedDependencies(); + const user = useRootStore((store) => store.account); + return useQueries({ + queries: marketsData.map( + (marketData) => + ({ + queryKey: queryKeysFactory.ghoUserReserveData(user, marketData), + queryFn: () => uiGhoService.getGhoUserData(marketData, user), + refetchInterval: POLLING_INTERVAL, + enabled: !!user && !!marketData.addresses.GHO_TOKEN_ADDRESS, + ...opts, + } as UseQueryOptions) + ), + }); +}; + +export const useUserGhoPoolReserve = ( + marketData: MarketDataType, + opts?: HookOpts +) => { + return useUserGhoPoolsReserve([marketData], opts)[0]; +}; diff --git a/src/hooks/pool/utils.ts b/src/hooks/pool/utils.ts new file mode 100644 index 0000000000..09047bea21 --- /dev/null +++ b/src/hooks/pool/utils.ts @@ -0,0 +1,39 @@ +import { UseQueryResult } from '@tanstack/react-query'; + +export type SimplifiedUseQueryResult = + | Pick, 'data' | 'error' | 'isLoading'> + | { + isLoading: false; + data: TData; + error: null; + }; + +type NonUndefined = T extends undefined ? never : T; + +export const combineQueries = ( + queries: Queries, + combiner: ( + ...data: { + [K in keyof Queries]: NonUndefined; + } + ) => P +): SimplifiedUseQueryResult => { + const isLoading = queries.some((elem) => elem.isLoading); + const isAllDataDefined = queries.every((elem) => elem.data); + const allData = queries.map((elem) => elem.data) as { + [K in keyof Queries]: NonUndefined; + }; + const error = queries.find((elem) => elem.error)?.error; + if (!error && !isLoading) { + return { + isLoading: false, + data: combiner(...allData), + error: null, + }; + } + return { + isLoading: isLoading, + data: isAllDataDefined ? combiner(...allData) : undefined, + error, + }; +}; diff --git a/src/hooks/useReserveActionState.tsx b/src/hooks/useReserveActionState.tsx index b25becce71..b808b8899c 100644 --- a/src/hooks/useReserveActionState.tsx +++ b/src/hooks/useReserveActionState.tsx @@ -11,6 +11,7 @@ import { useAssetCaps } from 'src/hooks/useAssetCaps'; import { WalletEmptyInfo } from 'src/modules/dashboard/lists/SupplyAssetsList/WalletEmptyInfo'; import { useRootStore } from 'src/store/root'; import { assetCanBeBorrowedByUser } from 'src/utils/getMaxAmountAvailableToBorrow'; +import { displayGho } from 'src/utils/ghoUtilities'; import { useModalContext } from './useModal'; @@ -29,14 +30,11 @@ export const useReserveActionState = ({ }: ReserveActionStateProps) => { const { user, eModes } = useAppDataContext(); const { supplyCap, borrowCap, debtCeiling } = useAssetCaps(); - const [currentMarket, currentNetworkConfig, currentChainId, displayGho] = useRootStore( - (store) => [ - store.currentMarket, - store.currentNetworkConfig, - store.currentChainId, - store.displayGho, - ] - ); + const [currentMarket, currentNetworkConfig, currentChainId] = useRootStore((store) => [ + store.currentMarket, + store.currentNetworkConfig, + store.currentChainId, + ]); const { openFaucet } = useModalContext(); const { bridge, name: networkName } = currentNetworkConfig; diff --git a/src/modules/dashboard/lists/BorrowAssetsList/BorrowAssetsList.tsx b/src/modules/dashboard/lists/BorrowAssetsList/BorrowAssetsList.tsx index 1c4c320e37..3f62619b74 100644 --- a/src/modules/dashboard/lists/BorrowAssetsList/BorrowAssetsList.tsx +++ b/src/modules/dashboard/lists/BorrowAssetsList/BorrowAssetsList.tsx @@ -10,9 +10,8 @@ import { ListHeaderWrapper } from 'src/components/lists/ListHeaderWrapper'; import { Warning } from 'src/components/primitives/Warning'; import { MarketWarning } from 'src/components/transactions/Warnings/MarketWarning'; import { AssetCapsProvider } from 'src/hooks/useAssetCaps'; -import { useRootStore } from 'src/store/root'; import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; -import { findAndFilterGhoReserve } from 'src/utils/ghoUtilities'; +import { displayGho, findAndFilterGhoReserve } from 'src/utils/ghoUtilities'; import { GENERAL } from 'src/utils/mixPanelEvents'; import { CapType } from '../../../../components/caps/helper'; @@ -93,7 +92,6 @@ const head = [ export const BorrowAssetsList = () => { const { currentNetworkConfig, currentMarketData, currentMarket } = useProtocolDataContext(); const { user, reserves, marketReferencePriceInUsd, loading } = useAppDataContext(); - const [displayGho] = useRootStore((store) => [store.displayGho]); const theme = useTheme(); const downToXSM = useMediaQuery(theme.breakpoints.down('xsm')); const [sortName, setSortName] = useState(''); diff --git a/src/modules/dashboard/lists/BorrowAssetsList/GhoBorrowAssetsListItem.tsx b/src/modules/dashboard/lists/BorrowAssetsList/GhoBorrowAssetsListItem.tsx index 4c3a15c553..6685237d13 100644 --- a/src/modules/dashboard/lists/BorrowAssetsList/GhoBorrowAssetsListItem.tsx +++ b/src/modules/dashboard/lists/BorrowAssetsList/GhoBorrowAssetsListItem.tsx @@ -11,7 +11,6 @@ import { TokenIcon } from 'src/components/primitives/TokenIcon'; import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; import { useModalContext } from 'src/hooks/useModal'; import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; -import { useRootStore } from 'src/store/root'; import { CustomMarket } from 'src/ui-config/marketsConfig'; import { DASHBOARD_LIST_COLUMN_WIDTHS } from 'src/utils/dashboardSortUtils'; import { getMaxGhoMintAmount } from 'src/utils/getMaxAmountAvailableToBorrow'; @@ -35,8 +34,7 @@ export const GhoBorrowAssetsListItem = ({ const { openBorrow } = useModalContext(); const { user } = useAppDataContext(); const { currentMarket } = useProtocolDataContext(); - const { ghoReserveData, ghoUserData, ghoLoadingData } = useAppDataContext(); - const { ghoUserDataFetched } = useRootStore(); + const { ghoReserveData, ghoUserData, ghoUserLoadingData, ghoLoadingData } = useAppDataContext(); const theme = useTheme(); const downToXSM = useMediaQuery(theme.breakpoints.down('xsm')); @@ -61,7 +59,7 @@ export const GhoBorrowAssetsListItem = ({ ghoUserData.userGhoAvailableToBorrowAtDiscount, ghoReserveData.ghoBorrowAPYWithMaxDiscount ); - const ghoApyRange: [number, number] | undefined = ghoUserDataFetched + const ghoApyRange: [number, number] | undefined = !ghoUserLoadingData ? [ ghoUserData.userGhoAvailableToBorrowAtDiscount === 0 ? ghoReserveData.ghoBorrowAPYWithMaxDiscount @@ -80,7 +78,7 @@ export const GhoBorrowAssetsListItem = ({ borrowButtonDisable, userDiscountTokenBalance: ghoUserData.userDiscountTokenBalance, ghoApyRange, - ghoUserDataFetched, + ghoUserDataFetched: !ghoUserLoadingData, userBorrowApyAfterNewBorrow, ghoLoadingData, onBorrowClick: () => openBorrow(underlyingAsset, currentMarket, name, 'dashboard'), diff --git a/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItemWrapper.tsx b/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItemWrapper.tsx index 16d88ff008..402d15730a 100644 --- a/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItemWrapper.tsx +++ b/src/modules/dashboard/lists/BorrowedPositionsList/BorrowedPositionsListItemWrapper.tsx @@ -1,13 +1,12 @@ import { AssetCapsProvider } from 'src/hooks/useAssetCaps'; import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; -import { useRootStore } from 'src/store/root'; import { DashboardReserve } from 'src/utils/dashboardSortUtils'; +import { displayGho } from 'src/utils/ghoUtilities'; import { BorrowedPositionsListItem } from './BorrowedPositionsListItem'; import { GhoBorrowedPositionsListItem } from './GhoBorrowedPositionsListItem'; export const BorrowedPositionsListItemWrapper = ({ item }: { item: DashboardReserve }) => { - const [displayGho] = useRootStore((store) => [store.displayGho]); const { currentMarket } = useProtocolDataContext(); return ( diff --git a/src/modules/dashboard/lists/BorrowedPositionsList/GhoBorrowedPositionsListItem.tsx b/src/modules/dashboard/lists/BorrowedPositionsList/GhoBorrowedPositionsListItem.tsx index 35f8b47963..3fe1ab4413 100644 --- a/src/modules/dashboard/lists/BorrowedPositionsList/GhoBorrowedPositionsListItem.tsx +++ b/src/modules/dashboard/lists/BorrowedPositionsList/GhoBorrowedPositionsListItem.tsx @@ -7,12 +7,13 @@ import { GhoIncentivesCard } from 'src/components/incentives/GhoIncentivesCard'; import { FixedAPYTooltipText } from 'src/components/infoTooltips/FixedAPYTooltip'; import { ROUTES } from 'src/components/primitives/Link'; import { Row } from 'src/components/primitives/Row'; +import { useGhoPoolReserve } from 'src/hooks/pool/useGhoPoolReserve'; +import { useUserGhoPoolReserve } from 'src/hooks/pool/useUserGhoPoolReserve'; import { useModalContext } from 'src/hooks/useModal'; -import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; import { useRootStore } from 'src/store/root'; import { CustomMarket } from 'src/ui-config/marketsConfig'; import { getMaxGhoMintAmount } from 'src/utils/getMaxAmountAvailableToBorrow'; -import { weightedAverageAPY } from 'src/utils/ghoUtilities'; +import { ghoUserQualifiesForDiscount, weightedAverageAPY } from 'src/utils/ghoUtilities'; import { isFeatureEnabled } from 'src/utils/marketsAndNetworksConfig'; import { ListColumn } from '../../../../components/lists/ListColumn'; @@ -32,12 +33,12 @@ export const GhoBorrowedPositionsListItem = ({ borrowRateMode, }: ComputedUserReserveData & { borrowRateMode: InterestRate }) => { const { openBorrow, openRepay, openDebtSwitch } = useModalContext(); - const { currentMarket, currentMarketData } = useProtocolDataContext(); - const { ghoLoadingData, ghoReserveData, ghoUserData, user } = useAppDataContext(); - const [ghoUserDataFetched, ghoUserQualifiesForDiscount] = useRootStore((store) => [ - store.ghoUserDataFetched, - store.ghoUserQualifiesForDiscount, - ]); + const currentMarket = useRootStore((store) => store.currentMarket); + const currentMarketData = useRootStore((store) => store.currentMarketData); + const { ghoLoadingData, ghoReserveData, ghoUserData, ghoUserLoadingData, user } = + useAppDataContext(); + const { data: _ghoUserData } = useUserGhoPoolReserve(currentMarketData); + const { data: _ghoReserveData } = useGhoPoolReserve(currentMarketData); const theme = useTheme(); const downToXSM = useMediaQuery(theme.breakpoints.down('xsm')); @@ -52,7 +53,10 @@ export const GhoBorrowedPositionsListItem = ({ ghoReserveData.ghoBorrowAPYWithMaxDiscount ); - const hasDiscount = ghoUserQualifiesForDiscount(); + const hasDiscount = + _ghoUserData && _ghoReserveData + ? ghoUserQualifiesForDiscount(_ghoReserveData, _ghoUserData) + : false; const { isActive, isFrozen, isPaused, borrowingEnabled } = reserve; const maxAmountUserCanMint = Number(getMaxGhoMintAmount(user, reserve)); @@ -67,7 +71,7 @@ export const GhoBorrowedPositionsListItem = ({ userGhoBorrowBalance: ghoUserData.userGhoBorrowBalance, hasDiscount, ghoLoadingData, - ghoUserDataFetched, + ghoUserDataFetched: !ghoUserLoadingData, borrowRateAfterDiscount, currentMarket, userDiscountTokenBalance: ghoUserData.userDiscountTokenBalance, diff --git a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx index 5cf330f60b..1699413633 100644 --- a/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx +++ b/src/modules/dashboard/lists/SupplyAssetsList/SupplyAssetsList.tsx @@ -12,6 +12,7 @@ import { MarketWarning } from 'src/components/transactions/Warnings/MarketWarnin import { AssetCapsProvider } from 'src/hooks/useAssetCaps'; import { useRootStore } from 'src/store/root'; import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; +import { displayGho } from 'src/utils/ghoUtilities'; import { ListWrapper } from '../../../../components/lists/ListWrapper'; import { Link, ROUTES } from '../../../../components/primitives/Link'; @@ -54,7 +55,6 @@ export const SupplyAssetsList = () => { loading: loadingReserves, } = useAppDataContext(); const { walletBalances, loading } = useWalletBalances(currentMarketData); - const [displayGho] = useRootStore((store) => [store.displayGho]); const theme = useTheme(); const downToXSM = useMediaQuery(theme.breakpoints.down('xsm')); diff --git a/src/modules/reserve-overview/ReserveActions.tsx b/src/modules/reserve-overview/ReserveActions.tsx index 91551904fb..e2b92ab673 100644 --- a/src/modules/reserve-overview/ReserveActions.tsx +++ b/src/modules/reserve-overview/ReserveActions.tsx @@ -36,6 +36,7 @@ import { getMaxGhoMintAmount, } from 'src/utils/getMaxAmountAvailableToBorrow'; import { getMaxAmountAvailableToSupply } from 'src/utils/getMaxAmountAvailableToSupply'; +import { displayGho } from 'src/utils/ghoUtilities'; import { GENERAL } from 'src/utils/mixPanelEvents'; import { amountToUsd } from 'src/utils/utils'; @@ -77,9 +78,8 @@ export const ReserveActions = ({ reserve }: ReserveActionsProps) => { } = useAppDataContext(); const { walletBalances, loading: loadingWalletBalance } = useWalletBalances(currentMarketData); - const [minRemainingBaseTokenBalance, displayGho] = useRootStore((store) => [ + const [minRemainingBaseTokenBalance] = useRootStore((store) => [ store.poolComputed.minRemainingBaseTokenBalance, - store.displayGho, ]); const { baseAssetSymbol } = currentNetworkConfig; let balance = walletBalances[reserve.underlyingAsset]; diff --git a/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx b/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx index 469f100f1a..d8e0c7a269 100644 --- a/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx +++ b/src/modules/reserve-overview/ReserveConfigurationWrapper.tsx @@ -3,7 +3,7 @@ import { Box, Paper, Typography, useMediaQuery, useTheme } from '@mui/material'; import dynamic from 'next/dynamic'; import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider'; import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; -import { useRootStore } from 'src/store/root'; +import { displayGho } from 'src/utils/ghoUtilities'; type ReserveConfigurationProps = { reserve: ComputedReserveData; @@ -19,7 +19,6 @@ const ReserveConfiguration = dynamic(() => export const ReserveConfigurationWrapper: React.FC = ({ reserve }) => { const { currentMarket } = useProtocolDataContext(); - const [displayGho] = useRootStore((store) => [store.displayGho]); const { breakpoints } = useTheme(); const downToXsm = useMediaQuery(breakpoints.down('xsm')); const isGho = displayGho({ symbol: reserve.symbol, currentMarket }); diff --git a/src/modules/reserve-overview/ReserveTopDetailsWrapper.tsx b/src/modules/reserve-overview/ReserveTopDetailsWrapper.tsx index f54eba9e3c..e6a9adba16 100644 --- a/src/modules/reserve-overview/ReserveTopDetailsWrapper.tsx +++ b/src/modules/reserve-overview/ReserveTopDetailsWrapper.tsx @@ -14,7 +14,7 @@ import { useRouter } from 'next/router'; import { getMarketInfoById, MarketLogo } from 'src/components/MarketSwitcher'; import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; -import { useRootStore } from 'src/store/root'; +import { displayGho } from 'src/utils/ghoUtilities'; import { TopInfoPanel } from '../../components/TopInfoPanel/TopInfoPanel'; import { TopInfoPanelItem } from '../../components/TopInfoPanel/TopInfoPanelItem'; @@ -37,7 +37,6 @@ export const ReserveTopDetailsWrapper = ({ underlyingAsset }: ReserveTopDetailsP const { currentMarket, currentChainId } = useProtocolDataContext(); const { market, network } = getMarketInfoById(currentMarket); const { addERC20Token, switchNetwork, chainId: connectedChainId, connected } = useWeb3Context(); - const [displayGho] = useRootStore((store) => [store.displayGho]); const theme = useTheme(); const downToSM = useMediaQuery(theme.breakpoints.down('sm')); diff --git a/src/services/UiGhoService.ts b/src/services/UiGhoService.ts new file mode 100644 index 0000000000..79a2edba76 --- /dev/null +++ b/src/services/UiGhoService.ts @@ -0,0 +1,34 @@ +import { GhoService } from '@aave/contract-helpers'; +import { Provider } from '@ethersproject/providers'; +import { MarketDataType } from 'src/ui-config/marketsConfig'; +import { GHO_SUPPORTED_MARKETS } from 'src/utils/ghoUtilities'; +import invariant from 'tiny-invariant'; + +export class UiGhoService { + constructor(private readonly getProvider: (chainId: number) => Provider) {} + + private getUiGhoProvider(marketData: MarketDataType) { + const provider = this.getProvider(marketData.chainId); + const isGhoSupportedMarket = GHO_SUPPORTED_MARKETS.includes(marketData.market); + invariant(isGhoSupportedMarket, 'Gho is not supported in this market'); + const { GHO_TOKEN_ADDRESS: ghoTokenAddress, GHO_UI_DATA_PROVIDER: uiGhoDataProviderAddress } = + marketData.addresses; + invariant( + ghoTokenAddress && uiGhoDataProviderAddress, + 'Gho token address or UI Gho data provider address not found for this market' + ); + return new GhoService({ + provider, + uiGhoDataProviderAddress: uiGhoDataProviderAddress, + }); + } + + async getGhoReserveData(marketData: MarketDataType) { + const uiGhoProvider = this.getUiGhoProvider(marketData); + return uiGhoProvider.getGhoReserveData(); + } + async getGhoUserData(marketData: MarketDataType, user: string) { + const uiGhoProvider = this.getUiGhoProvider(marketData); + return uiGhoProvider.getGhoUserData(user); + } +} diff --git a/src/store/ghoSlice.ts b/src/store/ghoSlice.ts deleted file mode 100644 index 627330aca5..0000000000 --- a/src/store/ghoSlice.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { GhoService } from '@aave/contract-helpers'; -import { GhoReserveData, GhoUserData, normalize } from '@aave/math-utils'; -import { GHO_SUPPORTED_MARKETS } from 'src/utils/ghoUtilities'; -import { getProvider } from 'src/utils/marketsAndNetworksConfig'; -import { StateCreator } from 'zustand'; - -import { RootStore } from './root'; - -interface GhoMarketConfig { - ghoTokenAddress: string; - uiGhoDataProviderAddress: string; -} - -interface GhoUtilMintingAvailableParams { - symbol: string; - currentMarket: string; -} - -export interface GhoSlice { - ghoReserveData: GhoReserveData; - ghoUserData: GhoUserData; - ghoReserveDataFetched: boolean; - ghoUserDataFetched: boolean; - ghoUserQualifiesForDiscount: (futureBorrowAmount?: string) => boolean; - ghoMarketConfig: () => GhoMarketConfig | undefined; - refreshGhoData: () => Promise; - displayGho: ({ symbol, currentMarket }: GhoUtilMintingAvailableParams) => boolean; -} - -export const createGhoSlice: StateCreator< - RootStore, - [['zustand/subscribeWithSelector', never], ['zustand/devtools', never]], - [], - GhoSlice -> = (set, get) => { - return { - ghoReserveData: { - ghoBaseVariableBorrowRate: '0', - ghoDiscountedPerToken: '0', - ghoDiscountRate: '0', - ghoMinDebtTokenBalanceForDiscount: '0', - ghoMinDiscountTokenBalanceForDiscount: '0', - ghoReserveLastUpdateTimestamp: '0', - ghoCurrentBorrowIndex: '0', - aaveFacilitatorBucketLevel: '0', - aaveFacilitatorBucketMaxCapacity: '0', - }, - ghoUserData: { - userGhoDiscountPercent: '0', - userDiscountTokenBalance: '0', - userPreviousGhoBorrowIndex: '0', - userGhoScaledBorrowBalance: '0', - }, - ghoReserveDataFetched: false, - ghoUserDataFetched: false, - displayGho: ({ symbol, currentMarket }: GhoUtilMintingAvailableParams): boolean => { - return symbol === 'GHO' && GHO_SUPPORTED_MARKETS.includes(currentMarket); - }, - ghoUserQualifiesForDiscount: (futureBorrowAmount = '0') => { - const ghoReserveDataFetched = get().ghoReserveDataFetched; - const ghoUserDataFetched = get().ghoUserDataFetched; - - if (!ghoReserveDataFetched || !ghoUserDataFetched) return false; - - const ghoReserveData = get().ghoReserveData; - const ghoUserData = get().ghoUserData; - - const borrowBalance = Number(normalize(ghoUserData.userGhoScaledBorrowBalance, 18)); - const minBorrowBalanceForDiscount = Number( - normalize(ghoReserveData.ghoMinDebtTokenBalanceForDiscount, 18) - ); - - const stkAaveBalance = Number(normalize(ghoUserData.userDiscountTokenBalance, 18)); - const minStkAaveBalanceForDiscount = Number( - normalize(ghoReserveData.ghoMinDiscountTokenBalanceForDiscount, 18) - ); - - return ( - borrowBalance + Number(futureBorrowAmount) >= minBorrowBalanceForDiscount && - stkAaveBalance >= minStkAaveBalanceForDiscount - ); - }, - ghoMarketConfig: () => { - const market = get().currentMarket; - if (!GHO_SUPPORTED_MARKETS.includes(market)) { - return undefined; - } - - const { GHO_TOKEN_ADDRESS: ghoTokenAddress, GHO_UI_DATA_PROVIDER: uiGhoDataProviderAddress } = - get().currentMarketData.addresses; - if (!ghoTokenAddress || !uiGhoDataProviderAddress) { - return undefined; - } - - return { - ghoTokenAddress, - uiGhoDataProviderAddress, - }; - }, - refreshGhoData: async () => { - const ghoConfig = get().ghoMarketConfig(); - if (!ghoConfig) return; - - const account = get().account; - - const ghoService = new GhoService({ - provider: getProvider(get().currentMarketData.chainId), - uiGhoDataProviderAddress: ghoConfig.uiGhoDataProviderAddress, - }); - - if (account) { - try { - const [ghoReserveData, ghoUserData] = await Promise.all([ - ghoService.getGhoReserveData(), - ghoService.getGhoUserData(account), - ]); - - set({ - ghoReserveData: ghoReserveData, - ghoUserData: ghoUserData, - ghoReserveDataFetched: true, - ghoUserDataFetched: true, - }); - } catch (err) { - console.log('error', err); - } - } else { - try { - const ghoReserveData = await ghoService.getGhoReserveData(); - - set({ - ghoReserveData: ghoReserveData, - ghoReserveDataFetched: true, - ghoUserDataFetched: false, - }); - } catch (err) { - console.log('error', err); - } - } - }, - }; -}; diff --git a/src/store/root.ts b/src/store/root.ts index 2a0173b20d..5ddcd2fbca 100644 --- a/src/store/root.ts +++ b/src/store/root.ts @@ -4,7 +4,6 @@ import create from 'zustand'; import { devtools, subscribeWithSelector } from 'zustand/middleware'; import { AnalyticsSlice, createAnalyticsSlice } from './analyticsSlice'; -import { createGhoSlice, GhoSlice } from './ghoSlice'; import { createGovernanceSlice, GovernanceSlice } from './governanceSlice'; import { createIncentiveSlice, IncentiveSlice } from './incentiveSlice'; import { createLayoutSlice, LayoutSlice } from './layoutSlice'; @@ -27,7 +26,6 @@ export type RootStore = StakeSlice & IncentiveSlice & GovernanceSlice & V3MigrationSlice & - GhoSlice & WalletDomainsSlice & AnalyticsSlice & TransactionsSlice & @@ -44,7 +42,6 @@ export const useRootStore = create()( ...createIncentiveSlice(...args), ...createGovernanceSlice(...args), ...createV3MigrationSlice(...args), - ...createGhoSlice(...args), ...createWalletDomainsSlice(...args), ...createAnalyticsSlice(...args), ...createTransactionsSlice(...args), @@ -84,10 +81,6 @@ export const useIncentiveDataSubscription = createSingletonSubscriber(() => { return useRootStore.getState().refreshIncentiveData(); }, 60000); -export const useGhoDataSubscription = createSingletonSubscriber(() => { - return useRootStore.getState().refreshGhoData(); -}, 60000); - useRootStore.subscribe( (state) => state.account, (account) => { diff --git a/src/ui-config/SharedDependenciesProvider.tsx b/src/ui-config/SharedDependenciesProvider.tsx index 952b14d538..491b9034bf 100644 --- a/src/ui-config/SharedDependenciesProvider.tsx +++ b/src/ui-config/SharedDependenciesProvider.tsx @@ -4,6 +4,7 @@ import { DelegationTokenService } from 'src/services/DelegationTokenService'; import { GovernanceService } from 'src/services/GovernanceService'; import { GovernanceV3Service } from 'src/services/GovernanceV3Service'; import { StkAbptMigrationService } from 'src/services/StkAbptMigrationService'; +import { UiGhoService } from 'src/services/UiGhoService'; import { UiIncentivesService } from 'src/services/UIIncentivesService'; import { UiPoolService } from 'src/services/UIPoolService'; import { UiStakeDataService } from 'src/services/UiStakeDataService'; @@ -25,6 +26,7 @@ interface SharedDependenciesContext { approvedAmountService: ApprovedAmountService; uiIncentivesService: UiIncentivesService; uiPoolService: UiPoolService; + uiGhoService: UiGhoService; delegationTokenService: DelegationTokenService; stkAbptMigrationService: StkAbptMigrationService; } @@ -59,6 +61,8 @@ export const SharedDependenciesProvider: React.FC = ({ children }) => { const uiPoolService = new UiPoolService(getProvider); const uiIncentivesService = new UiIncentivesService(getProvider); + const uiGhoService = new UiGhoService(getProvider); + return ( { approvedAmountService, uiPoolService, uiIncentivesService, + uiGhoService, delegationTokenService, stkAbptMigrationService, }} diff --git a/src/ui-config/queries.ts b/src/ui-config/queries.ts index febf35fb58..3d1dee942f 100644 --- a/src/ui-config/queries.ts +++ b/src/ui-config/queries.ts @@ -87,6 +87,17 @@ export const queryKeysFactory = { user: string ) => [...queryKeysFactory.user(user), chainId, amount, srcToken, destToken, 'paraswapRates'], gasPrices: (chainId: number) => [chainId, 'gasPrices'], + ghoReserveData: (marketData: MarketDataType) => [ + ...queryKeysFactory.gho, + ...queryKeysFactory.market(marketData), + 'ghoReserveData', + ], + ghoUserReserveData: (user: string, marketData: MarketDataType) => [ + ...queryKeysFactory.gho, + ...queryKeysFactory.user(user), + ...queryKeysFactory.market(marketData), + 'ghoUserReserveData', + ], poolApprovedAmount: (user: string, token: string, marketData: MarketDataType) => [ ...queryKeysFactory.pool, ...queryKeysFactory.user(user), diff --git a/src/utils/ghoUtilities.tsx b/src/utils/ghoUtilities.tsx index 61d5a434be..c189e20410 100644 --- a/src/utils/ghoUtilities.tsx +++ b/src/utils/ghoUtilities.tsx @@ -1,3 +1,4 @@ +import { GhoReserveData, GhoUserData, normalize } from '@aave/math-utils'; import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider'; export const GHO_SYMBOL = 'GHO'; @@ -98,3 +99,33 @@ export const findAndFilterGhoReserve = (reserves: A } ); }; + +export const displayGho = ({ symbol, currentMarket }: GhoUtilMintingAvailableParams): boolean => { + return symbol === 'GHO' && GHO_SUPPORTED_MARKETS.includes(currentMarket); +}; + +interface GhoUtilMintingAvailableParams { + symbol: string; + currentMarket: string; +} + +export const ghoUserQualifiesForDiscount = ( + ghoReserveData: GhoReserveData, + ghoUserData: GhoUserData, + futureBorrowAmount = '0' +) => { + const borrowBalance = Number(normalize(ghoUserData.userGhoScaledBorrowBalance, 18)); + const minBorrowBalanceForDiscount = Number( + normalize(ghoReserveData.ghoMinDebtTokenBalanceForDiscount, 18) + ); + + const stkAaveBalance = Number(normalize(ghoUserData.userDiscountTokenBalance, 18)); + const minStkAaveBalanceForDiscount = Number( + normalize(ghoReserveData.ghoMinDiscountTokenBalanceForDiscount, 18) + ); + + return ( + borrowBalance + Number(futureBorrowAmount) >= minBorrowBalanceForDiscount && + stkAaveBalance >= minStkAaveBalanceForDiscount + ); +}; diff --git a/yarn.lock b/yarn.lock index ffc05ddcb2..5159f792b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9707,6 +9707,11 @@ messageformat-parser@^4.1.3: resolved "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.3.tgz" integrity sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg== +micro-memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.1.2.tgz#ce719c1ba1e41592f1cd91c64c5f41dcbf135f36" + integrity sha512-+HzcV2H+rbSJzApgkj0NdTakkC+bnyeiUxgT6/m7mjcz1CmM22KYFKp+EVj1sWe4UYcnriJr5uqHQD/gMHLD+g== + micromark-core-commonmark@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"