From 1d1458c08a545fd2fdd067389987e99b842b68de Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 6 Dec 2023 19:17:05 +0600 Subject: [PATCH] feat(balances): update balances for priority tokens (#3417) --- .../TradeApprove/TradeApproveButton.tsx | 5 +- .../common/containers/TradeApprove/index.ts | 1 - .../TradeApprove/useTradeApproveState.ts | 14 --- .../src/common/hooks/useApproveState.ts | 36 +++--- .../src/legacy/hooks/useTokenAllowance.ts | 18 ++- .../hooks/TransactionHooksMod.tsx | 8 +- .../OrdersTableContainer/OrderRow/index.tsx | 11 +- .../utils/getOrderParams.test.ts | 15 +-- .../utils/getOrderParams.ts | 4 +- .../modules/swap/containers/EthFlow/index.tsx | 5 +- .../swap/hooks/useSwapButtonContext.ts | 4 +- .../modules/tokens/hooks/useEnoughBalance.ts | 6 +- .../tokensList/pure/TokenListItem/index.tsx | 6 +- .../trade/containers/TradeWidget/index.tsx | 6 +- .../trade/hooks/usePriorityTokenAddresses.ts | 43 +++++++ .../hooks/useTradeFormValidationContext.ts | 4 +- .../src/hooks/useNativeTokenBalance.ts | 17 ++- .../hooks/usePersistBalancesAndAllowances.ts | 119 ++++++++++++++++++ libs/balances-and-allowances/src/index.ts | 1 + .../src/state/allowancesAtom.ts | 4 +- .../src/state/balancesAtom.ts | 4 +- .../updaters/BalancesAndAllowancesUpdater.ts | 84 +++---------- .../src/updaters/PriorityTokensUpdater.ts | 25 ++++ 23 files changed, 302 insertions(+), 138 deletions(-) delete mode 100644 apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveState.ts create mode 100644 apps/cowswap-frontend/src/modules/trade/hooks/usePriorityTokenAddresses.ts create mode 100644 libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts create mode 100644 libs/balances-and-allowances/src/updaters/PriorityTokensUpdater.ts diff --git a/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx b/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx index 9346f099fd..05eaa83f65 100644 --- a/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx +++ b/apps/cowswap-frontend/src/common/containers/TradeApprove/TradeApproveButton.tsx @@ -9,7 +9,8 @@ import { CowModal } from 'common/pure/Modal' import { TransactionErrorContent } from 'common/pure/TransactionErrorContent' import { useTradeApproveCallback } from './useTradeApproveCallback' -import { useTradeApproveState } from './useTradeApproveState' + +import { useApproveState } from '../../hooks/useApproveState' export interface TradeApproveButtonProps { amountToApprove: CurrencyAmount @@ -22,7 +23,7 @@ export function TradeApproveButton(props: TradeApproveButtonProps) { const currency = amountToApprove.currency - const approvalState = useTradeApproveState(amountToApprove) + const approvalState = useApproveState(amountToApprove) const tradeApproveCallback = useTradeApproveCallback(amountToApprove) const shouldZeroApprove = useShouldZeroApprove(amountToApprove) const zeroApprove = useZeroApprove(amountToApprove.currency) diff --git a/apps/cowswap-frontend/src/common/containers/TradeApprove/index.ts b/apps/cowswap-frontend/src/common/containers/TradeApprove/index.ts index 58fd2aa931..15ced7d78a 100644 --- a/apps/cowswap-frontend/src/common/containers/TradeApprove/index.ts +++ b/apps/cowswap-frontend/src/common/containers/TradeApprove/index.ts @@ -1,4 +1,3 @@ export * from './TradeApproveButton' export * from './TradeApproveModal' export * from './useTradeApproveCallback' -export * from './useTradeApproveState' diff --git a/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveState.ts b/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveState.ts deleted file mode 100644 index 0c0406a60a..0000000000 --- a/apps/cowswap-frontend/src/common/containers/TradeApprove/useTradeApproveState.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' - -import { Nullish } from 'types' - -import { ApprovalState } from 'legacy/hooks/useApproveCallback/useApproveCallbackMod' - -import { useApproveState } from 'common/hooks/useApproveState' -import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress' - -export function useTradeApproveState(amountToApprove: Nullish>): ApprovalState { - const spender = useTradeSpenderAddress() - - return useApproveState(amountToApprove, spender) -} diff --git a/apps/cowswap-frontend/src/common/hooks/useApproveState.ts b/apps/cowswap-frontend/src/common/hooks/useApproveState.ts index 52818488af..19843a7de2 100644 --- a/apps/cowswap-frontend/src/common/hooks/useApproveState.ts +++ b/apps/cowswap-frontend/src/common/hooks/useApproveState.ts @@ -1,36 +1,43 @@ import { useMemo } from 'react' +import { useTokensAllowances } from '@cowprotocol/balances-and-allowances' import { usePrevious } from '@cowprotocol/common-hooks' -import { FractionUtils, getWrappedToken } from '@cowprotocol/common-utils' -import { useWalletInfo } from '@cowprotocol/wallet' +import { getWrappedToken } from '@cowprotocol/common-utils' +import { BigNumber } from '@ethersproject/bignumber' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { Nullish } from 'types' import { ApprovalState } from 'legacy/hooks/useApproveCallback/useApproveCallbackMod' -import { useTokenAllowance } from 'legacy/hooks/useTokenAllowance' import { useHasPendingApproval } from 'legacy/state/enhancedTransactions/hooks' import { useSafeMemo } from 'common/hooks/useSafeMemo' +import { useTradeSpenderAddress } from './useTradeSpenderAddress' + function getCurrencyToApprove(amountToApprove: Nullish>): Token | undefined { if (!amountToApprove) return undefined return getWrappedToken(amountToApprove.currency) } -export function useApproveState(amountToApprove: Nullish>, spender?: string): ApprovalState { - const { account } = useWalletInfo() +export function useApproveState(amountToApprove: Nullish>): ApprovalState { + const spender = useTradeSpenderAddress() const token = getCurrencyToApprove(amountToApprove) - const currentAllowance = useTokenAllowance(token, account ?? undefined, spender) - const pendingApproval = useHasPendingApproval(token?.address, spender) + const tokenAddress = token?.address?.toLowerCase() + const allowances = useTokensAllowances() + const pendingApproval = useHasPendingApproval(tokenAddress, spender) + + const currentAllowance = tokenAddress ? allowances.values[tokenAddress] : undefined const approvalStateBase = useSafeMemo(() => { - if (!amountToApprove || !spender || !currentAllowance) { + if (!amountToApprove || !currentAllowance) { return ApprovalState.UNKNOWN } - if (FractionUtils.gte(currentAllowance, amountToApprove)) { + const amountToApproveString = amountToApprove.quotient.toString() + + if (currentAllowance.gte(amountToApproveString)) { return ApprovalState.APPROVED } @@ -38,12 +45,12 @@ export function useApproveState(amountToApprove: Nullish -): ApprovalState { +function useAuxApprovalState(approvalStateBase: ApprovalState, currentAllowance?: BigNumber): ApprovalState { const previousApprovalState = usePrevious(approvalStateBase) - const currentAllowanceString = currentAllowance?.quotient.toString() + const currentAllowanceString = currentAllowance?.toHexString() const previousAllowanceString = usePrevious(currentAllowanceString) // Has allowance actually updated? const allowanceHasNotChanged = previousAllowanceString === currentAllowanceString diff --git a/apps/cowswap-frontend/src/legacy/hooks/useTokenAllowance.ts b/apps/cowswap-frontend/src/legacy/hooks/useTokenAllowance.ts index 4aec999e1d..7b9e28de5f 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/useTokenAllowance.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/useTokenAllowance.ts @@ -3,9 +3,15 @@ import { useMemo } from 'react' import { useTokenContract } from '@cowprotocol/common-hooks' import { CurrencyAmount, Token } from '@uniswap/sdk-core' +import ms from 'ms.macro' import useSWR from 'swr' import { Nullish } from 'types' +const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`10s` } + +/** + * @deprecated use useTokensAllowances() hook instead + */ export function useTokenAllowance( token: Nullish, owner?: string, @@ -14,11 +20,15 @@ export function useTokenAllowance( const tokenAddress = token?.address const contract = useTokenContract(tokenAddress, false) - const { data: allowance } = useSWR(['useTokenAllowance', tokenAddress, owner, spender], async () => { - if (!owner || !spender) return undefined + const { data: allowance } = useSWR( + ['useTokenAllowance', tokenAddress, owner, spender], + async () => { + if (!owner || !spender) return undefined - return contract?.callStatic.allowance(owner, spender) - }) + return contract?.callStatic.allowance(owner, spender) + }, + ALLOWANCES_SWR_CONFIG + ) return useMemo( () => (token && allowance ? CurrencyAmount.fromRawAmount(token, allowance.toString()) : undefined), diff --git a/apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx b/apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx index 16bbf7479f..df54e6c469 100644 --- a/apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx +++ b/apps/cowswap-frontend/src/legacy/state/enhancedTransactions/hooks/TransactionHooksMod.tsx @@ -25,6 +25,7 @@ export function isTransactionRecent(tx: EnhancedTransactionDetails): boolean { // returns whether a token has a pending approval transaction export function useHasPendingApproval(tokenAddress: string | undefined, spender: string | undefined): boolean { const allTransactions = useAllTransactions() + return useMemo( () => typeof tokenAddress === 'string' && @@ -37,7 +38,12 @@ export function useHasPendingApproval(tokenAddress: string | undefined, spender: } else { const approval = tx.approval if (!approval) return false - return approval.spender === spender && approval.tokenAddress === tokenAddress && isTransactionRecent(tx) + + return ( + approval.spender.toLowerCase() === spender.toLowerCase() && + approval.tokenAddress.toLowerCase() === tokenAddress && + isTransactionRecent(tx) + ) } }), [allTransactions, spender, tokenAddress] diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx index 2daf55a29e..0cbbac2cfe 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx @@ -175,8 +175,14 @@ export function OrderRow({ const showCancellationModal = orderActions.getShowCancellationModal(order) + /** + * TODO: I'm not sure about !hasValidPendingPermit + * Before the fix it was: hasValidPendingPermit === false + * In useCheckHasValidPendingPermit() we intentionally return undefined in cases when we don't know the permit status + */ + const withAllowanceWarning = hasEnoughAllowance === false && !hasValidPendingPermit const withWarning = - (hasEnoughBalance === false || (hasEnoughAllowance === false && hasValidPendingPermit === false)) && + (hasEnoughBalance === false || withAllowanceWarning) && // show the warning only for pending and scheduled orders (status === OrderStatus.PENDING || status === OrderStatus.SCHEDULED) const theme = useContext(ThemeContext) @@ -185,6 +191,7 @@ export function OrderRow({ const isScheduledCreating = isOrderScheduled && Date.now() > creationTime.getTime() const expirationTimeAgo = useTimeAgo(expirationTime, TIME_AGO_UPDATE_INTERVAL) const creationTimeAgo = useTimeAgo(creationTime, TIME_AGO_UPDATE_INTERVAL) + // TODO: set the real value when API returns it // const executedTimeAgo = useTimeAgo(expirationTime, TIME_AGO_UPDATE_INTERVAL) const activityUrl = chainId ? getActivityUrl(chainId, order) : undefined @@ -359,7 +366,7 @@ export function OrderRow({ {hasEnoughBalance === false && ( )} - {hasEnoughAllowance === false && hasValidPendingPermit === false && ( + {withAllowanceWarning && ( orderActions.approveOrderToken(order.inputToken)} symbol={inputTokenSymbol} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.test.ts b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.test.ts index 756fae0bc8..55dc4c7b55 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.test.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.test.ts @@ -1,18 +1,19 @@ import { BigNumber } from '@ethersproject/bignumber' +import { BalancesAndAllowances } from 'modules/tokens' + import { getOrderParams } from './getOrderParams' -import { BalancesAndAllowances } from '../../../../tokens' import { ordersMock } from '../orders.mock' describe('getOrderParams', () => { const BASE_ORDER = ordersMock[0] const BASE_BALANCES_AND_ALLOWANCES: BalancesAndAllowances = { balances: { - [BASE_ORDER.inputToken.address]: BigNumber.from(BASE_ORDER.sellAmount), + [BASE_ORDER.inputToken.address.toLowerCase()]: BigNumber.from(BASE_ORDER.sellAmount), }, allowances: { - [BASE_ORDER.inputToken.address]: BigNumber.from(BASE_ORDER.sellAmount), + [BASE_ORDER.inputToken.address.toLowerCase()]: BigNumber.from(BASE_ORDER.sellAmount), }, isLoading: false, } @@ -66,7 +67,7 @@ describe('getOrderParams', () => { const balancesAndAllowances: BalancesAndAllowances = { ...BASE_BALANCES_AND_ALLOWANCES, balances: { - [order.inputToken.address]: BigNumber.from(String(+order.sellAmount * 0.00051)), + [order.inputToken.address.toLowerCase()]: BigNumber.from(String(+order.sellAmount * 0.00051)), }, } const result = getOrderParams(1, balancesAndAllowances, order) @@ -77,7 +78,7 @@ describe('getOrderParams', () => { const balancesAndAllowances: BalancesAndAllowances = { ...BASE_BALANCES_AND_ALLOWANCES, balances: { - [order.inputToken.address]: BigNumber.from(String(+order.sellAmount * 0.00049)), + [order.inputToken.address.toLowerCase()]: BigNumber.from(String(+order.sellAmount * 0.00049)), }, } const result = getOrderParams(1, balancesAndAllowances, order) @@ -89,7 +90,7 @@ describe('getOrderParams', () => { const balancesAndAllowances: BalancesAndAllowances = { ...BASE_BALANCES_AND_ALLOWANCES, allowances: { - [order.inputToken.address]: BigNumber.from(String(+order.sellAmount * 0.00051)), + [order.inputToken.address.toLowerCase()]: BigNumber.from(String(+order.sellAmount * 0.00051)), }, } const result = getOrderParams(1, balancesAndAllowances, order) @@ -100,7 +101,7 @@ describe('getOrderParams', () => { const balancesAndAllowances: BalancesAndAllowances = { ...BASE_BALANCES_AND_ALLOWANCES, allowances: { - [order.inputToken.address]: BigNumber.from(String(+order.sellAmount * 0.00049)), + [order.inputToken.address.toLowerCase()]: BigNumber.from(String(+order.sellAmount * 0.00049)), }, } const result = getOrderParams(1, balancesAndAllowances, order) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts index cf08d36a50..bb800a9232 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/utils/getOrderParams.ts @@ -36,8 +36,8 @@ export function getOrderParams( } const { balances, allowances } = balancesAndAllowances - const balance = balances[order.inputToken.address] - const allowance = allowances[order.inputToken.address] + const balance = balances[order.inputToken.address.toLowerCase()] + const allowance = allowances[order.inputToken.address.toLowerCase()] const { hasEnoughBalance, hasEnoughAllowance } = _hasEnoughBalanceAndAllowance({ partiallyFillable: order.partiallyFillable, diff --git a/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx index a4c4ac8fe9..f1ef6a2bd2 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx @@ -17,7 +17,8 @@ import { HandleSwapCallback } from 'modules/swap/pure/SwapButtons' import { ethFlowContextAtom } from 'modules/swap/state/EthFlow/ethFlowContextAtom' import { useWrappedToken } from 'modules/trade/hooks/useWrappedToken' -import { useTradeApproveCallback, useTradeApproveState } from 'common/containers/TradeApprove' +import { useTradeApproveCallback } from 'common/containers/TradeApprove' +import { useApproveState } from 'common/hooks/useApproveState' import { CowModal } from 'common/pure/Modal' import useNativeCurrency from 'lib/hooks/useNativeCurrency' @@ -44,7 +45,7 @@ function EthFlow({ const isExpertMode = useIsExpertMode() const native = useNativeCurrency() const wrapped = useWrappedToken() - const approvalState = useTradeApproveState(nativeInput || null) + const approvalState = useApproveState(nativeInput || null) const ethFlowContext = useAtomValue(ethFlowContextAtom) const approveCallback = useTradeApproveCallback( diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index 5d7d515380..ae160bd024 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -29,7 +29,7 @@ import { useIsNativeIn } from 'modules/trade/hooks/useIsNativeInOrOut' import { useIsWrappedOut } from 'modules/trade/hooks/useIsWrappedInOrOut' import { useWrappedToken } from 'modules/trade/hooks/useWrappedToken' -import { useTradeApproveState } from 'common/containers/TradeApprove/useTradeApproveState' +import { useApproveState } from 'common/hooks/useApproveState' import { useSafeBundleEthFlowContext } from './useSafeBundleEthFlowContext' import { useDerivedSwapInfo, useSwapActionHandlers } from './useSwapState' @@ -81,7 +81,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext const wrapUnwrapAmount = isNativeInSwap ? currencyAmountToTokenAmount(inputAmount) || undefined : inputAmount const hasEnoughWrappedBalanceForSwap = useHasEnoughWrappedBalanceForSwap(wrapUnwrapAmount) const wrapCallback = useWrapNativeFlow() - const approvalState = useTradeApproveState(slippageAdjustedSellAmount || null) + const approvalState = useApproveState(slippageAdjustedSellAmount || null) const handleSwap = useHandleSwap(priceImpactParams) diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts index 4034e0f208..6f3bd35770 100644 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts +++ b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts @@ -75,7 +75,7 @@ export function hasEnoughBalanceAndAllowance(params: EnoughBalanceParams): UseEn const isNativeCurrency = !!amount?.currency && getIsNativeToken(amount?.currency) const token = amount?.currency && getWrappedToken(amount.currency) - const tokenAddress = getAddress(token) + const tokenAddress = getAddress(token)?.toLowerCase() const enoughBalance = _enoughBalance(tokenAddress, amount, balances) const enoughAllowance = _enoughAllowance(tokenAddress, amount, allowances, isNativeCurrency) @@ -84,7 +84,7 @@ export function hasEnoughBalanceAndAllowance(params: EnoughBalanceParams): UseEn } function _enoughBalance( - tokenAddress: string | null, + tokenAddress: string | undefined, amount: CurrencyAmount, balances: BalancesState['values'] ): boolean | undefined { @@ -94,7 +94,7 @@ function _enoughBalance( } function _enoughAllowance( - tokenAddress: string | null, + tokenAddress: string | undefined, amount: CurrencyAmount, allowances: AllowancesState['values'] | undefined, isNativeCurrency: boolean diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx index 8ba5137b11..d8b859e795 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx @@ -25,7 +25,7 @@ export function TokenListItem(props: TokenListItemProps) { const isTokenSelected = token.address.toLowerCase() === selectedToken?.toLowerCase() - const balanceAmount = CurrencyAmount.fromRawAmount(token, balance?.toHexString() ?? 0) + const balanceAmount = balance ? CurrencyAmount.fromRawAmount(token, balance.toHexString()) : undefined return ( - - - + {balanceAmount && } ) } diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx index 0278dc1513..c6c81dec55 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx @@ -1,5 +1,6 @@ import { ReactNode, useEffect } from 'react' +import { PriorityTokensUpdater } from '@cowprotocol/balances-and-allowances' import { maxAmountSpend } from '@cowprotocol/common-utils' import { isInjectedWidget } from '@cowprotocol/common-utils' import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' @@ -28,6 +29,7 @@ import { PoweredFooter } from 'common/pure/PoweredFooter' import * as styledEl from './styled' import { TradeWidgetModals } from './TradeWidgetModals' +import { usePriorityTokenAddresses } from '../../hooks/usePriorityTokenAddresses' import { CommonTradeUpdater } from '../../updaters/CommonTradeUpdater' import { DisableNativeTokenSellingUpdater } from '../../updaters/DisableNativeTokenSellingUpdater' import { PriceImpactUpdater } from '../../updaters/PriceImpactUpdater' @@ -92,12 +94,13 @@ export function TradeWidget(props: TradeWidgetProps) { disablePriceImpact, } = params - const { chainId } = useWalletInfo() + const { chainId, account } = useWalletInfo() const isWrapOrUnwrap = useIsWrapOrUnwrap() const { allowsOffchainSigning } = useWalletDetails() const isChainIdUnsupported = useIsProviderNetworkUnsupported() const isSafeWallet = useIsSafeWallet() const openTokenSelectWidget = useOpenTokenSelectWidget() + const priorityTokenAddresses = usePriorityTokenAddresses() const currenciesLoadingInProgress = !inputCurrencyInfo.currency && !outputCurrencyInfo.currency @@ -132,6 +135,7 @@ export function TradeWidget(props: TradeWidgetProps) { return ( + {!disableQuotePolling && } diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/usePriorityTokenAddresses.ts b/apps/cowswap-frontend/src/modules/trade/hooks/usePriorityTokenAddresses.ts new file mode 100644 index 0000000000..204f749915 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/hooks/usePriorityTokenAddresses.ts @@ -0,0 +1,43 @@ +import { useMemo } from 'react' + +import { getAddress, isTruthy } from '@cowprotocol/common-utils' +import { OrderClass } from '@cowprotocol/cow-sdk' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useSelector } from 'react-redux' + +import { AppState } from 'legacy/state' +import { PartialOrdersMap } from 'legacy/state/orders/reducer' + +import { useDerivedTradeState } from './useDerivedTradeState' + +export function usePriorityTokenAddresses(): string[] { + const { chainId } = useWalletInfo() + const tradeState = useDerivedTradeState() + + const pending = useSelector((state) => { + return state.orders?.[chainId]?.pending + }) + + const pendingOrdersTokenAddresses = useMemo(() => { + if (!pending) return undefined + + return Object.values(pending) + .filter(isTruthy) + .filter(({ order }) => order.class === OrderClass.MARKET) + .map(({ order }) => { + return [order.inputToken.address, order.outputToken.address] + }) + .flat() + }, [pending]) + + const inputCurrency = tradeState?.state?.inputCurrency + const outputCurrency = tradeState?.state?.outputCurrency + + const inputCurrencyAddress = getAddress(inputCurrency) + const outputCurrencyAddress = getAddress(outputCurrency) + + return useMemo(() => { + return (pendingOrdersTokenAddresses || []).concat(inputCurrencyAddress || [], outputCurrencyAddress || []) + }, [inputCurrencyAddress, outputCurrencyAddress, pendingOrdersTokenAddresses]) +} diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts index 2c469ad357..f17a7d0daf 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts @@ -10,7 +10,7 @@ import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState' import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' import { useTradeQuote } from 'modules/tradeQuote' -import { useTradeApproveState } from 'common/containers/TradeApprove' +import { useApproveState } from 'common/hooks/useApproveState' import { TradeFormValidationCommonContext } from '../types' @@ -20,7 +20,7 @@ export function useTradeFormValidationContext(): TradeFormValidationCommonContex const tradeQuote = useTradeQuote() const { inputCurrency, outputCurrency, slippageAdjustedSellAmount, recipient, tradeType } = derivedTradeState || {} - const approvalState = useTradeApproveState(slippageAdjustedSellAmount) + const approvalState = useApproveState(slippageAdjustedSellAmount) const { address: recipientEnsAddress } = useENSAddress(recipient) const isSwapUnsupported = useIsTradeUnsupported(inputCurrency, outputCurrency) || isUnsupportedTokenInQuote(tradeQuote) diff --git a/libs/balances-and-allowances/src/hooks/useNativeTokenBalance.ts b/libs/balances-and-allowances/src/hooks/useNativeTokenBalance.ts index 2628296b09..1c8a237759 100644 --- a/libs/balances-and-allowances/src/hooks/useNativeTokenBalance.ts +++ b/libs/balances-and-allowances/src/hooks/useNativeTokenBalance.ts @@ -3,19 +3,26 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { BigNumber } from '@ethersproject/bignumber' import { useWeb3React } from '@web3-react/core' +import ms from 'ms.macro' import useSWR, { SWRResponse } from 'swr' +const SWR_CONFIG = { refreshInterval: ms`11s` } + export function useNativeTokenBalance(customAccount?: string): SWRResponse { const { provider } = useWeb3React() const { account } = useWalletInfo() const balanceAccount = customAccount || account - return useSWR(['useNativeTokenBalance', balanceAccount, provider], async () => { - if (!provider || !balanceAccount) return undefined + return useSWR( + ['useNativeTokenBalance', balanceAccount, provider], + async () => { + if (!provider || !balanceAccount) return undefined - const contract = getMulticallContract(provider) + const contract = getMulticallContract(provider) - return contract.callStatic.getEthBalance(balanceAccount) - }) + return contract.callStatic.getEthBalance(balanceAccount) + }, + SWR_CONFIG + ) } diff --git a/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts b/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts new file mode 100644 index 0000000000..5bf5299a34 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/usePersistBalancesAndAllowances.ts @@ -0,0 +1,119 @@ +import { useSetAtom } from 'jotai/index' +import { useResetAtom } from 'jotai/utils' +import { useEffect, useMemo } from 'react' + +import { ERC_20_INTERFACE } from '@cowprotocol/abis' +import { GP_VAULT_RELAYER } from '@cowprotocol/common-const' +import { getIsNativeToken } from '@cowprotocol/common-utils' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { useMultipleContractSingleData } from '@cowprotocol/multicall' +import { BigNumber } from '@ethersproject/bignumber' + +import { SWRConfiguration } from 'swr' + +import { AllowancesState, allowancesState } from '../state/allowancesAtom' +import { balancesAtom, BalancesState } from '../state/balancesAtom' + +const MULTICALL_OPTIONS = {} + +export interface PersistBalancesAndAllowancesParams { + account: string | undefined + chainId: SupportedChainId + tokenAddresses: string[] + balancesSwrConfig: SWRConfiguration + allowancesSwrConfig: SWRConfiguration + setLoadingState?: boolean +} + +export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowancesParams) { + const { account, chainId, tokenAddresses, setLoadingState, balancesSwrConfig, allowancesSwrConfig } = params + + const setBalances = useSetAtom(balancesAtom) + const setAllowances = useSetAtom(allowancesState) + + const resetBalances = useResetAtom(balancesAtom) + const resetAllowances = useResetAtom(allowancesState) + + const spender = GP_VAULT_RELAYER[chainId] + + const balanceOfParams = useMemo(() => (account ? [account] : undefined), [account]) + const allowanceParams = useMemo(() => (account && spender ? [account, spender] : undefined), [account, spender]) + + const { isLoading: isBalancesLoading, data: balances } = useMultipleContractSingleData<{ balance: BigNumber }>( + tokenAddresses, + ERC_20_INTERFACE, + 'balanceOf', + balanceOfParams, + MULTICALL_OPTIONS, + balancesSwrConfig + ) + + const { isLoading: isAllowancesLoading, data: allowances } = useMultipleContractSingleData<[BigNumber]>( + tokenAddresses, + ERC_20_INTERFACE, + 'allowance', + allowanceParams, + MULTICALL_OPTIONS, + allowancesSwrConfig + ) + + // Set balances loading state + useEffect(() => { + if (!setLoadingState) return + + setBalances((state) => ({ ...state, isLoading: isBalancesLoading })) + }, [setBalances, isBalancesLoading, setLoadingState]) + + // Set allwoances loading state + useEffect(() => { + if (!setLoadingState) return + + setAllowances((state) => ({ ...state, isLoading: isAllowancesLoading })) + }, [setAllowances, isAllowancesLoading, setLoadingState]) + + // Set balances to the store + useEffect(() => { + if (!balances || !balances.length) return + + const balancesState = tokenAddresses.reduce((acc, address, index) => { + if (getIsNativeToken(chainId, address)) return acc + + acc[address.toLowerCase()] = balances[index]?.balance + return acc + }, {}) + + setBalances((state) => { + return { + ...state, + values: { ...state.values, ...balancesState }, + ...(setLoadingState ? { isLoading: false } : {}), + } + }) + }, [balances, tokenAddresses, setBalances, chainId, setLoadingState]) + + // Set allowances to the store + useEffect(() => { + if (!allowances || !allowances.length) return + + const allowancesState = tokenAddresses.reduce((acc, address, index) => { + acc[address.toLowerCase()] = allowances[index]?.[0] + return acc + }, {}) + + setAllowances((state) => { + return { + ...state, + values: { ...state.values, ...allowancesState }, + ...(setLoadingState ? { isLoading: false } : {}), + } + }) + }, [allowances, tokenAddresses, setAllowances, setLoadingState]) + + // Reset states when wallet is not connected + useEffect(() => { + if (!account) { + resetBalances() + resetAllowances() + } + }, [account, resetAllowances, resetBalances]) +} diff --git a/libs/balances-and-allowances/src/index.ts b/libs/balances-and-allowances/src/index.ts index 9b0bb0078e..9cc40fa226 100644 --- a/libs/balances-and-allowances/src/index.ts +++ b/libs/balances-and-allowances/src/index.ts @@ -1,5 +1,6 @@ // Updater export { BalancesAndAllowancesUpdater } from './updaters/BalancesAndAllowancesUpdater' +export { PriorityTokensUpdater } from './updaters/PriorityTokensUpdater' // Hooks export { useTokensBalances } from './hooks/useTokensBalances' diff --git a/libs/balances-and-allowances/src/state/allowancesAtom.ts b/libs/balances-and-allowances/src/state/allowancesAtom.ts index a534efc19d..3a75a1832b 100644 --- a/libs/balances-and-allowances/src/state/allowancesAtom.ts +++ b/libs/balances-and-allowances/src/state/allowancesAtom.ts @@ -1,7 +1,7 @@ -import { atom } from 'jotai' +import { atomWithReset } from 'jotai/utils' import { Erc20MulticallState } from '../types' export interface AllowancesState extends Erc20MulticallState {} -export const allowancesState = atom({ isLoading: false, values: {} }) +export const allowancesState = atomWithReset({ isLoading: false, values: {} }) diff --git a/libs/balances-and-allowances/src/state/balancesAtom.ts b/libs/balances-and-allowances/src/state/balancesAtom.ts index f66e7adb59..8e1a8ec6d5 100644 --- a/libs/balances-and-allowances/src/state/balancesAtom.ts +++ b/libs/balances-and-allowances/src/state/balancesAtom.ts @@ -1,7 +1,7 @@ -import { atom } from 'jotai' +import { atomWithReset } from 'jotai/utils' import { Erc20MulticallState } from '../types' export interface BalancesState extends Erc20MulticallState {} -export const balancesAtom = atom({ isLoading: false, values: {} }) +export const balancesAtom = atomWithReset({ isLoading: false, values: {} }) diff --git a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts index 53459336d7..b04cdeb896 100644 --- a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts +++ b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts @@ -1,96 +1,48 @@ -import { useSetAtom } from 'jotai' +import { useSetAtom } from 'jotai/index' import { useEffect, useMemo } from 'react' -import { ERC_20_INTERFACE } from '@cowprotocol/abis' -import { GP_VAULT_RELAYER, NATIVE_CURRENCY_BUY_TOKEN } from '@cowprotocol/common-const' +import { NATIVE_CURRENCY_BUY_TOKEN } from '@cowprotocol/common-const' import type { SupportedChainId } from '@cowprotocol/cow-sdk' -import { useMultipleContractSingleData } from '@cowprotocol/multicall' import { useAllTokens } from '@cowprotocol/tokens' -import type { BigNumber } from '@ethersproject/bignumber' import ms from 'ms.macro' import { useNativeTokenBalance } from '../hooks/useNativeTokenBalance' -import { AllowancesState, allowancesState } from '../state/allowancesAtom' -import { balancesAtom, BalancesState } from '../state/balancesAtom' +import { usePersistBalancesAndAllowances } from '../hooks/usePersistBalancesAndAllowances' +import { balancesAtom } from '../state/balancesAtom' -const MULTICALL_OPTIONS = {} // A small gap between balances and allowances refresh intervals is needed to avoid high load to the node at the same time -const BALANCES_SWR_CONFIG = { refreshInterval: ms`28s` } -const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`30s` } +const BALANCES_SWR_CONFIG = { refreshInterval: ms`31s` } +const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`33s` } export interface BalancesAndAllowancesUpdaterProps { account: string | undefined chainId: SupportedChainId } export function BalancesAndAllowancesUpdater({ account, chainId }: BalancesAndAllowancesUpdaterProps) { - const allTokens = useAllTokens() - const setBalances = useSetAtom(balancesAtom) - const setAllowances = useSetAtom(allowancesState) - const { data: nativeTokenBalance } = useNativeTokenBalance() - const spender = GP_VAULT_RELAYER[chainId] + const allTokens = useAllTokens() + const { data: nativeTokenBalance } = useNativeTokenBalance() const tokenAddresses = useMemo(() => allTokens.map((token) => token.address), [allTokens]) - const balanceOfParams = useMemo(() => (account ? [account] : undefined), [account]) - const allowanceParams = useMemo(() => (account && spender ? [account, spender] : undefined), [account, spender]) - const { isLoading: isBalancesLoading, data: balances } = useMultipleContractSingleData<{ balance: BigNumber }>( + usePersistBalancesAndAllowances({ + account, + chainId, tokenAddresses, - ERC_20_INTERFACE, - 'balanceOf', - balanceOfParams, - MULTICALL_OPTIONS, - BALANCES_SWR_CONFIG - ) - - const { isLoading: isAllowancesLoading, data: allowances } = useMultipleContractSingleData<[BigNumber]>( - tokenAddresses, - ERC_20_INTERFACE, - 'allowance', - allowanceParams, - MULTICALL_OPTIONS, - ALLOWANCES_SWR_CONFIG - ) - - // Set balances loading state - useEffect(() => { - setBalances((state) => ({ ...state, isLoading: isBalancesLoading })) - }, [setBalances, isBalancesLoading]) + setLoadingState: true, + balancesSwrConfig: BALANCES_SWR_CONFIG, + allowancesSwrConfig: ALLOWANCES_SWR_CONFIG, + }) - // Set allowances loading state + // Add native token balance to the store as well useEffect(() => { - setAllowances((state) => ({ ...state, isLoading: isAllowancesLoading })) - }, [setAllowances, isAllowancesLoading]) - - // Set balances to the store - useEffect(() => { - if (!balances || !balances.length) return - - const balancesState = tokenAddresses.reduce((acc, address, index) => { - acc[address.toLowerCase()] = balances[index]?.balance - return acc - }, {}) - - // Add native token balance to the store as well const nativeToken = NATIVE_CURRENCY_BUY_TOKEN[chainId] const nativeBalanceState = nativeTokenBalance ? { [nativeToken.address.toLowerCase()]: nativeTokenBalance } : {} - setBalances({ isLoading: false, values: { ...balancesState, ...nativeBalanceState } }) - }, [balances, tokenAddresses, setBalances, nativeTokenBalance, chainId]) - - // Set allowances to the store - useEffect(() => { - if (!allowances || !allowances.length) return - - const allowancesState = tokenAddresses.reduce((acc, address, index) => { - acc[address.toLowerCase()] = allowances[index]?.[0] - return acc - }, {}) - - setAllowances({ isLoading: false, values: allowancesState }) - }, [allowances, tokenAddresses, setAllowances]) + setBalances((state) => ({ ...state, values: { ...state.values, ...nativeBalanceState } })) + }, [nativeTokenBalance, chainId, setBalances]) return null } diff --git a/libs/balances-and-allowances/src/updaters/PriorityTokensUpdater.ts b/libs/balances-and-allowances/src/updaters/PriorityTokensUpdater.ts new file mode 100644 index 0000000000..d61c782f44 --- /dev/null +++ b/libs/balances-and-allowances/src/updaters/PriorityTokensUpdater.ts @@ -0,0 +1,25 @@ +import type { SupportedChainId } from '@cowprotocol/cow-sdk' + +import ms from 'ms.macro' + +import { usePersistBalancesAndAllowances } from '../hooks/usePersistBalancesAndAllowances' + +// A small gap between balances and allowances refresh intervals is needed to avoid high load to the node at the same time +const BALANCES_SWR_CONFIG = { refreshInterval: ms`8s` } +const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`11s` } + +export interface PriorityTokensUpdaterProps { + account: string | undefined + chainId: SupportedChainId + tokenAddresses: string[] +} + +export function PriorityTokensUpdater(props: PriorityTokensUpdaterProps) { + usePersistBalancesAndAllowances({ + ...props, + balancesSwrConfig: BALANCES_SWR_CONFIG, + allowancesSwrConfig: ALLOWANCES_SWR_CONFIG, + }) + + return null +}