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/common/hooks/useNeedsApproval.ts b/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts index debf82a764..c64f4271f1 100644 --- a/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts +++ b/apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts @@ -1,11 +1,9 @@ +import { useTokensAllowances } from '@cowprotocol/balances-and-allowances' import { getWrappedToken, isEnoughAmount } from '@cowprotocol/common-utils' -import { useWalletInfo } from '@cowprotocol/wallet' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Nullish } from 'types' -import { useBalancesAndAllowances } from 'modules/tokens' - import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress' /** @@ -22,19 +20,16 @@ import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress' * @returns {boolean} */ export function useNeedsApproval(amount: Nullish>): boolean { - const { account } = useWalletInfo() const spender = useTradeSpenderAddress() const token = amount ? getWrappedToken(amount.currency) : undefined - const tokens = token ? [token] : [] - const balancesAndAllowances = useBalancesAndAllowances({ account, spender, tokens }) - - const { allowances } = balancesAndAllowances + const { values: allowances } = useTokensAllowances() - const allowance = token && allowances[token.address]?.value + const allowance = token && allowances[token.address.toLowerCase()] if (!allowance) { return true } + if (!token || !amount || !spender) { return false } diff --git a/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts index 20270205bc..5b1dcda134 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts @@ -1,8 +1,9 @@ import { useSetAtom } from 'jotai' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { priceOutOfRangeAnalytics } from '@cowprotocol/analytics' -import { GP_VAULT_RELAYER, NATIVE_CURRENCY_BUY_ADDRESS, WRAPPED_NATIVE_CURRENCY } from '@cowprotocol/common-const' +import { useTokensBalances } from '@cowprotocol/balances-and-allowances' +import { NATIVE_CURRENCY_BUY_ADDRESS, WRAPPED_NATIVE_CURRENCY } from '@cowprotocol/common-const' import { useIsWindowVisible } from '@cowprotocol/common-hooks' import { getPromiseFulfilledValue } from '@cowprotocol/common-utils' import { timestamp } from '@cowprotocol/contracts' @@ -26,7 +27,7 @@ import { import { getBestQuote } from 'legacy/utils/price' import { updatePendingOrderPricesAtom } from 'modules/orders/state/pendingOrdersPricesAtom' -import { hasEnoughBalanceAndAllowance, useBalancesAndAllowances } from 'modules/tokens' +import { hasEnoughBalanceAndAllowance } from 'modules/tokens' import { getPriceQuality } from 'api/gnosisProtocol/api' @@ -46,15 +47,7 @@ export function UnfillableOrdersUpdater(): null { const setIsOrderUnfillable = useSetIsOrderUnfillable() const strategy = useGetGpPriceStrategy() - const tokens = useMemo(() => { - return pending.map((order) => order.inputToken) - }, [pending]) - - const { balances } = useBalancesAndAllowances({ - account, - spender: chainId ? GP_VAULT_RELAYER[chainId] : undefined, - tokens: tokens, - }) + const { values: balances } = useTokensBalances() // Ref, so we don't rerun useEffect const pendingRef = useRef(pending) diff --git a/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx b/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx index 2abed3be76..87c052e05c 100644 --- a/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/Header/AccountElement/index.tsx @@ -1,11 +1,11 @@ import React from 'react' +import { useNativeCurrencyAmount } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCY_BUY_TOKEN } from '@cowprotocol/common-const' import { TokenAmount } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' import { useToggleAccountModal } from 'modules/account' -import { useNativeCurrencyBalances } from 'modules/tokens/hooks/useCurrencyBalance' import { Web3Status } from 'modules/wallet/containers/Web3Status' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' @@ -21,7 +21,7 @@ interface AccountElementProps { export function AccountElement({ className, isWidgetMode, pendingActivities }: AccountElementProps) { const { account, chainId } = useWalletInfo() const isChainIdUnsupported = useIsProviderNetworkUnsupported() - const userEthBalance = useNativeCurrencyBalances(account ? [account] : [])?.[account ?? ''] + const userEthBalance = useNativeCurrencyAmount() const toggleAccountModal = useToggleAccountModal() const nativeToken = NATIVE_CURRENCY_BUY_TOKEN[chainId].symbol diff --git a/apps/cowswap-frontend/src/legacy/components/Header/Polling.tsx b/apps/cowswap-frontend/src/legacy/components/Header/Polling.tsx index 1e22b2748b..0dcc4c2ea5 100644 --- a/apps/cowswap-frontend/src/legacy/components/Header/Polling.tsx +++ b/apps/cowswap-frontend/src/legacy/components/Header/Polling.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react' -import { getChainInfo } from '@cowprotocol/common-const' -import { useBlockNumber, useMachineTimeMs, useTheme } from '@cowprotocol/common-hooks' +import { useBlockNumber, useIsOnline, useTheme } from '@cowprotocol/common-hooks' import { ExplorerDataType, getExplorerLink } from '@cowprotocol/common-utils' import { RowFixed } from '@cowprotocol/ui' import { MouseoverTooltip, ExternalLink } from '@cowprotocol/ui' @@ -9,10 +8,8 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { Trans } from '@lingui/macro' import JSBI from 'jsbi' -import ms from 'ms.macro' import styled, { keyframes } from 'styled-components/macro' -import useCurrentBlockTimestamp from 'legacy/hooks/useCurrentBlockTimestamp' import useGasPrice from 'legacy/hooks/useGasPrice' import { ThemedText } from 'legacy/theme' @@ -157,25 +154,18 @@ const Wrapper = styled.div` } ` -const DEFAULT_MS_BEFORE_WARNING = ms`10m` -const NETWORK_HEALTH_CHECK_MS = ms`10s` - export function Polling() { const { chainId } = useWalletInfo() const blockNumber = useBlockNumber() const [isMounting, setIsMounting] = useState(false) const [isHover, setIsHover] = useState(false) - const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS) - const blockTime = useCurrentBlockTimestamp() + const isOnline = useIsOnline() const theme = useTheme() const ethGasPrice = useGasPrice() const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined - const waitMsBeforeWarning = - (chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING - - const warning = Boolean(!!blockTime && machineTime - blockTime.mul(1000).toNumber() > waitMsBeforeWarning) + const warning = !isOnline useEffect( () => { diff --git a/apps/cowswap-frontend/src/legacy/components/Tokens/TokensTable.tsx b/apps/cowswap-frontend/src/legacy/components/Tokens/TokensTable.tsx index a40539d652..6fbac39a52 100644 --- a/apps/cowswap-frontend/src/legacy/components/Tokens/TokensTable.tsx +++ b/apps/cowswap-frontend/src/legacy/components/Tokens/TokensTable.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { BalancesState } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { useFilterTokens, usePrevious } from '@cowprotocol/common-hooks' +import { CurrencyAmount } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' @@ -10,8 +12,6 @@ import useTransactionConfirmationModal from 'legacy/hooks/useTransactionConfirma import { useToggleWalletModal } from 'legacy/state/application/hooks' import { ConfirmOperationType } from 'legacy/state/types' -import { TokenAmounts } from 'modules/tokens' - import { balanceComparator, useTokenComparator } from './sorting' import { Arrow, @@ -36,12 +36,10 @@ enum SORT_FIELD { BALANCE = 'balance', } -type BalanceType = [TokenAmounts, boolean] - type TokenTableParams = { tokensData: TokenWithLogo[] | undefined maxItems?: number - balances?: BalanceType + balances?: BalancesState['values'] page: number setPage: (page: number) => void query: string @@ -113,8 +111,8 @@ export default function TokenTable({ // If the sort field is Balance if (!balances) return 0 - const balanceA = balances[0][tokenA.address]?.value - const balanceB = balances[0][tokenB.address]?.value + const balanceA = balances[tokenA.address.toLowerCase()] + const balanceB = balances[tokenB.address.toLowerCase()] const balanceComp = balanceComparator(balanceA, balanceB) return applyDirection(balanceComp > 0, sortDirection) @@ -202,13 +200,16 @@ export default function TokenTable({ {tokensData && sortedTokens.length !== 0 ? ( sortedTokens.map((data, i) => { + const balanceRaw = balances && balances[data.address.toLowerCase()] + const balance = balanceRaw ? CurrencyAmount.fromRawAmount(data, balanceRaw.toHexString()) : undefined + if (data) { return ( , balanceB?: CurrencyAmount) { +export function balanceComparator(balanceA: BigNumber | undefined, balanceB: BigNumber | undefined) { if (balanceA && balanceB) { - return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1 - } else if (balanceA && balanceA.greaterThan('0')) { + return balanceA.gt(balanceB) ? -1 : balanceA.eq(balanceB) ? 0 : 1 + } else if (balanceA && balanceA.gt('0')) { return -1 - } else if (balanceB && balanceB.greaterThan('0')) { + } else if (balanceB && balanceB.gt('0')) { return 1 } return 0 } -function getTokenComparator(balances: [TokenAmounts, boolean]): (tokenA: Token, tokenB: Token) => number { +function getTokenComparator(balances: BalancesState['values']): (tokenA: Token, tokenB: Token) => number { return function sortTokens(tokenA: Token, tokenB: Token): number { // -1 = a is first // 1 = b is first // sort by balances - const balanceA = balances[0][tokenA.address]?.value - const balanceB = balances[0][tokenB.address]?.value + const balanceA = balances[tokenA.address.toLowerCase()] + const balanceB = balances[tokenB.address.toLowerCase()] const balanceComp = balanceComparator(balanceA, balanceB) if (balanceComp !== 0) return balanceComp @@ -51,7 +50,7 @@ function getTokenComparator(balances: [TokenAmounts, boolean]): (tokenA: Token, } export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number { - const balances = useAllTokensBalances() + const { values: balances } = useTokensBalances() const comparator = useMemo(() => getTokenComparator(balances), [balances]) diff --git a/apps/cowswap-frontend/src/legacy/hooks/useCombinedBalance.ts b/apps/cowswap-frontend/src/legacy/hooks/useCombinedBalance.ts index 24ada8485b..889fd490d1 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/useCombinedBalance.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/useCombinedBalance.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react' +import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { COW } from '@cowprotocol/common-const' import { useWalletInfo } from '@cowprotocol/wallet' import { CurrencyAmount } from '@uniswap/sdk-core' @@ -8,15 +9,14 @@ import JSBI from 'jsbi' import { useVCowData } from 'legacy/state/cowToken/hooks' -import { useTokenBalance } from 'modules/tokens/hooks/useCurrencyBalance' - /** * Hook that returns COW balance */ function useCowBalance() { - const { chainId, account } = useWalletInfo() + const { chainId } = useWalletInfo() const cowToken = chainId ? COW[chainId] : undefined - return useTokenBalance(account || undefined, cowToken) + + return useCurrencyAmountBalance(cowToken) } /** diff --git a/apps/cowswap-frontend/src/legacy/hooks/useCurrentBlockTimestamp.ts b/apps/cowswap-frontend/src/legacy/hooks/useCurrentBlockTimestamp.ts deleted file mode 100644 index 701b902783..0000000000 --- a/apps/cowswap-frontend/src/legacy/hooks/useCurrentBlockTimestamp.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useMemo } from 'react' - -import { useInterfaceMulticall } from '@cowprotocol/common-hooks' -import { BigNumber } from '@ethersproject/bignumber' - -import { useSingleCallResult } from 'lib/hooks/multicall' - -// gets the current timestamp from the blockchain -export default function useCurrentBlockTimestamp(): BigNumber | undefined { - const multicall = useInterfaceMulticall() - const resultStr: string | undefined = useSingleCallResult( - multicall, - 'getCurrentBlockTimestamp' - )?.result?.[0]?.toString() - return useMemo(() => (typeof resultStr === 'string' ? BigNumber.from(resultStr) : undefined), [resultStr]) -} diff --git a/apps/cowswap-frontend/src/legacy/hooks/useGasPrice.ts b/apps/cowswap-frontend/src/legacy/hooks/useGasPrice.ts index 327f0fa5b2..024b2a8c6b 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/useGasPrice.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/useGasPrice.ts @@ -4,8 +4,7 @@ import { useContract } from '@cowprotocol/common-hooks' import { useENSAddress } from '@cowprotocol/ens' import JSBI from 'jsbi' - -import { useSingleCallResult } from 'lib/hooks/multicall' +import useSWR from 'swr' const CHAIN_DATA_ABI = [ { @@ -24,6 +23,10 @@ export default function useGasPrice(): JSBI | undefined { const { address } = useENSAddress('fast-gas-gwei.data.eth') const contract = useContract(address ?? undefined, CHAIN_DATA_ABI, false) - const resultStr = useSingleCallResult(contract, 'latestAnswer').result?.[0]?.toString() + const { data: result } = useSWR(['useGasPrice', contract], async () => { + return contract?.callStatic.latestAnswer() + }) + const resultStr = result?.toString() + return useMemo(() => (typeof resultStr === 'string' ? JSBI.BigInt(resultStr) : undefined), [resultStr]) } diff --git a/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/useFiatValuePriceImpact.ts b/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/useFiatValuePriceImpact.ts index 519cc9db61..9938adf191 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/useFiatValuePriceImpact.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/useFiatValuePriceImpact.ts @@ -1,5 +1,8 @@ +import { useMemo } from 'react' + import { ONE_HUNDRED_PERCENT } from '@cowprotocol/common-const' import { useDebounce } from '@cowprotocol/common-hooks' +import { getWrappedToken } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import JSBI from 'jsbi' @@ -10,28 +13,32 @@ import { useTradeUsdAmounts } from 'modules/usdAmount' import { useSafeMemo } from 'common/hooks/useSafeMemo' -const FIAT_VALUE_LOADING_THRESHOLD = ms`0.1s` +const TRADE_SET_UP_DEBOUNCE_TIME = ms`100ms` export function useFiatValuePriceImpact() { const { state } = useDerivedTradeState() const { inputCurrencyAmount, outputCurrencyAmount, inputCurrency, outputCurrency } = state || {} - const isTradeSetUp = !!inputCurrency && !!outputCurrency + const inputToken = useMemo(() => (inputCurrency ? getWrappedToken(inputCurrency) : undefined), [inputCurrency]) + const outputToken = useMemo(() => (outputCurrency ? getWrappedToken(outputCurrency) : undefined), [outputCurrency]) + + const isTradeSetUp = useDebounce(!!inputToken && !!outputToken, TRADE_SET_UP_DEBOUNCE_TIME) const { inputAmount: { value: fiatValueInput, isLoading: inputIsLoading }, outputAmount: { value: fiatValueOutput, isLoading: outputIsLoading }, - } = useTradeUsdAmounts(inputCurrencyAmount, outputCurrencyAmount) + } = useTradeUsdAmounts(inputCurrencyAmount, outputCurrencyAmount, inputToken, outputToken) - // Consider the price impact loading if either the input or output amount is falsy - // Debounce the loading state to prevent the price impact from flashing - const isLoading = useDebounce(isTradeSetUp ? inputIsLoading || outputIsLoading : false, FIAT_VALUE_LOADING_THRESHOLD) + const isLoading = inputIsLoading || outputIsLoading return useSafeMemo(() => { + // Don't calculate price impact if trade is not set up (both trade assets are not set) + if (!isTradeSetUp) return null + const priceImpact = computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput) return { priceImpact, isLoading } - }, [fiatValueInput, fiatValueOutput, isLoading]) + }, [isTradeSetUp, fiatValueInput, fiatValueOutput, isLoading]) } function computeFiatValuePriceImpact( diff --git a/apps/cowswap-frontend/src/legacy/hooks/useTokenAllowance.ts b/apps/cowswap-frontend/src/legacy/hooks/useTokenAllowance.ts index 0de703a8f2..7b9e28de5f 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/useTokenAllowance.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/useTokenAllowance.ts @@ -3,19 +3,32 @@ 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' -import { useSingleCallResult } from 'lib/hooks/multicall' +const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`10s` } +/** + * @deprecated use useTokensAllowances() hook instead + */ export function useTokenAllowance( token: Nullish, owner?: string, spender?: string ): CurrencyAmount | undefined { - const contract = useTokenContract(token?.address, false) + const tokenAddress = token?.address + const contract = useTokenContract(tokenAddress, false) - const inputs = useMemo(() => [owner, spender], [owner, spender]) - const allowance = useSingleCallResult(contract, 'allowance', inputs).result + const { data: allowance } = useSWR( + ['useTokenAllowance', tokenAddress, owner, spender], + async () => { + if (!owner || !spender) return undefined + + 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/hooks/useTransactionDeadline.ts b/apps/cowswap-frontend/src/legacy/hooks/useTransactionDeadline.ts deleted file mode 100644 index 348b104821..0000000000 --- a/apps/cowswap-frontend/src/legacy/hooks/useTransactionDeadline.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useMemo } from 'react' - -import { BigNumber } from '@ethersproject/bignumber' - -import { useAppSelector } from 'legacy/state/hooks' - -import useCurrentBlockTimestamp from './useCurrentBlockTimestamp' - -// combines the block timestamp with the user setting to give the deadline that should be used for any submitted transaction -export default function useTransactionDeadline(): BigNumber | undefined { - const ttl = useAppSelector((state) => state.user.userDeadline) - const blockTimestamp = useCurrentBlockTimestamp() - return useMemo(() => { - if (blockTimestamp && ttl) return blockTimestamp.add(ttl) - return undefined - }, [blockTimestamp, ttl]) -} diff --git a/apps/cowswap-frontend/src/legacy/state/claim/hooks/index.ts b/apps/cowswap-frontend/src/legacy/state/claim/hooks/index.ts index 9b96e29fb0..9aa5f9532b 100644 --- a/apps/cowswap-frontend/src/legacy/state/claim/hooks/index.ts +++ b/apps/cowswap-frontend/src/legacy/state/claim/hooks/index.ts @@ -5,19 +5,17 @@ import { TokenWithLogo, V_COW } from '@cowprotocol/common-const' import { useIsMounted, useVCowContract } from '@cowprotocol/common-hooks' import { calculateGasMargin, formatTokenAmount, isAddress } from '@cowprotocol/common-utils' import { SupportedChainId as ChainId, SupportedChainId } from '@cowprotocol/cow-sdk' +import { useSingleContractMultipleData } from '@cowprotocol/multicall' import { useWalletInfo } from '@cowprotocol/wallet' import { BigNumber } from '@ethersproject/bignumber' import { TransactionResponse } from '@ethersproject/providers' import { parseUnits } from '@ethersproject/units' -import { CallState } from '@uniswap/redux-multicall' import { CurrencyAmount, Price, Token } from '@uniswap/sdk-core' import JSBI from 'jsbi' import ms from 'ms.macro' import { useDispatch, useSelector } from 'react-redux' -import { useSingleContractMultipleData } from 'lib/hooks/multicall' - import { PAID_CLAIM_TYPES } from './const' import { ClaimInput, ClaimType, RepoClaims, UserClaims, VCowPrices } from './types' import { @@ -101,7 +99,7 @@ export function useClassifiedUserClaims(account: Account, optionalChainId?: Supp expired: [], }) - const contract = useVCowContract() + const contract = useVCowContract() || undefined const { isInvestmentWindowOpen, isAirdropWindowOpen } = useClaimTimeInfo() @@ -109,7 +107,11 @@ export function useClassifiedUserClaims(account: Account, optionalChainId?: Supp // we check for all claims because expired now might have been claimed before const claimIndexes = useMemo(() => userClaims?.map(({ index }) => [index]) || [], [userClaims]) - const results = useSingleContractMultipleData(contract, 'isClaimed', claimIndexes) + const { data: results, isLoading: isClaimLoading } = useSingleContractMultipleData( + contract, + 'isClaimed', + claimIndexes + ) useEffect(() => { const available: UserClaims = [] @@ -125,18 +127,16 @@ export function useClassifiedUserClaims(account: Account, optionalChainId?: Supp let isContractCallLoading = false - results.forEach((result: CallState, index: number) => { + results?.forEach((result, index: number) => { const claim = userClaims[index] // Use the loading state from the multicall results - if (!isContractCallLoading && result.loading) { + if (!isContractCallLoading && isClaimLoading) { isContractCallLoading = true } if ( - result.valid && // result is valid - !result.loading && // result is not loading - result.result?.[0] === true // result true means claimed + result?.[0] === true // result true means claimed ) { claimed.push(claim) } else if (!isAirdropWindowOpen || (!isInvestmentWindowOpen && PAID_CLAIM_TYPES.includes(claim.type))) { @@ -148,7 +148,7 @@ export function useClassifiedUserClaims(account: Account, optionalChainId?: Supp setIsLoading(isContractCallLoading) setClaims({ available, expired, claimed }) - }, [isAirdropWindowOpen, isInvestmentWindowOpen, results, userClaims]) + }, [isAirdropWindowOpen, isInvestmentWindowOpen, results, userClaims, isClaimLoading]) return { ...claims, isLoading: isLoading || areClaimsLoading } } diff --git a/apps/cowswap-frontend/src/legacy/state/cowToken/hooks.ts b/apps/cowswap-frontend/src/legacy/state/cowToken/hooks.ts index 6558986cbc..a2d2b8cc65 100644 --- a/apps/cowswap-frontend/src/legacy/state/cowToken/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/cowToken/hooks.ts @@ -3,10 +3,11 @@ import { useCallback, useMemo } from 'react' import { V_COW } from '@cowprotocol/common-const' import { useVCowContract } from '@cowprotocol/common-hooks' import { useWalletInfo } from '@cowprotocol/wallet' +import type { BigNumber } from '@ethersproject/bignumber' import { TransactionResponse } from '@ethersproject/providers' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { CallStateResult as Result, useSingleCallResult } from 'lib/hooks/multicall' +import useSWR from 'swr' import { setSwapVCowStatus, SwapVCowStatus } from './actions' @@ -33,18 +34,18 @@ interface SwapVCowCallbackParams { /** * Hook that parses the result input with BigNumber value to CurrencyAmount */ -function useParseVCowResult(result: Result | undefined) { +function useParseVCowResult(result: BigNumber | undefined) { const { chainId } = useWalletInfo() - const vCowToken = chainId ? V_COW[chainId] : undefined + const vCowToken = V_COW[chainId] return useMemo(() => { - if (!chainId || !vCowToken || !result) { + if (!vCowToken || !result) { return } - return CurrencyAmount.fromRawAmount(vCowToken, result[0].toString()) - }, [chainId, result, vCowToken]) + return CurrencyAmount.fromRawAmount(vCowToken, result.toString()) + }, [result, vCowToken]) } /** @@ -54,12 +55,23 @@ export function useVCowData(): VCowData { const vCowContract = useVCowContract() const { account } = useWalletInfo() - const { loading: isVestedLoading, result: vestedResult } = useSingleCallResult(vCowContract, 'swappableBalanceOf', [ - account ?? undefined, - ]) - const { loading: isTotalLoading, result: totalResult } = useSingleCallResult(vCowContract, 'balanceOf', [ - account ?? undefined, - ]) + const { data: vestedResult, isLoading: isVestedLoading } = useSWR( + ['useVCowData.swappableBalanceOf', account, vCowContract], + async () => { + if (!account || !vCowContract) return undefined + + return vCowContract.swappableBalanceOf(account) + } + ) + + const { data: totalResult, isLoading: isTotalLoading } = useSWR( + ['useVCowData.balanceOf', account, vCowContract], + async () => { + if (!account || !vCowContract) return undefined + + return vCowContract.balanceOf(account) + } + ) const vested = useParseVCowResult(vestedResult) const total = useParseVCowResult(totalResult) 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/legacy/state/index.ts b/apps/cowswap-frontend/src/legacy/state/index.ts index 278de6c393..a54ea5e3f3 100644 --- a/apps/cowswap-frontend/src/legacy/state/index.ts +++ b/apps/cowswap-frontend/src/legacy/state/index.ts @@ -12,7 +12,6 @@ import enhancedTransactions from './enhancedTransactions/reducer' import gas from './gas/reducer' import { updateVersion } from './global/actions' import logs from './logs/slice' -import { multicall } from './multicall' import { appziMiddleware, popupMiddleware, soundMiddleware } from './orders/middleware' import orders from './orders/reducer' import { priceMiddleware } from './price/middleware' @@ -26,7 +25,6 @@ const reducers = { user, connection, swap, - multicall: multicall.reducer, logs, transactions: enhancedTransactions, // replace transactions state by "enhancedTransactions" orders, diff --git a/apps/cowswap-frontend/src/legacy/state/multicall.tsx b/apps/cowswap-frontend/src/legacy/state/multicall.tsx deleted file mode 100644 index 130950512b..0000000000 --- a/apps/cowswap-frontend/src/legacy/state/multicall.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from 'react' - -import { useInterfaceMulticall, useBlockNumber } from '@cowprotocol/common-hooks' -import { networkConnection } from '@cowprotocol/wallet' -import { Web3Provider } from '@ethersproject/providers' -import { createMulticall } from '@uniswap/redux-multicall' -import { useWeb3React } from '@web3-react/core' - -// TODO: enable only for MevBlocker -const shouldUseNetworkProvider = true - -export const multicall = createMulticall() - -export function MulticallUpdater() { - const { chainId: currentChainId, connector } = useWeb3React() - const latestBlockNumber = useBlockNumber() - - const customProvider = networkConnection.connector.customProvider as Web3Provider | undefined - const contract = useInterfaceMulticall(shouldUseNetworkProvider ? customProvider : undefined) - - // Multicall uses the network connector because of Mevblocker issue - // So, the networkConnection should be synced with the current provider - useEffect(() => { - if (!shouldUseNetworkProvider) return - - if (currentChainId && connector !== networkConnection.connector) { - networkConnection.connector.activate(currentChainId) - } - }, [currentChainId, connector]) - - return -} diff --git a/apps/cowswap-frontend/src/lib/hooks/multicall.ts b/apps/cowswap-frontend/src/lib/hooks/multicall.ts deleted file mode 100644 index 21c7aa5138..0000000000 --- a/apps/cowswap-frontend/src/lib/hooks/multicall.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useBlockNumber } from '@cowprotocol/common-hooks' -import { useWalletInfo } from '@cowprotocol/wallet' - -import { multicall } from 'legacy/state/multicall' -import { SkipFirst } from 'legacy/types/tuple' - -export type { CallStateResult } from '@uniswap/redux-multicall' // re-export for convenience -export { NEVER_RELOAD } from '@uniswap/redux-multicall' // re-export for convenience - -// Create wrappers for hooks so consumers don't need to get latest block themselves - -type SkipFirstTwoParams any> = SkipFirst, 2> - -export function useMultipleContractSingleData( - ...args: SkipFirstTwoParams -) { - const { chainId, latestBlock } = useCallContext() - return multicall.hooks.useMultipleContractSingleData(chainId, latestBlock, ...args) -} - -export function useSingleCallResult(...args: SkipFirstTwoParams) { - const { chainId, latestBlock } = useCallContext() - return multicall.hooks.useSingleCallResult(chainId, latestBlock, ...args) -} - -export function useSingleContractMultipleData( - ...args: SkipFirstTwoParams -) { - const { chainId, latestBlock } = useCallContext() - return multicall.hooks.useSingleContractMultipleData(chainId, latestBlock, ...args) -} - -export function useSingleContractWithCallData( - ...args: SkipFirstTwoParams -) { - const { chainId, latestBlock } = useCallContext() - return multicall.hooks.useSingleContractWithCallData(chainId, latestBlock, ...args) -} - -function useCallContext() { - const { chainId } = useWalletInfo() - const latestBlock = useBlockNumber() - return { chainId, latestBlock } -} diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index 4d12a73956..b755c69546 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -1,8 +1,8 @@ +import { BalancesAndAllowancesUpdater } from '@cowprotocol/balances-and-allowances' import { TokensListsUpdater, UnsupportedTokensUpdater } from '@cowprotocol/tokens' import { useWalletInfo, WalletUpdater } from '@cowprotocol/wallet' import { GasPriceStrategyUpdater } from 'legacy/state/gas/gas-price-strategy-updater' -import { MulticallUpdater } from 'legacy/state/multicall' import { UploadToIpfsUpdater } from 'modules/appData/updater/UploadToIpfsUpdater' import { InjectedWidgetUpdater } from 'modules/injectedWidget' @@ -30,7 +30,7 @@ import { ThemeFromUrlUpdater } from 'common/updaters/ThemeFromUrlUpdater' import { UserUpdater } from 'common/updaters/UserUpdater' export function Updaters() { - const { chainId } = useWalletInfo() + const { chainId, account } = useWalletInfo() return ( <> @@ -40,7 +40,6 @@ export function Updaters() { - @@ -61,6 +60,7 @@ export function Updaters() { + ) } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx index c82bd3c577..3b66f62476 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx @@ -75,19 +75,10 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { const canTrade = localFormValidation === null && primaryFormValidation === null && !tradeQuote.error const showPriceImpactWarning = - canTrade && - !tradeQuote.isLoading && - !expertMode && - !!account && - !priceImpactParams.loading && - !priceImpactParams.priceImpact + canTrade && !expertMode && !!account && !priceImpactParams.loading && !priceImpactParams.priceImpact const showRateImpactWarning = - canTrade && - !tradeQuote.isLoading && - inputCurrency && - !isFractionFalsy(inputCurrencyAmount) && - !isFractionFalsy(outputCurrencyAmount) + canTrade && inputCurrency && !isFractionFalsy(inputCurrencyAmount) && !isFractionFalsy(outputCurrencyAmount) const feePercentage = calculatePercentageInRelationToReference({ value: feeAmount, reference: inputCurrencyAmount }) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts index f9aba04437..a85aa5f09a 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts @@ -15,7 +15,7 @@ import { useAppData } from 'modules/appData' import { useRateImpact } from 'modules/limitOrders/hooks/useRateImpact' import { TradeFlowContext } from 'modules/limitOrders/services/types' import { limitOrdersSettingsAtom } from 'modules/limitOrders/state/limitOrdersSettingsAtom' -import { useGeneratePermitHook, useIsTokenPermittable } from 'modules/permit' +import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' import { useEnoughBalanceAndAllowance } from 'modules/tokens' import { TradeType } from 'modules/trade' import { useTradeQuote } from 'modules/tradeQuote' @@ -47,7 +47,7 @@ export function useTradeFlowContext(): TradeFlowContext | null { const quoteState = useTradeQuote() const rateImpact = useRateImpact() const settingsState = useAtomValue(limitOrdersSettingsAtom) - const permitInfo = useIsTokenPermittable(state.inputCurrency, TradeType.LIMIT_ORDER) + const permitInfo = usePermitInfo(state.inputCurrency, TradeType.LIMIT_ORDER) const checkAllowanceAddress = GP_VAULT_RELAYER[chainId] const { enoughAllowance } = useEnoughBalanceAndAllowance({ diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts index 7d4a07a6f1..f71190c0b0 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts @@ -1,5 +1,5 @@ import { OrderClass } from '@cowprotocol/cow-sdk' -import { PERMIT_SIGNER } from '@cowprotocol/permit-utils' +import { PERMIT_SIGNER, isSupportedPermitInfo } from '@cowprotocol/permit-utils' import { Percent } from '@uniswap/sdk-core' import * as Sentry from '@sentry/browser' @@ -60,7 +60,7 @@ export async function tradeFlow( try { logTradeFlow('LIMIT ORDER FLOW', 'STEP 2: handle permit') - if (permitInfo) beforePermit() + if (isSupportedPermitInfo(permitInfo)) beforePermit() postOrderParams.appData = await handlePermit({ permitInfo, diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx index d98cc04619..7dff122c72 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx @@ -1,7 +1,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback, useEffect, useMemo } from 'react' -import { GP_VAULT_RELAYER } from '@cowprotocol/common-const' +import { useTokensAllowances, useTokensBalances } from '@cowprotocol/balances-and-allowances' import { useIsSafeViaWc, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { useLocation, useNavigate } from 'react-router-dom' @@ -18,7 +18,6 @@ import { useSelectReceiptOrder } from 'modules/ordersTable/containers/OrdersRece import { OrderActions } from 'modules/ordersTable/pure/OrdersTableContainer/types' import { buildOrdersTableUrl, parseOrdersTableUrl } from 'modules/ordersTable/utils/buildOrdersTableUrl' import { PendingPermitUpdater, useGetOrdersPermitStatus } from 'modules/permit' -import { useBalancesAndAllowances } from 'modules/tokens' import { useCancelOrder } from 'common/hooks/useCancelOrder' import { useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' @@ -31,8 +30,9 @@ import { OrdersTableList, useOrdersTableList } from './hooks/useOrdersTableList' import { useOrdersTableTokenApprove } from './hooks/useOrdersTableTokenApprove' import { useValidatePageUrlParams } from './hooks/useValidatePageUrlParams' +import { BalancesAndAllowances } from '../../../tokens' import { OrdersTableContainer, TabOrderTypes } from '../../pure/OrdersTableContainer' -import { getParsedOrderFromTableItem, OrderTableItem, tableItemsToOrders } from '../../utils/orderTableGroupUtils' +import { OrderTableItem, tableItemsToOrders } from '../../utils/orderTableGroupUtils' function getOrdersListByIndex(ordersList: OrdersTableList, id: string): OrderTableItem[] { return id === OPEN_TAB.id ? ordersList.pending : ordersList.history @@ -77,8 +77,6 @@ export function OrdersTableWidget({ const isSafeViaWc = useIsSafeViaWc() const ordersPermitStatus = useGetOrdersPermitStatus() - const spender = useMemo(() => (chainId ? GP_VAULT_RELAYER[chainId] : undefined), [chainId]) - const { currentTabId, currentPageNumber } = useMemo(() => { const params = parseOrdersTableUrl(location.search) @@ -100,15 +98,19 @@ export function OrdersTableWidget({ const isOpenOrdersTab = useMemo(() => OPEN_TAB.id === currentTabId, [currentTabId]) - // Get tokens from pending orders (only if the OPEN orders tab is opened) - const tokens = useMemo(() => { - const pendingOrders = isOpenOrdersTab ? ordersList.pending : [] + const balancesState = useTokensBalances() + const allowancesState = useTokensAllowances() - return pendingOrders.map((item) => getParsedOrderFromTableItem(item).inputToken) - }, [isOpenOrdersTab, ordersList.pending]) + const balancesAndAllowances: BalancesAndAllowances = useMemo(() => { + const { isLoading: balancesLoading, values: balances } = balancesState + const { isLoading: allowancesLoading, values: allowances } = allowancesState + return { + isLoading: balancesLoading || allowancesLoading, + balances, + allowances, + } + }, [balancesState, allowancesState]) - // Get effective balance - const balancesAndAllowances = useBalancesAndAllowances({ account, spender, tokens }) const { pendingActivity } = useCategorizeRecentActivity() const toggleOrdersForCancellation = useCallback( 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 55b76a0a15..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,30 +1,19 @@ -import { CurrencyAmount } from '@uniswap/sdk-core' +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]: { - value: CurrencyAmount.fromRawAmount(BASE_ORDER.inputToken, BASE_ORDER.sellAmount), - loading: false, - syncing: false, - valid: true, - error: false, - }, + [BASE_ORDER.inputToken.address.toLowerCase()]: BigNumber.from(BASE_ORDER.sellAmount), }, allowances: { - [BASE_ORDER.inputToken.address]: { - value: CurrencyAmount.fromRawAmount(BASE_ORDER.inputToken, BASE_ORDER.sellAmount), - loading: false, - syncing: false, - valid: true, - error: false, - }, + [BASE_ORDER.inputToken.address.toLowerCase()]: BigNumber.from(BASE_ORDER.sellAmount), }, isLoading: false, } @@ -78,10 +67,7 @@ describe('getOrderParams', () => { const balancesAndAllowances: BalancesAndAllowances = { ...BASE_BALANCES_AND_ALLOWANCES, balances: { - [order.inputToken.address]: { - ...BASE_BALANCES_AND_ALLOWANCES.balances[order.inputToken.address], - value: CurrencyAmount.fromRawAmount(order.inputToken, String(+order.sellAmount * 0.00051)), - }, + [order.inputToken.address.toLowerCase()]: BigNumber.from(String(+order.sellAmount * 0.00051)), }, } const result = getOrderParams(1, balancesAndAllowances, order) @@ -92,10 +78,7 @@ describe('getOrderParams', () => { const balancesAndAllowances: BalancesAndAllowances = { ...BASE_BALANCES_AND_ALLOWANCES, balances: { - [order.inputToken.address]: { - ...BASE_BALANCES_AND_ALLOWANCES.balances[order.inputToken.address], - value: CurrencyAmount.fromRawAmount(order.inputToken, String(+order.sellAmount * 0.00049)), - }, + [order.inputToken.address.toLowerCase()]: BigNumber.from(String(+order.sellAmount * 0.00049)), }, } const result = getOrderParams(1, balancesAndAllowances, order) @@ -107,10 +90,7 @@ describe('getOrderParams', () => { const balancesAndAllowances: BalancesAndAllowances = { ...BASE_BALANCES_AND_ALLOWANCES, allowances: { - [order.inputToken.address]: { - ...BASE_BALANCES_AND_ALLOWANCES.allowances[order.inputToken.address], - value: CurrencyAmount.fromRawAmount(order.inputToken, String(+order.sellAmount * 0.00051)), - }, + [order.inputToken.address.toLowerCase()]: BigNumber.from(String(+order.sellAmount * 0.00051)), }, } const result = getOrderParams(1, balancesAndAllowances, order) @@ -121,10 +101,7 @@ describe('getOrderParams', () => { const balancesAndAllowances: BalancesAndAllowances = { ...BASE_BALANCES_AND_ALLOWANCES, allowances: { - [order.inputToken.address]: { - ...BASE_BALANCES_AND_ALLOWANCES.allowances[order.inputToken.address], - value: CurrencyAmount.fromRawAmount(order.inputToken, 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 683eea861e..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 @@ -1,5 +1,6 @@ import { isEnoughAmount } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { BigNumber } from '@ethersproject/bignumber' import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' import { BalancesAndAllowances } from 'modules/tokens' @@ -35,8 +36,8 @@ export function getOrderParams( } const { balances, allowances } = balancesAndAllowances - const balance = balances[order.inputToken.address]?.value - const allowance = allowances[order.inputToken.address]?.value + const balance = balances[order.inputToken.address.toLowerCase()] + const allowance = allowances[order.inputToken.address.toLowerCase()] const { hasEnoughBalance, hasEnoughAllowance } = _hasEnoughBalanceAndAllowance({ partiallyFillable: order.partiallyFillable, @@ -56,10 +57,10 @@ export function getOrderParams( } function _hasEnoughBalanceAndAllowance(params: { - balance: CurrencyAmount | undefined + balance: BigNumber | undefined + allowance: BigNumber | undefined partiallyFillable: boolean sellAmount: CurrencyAmount - allowance: CurrencyAmount | undefined }): { hasEnoughBalance: boolean | undefined hasEnoughAllowance: boolean | undefined diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useAccountAgnosticPermitHookData.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useAccountAgnosticPermitHookData.ts index 580e46f2ee..a4e2c94754 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useAccountAgnosticPermitHookData.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useAccountAgnosticPermitHookData.ts @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react' -import { PermitHookData } from '@cowprotocol/permit-utils' +import { isSupportedPermitInfo, PermitHookData } from '@cowprotocol/permit-utils' import { useDerivedTradeState } from 'modules/trade' import { useSafeMemo } from 'common/hooks/useSafeMemo' import { useGeneratePermitHook } from './useGeneratePermitHook' -import { useIsTokenPermittable } from './useIsTokenPermittable' +import { usePermitInfo } from './usePermitInfo' import { GeneratePermitHookParams } from '../types' @@ -41,10 +41,10 @@ function useGeneratePermitHookParams(): GeneratePermitHookParams | undefined { const { state } = useDerivedTradeState() const { inputCurrency, tradeType } = state || {} - const permitInfo = useIsTokenPermittable(inputCurrency, tradeType) + const permitInfo = usePermitInfo(inputCurrency, tradeType) return useSafeMemo(() => { - if (!inputCurrency || !('address' in inputCurrency) || !permitInfo) return undefined + if (!inputCurrency || !('address' in inputCurrency) || !isSupportedPermitInfo(permitInfo)) return undefined return { inputToken: { address: inputCurrency.address, name: inputCurrency.name }, diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts index b32c0410ad..045e31ad8f 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useCheckHasValidPendingPermit.ts @@ -64,7 +64,7 @@ async function checkHasValidPendingPermit( const eip2162Utils = getPermitUtilsInstance(chainId, provider, order.owner) const tokenAddress = order.inputToken.address - const tokenName = order.inputToken.name || tokenAddress + const tokenName = order.inputToken.name const checkedHooks = await Promise.all( preHooks.map(({ callData }) => diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts index 2a821920e9..2110d9f934 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useGeneratePermitHook.ts @@ -2,7 +2,12 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' import { GP_VAULT_RELAYER } from '@cowprotocol/common-const' -import { generatePermitHook, getPermitUtilsInstance, PermitHookData } from '@cowprotocol/permit-utils' +import { + generatePermitHook, + getPermitUtilsInstance, + isSupportedPermitInfo, + PermitHookData, +} from '@cowprotocol/permit-utils' import { useWalletInfo } from '@cowprotocol/wallet' import { useWeb3React } from '@web3-react/core' @@ -38,7 +43,7 @@ export function useGeneratePermitHook(): GeneratePermitHook { async (params: GeneratePermitHookParams): Promise => { const { inputToken, account, permitInfo } = params - if (!provider) { + if (!provider || !isSupportedPermitInfo(permitInfo)) { return } diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts index bbcdd4ca2b..2fd0801779 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts @@ -1,6 +1,7 @@ import { useAtomValue } from 'jotai' import { useMemo, useRef } from 'react' +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' import { useWalletInfo } from '@cowprotocol/wallet' import { useIsPermitEnabled } from 'common/hooks/featureFlags/useIsPermitEnabled' @@ -33,11 +34,11 @@ export function usePermitCompatibleTokens(): PermitCompatibleTokens { const permitCompatibleTokens: PermitCompatibleTokens = {} for (const address of Object.keys(preGeneratedPermitInfoRef.current)) { - permitCompatibleTokens[address.toLowerCase()] = !!preGeneratedPermitInfoRef.current[address] + permitCompatibleTokens[address.toLowerCase()] = isSupportedPermitInfo(preGeneratedPermitInfoRef.current[address]) } for (const address of Object.keys(localPermitInfoRef.current)) { - permitCompatibleTokens[address.toLowerCase()] = !!localPermitInfoRef.current[address] + permitCompatibleTokens[address.toLowerCase()] = isSupportedPermitInfo(localPermitInfoRef.current[address]) } return permitCompatibleTokens diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts similarity index 74% rename from apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts rename to apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts index 025b281819..636b006b00 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitInfo.ts @@ -4,7 +4,7 @@ import { useEffect, useMemo } from 'react' import { GP_VAULT_RELAYER } from '@cowprotocol/common-const' import { getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { getTokenPermitInfo } from '@cowprotocol/permit-utils' +import { getTokenPermitInfo, PermitInfo } from '@cowprotocol/permit-utils' import { useWalletInfo } from '@cowprotocol/wallet' import { Currency } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' @@ -21,24 +21,25 @@ import { ORDER_TYPE_SUPPORTS_PERMIT } from '../const' import { addPermitInfoForTokenAtom, permittableTokensAtom } from '../state/permittableTokensAtom' import { IsTokenPermittableResult } from '../types' +const UNSUPPORTED: PermitInfo = { type: 'unsupported', name: 'native' } + /** - * Checks whether the token is permittable, and caches the result on localStorage + * Check whether the token is permittable, and returns the permit info for it + * Tries to find it out from the pre-generated list + * If not found, tries to load the info from chain + * The result will be cached on localStorage if a final conclusion is found * * When it is, returned type is `{type: 'dai'|'permit', gasLimit: number} - * When it is not, returned type is `false` + * When it is not, returned type is `{type: 'unsupported'}` * When it is unknown, returned type is `undefined` - * */ -export function useIsTokenPermittable( - token: Nullish, - tradeType: Nullish -): IsTokenPermittableResult { +export function usePermitInfo(token: Nullish, tradeType: Nullish): IsTokenPermittableResult { const { chainId } = useWalletInfo() const { provider } = useWeb3React() const lowerCaseAddress = token ? getWrappedToken(token).address?.toLowerCase() : undefined const isNative = !!token && getIsNativeToken(token) - const tokenName = token?.name || lowerCaseAddress || '' + const tokenName = token?.name // Avoid building permit info in the first place if order type is not supported const isPermitSupported = !!tradeType && ORDER_TYPE_SUPPORTS_PERMIT[tradeType] @@ -46,7 +47,7 @@ export function useIsTokenPermittable( const isPermitEnabled = useIsPermitEnabled() && isPermitSupported const addPermitInfo = useAddPermitInfo() - const permitInfo = usePermitInfo(chainId, isPermitEnabled ? lowerCaseAddress : undefined) + const permitInfo = _usePermitInfo(chainId, isPermitEnabled ? lowerCaseAddress : undefined) const { permitInfo: preGeneratedInfo, isLoading: preGeneratedIsLoading } = usePreGeneratedPermitInfoForToken( isPermitEnabled ? token : undefined ) @@ -70,16 +71,13 @@ export function useIsTokenPermittable( } getTokenPermitInfo({ spender, tokenAddress: lowerCaseAddress, tokenName, chainId, provider }).then((result) => { - if (!result) { - // When falsy, we know it doesn't support permit. Cache it. - addPermitInfo({ chainId, tokenAddress: lowerCaseAddress, permitInfo: false }) - } else if ('error' in result) { + if ('error' in result) { // When error, we don't know. Log and don't cache. console.debug( `useIsTokenPermittable: failed to check whether token ${lowerCaseAddress} is permittable: ${result.error}` ) } else { - // Otherwise, we know it is permittable. Cache it. + // Otherwise, we know it is permittable or not. Cache it. addPermitInfo({ chainId, tokenAddress: lowerCaseAddress, permitInfo: result }) } }) @@ -98,7 +96,7 @@ export function useIsTokenPermittable( ]) if (isNative) { - return false + return UNSUPPORTED } return preGeneratedInfo ?? permitInfo @@ -111,14 +109,7 @@ function useAddPermitInfo() { return useSetAtom(addPermitInfoForTokenAtom) } -/** - * Returns whether a token is permittable. - * - * When it is, returned type is `{type: 'dai'|'permit', gasLimit: number}` - * When it is not, returned type is `false` - * When it is unknown, returned type is `undefined` - */ -function usePermitInfo(chainId: SupportedChainId, tokenAddress: string | undefined): IsTokenPermittableResult { +function _usePermitInfo(chainId: SupportedChainId, tokenAddress: string | undefined): IsTokenPermittableResult { const permittableTokens = useAtomValue(permittableTokensAtom) return useMemo(() => { diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts index 3ea379ee01..856515fa8e 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts @@ -21,9 +21,33 @@ export function usePreGeneratedPermitInfo(): { const { data, isLoading } = useSWR( url, - (url: string): Promise> => fetch(url).then((r) => r.json()), + (url: string): Promise> => + fetch(url) + .then((r) => r.json()) + .then(migrateData), { ...SWR_NO_REFRESH_OPTIONS, fallbackData: {} } ) return { allPermitInfo: data, isLoading } } + +type OldPermitInfo = PermitInfo | false + +const UNSUPPORTED: PermitInfo = { type: 'unsupported' } + +/** + * Handles data migration from former way of storing unsupported tokens to the new one + */ +function migrateData(data: Record): Record { + const migrated: Record = {} + + for (const [k, v] of Object.entries(data)) { + if (v === false) { + migrated[k] = UNSUPPORTED + } else { + migrated[k] = v + } + } + + return migrated +} diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useTokenSupportsPermit.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useTokenSupportsPermit.ts new file mode 100644 index 0000000000..993ccf4937 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useTokenSupportsPermit.ts @@ -0,0 +1,20 @@ +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' +import { Currency } from '@uniswap/sdk-core' + +import { Nullish } from 'types' + +import { TradeType } from 'modules/trade' + +import { usePermitInfo } from './usePermitInfo' + +/** + * Whether the token supports permit for given trade type + * + * @param token + * @param tradeType + */ +export function useTokenSupportsPermit(token: Nullish, tradeType: Nullish): boolean { + const permitInfo = usePermitInfo(token, tradeType) + + return isSupportedPermitInfo(permitInfo) +} diff --git a/apps/cowswap-frontend/src/modules/permit/index.ts b/apps/cowswap-frontend/src/modules/permit/index.ts index c7e91b4054..bc55f16c71 100644 --- a/apps/cowswap-frontend/src/modules/permit/index.ts +++ b/apps/cowswap-frontend/src/modules/permit/index.ts @@ -1,8 +1,9 @@ export * from './hooks/useAccountAgnosticPermitHookData' export * from './hooks/useGeneratePermitHook' -export * from './hooks/useIsTokenPermittable' +export * from './hooks/usePermitInfo' export * from './hooks/useOrdersPermitStatus' export * from './hooks/usePermitCompatibleTokens' +export * from './hooks/useTokenSupportsPermit' export * from './types' export * from './updaters/PendingPermitUpdater' export * from './utils/handlePermit' diff --git a/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts b/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts index aeebdf1716..c3b0727ffb 100644 --- a/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts +++ b/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts @@ -9,10 +9,9 @@ import { AddPermitTokenParams, PermittableTokens } from '../types' * Atom that stores the permittable tokens info for each chain on localStorage. * It's meant to be shared across different tabs, thus no special storage handling. * - * Contains either the permit info with `type` and `gasLimit` when supported or - * `false` when not supported + * Contains either the permit info for every token checked locally */ -export const permittableTokensAtom = atomWithStorage('permittableTokens:v1', { +export const permittableTokensAtom = atomWithStorage('permittableTokens:v2', { [SupportedChainId.MAINNET]: {}, [SupportedChainId.GOERLI]: {}, [SupportedChainId.GNOSIS_CHAIN]: {}, diff --git a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts index 68ae1fd5f4..f200596b43 100644 --- a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts @@ -1,3 +1,5 @@ +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' + import { AppDataInfo, buildAppDataHooks, updateHooksOnAppData } from 'modules/appData' import { HandlePermitParams } from '../types' @@ -15,7 +17,7 @@ import { HandlePermitParams } from '../types' export async function handlePermit(params: HandlePermitParams): Promise { const { permitInfo, inputToken, account, appData, generatePermitHook } = params - if (permitInfo && 'address' in inputToken) { + if (isSupportedPermitInfo(permitInfo) && 'address' in inputToken) { // permitInfo will only be set if there's NOT enough allowance const permitData = await generatePermitHook({ 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 e0d507ecc3..f1ef6a2bd2 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/EthFlow/index.tsx @@ -1,6 +1,7 @@ import { useAtomValue } from 'jotai' import { useMemo } from 'react' +import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { currencyAmountToTokenAmount } from '@cowprotocol/common-utils' import { useWalletInfo } from '@cowprotocol/wallet' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' @@ -14,10 +15,10 @@ import { EthFlowModalContent } from 'modules/swap/pure/EthFlow/EthFlowModalConte import { WrappingPreviewProps } from 'modules/swap/pure/EthFlow/WrappingPreview' import { HandleSwapCallback } from 'modules/swap/pure/SwapButtons' import { ethFlowContextAtom } from 'modules/swap/state/EthFlow/ethFlowContextAtom' -import { useCurrencyBalances } from 'modules/tokens/hooks/useCurrencyBalance' 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' @@ -40,11 +41,11 @@ function EthFlow({ directSwapCallback, hasEnoughWrappedBalanceForSwap, }: EthFlowProps) { - const { account, chainId } = useWalletInfo() + const { chainId } = useWalletInfo() 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( @@ -59,7 +60,10 @@ function EthFlow({ const approveActivity = useSingleActivityDescriptor({ chainId, id: ethFlowContext.approve.txHash || undefined }) const wrapActivity = useSingleActivityDescriptor({ chainId, id: ethFlowContext.wrap.txHash || undefined }) - const [nativeBalance, wrappedBalance] = useCurrencyBalances(account, [native, wrapped]) + + const nativeBalance = useCurrencyAmountBalance(native) + const wrappedBalance = useCurrencyAmountBalance(wrapped) + // user safety checks to make sure any on-chain native currency operations are economically safe // shows user warning with remaining available TXs if a certain threshold is reached const { balanceChecks } = useRemainingNativeTxsAndCosts({ diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx index 8dab3406f2..675e27f634 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -1,5 +1,7 @@ import React, { useMemo, useState } from 'react' +import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' +import { NATIVE_CURRENCY_BUY_TOKEN, TokenWithLogo } from '@cowprotocol/common-const' import { isFractionFalsy } from '@cowprotocol/common-utils' import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { useIsSafeViaWc, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' @@ -30,7 +32,6 @@ import { SwapWarningsTop, SwapWarningsTopProps, } from 'modules/swap/pure/warnings' -import useCurrencyBalance from 'modules/tokens/hooks/useCurrencyBalance' import { TradeWidget, TradeWidgetContainer, useTradePriceImpact } from 'modules/trade' import { useTradeRouteContext } from 'modules/trade/hooks/useTradeRouteContext' import { useWrappedToken } from 'modules/trade/hooks/useWrappedToken' @@ -87,8 +88,25 @@ export function SwapWidget() { address: currenciesIds.INPUT, }) - const inputCurrencyBalance = useCurrencyBalance(account ?? undefined, currencies.INPUT) || null - const outputCurrencyBalance = useCurrencyBalance(account ?? undefined, currencies.OUTPUT) || null + const inputToken = useMemo(() => { + if (!currencies.INPUT) return currencies.INPUT + + if (currencies.INPUT.isNative) return NATIVE_CURRENCY_BUY_TOKEN[chainId] + + return TokenWithLogo.fromToken(currencies.INPUT) + }, [chainId, currencies.INPUT]) + + const outputToken = useMemo(() => { + if (!currencies.OUTPUT) return currencies.OUTPUT + + if (currencies.OUTPUT.isNative) return NATIVE_CURRENCY_BUY_TOKEN[chainId] + + return TokenWithLogo.fromToken(currencies.OUTPUT) + }, [chainId, currencies.OUTPUT]) + + const inputCurrencyBalance = useCurrencyAmountBalance(inputToken) || null + const outputCurrencyBalance = useCurrencyAmountBalance(outputToken) || null + const isSellTrade = independentField === Field.INPUT const { @@ -126,8 +144,17 @@ export function SwapWidget() { const [showNativeWrapModal, setOpenNativeWrapModal] = useState(false) const showCowSubsidyModal = useModalIsOpen(ApplicationModal.COW_SUBSIDY) + // Hide the price impact warning when there is priceImpact value or when it's loading + // The loading values is debounced in useFiatValuePriceImpact() to avoid flickering + const hideUnknownImpactWarning = + isFractionFalsy(parsedAmounts.INPUT) || + isFractionFalsy(parsedAmounts.OUTPUT) || + !!priceImpactParams.priceImpact || + priceImpactParams.loading + const { feeWarningAccepted, setFeeWarningAccepted } = useHighFeeWarning(trade) - const { impactWarningAccepted, setImpactWarningAccepted } = useUnknownImpactWarning() + const { impactWarningAccepted: _impactWarningAccepted, setImpactWarningAccepted } = useUnknownImpactWarning() + const impactWarningAccepted = hideUnknownImpactWarning || _impactWarningAccepted const openNativeWrapModal = () => setOpenNativeWrapModal(true) const dismissNativeWrapModal = () => setOpenNativeWrapModal(false) @@ -186,14 +213,6 @@ export function SwapWidget() { const nativeCurrencySymbol = useNativeCurrency().symbol || 'ETH' const wrappedCurrencySymbol = useWrappedToken().symbol || 'WETH' - // Hide the price impact warning when there is priceImpact value or when it's loading - // The loading values is debounced in useFiatValuePriceImpact() to avoid flickering - const hideUnknownImpactWarning = - isFractionFalsy(parsedAmounts.INPUT) || - isFractionFalsy(parsedAmounts.OUTPUT) || - !!priceImpactParams.priceImpact || - priceImpactParams.loading - const swapWarningsTopProps: SwapWarningsTopProps = { chainId, trade, diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index a95c5937d5..ae160bd024 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -1,3 +1,4 @@ +import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { currencyAmountToTokenAmount, getWrappedToken } from '@cowprotocol/common-utils' import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { @@ -15,7 +16,7 @@ import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/ import { Field } from 'legacy/state/types' import { useExpertModeManager } from 'legacy/state/user/hooks' -import { useIsTokenPermittable } from 'modules/permit' +import { useTokenSupportsPermit } from 'modules/permit' import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState' import { useEthFlowContext } from 'modules/swap/hooks/useEthFlowContext' import { useHandleSwap } from 'modules/swap/hooks/useHandleSwap' @@ -23,13 +24,12 @@ import { useSafeBundleApprovalFlowContext } from 'modules/swap/hooks/useSafeBund import { useSwapConfirmManager } from 'modules/swap/hooks/useSwapConfirmManager' import { useSwapFlowContext } from 'modules/swap/hooks/useSwapFlowContext' import { SwapButtonsContext } from 'modules/swap/pure/SwapButtons' -import useCurrencyBalance from 'modules/tokens/hooks/useCurrencyBalance' import { TradeType, useWrapNativeFlow } from 'modules/trade' 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) @@ -93,7 +93,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext const isSwapUnsupported = useIsTradeUnsupported(currencyIn, currencyOut) const isSmartContractWallet = useIsSmartContractWallet() const isBundlingSupported = useIsBundlingSupported() - const isPermitSupported = !!useIsTokenPermittable(currencyIn, TradeType.SWAP) + const isPermitSupported = useTokenSupportsPermit(currencyIn, TradeType.SWAP) const swapButtonState = getSwapButtonState({ account, @@ -139,8 +139,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext function useHasEnoughWrappedBalanceForSwap(inputAmount?: CurrencyAmount): boolean { const { currencies } = useDerivedSwapInfo() - const { account } = useWalletInfo() - const wrappedBalance = useCurrencyBalance(account ?? undefined, currencies.INPUT && getWrappedToken(currencies.INPUT)) + const wrappedBalance = useCurrencyAmountBalance(currencies.INPUT ? getWrappedToken(currencies.INPUT) : undefined) // is an native currency trade but wrapped token has enough balance return !!(wrappedBalance && inputAmount && !wrappedBalance.lessThan(inputAmount)) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index d8bcda4991..bcd6267a46 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -5,7 +5,7 @@ import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' import { TradeType as UniTradeType } from '@uniswap/sdk-core' -import { useGeneratePermitHook, useIsTokenPermittable } from 'modules/permit' +import { useGeneratePermitHook, usePermitInfo } from 'modules/permit' import { FlowType, getFlowContext, @@ -20,7 +20,7 @@ export function useSwapFlowContext(): SwapFlowContext | null { const contract = useGP2SettlementContract() const baseProps = useBaseFlowContextSetup() const sellCurrency = baseProps.trade?.inputAmount?.currency - const permitInfo = useIsTokenPermittable(sellCurrency, TradeType.SWAP) + const permitInfo = usePermitInfo(sellCurrency, TradeType.SWAP) const generatePermitHook = useGeneratePermitHook() const checkAllowanceAddress = GP_VAULT_RELAYER[baseProps.chainId || SupportedChainId.MAINNET] diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index 6e7a82e992..214c27a068 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { changeSwapAmountAnalytics, switchTokensAnalytics } from '@cowprotocol/analytics' +import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { FEE_SIZE_THRESHOLD } from '@cowprotocol/common-const' import { formatSymbol, getIsNativeToken, isAddress, tryParseCurrencyAmount } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' @@ -12,7 +13,6 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { t } from '@lingui/macro' -import { PriceImpact } from 'legacy/hooks/usePriceImpact' import { AppState } from 'legacy/state' import { useAppDispatch, useAppSelector } from 'legacy/state/hooks' import { useGetQuoteAndStatus, useQuote } from 'legacy/state/price/hooks' @@ -23,7 +23,6 @@ import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { useIsExpertMode } from 'legacy/state/user/hooks' -import { useCurrencyBalances } from 'modules/tokens/hooks/useCurrencyBalance' import { useNavigateOnCurrencySelection } from 'modules/trade/hooks/useNavigateOnCurrencySelection' import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate' @@ -177,27 +176,11 @@ export function useUnknownImpactWarning() { }, [INPUT.currencyId, OUTPUT.currencyId, independentField]) return { - impactWarningAccepted: _computeUnknownPriceImpactAcceptedState({ - impactWarningAccepted, - isExpertMode, - }), + impactWarningAccepted: isExpertMode || impactWarningAccepted, setImpactWarningAccepted, } } -function _computeUnknownPriceImpactAcceptedState({ - impactWarningAccepted, - isExpertMode, -}: { - impactWarningAccepted: boolean - priceImpactParams?: PriceImpact - isExpertMode: boolean -}) { - if (isExpertMode || impactWarningAccepted) return true - - return true -} - // from the current swap inputs, compute the best trade and return it. export function useDerivedSwapInfo(): DerivedSwapInfo { const { account, chainId } = useWalletInfo() // MOD: chainId @@ -220,10 +203,8 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { const recipientLookup = useENS(recipient ?? undefined) const to: string | null = (recipient ? recipientLookup.address : account) ?? null - const relevantTokenBalances = useCurrencyBalances( - account ?? undefined, - useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency]) - ) + const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency) + const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency) const isExactIn: boolean = independentField === Field.INPUT const parsedAmount = useMemo( @@ -285,10 +266,10 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { const currencyBalances = useMemo( () => ({ - [Field.INPUT]: relevantTokenBalances[0], - [Field.OUTPUT]: relevantTokenBalances[1], + [Field.INPUT]: inputCurrencyBalance, + [Field.OUTPUT]: outputCurrencyBalance, }), - [relevantTokenBalances] + [inputCurrencyBalance, outputCurrencyBalance] ) // allowed slippage is either auto slippage, or custom user defined slippage if auto slippage disabled diff --git a/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowBanner/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowBanner/index.tsx index d972e7937d..117bd96281 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowBanner/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/EthFlow/EthFlowBanner/index.tsx @@ -55,7 +55,7 @@ export function EthFlowBannerContent(props: EthFlowBannerContentProps) { before placing your order. ) : null}{' '} - This way, you'll take of advantage of: + This way, you'll take advantage of:

  • Lower overall fees
  • diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx index 13f31c7ec3..8e93f597b9 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx @@ -150,14 +150,9 @@ const swapButtonStateMap: { [key in SwapButtonState]: (props: SwapButtonsContext - Approve  - { - - } -  and Swap + Approve{' '} + {' '} + and Swap @@ -166,14 +161,9 @@ const swapButtonStateMap: { [key in SwapButtonState]: (props: SwapButtonsContext - Confirm (Approve  - { - - } -  and Swap) + Confirm (Approve{' '} + {' '} + and Swap) diff --git a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx index 3682af3634..1c50d2163e 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx @@ -84,8 +84,9 @@ export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps) /> {!hideUnknownImpactWarning && ( setImpactWarningAccepted((state) => !state) : undefined} + acceptCallback={() => setImpactWarningAccepted((state) => !state)} /> )} {showApprovalBundlingBanner && } diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts index 48e61f8b67..a329f86e8d 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts @@ -1,3 +1,4 @@ +import { isSupportedPermitInfo } from '@cowprotocol/permit-utils' import { Percent } from '@uniswap/sdk-core' import { PriceImpact } from 'legacy/hooks/usePriceImpact' @@ -26,7 +27,7 @@ export async function swapFlow( try { logTradeFlow('SWAP FLOW', 'STEP 2: handle permit') - if (input.permitInfo) input.swapConfirmManager.requestPermitSignature() + if (isSupportedPermitInfo(input.permitInfo)) input.swapConfirmManager.requestPermitSignature() input.orderParams.appData = await handlePermit({ appData: input.orderParams.appData, diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useBalancesAndAllowances.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useBalancesAndAllowances.ts deleted file mode 100644 index 7d16e34069..0000000000 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useBalancesAndAllowances.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useOnchainBalancesAndAllowances } from './useOnchainBalances' - -import { BalancesAndAllowances, BalancesAndAllowancesParams } from '../types' - -/** - * Return the balances and allowances of the tokens. - * - * This hook is different from the useOnchainBalancesAndAllowance one in the fact that the user might contain some - * un-commited transaction that might affect the balances. - */ -export function useBalancesAndAllowances(params: BalancesAndAllowancesParams): BalancesAndAllowances { - const balancesAndAllowances = useOnchainBalancesAndAllowances(params) - - // TODO: This function still has too many re-renders, we shold investigate (for now, focusing on only the re-factor) - // console.debug('[usebalancesAndAllowances] Get balancesAndAllowances', params, balancesAndAllowances) - - // TODO: Apply all the balance transformations (i.e. bundled tx) - - return balancesAndAllowances -} diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useCurrencyBalance.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useCurrencyBalance.ts deleted file mode 100644 index 2e5df074c4..0000000000 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useCurrencyBalance.ts +++ /dev/null @@ -1,126 +0,0 @@ -// TODO: Most of the hooks in this file are legacy and should be adapted and re-rexported from the token module - -import { useMemo } from 'react' - -import { NATIVE_CURRENCY_BUY_TOKEN } from '@cowprotocol/common-const' -import { useInterfaceMulticall } from '@cowprotocol/common-hooks' -import { getIsNativeToken, isAddress } from '@cowprotocol/common-utils' -import { useWalletInfo } from '@cowprotocol/wallet' -import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' - -import JSBI from 'jsbi' - -import { useOnchainBalances } from 'modules/tokens' -import { TokenAmounts } from 'modules/tokens' - -import { useSingleContractMultipleData } from 'lib/hooks/multicall' - -// TODO: Move this hooks to some other module. It doens't belong with the tokens -/** - * @deprecated Use useNativeBalance instead - * Returns a map of the given addresses to their eventually consistent ETH balances. - */ -export function useNativeCurrencyBalances( - uncheckedAddresses?: (string | undefined)[], - lowerCaseAddress = false -): { - [address: string]: CurrencyAmount | undefined -} { - const { chainId } = useWalletInfo() - const multicallContract = useInterfaceMulticall() - - const validAddressInputs: [string][] = useMemo( - () => - uncheckedAddresses - ? uncheckedAddresses - .map(isAddress) - .filter((a): a is string => a !== false) - .sort() - .map((addr) => [addr]) - : [], - [uncheckedAddresses] - ) - - const results = useSingleContractMultipleData(multicallContract, 'getEthBalance', validAddressInputs) - - return useMemo( - () => - validAddressInputs.reduce<{ [address: string]: CurrencyAmount }>((memo, [address], i) => { - const value = results?.[i]?.result?.[0] - if (value && chainId) - memo[lowerCaseAddress ? address.toLowerCase() : address] = CurrencyAmount.fromRawAmount( - NATIVE_CURRENCY_BUY_TOKEN[chainId], - JSBI.BigInt(value.toString()) - ) - return memo - }, {}), - [validAddressInputs, chainId, results, lowerCaseAddress] - ) -} - -// get the balance for a single token/account combo -/** - * @deprecated Use effective balance instead - */ -export function useTokenBalance(account?: string, token?: Token): CurrencyAmount | undefined { - const tokens = useMemo(() => [token], [token]) - const tokenBalances = useOnchainBalances({ - account, - tokens, - }) - if (!token) return undefined - - return tokenBalances.amounts[token.address]?.value -} - -// get the balance for a single token/account combo -/** - * @deprecated Use effective balance instead - */ -export function useTokenBalances(account?: string, tokens?: (Token | undefined)[]): TokenAmounts { - return useOnchainBalances({ account, tokens }).amounts -} - -/** - * @deprecated Use effective balance instead - */ -export function useCurrencyBalances( - account?: string, - currencies?: (Currency | undefined | null)[] -): (CurrencyAmount | undefined)[] { - const tokens = useMemo( - () => currencies?.filter((currency): currency is Token => (currency && !getIsNativeToken(currency)) ?? false) ?? [], - [currencies] - ) - - const tokenBalances = useTokenBalances(account, tokens) - const containsETH: boolean = useMemo( - () => currencies?.some((currency) => currency && getIsNativeToken(currency)) ?? false, - [currencies] - ) - const ethBalance = useNativeCurrencyBalances(useMemo(() => (containsETH ? [account] : []), [containsETH, account])) - - return useMemo( - () => - currencies?.map((currency) => { - if (!account || !currency) return undefined - if (getIsNativeToken(currency)) return ethBalance[account] - - return tokenBalances[currency.address]?.value - }) ?? [], - [account, currencies, ethBalance, tokenBalances] - ) -} - -/** - * @deprecated Use effective balance instead - */ -export default function useCurrencyBalance( - account?: string, - currency?: Currency | null -): CurrencyAmount | undefined { - return useCurrencyBalances( - account, - useMemo(() => [currency], [currency]) - )[0] -} diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts index 170c9a44e7..6f3bd35770 100644 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts +++ b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts @@ -1,12 +1,11 @@ +import { + AllowancesState, + BalancesState, + useTokensAllowances, + useTokensBalances, +} from '@cowprotocol/balances-and-allowances' import { isEnoughAmount, getAddress, getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils' -import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' - -import useNativeCurrency from 'lib/hooks/useNativeCurrency' - -import { useBalancesAndAllowances } from './useBalancesAndAllowances' -import { useCurrencyBalances } from './useCurrencyBalance' - -import { TokenAmounts } from '../types' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' export interface UseEnoughBalanceParams { /** @@ -38,24 +37,15 @@ const DEFAULT_BALANCE_AND_ALLOWANCE = { enoughBalance: undefined, enoughAllowanc * @returns UseEnoughBalanceAndAllowanceResult */ export function useEnoughBalanceAndAllowance(params: UseEnoughBalanceParams): UseEnoughBalanceAndAllowanceResult { - const { account, amount, checkAllowanceAddress } = params - const isNativeCurrency = !!amount?.currency && getIsNativeToken(amount?.currency) - const token = amount?.currency && getWrappedToken(amount.currency) - - const { balances, allowances } = useBalancesAndAllowances({ - account, - spender: checkAllowanceAddress, - tokens: !isNativeCurrency && account && token ? [token as Token] : [], - }) + const { checkAllowanceAddress } = params - const native = useNativeCurrency() - const [nativeBalance] = useCurrencyBalances(isNativeCurrency ? account : undefined, [native]) + const { values: balances } = useTokensBalances() + const { values: allowances } = useTokensAllowances() return hasEnoughBalanceAndAllowance({ ...params, balances, allowances: checkAllowanceAddress ? allowances : undefined, - nativeBalance, }) } @@ -63,17 +53,12 @@ export interface EnoughBalanceParams extends Omit + allowances?: AllowancesState['values'] } /** @@ -82,7 +67,7 @@ export interface EnoughBalanceParams extends Omit, - balances: TokenAmounts, - isNativeCurrency: boolean, - nativeBalance: CurrencyAmount | undefined + balances: BalancesState['values'] ): boolean | undefined { - const balance = tokenAddress ? balances[tokenAddress]?.value : undefined - const balanceAmount = isNativeCurrency ? nativeBalance : balance || undefined - return isEnoughAmount(amount, balanceAmount) + const balance = tokenAddress ? balances[tokenAddress] : undefined + + return isEnoughAmount(amount, balance) } function _enoughAllowance( - tokenAddress: string | null, + tokenAddress: string | undefined, amount: CurrencyAmount, - allowances: TokenAmounts | undefined, + allowances: AllowancesState['values'] | undefined, isNativeCurrency: boolean ): boolean | undefined { if (!tokenAddress || !allowances) { @@ -122,6 +105,7 @@ function _enoughAllowance( if (isNativeCurrency) { return true } - const allowance = allowances[tokenAddress]?.value + const allowance = allowances[tokenAddress] + return allowance && isEnoughAmount(amount, allowance) } diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useOnchainBalances.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useOnchainBalances.ts deleted file mode 100644 index d754ce35da..0000000000 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useOnchainBalances.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { useMemo } from 'react' - -import { Erc20Interface, Erc20Abi } from '@cowprotocol/abis' -import { isAddress } from '@cowprotocol/common-utils' -import { Interface } from '@ethersproject/abi' -import { ListenerOptionsWithGas } from '@uniswap/redux-multicall' -import { CurrencyAmount, Token } from '@uniswap/sdk-core' - -import JSBI from 'jsbi' - -import { useMultipleContractSingleData } from 'lib/hooks/multicall' - -import { BalancesAndAllowances, BalancesAndAllowancesParams, TokenAmounts, TokenAmountsResult } from '../types' - -const ERC20Interface = new Interface(Erc20Abi) as Erc20Interface -const DEFAULT_LISTENER_OPTIONS: ListenerOptionsWithGas = { gasRequired: 185_000, blocksPerFetch: 5 } - -export interface OnchainAmountsParams { - account?: string - tokens?: (Token | undefined)[] - blocksPerFetch?: number -} - -export type OnchainBalancesParams = OnchainAmountsParams - -export type OnchainAllowancesParams = OnchainAmountsParams & { spender?: string } - -export function useOnchainBalances(params: OnchainBalancesParams): TokenAmountsResult { - const { account } = params - const callParams = useMemo(() => [account], [account]) - return useOnchainErc20Amounts('balanceOf', callParams, params) -} - -export function useOnchainAllowances(params: OnchainAllowancesParams): TokenAmountsResult { - const { account, spender } = params - const callParams = useMemo(() => [account, spender], [account, spender]) - return useOnchainErc20Amounts('allowance', callParams, params) -} - -/** - * Fetches - * @param params - * @returns - */ -export function useOnchainBalancesAndAllowances(params: BalancesAndAllowancesParams): BalancesAndAllowances { - const { account, spender, tokens, blocksPerFetchAllowance, blocksPerFetchBalance } = params - - const { amounts: balances, isLoading: areBalancesLoading } = useOnchainBalances({ - account, - tokens, - blocksPerFetch: blocksPerFetchBalance, - }) - const { amounts: allowances, isLoading: areAllowancesLoading } = useOnchainAllowances({ - account, - tokens, - spender, - blocksPerFetch: blocksPerFetchAllowance, - }) - - return useMemo( - () => ({ - balances, - allowances, - isLoading: areBalancesLoading || areAllowancesLoading, - }), - [balances, allowances, areBalancesLoading, areAllowancesLoading] - ) -} - -function useOnchainErc20Amounts( - erc20Method: 'balanceOf' | 'allowance', - callParams: (string | undefined)[], - params: OnchainAmountsParams -): TokenAmountsResult { - const { account, blocksPerFetch, tokens } = params - - const validatedTokens: Token[] = useMemo( - () => tokens?.filter((t?: Token): t is Token => isAddress(t?.address) !== false) ?? [], - [tokens] - ) - const validatedTokenAddresses = useMemo(() => validatedTokens.map((vt) => vt.address), [validatedTokens]) - - // Do on-chain calls - const balancesCallState = useMultipleContractSingleData( - validatedTokenAddresses, - ERC20Interface, - erc20Method, - callParams, - blocksPerFetch ? { ...DEFAULT_LISTENER_OPTIONS, blocksPerFetch } : DEFAULT_LISTENER_OPTIONS - ) - - const isLoading: boolean = useMemo( - () => balancesCallState.some((callState) => callState.loading), - [balancesCallState] - ) - - // Return amounts - return useMemo(() => { - if (!account || validatedTokens.length === 0 || balancesCallState.length === 0) { - return { isLoading, amounts: {} } - } - - const tokenBalances = validatedTokens.reduce((acc, token, i) => { - const { error, loading, result, syncing, valid } = balancesCallState[i] - const value = result?.[0] - const amount = value ? JSBI.BigInt(value.toString()) : null - - acc[token.address] = { - value: amount ? CurrencyAmount.fromRawAmount(token, amount) : acc[token.address]?.value, - loading, - error, - syncing, - valid, - } - return acc - }, {}) - - return { amounts: tokenBalances, isLoading } - }, [account, validatedTokens, balancesCallState, isLoading]) -} diff --git a/apps/cowswap-frontend/src/modules/tokens/index.ts b/apps/cowswap-frontend/src/modules/tokens/index.ts index 8d49046003..5af39d0bd9 100644 --- a/apps/cowswap-frontend/src/modules/tokens/index.ts +++ b/apps/cowswap-frontend/src/modules/tokens/index.ts @@ -1,9 +1,4 @@ export * from './types' -// Exported to allow to get the balance when the allowance is not needed (i.e. in token selection) -export { useOnchainBalances } from './hooks/useOnchainBalances' -export type { OnchainAllowancesParams } from './hooks/useOnchainBalances' - // Exported for all other cases, when we need the effective balance (i.e. ) -export * from './hooks/useBalancesAndAllowances' export * from './hooks/useEnoughBalance' diff --git a/apps/cowswap-frontend/src/modules/tokens/types.ts b/apps/cowswap-frontend/src/modules/tokens/types.ts index 294bad0111..1ecb15a565 100644 --- a/apps/cowswap-frontend/src/modules/tokens/types.ts +++ b/apps/cowswap-frontend/src/modules/tokens/types.ts @@ -1,3 +1,4 @@ +import { AllowancesState, BalancesState } from '@cowprotocol/balances-and-allowances' import { CurrencyAmount, Token } from '@uniswap/sdk-core' export interface OnchainState { @@ -47,7 +48,7 @@ export interface BalancesAndAllowancesParams { } export interface BalancesAndAllowances { - balances: TokenAmounts - allowances: TokenAmounts + balances: BalancesState['values'] + allowances: AllowancesState['values'] isLoading: boolean } diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index 687c0214cc..fc132d6b74 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -2,6 +2,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useCallback, useState } from 'react' import { addListAnalytics } from '@cowprotocol/analytics' +import { useTokensBalances } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { ListState, @@ -20,7 +21,6 @@ import { usePermitCompatibleTokens } from 'modules/permit' import { CowModal } from 'common/pure/Modal' -import { useAllTokensBalances } from '../../hooks/useAllTokensBalances' import { ImportListModal } from '../../pure/ImportListModal' import { ImportTokenModal } from '../../pure/ImportTokenModal' import { SelectTokenModal } from '../../pure/SelectTokenModal' @@ -49,7 +49,7 @@ export function SelectTokenWidget() { const favouriteTokens = useFavouriteTokens() const userAddedTokens = useUserAddedTokens() const allTokenLists = useAllListsList() - const [balances, balancesLoading] = useAllTokensBalances() + const balancesState = useTokensBalances() const unsupportedTokens = useUnsupportedTokens() const permitCompatibleTokens = usePermitCompatibleTokens() @@ -131,9 +131,8 @@ export function SelectTokenWidget() { selectedToken={selectedToken} allTokens={allTokens} favouriteTokens={favouriteTokens} - balances={balances} + balancesState={balancesState} permitCompatibleTokens={permitCompatibleTokens} - balancesLoading={balancesLoading} onSelectToken={onSelectToken} onInputPressEnter={onInputPressEnter} onDismiss={onDismiss} diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx index c976379c1d..2a24a5bd73 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/TokenSearchResults/index.tsx @@ -25,13 +25,14 @@ export interface TokenSearchResultsProps extends SelectTokenContext { export function TokenSearchResults({ searchInput, - balances, + balancesState, selectedToken, onSelectToken, unsupportedTokens, permitCompatibleTokens, }: TokenSearchResultsProps) { const searchResults = useSearchToken(searchInput) + const { values: balances } = balancesState const { inactiveListsResult, blockchainResult, activeListsResult, externalApiResult, isLoading } = searchResults @@ -69,6 +70,8 @@ export function TokenSearchResults({ return [matched, remaining] }, [activeListsResult, searchInput]) + const matchedTokenAddress = matchedToken?.address.toLowerCase() + // On press Enter, select first token if only one token is found or it's fully matches to the search input const onInputPressEnter = useCallback(() => { if (!searchInput || !activeListsResult) return @@ -99,14 +102,14 @@ export function TokenSearchResults({ return ( <> {/*Exact match*/} - {matchedToken && ( + {matchedToken && matchedTokenAddress && ( )} {/*Tokens from active lists*/} @@ -121,7 +124,7 @@ export function TokenSearchResults({ isPermitCompatible={permitCompatibleTokens[addressLowerCase]} selectedToken={selectedToken} token={token} - balance={balances ? balances[token.address]?.value : undefined} + balance={balances ? balances[token.address.toLowerCase()] : undefined} onSelectToken={onSelectToken} /> ) diff --git a/apps/cowswap-frontend/src/modules/tokensList/hooks/useAllTokensBalances.ts b/apps/cowswap-frontend/src/modules/tokensList/hooks/useAllTokensBalances.ts deleted file mode 100644 index 8f088580df..0000000000 --- a/apps/cowswap-frontend/src/modules/tokensList/hooks/useAllTokensBalances.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useMemo } from 'react' - -import { TokenWithLogo } from '@cowprotocol/common-const' -import { useAllTokens } from '@cowprotocol/tokens' -import { useWalletInfo } from '@cowprotocol/wallet' -import { CurrencyAmount } from '@uniswap/sdk-core' - -import { OnchainState, TokenAmounts, useOnchainBalances } from 'modules/tokens' - -import { useNativeBalance } from 'common/hooks/useNativeBalance' - -const defaultBalancesState: [TokenAmounts, boolean] = [{}, false] - -/** - * Returns balances for all tokens + native token - */ -export function useAllTokensBalances(): [TokenAmounts, boolean] { - const { account } = useWalletInfo() - const allTokens = useAllTokens() - const nativeBalance = useNativeBalance() - const { amounts: onChainBalances, isLoading } = useOnchainBalances({ account, tokens: allTokens }) - - const balances = useMemo(() => { - if (!account || !onChainBalances) return null - - if (!nativeBalance.data) return onChainBalances - - const { data, error } = nativeBalance - - const nativeOnChainState: OnchainState> = { - value: data, - loading: isLoading, - error, - syncing: false, - valid: true, - } - - return { ...onChainBalances, [data.currency.address]: nativeOnChainState } - }, [account, onChainBalances, nativeBalance, isLoading]) - - if (!balances) return defaultBalancesState - - return [balances, isLoading] -} diff --git a/apps/cowswap-frontend/src/modules/tokensList/index.ts b/apps/cowswap-frontend/src/modules/tokensList/index.ts index a08992407a..56c8947a7a 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/index.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/index.ts @@ -1,4 +1,3 @@ export { SelectTokenWidget } from './containers/SelectTokenWidget' export { AutoImportTokens } from './containers/AutoImportTokens' export { useOpenTokenSelectWidget } from './hooks/useOpenTokenSelectWidget' -export { useAllTokensBalances } from './hooks/useAllTokensBalances' diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx index 8d4b48cdc4..39a27a876e 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx @@ -1,10 +1,9 @@ +import { BalancesState } from '@cowprotocol/balances-and-allowances' import { getRandomInt } from '@cowprotocol/common-utils' -import { CurrencyAmount } from '@uniswap/sdk-core' +import { BigNumber } from '@ethersproject/bignumber' import styled from 'styled-components/macro' -import { TokenAmounts } from 'modules/tokens' - import { allTokensMock, favouriteTokensMock } from '../../mocks' import { SelectTokenModal, SelectTokenModalProps } from './index' @@ -20,14 +19,8 @@ const unsupportedTokens = {} const selectedToken = favouriteTokensMock[0].address -const balances = allTokensMock.reduce((acc, token) => { - acc[token.address] = { - value: CurrencyAmount.fromRawAmount(token, getRandomInt(20_000, 120_000_000) * 10 ** token.decimals), - loading: false, - syncing: false, - valid: true, - error: false, - } +const balances = allTokensMock.reduce((acc, token) => { + acc[token.address] = BigNumber.from(getRandomInt(20_000, 120_000_000) * 10 ** token.decimals) return acc }, {}) @@ -37,8 +30,10 @@ const defaultProps: SelectTokenModalProps = { unsupportedTokens, allTokens: allTokensMock, favouriteTokens: favouriteTokensMock, - balances, - balancesLoading: false, + balancesState: { + values: balances, + isLoading: false, + }, selectedToken, onSelectToken() { console.log('onSelectToken') diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index b095e55709..06b4ee0162 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' +import { BalancesState } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { UnsupportedTokensState } from '@cowprotocol/tokens' @@ -9,7 +10,6 @@ import { PermitCompatibleTokens } from 'modules/permit' import * as styledEl from './styled' -import { TokenAmounts } from '../../../tokens' import { TokenSearchResults } from '../../containers/TokenSearchResults' import { SelectTokenContext } from '../../types' import { IconButton } from '../commonElements' @@ -19,9 +19,8 @@ import { TokensVirtualList } from '../TokensVirtualList' export interface SelectTokenModalProps { allTokens: TokenWithLogo[] favouriteTokens: TokenWithLogo[] - balances: TokenAmounts + balancesState: BalancesState unsupportedTokens: UnsupportedTokensState - balancesLoading: boolean selectedToken?: string permitCompatibleTokens: PermitCompatibleTokens onSelectToken(token: TokenWithLogo): void @@ -37,8 +36,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) { favouriteTokens, allTokens, selectedToken, - balances, - balancesLoading, + balancesState, unsupportedTokens, permitCompatibleTokens, onSelectToken, @@ -50,8 +48,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) { const [inputValue, setInputValue] = useState(defaultInputValue) const selectTokenContext: SelectTokenContext = { - balances, - balancesLoading, + balancesState, selectedToken, onSelectToken, unsupportedTokens, 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 2d75c25100..d8b859e795 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx @@ -1,6 +1,7 @@ import { TokenWithLogo } from '@cowprotocol/common-const' import { TokenAmount } from '@cowprotocol/ui' -import { CurrencyAmount, Token } from '@uniswap/sdk-core' +import { BigNumber } from '@ethersproject/bignumber' +import { CurrencyAmount } from '@uniswap/sdk-core' import * as styledEl from './styled' @@ -12,7 +13,7 @@ import type { VirtualItem } from '@tanstack/react-virtual' export interface TokenListItemProps { token: TokenWithLogo selectedToken?: string - balance: CurrencyAmount | undefined + balance: BigNumber | undefined onSelectToken(token: TokenWithLogo): void virtualRow?: VirtualItem isUnsupported: boolean @@ -24,6 +25,8 @@ export function TokenListItem(props: TokenListItemProps) { const isTokenSelected = token.address.toLowerCase() === selectedToken?.toLowerCase() + const balanceAmount = balance ? CurrencyAmount.fromRawAmount(token, balance.toHexString()) : undefined + return ( - - - + {balanceAmount && } ) } diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index e4b8c7c132..14f46e9e0a 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -28,15 +28,9 @@ export interface TokensVirtualListProps extends SelectTokenContext { } export function TokensVirtualList(props: TokensVirtualListProps) { - const { - allTokens, - selectedToken, - balances, - onSelectToken, - unsupportedTokens, - permitCompatibleTokens, - balancesLoading, - } = props + const { allTokens, selectedToken, balancesState, onSelectToken, unsupportedTokens, permitCompatibleTokens } = props + const { values: balances, isLoading: balancesLoading } = balancesState + const scrollTimeoutRef = useRef() const parentRef = useRef(null) const wrapperRef = useRef(null) @@ -71,9 +65,9 @@ export function TokensVirtualList(props: TokensVirtualListProps) { {getVirtualItems().map((virtualRow) => { const token = sortedTokens[virtualRow.index] const addressLowerCase = token.address.toLowerCase() - const balance = balances ? balances[token.address] : null + const balance = balances ? balances[token.address.toLowerCase()] : undefined - if (balance?.loading || balancesLoading) { + if (balancesLoading) { return {threeDivs()} } @@ -85,7 +79,7 @@ export function TokensVirtualList(props: TokensVirtualListProps) { isUnsupported={!!unsupportedTokens[addressLowerCase]} isPermitCompatible={permitCompatibleTokens[addressLowerCase]} selectedToken={selectedToken} - balance={balance?.value} + balance={balance} onSelectToken={onSelectToken} /> ) diff --git a/apps/cowswap-frontend/src/modules/tokensList/types.ts b/apps/cowswap-frontend/src/modules/tokensList/types.ts index fce0f34410..c0afa14d3b 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/types.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/types.ts @@ -1,11 +1,10 @@ +import { BalancesState } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { PermitCompatibleTokens } from 'modules/permit' -import { TokenAmounts } from 'modules/tokens' export interface SelectTokenContext { - balances: TokenAmounts | null - balancesLoading: boolean + balancesState: BalancesState selectedToken?: string onSelectToken(token: TokenWithLogo): void unsupportedTokens: { [tokenAddress: string]: { dateAdded: number } } diff --git a/apps/cowswap-frontend/src/modules/tokensList/utils/tokensListSorter.ts b/apps/cowswap-frontend/src/modules/tokensList/utils/tokensListSorter.ts index fe689c9e11..abe65adedd 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/utils/tokensListSorter.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/utils/tokensListSorter.ts @@ -1,12 +1,11 @@ +import { BalancesState } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { getIsNativeToken } from '@cowprotocol/common-utils' -import { TokenAmounts } from 'modules/tokens' - -export function tokensListSorter(balances: TokenAmounts): (a: TokenWithLogo, b: TokenWithLogo) => number { +export function tokensListSorter(balances: BalancesState['values']): (a: TokenWithLogo, b: TokenWithLogo) => number { return (a: TokenWithLogo, b: TokenWithLogo) => { - const aBalance = balances[a.address] - const bBalance = balances[b.address] + const aBalance = balances[a.address.toLowerCase()] + const bBalance = balances[b.address.toLowerCase()] const aIsNative = getIsNativeToken(a) const bIsNative = getIsNativeToken(b) @@ -15,8 +14,8 @@ export function tokensListSorter(balances: TokenAmounts): (a: TokenWithLogo, b: return aIsNative ? -1 : 1 } - if (aBalance?.value && bBalance?.value) { - return +bBalance.value.toExact() - +aBalance.value.toExact() + if (aBalance && bBalance) { + return +bBalance.sub(aBalance) } return 0 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/useBuildTradeDerivedState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts index 641be06795..94181f94aa 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts @@ -1,17 +1,15 @@ import { Atom, useAtomValue } from 'jotai' +import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { tryParseFractionalAmount } from '@cowprotocol/common-utils' import { useTokenBySymbolOrAddress } from '@cowprotocol/tokens' -import { useWalletInfo } from '@cowprotocol/wallet' -import useCurrencyBalance from 'modules/tokens/hooks/useCurrencyBalance' import { ExtendedTradeRawState } from 'modules/trade/types/TradeRawState' import { useTradeUsdAmounts } from 'modules/usdAmount' import { useSafeMemoObject } from 'common/hooks/useSafeMemo' export function useBuildTradeDerivedState(stateAtom: Atom) { - const { account } = useWalletInfo() const rawState = useAtomValue(stateAtom) const recipient = rawState.recipient @@ -22,8 +20,9 @@ export function useBuildTradeDerivedState(stateAtom: Atom const outputCurrency = useTokenBySymbolOrAddress(rawState.outputCurrencyId) const inputCurrencyAmount = tryParseFractionalAmount(inputCurrency, rawState.inputCurrencyAmount) const outputCurrencyAmount = tryParseFractionalAmount(outputCurrency, rawState.outputCurrencyAmount) - const inputCurrencyBalance = useCurrencyBalance(account, inputCurrency) || null - const outputCurrencyBalance = useCurrencyBalance(account, outputCurrency) || null + + const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency) || null + const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency) || null const { inputAmount: { value: inputCurrencyFiatAmount }, 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/trade/hooks/useWrappedToken.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useWrappedToken.ts index 3a3b9bc84a..d3811a6c7b 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useWrappedToken.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useWrappedToken.ts @@ -1,8 +1,8 @@ +import { TokenWithLogo } from '@cowprotocol/common-const' import { getWrappedToken } from '@cowprotocol/common-utils' -import { Token } from '@uniswap/sdk-core' import useNativeCurrency from 'lib/hooks/useNativeCurrency' -export function useWrappedToken(): Token { +export function useWrappedToken(): TokenWithLogo { return getWrappedToken(useNativeCurrency()) } diff --git a/apps/cowswap-frontend/src/modules/trade/state/priceImpactAtom.ts b/apps/cowswap-frontend/src/modules/trade/state/priceImpactAtom.ts index d2a1cff63a..9f71f120f7 100644 --- a/apps/cowswap-frontend/src/modules/trade/state/priceImpactAtom.ts +++ b/apps/cowswap-frontend/src/modules/trade/state/priceImpactAtom.ts @@ -4,5 +4,7 @@ import { PriceImpact } from 'legacy/hooks/usePriceImpact' export const priceImpactAtom = atom({ priceImpact: undefined, - loading: false, + // Consider is loading by default to avoid flickering + // PriceImpactUpdater will set it to false anyway + loading: true, }) diff --git a/apps/cowswap-frontend/src/modules/trade/updaters/PriceImpactUpdater.tsx b/apps/cowswap-frontend/src/modules/trade/updaters/PriceImpactUpdater.tsx index fe780c1127..94415bf795 100644 --- a/apps/cowswap-frontend/src/modules/trade/updaters/PriceImpactUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/trade/updaters/PriceImpactUpdater.tsx @@ -8,11 +8,15 @@ import { priceImpactAtom } from '../state/priceImpactAtom' export function PriceImpactUpdater() { const updatePriceImpact = useSetAtom(priceImpactAtom) - const { isLoading, priceImpact } = useFiatValuePriceImpact() + const priceImpactState = useFiatValuePriceImpact() useSafeEffect(() => { + if (!priceImpactState) return + + const { isLoading, priceImpact } = priceImpactState + updatePriceImpact({ loading: isLoading, priceImpact }) - }, [isLoading, updatePriceImpact, priceImpact]) + }, [updatePriceImpact, priceImpactState]) return null } diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts index 003bd0cc33..f17a7d0daf 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/hooks/useTradeFormValidationContext.ts @@ -5,12 +5,12 @@ import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { useGnosisSafeInfo, useIsBundlingSupported, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { isUnsupportedTokenInQuote } from 'modules/limitOrders/utils/isUnsupportedTokenInQuote' -import { useIsTokenPermittable } from 'modules/permit' +import { useTokenSupportsPermit } from 'modules/permit' 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) @@ -32,7 +32,7 @@ export function useTradeFormValidationContext(): TradeFormValidationCommonContex const isSafeReadonlyUser = gnosisSafeInfo?.isReadOnly || false - const isPermitSupported = !!useIsTokenPermittable(inputCurrency, tradeType) + const isPermitSupported = useTokenSupportsPermit(inputCurrency, tradeType) const commonContext = { account, diff --git a/apps/cowswap-frontend/src/modules/twap/hooks/useTwapOrdersAuthMulticall.ts b/apps/cowswap-frontend/src/modules/twap/hooks/useTwapOrdersAuthMulticall.ts index 4f39c7570a..01e93f2813 100644 --- a/apps/cowswap-frontend/src/modules/twap/hooks/useTwapOrdersAuthMulticall.ts +++ b/apps/cowswap-frontend/src/modules/twap/hooks/useTwapOrdersAuthMulticall.ts @@ -1,13 +1,14 @@ import { useMemo } from 'react' import { ComposableCoW } from '@cowprotocol/abis' -import { ListenerOptionsWithGas } from '@uniswap/redux-multicall' +import { useSingleContractMultipleData } from '@cowprotocol/multicall' -import { useSingleContractMultipleData } from 'lib/hooks/multicall' +import ms from 'ms.macro' import { TwapOrderInfo, TwapOrdersAuthResult } from '../types' -const DEFAULT_LISTENER_OPTIONS: ListenerOptionsWithGas = { gasRequired: 185_000, blocksPerFetch: 5 } +const MULTICALL_OPTIONS = {} +const SWR_CONFIG = { refreshInterval: ms`30s` } export function useTwapOrdersAuthMulticall( safeAddress: string, @@ -18,15 +19,21 @@ export function useTwapOrdersAuthMulticall( return ordersInfo.map(({ id }) => [safeAddress, id]) }, [safeAddress, ordersInfo]) - const results = useSingleContractMultipleData(composableCowContract, 'singleOrders', input, DEFAULT_LISTENER_OPTIONS) + const results = useSingleContractMultipleData( + composableCowContract, + 'singleOrders', + input, + MULTICALL_OPTIONS, + SWR_CONFIG + ) return useMemo(() => { - const loadedResults = results.filter((result) => !result.loading && result.valid) + const loadedResults = results.data - if (loadedResults.length !== ordersInfo.length) return null + if (results.isLoading || !loadedResults || loadedResults.length !== ordersInfo.length) return null return ordersInfo.reduce((acc, val, index) => { - acc[val.id] = loadedResults[index].result?.[0] + acc[val.id] = loadedResults[index] return acc }, {} as TwapOrdersAuthResult) }, [ordersInfo, results]) diff --git a/apps/cowswap-frontend/src/modules/twap/hooks/useTwapOrdersTradeableMulticall.ts b/apps/cowswap-frontend/src/modules/twap/hooks/useTwapOrdersTradeableMulticall.ts deleted file mode 100644 index 458b109c44..0000000000 --- a/apps/cowswap-frontend/src/modules/twap/hooks/useTwapOrdersTradeableMulticall.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useMemo } from 'react' - -import { ComposableCoW, GPv2Order } from '@cowprotocol/abis' -import { ListenerOptionsWithGas } from '@uniswap/redux-multicall' - -import { useSingleContractMultipleData } from 'lib/hooks/multicall' - -import { ConditionalOrderParams } from '../types' - -const DEFAULT_LISTENER_OPTIONS: ListenerOptionsWithGas = { gasRequired: 185_000, blocksPerFetch: 5 } - -export type TradeableOrderWithSignature = - | { - order: GPv2Order.DataStructOutput - signature: string - } - | undefined - -export function useTwapOrdersTradeableMulticall( - safeAddress: string, - composableCowContract: ComposableCoW, - conditionalOrderParams: ConditionalOrderParams[] -): TradeableOrderWithSignature[] { - const input = useMemo(() => { - return conditionalOrderParams.map((params) => { - return [safeAddress, [params.handler, params.salt, params.staticInput], '0x', []] - }) - }, [safeAddress, conditionalOrderParams]) - - const results = useSingleContractMultipleData( - composableCowContract, - 'getTradeableOrderWithSignature', - input, - DEFAULT_LISTENER_OPTIONS - ) - - return useMemo(() => { - return results.filter((result) => !result.loading).map((res) => res.result as TradeableOrderWithSignature) - }, [results]) -} diff --git a/apps/cowswap-frontend/src/modules/usdAmount/hooks/useTradeUsdAmounts.ts b/apps/cowswap-frontend/src/modules/usdAmount/hooks/useTradeUsdAmounts.ts index 00e56dbf84..94084c5270 100644 --- a/apps/cowswap-frontend/src/modules/usdAmount/hooks/useTradeUsdAmounts.ts +++ b/apps/cowswap-frontend/src/modules/usdAmount/hooks/useTradeUsdAmounts.ts @@ -1,3 +1,4 @@ +import { TokenWithLogo } from '@cowprotocol/common-const' import { isFractionFalsy } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' @@ -12,15 +13,25 @@ export interface TradeUSDAmounts { outputAmount: UsdAmountInfo } +/** + * Returns USD amounts for trade input and output + * USD amount needs two things to be calculated: + * - Token amount + * - Token price in USD + * If you want to start loading token prices in USD before token amount is ready, you can pass inputCurrency and outputCurrency + * One case when we need it is price impact calculation + */ export function useTradeUsdAmounts( inputAmount: Nullish>, - outputAmount: Nullish> + outputAmount: Nullish>, + inputCurrency?: Nullish, + outputCurrency?: Nullish ): TradeUSDAmounts { const isWrapOrUnwrap = useIsWrapOrUnwrap() const isTradeReady = !isWrapOrUnwrap && !isFractionFalsy(inputAmount) && !isFractionFalsy(outputAmount) return { - inputAmount: useUsdAmount(isTradeReady ? inputAmount : null), - outputAmount: useUsdAmount(isTradeReady ? outputAmount : null), + inputAmount: useUsdAmount(isTradeReady ? inputAmount : null, inputCurrency), + outputAmount: useUsdAmount(isTradeReady ? outputAmount : null, outputCurrency), } } diff --git a/apps/cowswap-frontend/src/modules/usdAmount/hooks/useUsdAmount.ts b/apps/cowswap-frontend/src/modules/usdAmount/hooks/useUsdAmount.ts index 13a29fee0f..a7db638ac6 100644 --- a/apps/cowswap-frontend/src/modules/usdAmount/hooks/useUsdAmount.ts +++ b/apps/cowswap-frontend/src/modules/usdAmount/hooks/useUsdAmount.ts @@ -1,3 +1,4 @@ +import { TokenWithLogo } from '@cowprotocol/common-const' import { currencyAmountToTokenAmount } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' @@ -14,18 +15,21 @@ export interface UsdAmountInfo { const DEFAULT_USD_AMOUNT_STATE = { value: null, isLoading: false } -export function useUsdAmount(_amount: Nullish>): UsdAmountInfo { - const amount = currencyAmountToTokenAmount(_amount) - const usdcPrice = useUsdPrice(amount?.currency) +export function useUsdAmount( + _amount: Nullish>, + currency?: Nullish +): UsdAmountInfo { + const amount = useSafeMemo(() => currencyAmountToTokenAmount(_amount), [_amount]) + const usdcPrice = useUsdPrice(amount?.currency || currency) return useSafeMemo(() => { - if (!usdcPrice || !amount) return DEFAULT_USD_AMOUNT_STATE + if (!usdcPrice) return DEFAULT_USD_AMOUNT_STATE const { price, isLoading } = usdcPrice return { - value: price === null ? null : price.quote(amount), + value: price === null || !amount ? null : price.quote(amount), isLoading, } - }, [usdcPrice, amount]) + }, [usdcPrice, amount, currency]) } diff --git a/apps/cowswap-frontend/src/modules/usdAmount/state/usdRawPricesAtom.ts b/apps/cowswap-frontend/src/modules/usdAmount/state/usdRawPricesAtom.ts index af2afd8f02..e261d86fc4 100644 --- a/apps/cowswap-frontend/src/modules/usdAmount/state/usdRawPricesAtom.ts +++ b/apps/cowswap-frontend/src/modules/usdAmount/state/usdRawPricesAtom.ts @@ -5,6 +5,7 @@ import { Fraction, Token } from '@uniswap/sdk-core' export interface UsdRawPriceState { updatedAt?: number + // When we couldn't load the price for any reaason (http error, invalid value, etc.), we set it to null price: Fraction | null currency: Token isLoading: boolean diff --git a/apps/cowswap-frontend/src/modules/usdAmount/updaters/UsdPricesUpdater.ts b/apps/cowswap-frontend/src/modules/usdAmount/updaters/UsdPricesUpdater.ts index 52e4d2a657..0d26b01183 100644 --- a/apps/cowswap-frontend/src/modules/usdAmount/updaters/UsdPricesUpdater.ts +++ b/apps/cowswap-frontend/src/modules/usdAmount/updaters/UsdPricesUpdater.ts @@ -2,7 +2,6 @@ import { useAtomValue, useSetAtom } from 'jotai' import { useEffect, useMemo } from 'react' import { USDC } from '@cowprotocol/common-const' -import { useDebounce } from '@cowprotocol/common-hooks' import { FractionUtils } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' @@ -28,8 +27,6 @@ const swrOptions: SWRConfiguration = { revalidateOnFocus: true, } -const USD_PRICES_QUEUE_DEBOUNCE_TIME = ms`0.5s` - export function UsdPricesUpdater() { const { chainId } = useWalletInfo() const setUsdPrices = useSetAtom(usdRawPricesAtom) @@ -38,16 +35,14 @@ export function UsdPricesUpdater() { const queue = useMemo(() => Object.values(currenciesUsdPriceQueue), [currenciesUsdPriceQueue]) - const debouncedQueue = useDebounce(queue, USD_PRICES_QUEUE_DEBOUNCE_TIME) - const swrResponse = useSWR( - ['UsdPricesUpdater', debouncedQueue, chainId], + ['UsdPricesUpdater', queue, chainId], () => { const getUsdcPrice = usdcPriceLoader(chainId) - setUsdPricesLoading(debouncedQueue) + setUsdPricesLoading(queue) - return processQueue(debouncedQueue, getUsdcPrice) + return processQueue(queue, getUsdcPrice) }, swrOptions ) diff --git a/apps/cowswap-frontend/src/modules/wallet/containers/AccountSelectorModal/index.tsx b/apps/cowswap-frontend/src/modules/wallet/containers/AccountSelectorModal/index.tsx index f344109ca0..2a8f0f7a1d 100644 --- a/apps/cowswap-frontend/src/modules/wallet/containers/AccountSelectorModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/wallet/containers/AccountSelectorModal/index.tsx @@ -1,6 +1,8 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai' import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useNativeTokensBalances } from '@cowprotocol/balances-and-allowances' +import { NATIVE_CURRENCY_BUY_TOKEN } from '@cowprotocol/common-const' import { useAddSnackbar } from '@cowprotocol/snackbars' import { accountsLoaders, @@ -10,19 +12,22 @@ import { AccountIndexSelect, HardWareWallet, getWeb3ReactConnection, + useWalletInfo, } from '@cowprotocol/wallet' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' import { Trans } from '@lingui/macro' -import { useNativeCurrencyBalances } from 'modules/tokens/hooks/useCurrencyBalance' - import { CowModal } from 'common/pure/Modal' import { accountSelectorModalAtom, toggleAccountSelectorModalAtom } from './state' import * as styledEl from './styled' +const EMPTY_BALANCES = {} + export function AccountSelectorModal() { + const { chainId } = useWalletInfo() const { isOpen } = useAtomValue(accountSelectorModalAtom) const closeModal = useSetAtom(toggleAccountSelectorModalAtom) @@ -30,6 +35,8 @@ export function AccountSelectorModal() { const { connector } = useWeb3React() const addSnackbar = useAddSnackbar() + const nativeToken = NATIVE_CURRENCY_BUY_TOKEN[chainId] + const connectionType = useMemo(() => getWeb3ReactConnection(connector).type, [connector]) const walletIcon = useMemo(() => getConnectionIcon(connectionType), [connectionType]) @@ -39,7 +46,23 @@ export function AccountSelectorModal() { const [accountsList, setAccountsList] = useState(null) - const balances = useNativeCurrencyBalances(accountsList || undefined, true) + const nativeTokensBalances = useNativeTokensBalances(accountsList || undefined) + + const balances = useMemo(() => { + if (!nativeTokensBalances) return EMPTY_BALANCES + + return Object.keys(nativeTokensBalances).reduce<{ [account: string]: CurrencyAmount | undefined }>( + (acc, key) => { + const balance = nativeTokensBalances[key] + + if (balance) { + acc[key] = CurrencyAmount.fromRawAmount(nativeToken, balance.toString()) + } + return acc + }, + {} + ) + }, [nativeTokensBalances, nativeToken]) const loadMoreAccounts = useCallback(async () => { if (!accountsLoader) return diff --git a/apps/cowswap-frontend/src/pages/Account/Balances.tsx b/apps/cowswap-frontend/src/pages/Account/Balances.tsx index 63b4991a21..b19da05a81 100644 --- a/apps/cowswap-frontend/src/pages/Account/Balances.tsx +++ b/apps/cowswap-frontend/src/pages/Account/Balances.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import ArrowIcon from '@cowprotocol/assets/cow-swap/arrow.svg' import CowImage from '@cowprotocol/assets/cow-swap/cow_v2.svg' import vCOWImage from '@cowprotocol/assets/cow-swap/vCOW.png' +import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { COW, COW_CONTRACT_ADDRESS, V_COW, V_COW_CONTRACT_ADDRESS } from '@cowprotocol/common-const' import { usePrevious } from '@cowprotocol/common-hooks' import { useBlockNumber } from '@cowprotocol/common-hooks' @@ -25,8 +26,6 @@ import { SwapVCowStatus } from 'legacy/state/cowToken/actions' import { useVCowData, useSwapVCowCallback, useSetSwapVCowStatus, useSwapVCowStatus } from 'legacy/state/cowToken/hooks' import { ConfirmOperationType } from 'legacy/state/types' -import { useTokenBalance } from 'modules/tokens/hooks/useCurrencyBalance' - import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' import { HelpCircle } from 'common/pure/HelpCircle' import { useCowFromLockedGnoBalances } from 'pages/Account/LockedGnoVesting/hooks' @@ -68,8 +67,7 @@ export default function Profile() { const cowToken = COW[chainId] const vCowToken = V_COW[chainId] // Cow balance - const cow = - useTokenBalance(account || undefined, chainId ? cowToken : undefined) || CurrencyAmount.fromRawAmount(cowToken, 0) + const cow = useCurrencyAmountBalance(chainId ? cowToken : undefined) || CurrencyAmount.fromRawAmount(cowToken, 0) // vCow balance values const { unvested, vested, total, isLoading: isVCowLoading } = useVCowData() diff --git a/apps/cowswap-frontend/src/pages/Account/LockedGnoVesting/hooks.ts b/apps/cowswap-frontend/src/pages/Account/LockedGnoVesting/hooks.ts index c0810359fb..f0a97c07c7 100644 --- a/apps/cowswap-frontend/src/pages/Account/LockedGnoVesting/hooks.ts +++ b/apps/cowswap-frontend/src/pages/Account/LockedGnoVesting/hooks.ts @@ -14,11 +14,11 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { ContractTransaction } from '@ethersproject/contracts' import { CurrencyAmount, Token } from '@uniswap/sdk-core' +import useSWR from 'swr' + import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks' import { ConfirmOperationType } from 'legacy/state/types' -import { useSingleCallResult } from 'lib/hooks/multicall' - import { fetchClaim } from './claimData' // We just generally use the mainnet version. We don't read from the contract anyways so the address doesn't matter @@ -59,16 +59,22 @@ export const useCowFromLockedGnoBalances = () => { .divide(LOCKED_GNO_VESTING_DURATION) const tokenDistro = useTokenDistroContract() - const { result, loading } = useSingleCallResult(allocated.greaterThan(0) ? tokenDistro : null, 'balances', [ - account ?? undefined, - ]) - const claimed = useMemo(() => CurrencyAmount.fromRawAmount(_COW, result ? result.claimed.toString() : 0), [result]) + + const { data, isLoading } = useSWR(['useCowFromLockedGnoBalances', account, allocated, tokenDistro], async () => { + if (account && tokenDistro && allocated.greaterThan(0)) { + return tokenDistro.balances(account) + } + + return null + }) + + const claimed = useMemo(() => CurrencyAmount.fromRawAmount(_COW, data ? data.claimed.toString() : 0), [data]) return { allocated, vested, claimed, - loading, + loading: isLoading, } } diff --git a/apps/cowswap-frontend/src/pages/Account/Tokens/TokensOverview.tsx b/apps/cowswap-frontend/src/pages/Account/Tokens/TokensOverview.tsx index 5d4b04e347..c6bf4c59aa 100644 --- a/apps/cowswap-frontend/src/pages/Account/Tokens/TokensOverview.tsx +++ b/apps/cowswap-frontend/src/pages/Account/Tokens/TokensOverview.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState, useCallback, useRef, ChangeEventHandler } from 'react' +import { useTokensBalances } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { useDebounce, useOnClickOutside, usePrevious, useTheme } from '@cowprotocol/common-hooks' import { isAddress, isTruthy } from '@cowprotocol/common-utils' @@ -14,7 +15,6 @@ import TokensTable from 'legacy/components/Tokens/TokensTable' import { CloseIcon } from 'legacy/theme' import { PageTitle } from 'modules/application/containers/PageTitle' -import { useAllTokensBalances } from 'modules/tokensList' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' @@ -68,7 +68,7 @@ export default function TokensOverview() { const theme = useTheme() const allTokens = useTokensByAddressMap() const favouriteTokens = useFavouriteTokens() - const balances = useAllTokensBalances() + const { values: balances } = useTokensBalances() // search - takes precedence re:filtering const [query, setQuery] = useState('') diff --git a/apps/cowswap-frontend/src/pages/Claim/ClaimSummary.tsx b/apps/cowswap-frontend/src/pages/Claim/ClaimSummary.tsx index e0590b6155..0d2986be4b 100644 --- a/apps/cowswap-frontend/src/pages/Claim/ClaimSummary.tsx +++ b/apps/cowswap-frontend/src/pages/Claim/ClaimSummary.tsx @@ -1,3 +1,4 @@ +import { useTokenBalanceForAccount } from '@cowprotocol/balances-and-allowances' import { V_COW } from '@cowprotocol/common-const' import { TokenAmount } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' @@ -11,8 +12,6 @@ import { ClaimStatus } from 'legacy/state/claim/actions' import { useClaimState } from 'legacy/state/claim/hooks' import { ClaimCommonTypes } from 'legacy/state/claim/types' -import { useTokenBalance } from 'modules/tokens/hooks/useCurrencyBalance' - import { ClaimSummary as ClaimSummaryWrapper, ClaimSummaryTitle, ClaimTotal } from './styled' type ClaimSummaryProps = Pick & { @@ -25,7 +24,7 @@ export function ClaimSummary({ hasClaims, isClaimed, unclaimedAmount }: ClaimSum const vCow = chainId ? V_COW[chainId] : undefined - const vCowBalance = useTokenBalance(activeClaimAccount || undefined, vCow) + const { data: vCowBalance } = useTokenBalanceForAccount(vCow, activeClaimAccount || undefined) const hasClaimSummary = claimStatus === ClaimStatus.DEFAULT && !isInvestFlowActive @@ -36,7 +35,7 @@ export function ClaimSummary({ hasClaims, isClaimed, unclaimedAmount }: ClaimSum if (hasClaims && activeClaimAccount && unclaimedAmount) { totalAvailableAmount = unclaimedAmount } else if (isClaimed) { - totalAvailableAmount = vCowBalance + totalAvailableAmount = vCowBalance ? CurrencyAmount.fromRawAmount(vCow, vCowBalance.toHexString()) : undefined } return ( diff --git a/apps/cowswap-frontend/src/pages/Claim/InvestmentFlow/InvestOption.tsx b/apps/cowswap-frontend/src/pages/Claim/InvestmentFlow/InvestOption.tsx index 31100f06f6..f87547303e 100644 --- a/apps/cowswap-frontend/src/pages/Claim/InvestmentFlow/InvestOption.tsx +++ b/apps/cowswap-frontend/src/pages/Claim/InvestmentFlow/InvestOption.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import CheckCircle from '@cowprotocol/assets/cow-swap/check.svg' import ImportantIcon from '@cowprotocol/assets/cow-swap/important.svg' -import { AVG_APPROVE_COST_GWEI, ONE_HUNDRED_PERCENT } from '@cowprotocol/common-const' +import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' +import { AVG_APPROVE_COST_GWEI, ONE_HUNDRED_PERCENT, TokenWithLogo } from '@cowprotocol/common-const' import { calculateGasMargin, getProviderErrorMessage, @@ -14,7 +15,7 @@ import { import { Loader, loadingOpacityMixin, ButtonSize, TokenAmount, ButtonConfirmed, Row } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' import { BigNumber } from '@ethersproject/bignumber' -import { CurrencyAmount } from '@uniswap/sdk-core' +import { CurrencyAmount, Token } from '@uniswap/sdk-core' import SVG from 'react-inlinesvg' import styled from 'styled-components/macro' @@ -30,8 +31,6 @@ import { EnhancedUserClaimData } from 'legacy/state/claim/types' import { useGasPrices } from 'legacy/state/gas/hooks' import { ConfirmOperationType } from 'legacy/state/types' -import useCurrencyBalance from 'modules/tokens/hooks/useCurrencyBalance' - import { IS_TESTING_ENV } from '../const' import { InvestAvailableBar, @@ -124,7 +123,9 @@ export default function InvestOption({ claim, openModal, closeModal }: InvestOpt const token = currencyAmount?.currency const isNative = !!token && getIsNativeToken(token) - const balance = useCurrencyBalance(account || undefined, token) + const balanceToken = useMemo(() => (token ? TokenWithLogo.fromToken(token as Token) : undefined), [token]) + + const balance = useCurrencyAmountBalance(balanceToken) const gasPrice = useGasPrices(isNative ? chainId : undefined) diff --git a/apps/cowswap-frontend/src/pages/error/AnySwapAffectedUsers/useIsAnySwapAffectedUser.ts b/apps/cowswap-frontend/src/pages/error/AnySwapAffectedUsers/useIsAnySwapAffectedUser.ts index 62a2618fc0..7468d508b2 100644 --- a/apps/cowswap-frontend/src/pages/error/AnySwapAffectedUsers/useIsAnySwapAffectedUser.ts +++ b/apps/cowswap-frontend/src/pages/error/AnySwapAffectedUsers/useIsAnySwapAffectedUser.ts @@ -4,11 +4,12 @@ import { Erc20Abi, Erc20Interface } from '@cowprotocol/abis' import { ZERO_ADDRESS } from '@cowprotocol/common-const' import { WRAPPED_NATIVE_CURRENCY as WETH } from '@cowprotocol/common-const' import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { useMultipleContractSingleData } from '@cowprotocol/multicall' import { useWalletInfo } from '@cowprotocol/wallet' import { Interface } from '@ethersproject/abi' import { BigNumber } from '@ethersproject/bignumber' -import { useMultipleContractSingleData } from 'lib/hooks/multicall' +import ms from 'ms.macro' const WETH_ADDRESS = WETH[ChainId.MAINNET].address const PERI_ADDRESS = '0x5d30aD9C6374Bf925D0A75454fa327AACf778492' @@ -23,28 +24,31 @@ const ANYSWAP_V4_CONTRACT = '0x6b7a87899490EcE95443e979cA9485CBE7E71522' // Uncomment to test logic: 0xC92...522 is mainnet VaultRelayer address. Use it with one account that has given some WETH allowance to it // const ANYSWAP_V4_CONTRACT = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' //'0x6b7a87899490EcE95443e979cA9485CBE7E71522' -const BLOCKS_PER_FETCH = 120 // 30min. It would actually suffice to check once, but we check every 120 blocks +const MULTICALL_OPTIONS = {} +const SWR_CONFIG = { refreshInterval: ms`30m` } export default function useIsAnySwapAffectedUser() { const { chainId, account } = useWalletInfo() - const result = useMultipleContractSingleData( + const { data: allowances } = useMultipleContractSingleData<[BigNumber]>( AFFECTED_TOKENS, ERC20_INTERFACE, 'allowance', [account || ZERO_ADDRESS, ANYSWAP_V4_CONTRACT], - { blocksPerFetch: BLOCKS_PER_FETCH } + MULTICALL_OPTIONS, + SWR_CONFIG ) return useMemo(() => { // The error affects Mainnet - if (chainId !== ChainId.MAINNET) { + if (chainId !== ChainId.MAINNET || !allowances) { return false } // Check if any of the tokens has allowance in the router contract - return result.some(({ result, loading, error, valid }) => { - const allowance = valid && !loading && !error && result ? (result[0] as BigNumber) : undefined + return allowances.some((result) => { + const allowance = result?.[0] + return allowance ? !allowance.isZero() : false }) - }, [chainId, result]) + }, [chainId, allowances]) } diff --git a/apps/cowswap-frontend/tsconfig.json b/apps/cowswap-frontend/tsconfig.json index 06f8eb146a..1e91f02c3d 100644 --- a/apps/cowswap-frontend/tsconfig.json +++ b/apps/cowswap-frontend/tsconfig.json @@ -24,7 +24,9 @@ "@cowprotocol/ens": ["../../../libs/ens/src/index.ts"], "@cowprotocol/core": ["../../../libs/core/src/index.ts"], "@cowprotocol/analytics": ["../../../libs/analytics/src/index.ts"], - "@cowprotocol/permit-utils": ["../../../libs/permit-utils/src/index.ts"] + "@cowprotocol/permit-utils": ["../../../libs/permit-utils/src/index.ts"], + "@cowprotocol/multicall": ["../../../libs/multicall/src/index.ts"], + "@cowprotocol/balances-and-allowances": ["../../../libs/balances-and-allowances/src/index.ts"] } }, "files": [], diff --git a/libs/abis/src/abis/Multicall3.json b/libs/abis/src/abis/Multicall3.json new file mode 100644 index 0000000000..fd19482d3e --- /dev/null +++ b/libs/abis/src/abis/Multicall3.json @@ -0,0 +1,69 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "getEthBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryAggregate", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/libs/abis/src/generated/custom/Multicall3.ts b/libs/abis/src/generated/custom/Multicall3.ts new file mode 100644 index 0000000000..2971b4b8b7 --- /dev/null +++ b/libs/abis/src/generated/custom/Multicall3.ts @@ -0,0 +1,143 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BytesLike, + CallOverrides, + ContractTransaction, + PayableOverrides, + PopulatedTransaction, + Signer, + utils, +} from 'ethers' +import type { FunctionFragment, Result } from '@ethersproject/abi' +import type { Listener, Provider } from '@ethersproject/providers' +import type { OnEvent, PromiseOrValue, TypedEvent, TypedEventFilter, TypedListener } from './common' + +export declare namespace Multicall3 { + export type CallStruct = { + target: PromiseOrValue + callData: PromiseOrValue + } + + export type CallStructOutput = [string, string] & { + target: string + callData: string + } + + export type ResultStruct = { + success: PromiseOrValue + returnData: PromiseOrValue + } + + export type ResultStructOutput = [boolean, string] & { + success: boolean + returnData: string + } +} + +export interface Multicall3Interface extends utils.Interface { + functions: { + 'getEthBalance(address)': FunctionFragment + 'tryAggregate(bool,(address,bytes)[])': FunctionFragment + } + + getFunction(nameOrSignatureOrTopic: 'getEthBalance' | 'tryAggregate'): FunctionFragment + + encodeFunctionData(functionFragment: 'getEthBalance', values: [PromiseOrValue]): string + + encodeFunctionData( + functionFragment: 'tryAggregate', + values: [PromiseOrValue, Multicall3.CallStruct[]] + ): string + + decodeFunctionResult(functionFragment: 'getEthBalance', data: BytesLike): Result + + decodeFunctionResult(functionFragment: 'tryAggregate', data: BytesLike): Result + + events: {} +} + +export interface Multicall3 extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this + + attach(addressOrName: string): this + + deployed(): Promise + + interface: Multicall3Interface + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise> + + listeners(eventFilter?: TypedEventFilter): Array> + + listeners(eventName?: string): Array + + removeAllListeners(eventFilter: TypedEventFilter): this + + removeAllListeners(eventName?: string): this + + off: OnEvent + on: OnEvent + once: OnEvent + removeListener: OnEvent + + functions: { + getEthBalance( + addr: PromiseOrValue, + overrides?: CallOverrides + ): Promise<[BigNumber] & { balance: BigNumber }> + + tryAggregate( + requireSuccess: PromiseOrValue, + calls: Multicall3.CallStruct[], + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise + } + + getEthBalance(addr: PromiseOrValue, overrides?: CallOverrides): Promise + + tryAggregate( + requireSuccess: PromiseOrValue, + calls: Multicall3.CallStruct[], + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise + + callStatic: { + getEthBalance(addr: PromiseOrValue, overrides?: CallOverrides): Promise + + tryAggregate( + requireSuccess: PromiseOrValue, + calls: Multicall3.CallStruct[], + overrides?: CallOverrides + ): Promise + } + + filters: {} + + estimateGas: { + getEthBalance(addr: PromiseOrValue, overrides?: CallOverrides): Promise + + tryAggregate( + requireSuccess: PromiseOrValue, + calls: Multicall3.CallStruct[], + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise + } + + populateTransaction: { + getEthBalance(addr: PromiseOrValue, overrides?: CallOverrides): Promise + + tryAggregate( + requireSuccess: PromiseOrValue, + calls: Multicall3.CallStruct[], + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise + } +} diff --git a/libs/abis/src/generated/custom/factories/Multicall3__factory.ts b/libs/abis/src/generated/custom/factories/Multicall3__factory.ts new file mode 100644 index 0000000000..52593a0a4b --- /dev/null +++ b/libs/abis/src/generated/custom/factories/Multicall3__factory.ts @@ -0,0 +1,89 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from 'ethers' +import type { Provider } from '@ethersproject/providers' +import type { Multicall3, Multicall3Interface } from '../Multicall3' + +const _abi = [ + { + inputs: [ + { + internalType: 'address', + name: 'addr', + type: 'address', + }, + ], + name: 'getEthBalance', + outputs: [ + { + internalType: 'uint256', + name: 'balance', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bool', + name: 'requireSuccess', + type: 'bool', + }, + { + components: [ + { + internalType: 'address', + name: 'target', + type: 'address', + }, + { + internalType: 'bytes', + name: 'callData', + type: 'bytes', + }, + ], + internalType: 'struct Multicall3.Call[]', + name: 'calls', + type: 'tuple[]', + }, + ], + name: 'tryAggregate', + outputs: [ + { + components: [ + { + internalType: 'bool', + name: 'success', + type: 'bool', + }, + { + internalType: 'bytes', + name: 'returnData', + type: 'bytes', + }, + ], + internalType: 'struct Multicall3.Result[]', + name: 'returnData', + type: 'tuple[]', + }, + ], + stateMutability: 'payable', + type: 'function', + }, +] as const + +export class Multicall3__factory { + static readonly abi = _abi + + static createInterface(): Multicall3Interface { + return new utils.Interface(_abi) as Multicall3Interface + } + + static connect(address: string, signerOrProvider: Signer | Provider): Multicall3 { + return new Contract(address, _abi, signerOrProvider) as Multicall3 + } +} diff --git a/libs/abis/src/generated/custom/index.ts b/libs/abis/src/generated/custom/index.ts index 858162a683..5646e70d4a 100644 --- a/libs/abis/src/generated/custom/index.ts +++ b/libs/abis/src/generated/custom/index.ts @@ -5,6 +5,7 @@ export type { ComposableCoW } from './ComposableCoW' export type { ExtensibleFallbackHandler } from './ExtensibleFallbackHandler' export type { GPv2Settlement } from './GPv2Settlement' export type { MerkleDrop } from './MerkleDrop' +export type { Multicall3 } from './Multicall3' export type { SignatureVerifierMuxer } from './SignatureVerifierMuxer' export type { TokenDistro } from './TokenDistro' export type { VCow } from './VCow' @@ -13,6 +14,7 @@ export { ComposableCoW__factory } from './factories/ComposableCoW__factory' export { ExtensibleFallbackHandler__factory } from './factories/ExtensibleFallbackHandler__factory' export { GPv2Settlement__factory } from './factories/GPv2Settlement__factory' export { MerkleDrop__factory } from './factories/MerkleDrop__factory' +export { Multicall3__factory } from './factories/Multicall3__factory' export { SignatureVerifierMuxer__factory } from './factories/SignatureVerifierMuxer__factory' export { TokenDistro__factory } from './factories/TokenDistro__factory' export { VCow__factory } from './factories/VCow__factory' diff --git a/libs/abis/src/index.ts b/libs/abis/src/index.ts index 7f3cce77fd..aa91576ffc 100644 --- a/libs/abis/src/index.ts +++ b/libs/abis/src/index.ts @@ -40,6 +40,11 @@ import _WethAbi from './abis-legacy/weth.json' import _UniswapInterfaceMulticallAbi from './abis-legacy/UniswapInterfaceMulticall.json' +import _Multicall3Abi from './abis/Multicall3.json' + +import { Interface } from '@ethersproject/abi' +import type { Erc20Interface } from './generated/legacy/Erc20' + export const GPv2SettlementAbi = _GPv2SettlementAbi export const ComposableCoWAbi = _ComposableCoWAbi export const vCowAbi = _vCowAbi @@ -77,7 +82,9 @@ export const EnsPublicResolverAbi = _EnsPublicResolverAbi export const EnsAbi = _EnsAbi export const Erc1155Abi = _Erc1155Abi export const Erc20Abi = _Erc20Abi +export const ERC_20_INTERFACE = new Interface(Erc20Abi) as Erc20Interface export const Erc20Bytes32Abi = _Erc20Bytes32Abi export const Erc721Abi = _Erc721Abi export const WethAbi = _WethAbi export const UniswapInterfaceMulticallAbi = _UniswapInterfaceMulticallAbi +export const Multicall3Abi = _Multicall3Abi diff --git a/libs/balances-and-allowances/.babelrc b/libs/balances-and-allowances/.babelrc new file mode 100644 index 0000000000..ef4889c1ab --- /dev/null +++ b/libs/balances-and-allowances/.babelrc @@ -0,0 +1,20 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [ + [ + "styled-components", + { + "pure": true, + "ssr": true + } + ] + ] +} diff --git a/libs/balances-and-allowances/.eslintrc.json b/libs/balances-and-allowances/.eslintrc.json new file mode 100644 index 0000000000..8e5b0907e8 --- /dev/null +++ b/libs/balances-and-allowances/.eslintrc.json @@ -0,0 +1,34 @@ +{ + "extends": [ + "plugin:@nx/react", + "../../.eslintrc-common.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} diff --git a/libs/balances-and-allowances/README.md b/libs/balances-and-allowances/README.md new file mode 100644 index 0000000000..2b58de429f --- /dev/null +++ b/libs/balances-and-allowances/README.md @@ -0,0 +1,26 @@ +# Balances and Allowances + +This lib is responsible for fetching balances and allowances for all tokens in the app. + +The most of the lib logic is concentrated in the `BalancesAndAllowancesUpdater`. +The updater depends on two main libraries `@cowprotocol/tokens` and `@cowprotocol/multicall`. +From tokens lib it gets the list of tokens using `useAllTokens()` hook and does multicall for them using `multicall` lib and just stores results into jotai stores. + +## Usage + +### Get balances +```ts +import { useTokensBalances } from '@cowprotocol/balances-and-allowances' + +const { values: balances, isLoading } = useTokensBalances() +``` + +Using this hook you even can get balance of the native token, you don't need to use another hook. + + +### Get allowances +```ts +import { useTokensAllowances } from '@cowprotocol/balances-and-allowances' + +const { values: allowances, isLoading } = useTokensAllowances() +``` diff --git a/libs/balances-and-allowances/jest.config.ts b/libs/balances-and-allowances/jest.config.ts new file mode 100644 index 0000000000..03a8390406 --- /dev/null +++ b/libs/balances-and-allowances/jest.config.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +export default { + displayName: 'balances-and-allowances', + preset: '../../jest.preset.js', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + setupFilesAfterEnv: ['../../jest.setup.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/balances-and-allowances', +} diff --git a/libs/balances-and-allowances/package.json b/libs/balances-and-allowances/package.json new file mode 100644 index 0000000000..bb7e12bb96 --- /dev/null +++ b/libs/balances-and-allowances/package.json @@ -0,0 +1,12 @@ +{ + "name": "@cowprotocol/balances-and-allowances", + "version": "0.0.1", + "main": "./index.js", + "types": "./index.d.ts", + "exports": { + ".": { + "import": "./index.mjs", + "require": "./index.js" + } + } +} diff --git a/libs/balances-and-allowances/project.json b/libs/balances-and-allowances/project.json new file mode 100644 index 0000000000..937e447f9c --- /dev/null +++ b/libs/balances-and-allowances/project.json @@ -0,0 +1,54 @@ +{ + "name": "@cowprotocol/balances-and-allowances", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/balances-and-allowances/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": [ + "{options.outputFile}" + ], + "options": { + "lintFilePatterns": [ + "libs/balances-and-allowances/**/*.{ts,tsx,js,jsx}" + ] + } + }, + "build": { + "executor": "@nx/vite:build", + "outputs": [ + "{options.outputPath}" + ], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/libs/balances-and-allowances" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], + "options": { + "jestConfig": "libs/balances-and-allowances/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + } +} diff --git a/libs/balances-and-allowances/src/hooks/useCurrencyAmountBalance.ts b/libs/balances-and-allowances/src/hooks/useCurrencyAmountBalance.ts new file mode 100644 index 0000000000..d50719a775 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/useCurrencyAmountBalance.ts @@ -0,0 +1,22 @@ +import { useMemo } from 'react' + +import { TokenWithLogo } from '@cowprotocol/common-const' +import { CurrencyAmount } from '@uniswap/sdk-core' + +import { useTokensBalances } from './useTokensBalances' + +export function useCurrencyAmountBalance( + token: TokenWithLogo | undefined | null +): CurrencyAmount | undefined { + const { values: balances } = useTokensBalances() + + return useMemo(() => { + if (!token) return undefined + + const balance = balances[token.address.toLowerCase()] + + if (!balance) return undefined + + return CurrencyAmount.fromRawAmount(token, balance.toHexString()) + }, [token, balances]) +} diff --git a/libs/balances-and-allowances/src/hooks/useNativeCurrencyAmount.ts b/libs/balances-and-allowances/src/hooks/useNativeCurrencyAmount.ts new file mode 100644 index 0000000000..33da5e73f0 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/useNativeCurrencyAmount.ts @@ -0,0 +1,20 @@ +import { useMemo } from 'react' + +import { NATIVE_CURRENCY_BUY_TOKEN, TokenWithLogo } from '@cowprotocol/common-const' +import { useWalletInfo } from '@cowprotocol/wallet' +import { CurrencyAmount } from '@uniswap/sdk-core' + +import { useNativeTokenBalance } from './useNativeTokenBalance' + +export function useNativeCurrencyAmount(): CurrencyAmount | undefined { + const { chainId } = useWalletInfo() + const { data: nativeTokenBalance } = useNativeTokenBalance() + + return useMemo(() => { + if (!nativeTokenBalance) return undefined + + const nativeToken = NATIVE_CURRENCY_BUY_TOKEN[chainId] + + return CurrencyAmount.fromRawAmount(nativeToken, nativeTokenBalance.toHexString()) + }, [chainId, nativeTokenBalance]) +} diff --git a/libs/balances-and-allowances/src/hooks/useNativeTokenBalance.ts b/libs/balances-and-allowances/src/hooks/useNativeTokenBalance.ts new file mode 100644 index 0000000000..1c8a237759 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/useNativeTokenBalance.ts @@ -0,0 +1,28 @@ +import { getMulticallContract } from '@cowprotocol/multicall' +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 + + const contract = getMulticallContract(provider) + + return contract.callStatic.getEthBalance(balanceAccount) + }, + SWR_CONFIG + ) +} diff --git a/libs/balances-and-allowances/src/hooks/useNativeTokensBalances.ts b/libs/balances-and-allowances/src/hooks/useNativeTokensBalances.ts new file mode 100644 index 0000000000..3634d8e763 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/useNativeTokensBalances.ts @@ -0,0 +1,25 @@ +import { useMemo } from 'react' + +import { getMulticallContract, useSingleContractMultipleData } from '@cowprotocol/multicall' +import { BigNumber } from '@ethersproject/bignumber' +import { useWeb3React } from '@web3-react/core' + +type NativeBalances = { [account: string]: BigNumber | undefined } + +export function useNativeTokensBalances(accounts: string[] | undefined): NativeBalances | undefined { + const { provider } = useWeb3React() + const contract = provider ? getMulticallContract(provider) : undefined + const params = useMemo(() => accounts?.map((account) => [account]), [accounts]) + + const { data: results } = useSingleContractMultipleData<[BigNumber]>(contract, 'getEthBalance', params) + + return useMemo(() => { + if (!results || !accounts) return undefined + + return results.reduce((acc, result, index) => { + acc[accounts[index]] = result?.[0] + + return acc + }, {}) + }, [results, accounts]) +} 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/hooks/useTokenBalanceForAccount.ts b/libs/balances-and-allowances/src/hooks/useTokenBalanceForAccount.ts new file mode 100644 index 0000000000..e6915f5263 --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/useTokenBalanceForAccount.ts @@ -0,0 +1,22 @@ +import { Erc20, ERC_20_INTERFACE } from '@cowprotocol/abis' +import { TokenWithLogo } from '@cowprotocol/common-const' +import { BigNumber } from '@ethersproject/bignumber' +import { Contract } from '@ethersproject/contracts' +import { useWeb3React } from '@web3-react/core' + +import useSWR, { SWRResponse } from 'swr' + +export function useTokenBalanceForAccount( + token: TokenWithLogo | undefined, + account: string | undefined +): SWRResponse { + const { provider } = useWeb3React() + + return useSWR(['useTokenBalanceForAccount', token, account], async () => { + if (!provider || !account || !token) return undefined + + const tokenContract = new Contract(token.address, ERC_20_INTERFACE, provider) as Erc20 + + return tokenContract.balanceOf(account) + }) +} diff --git a/libs/balances-and-allowances/src/hooks/useTokensAllowances.ts b/libs/balances-and-allowances/src/hooks/useTokensAllowances.ts new file mode 100644 index 0000000000..ad24dbc6fa --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/useTokensAllowances.ts @@ -0,0 +1,7 @@ +import { useAtomValue } from 'jotai' + +import { AllowancesState, allowancesState } from '../state/allowancesAtom' + +export function useTokensAllowances(): AllowancesState { + return useAtomValue(allowancesState) +} diff --git a/libs/balances-and-allowances/src/hooks/useTokensBalances.ts b/libs/balances-and-allowances/src/hooks/useTokensBalances.ts new file mode 100644 index 0000000000..dbeb5799bd --- /dev/null +++ b/libs/balances-and-allowances/src/hooks/useTokensBalances.ts @@ -0,0 +1,7 @@ +import { useAtomValue } from 'jotai' + +import { balancesAtom, BalancesState } from '../state/balancesAtom' + +export function useTokensBalances(): BalancesState { + return useAtomValue(balancesAtom) +} diff --git a/libs/balances-and-allowances/src/index.ts b/libs/balances-and-allowances/src/index.ts new file mode 100644 index 0000000000..9cc40fa226 --- /dev/null +++ b/libs/balances-and-allowances/src/index.ts @@ -0,0 +1,16 @@ +// Updater +export { BalancesAndAllowancesUpdater } from './updaters/BalancesAndAllowancesUpdater' +export { PriorityTokensUpdater } from './updaters/PriorityTokensUpdater' + +// Hooks +export { useTokensBalances } from './hooks/useTokensBalances' +export { useTokensAllowances } from './hooks/useTokensAllowances' +export { useNativeTokenBalance } from './hooks/useNativeTokenBalance' +export { useNativeTokensBalances } from './hooks/useNativeTokensBalances' +export { useNativeCurrencyAmount } from './hooks/useNativeCurrencyAmount' +export { useCurrencyAmountBalance } from './hooks/useCurrencyAmountBalance' +export { useTokenBalanceForAccount } from './hooks/useTokenBalanceForAccount' + +// Types +export type { BalancesState } from './state/balancesAtom' +export type { AllowancesState } from './state/allowancesAtom' diff --git a/libs/balances-and-allowances/src/state/allowancesAtom.ts b/libs/balances-and-allowances/src/state/allowancesAtom.ts new file mode 100644 index 0000000000..3a75a1832b --- /dev/null +++ b/libs/balances-and-allowances/src/state/allowancesAtom.ts @@ -0,0 +1,7 @@ +import { atomWithReset } from 'jotai/utils' + +import { Erc20MulticallState } from '../types' + +export interface AllowancesState extends Erc20MulticallState {} + +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 new file mode 100644 index 0000000000..8e1a8ec6d5 --- /dev/null +++ b/libs/balances-and-allowances/src/state/balancesAtom.ts @@ -0,0 +1,7 @@ +import { atomWithReset } from 'jotai/utils' + +import { Erc20MulticallState } from '../types' + +export interface BalancesState extends Erc20MulticallState {} + +export const balancesAtom = atomWithReset({ isLoading: false, values: {} }) diff --git a/libs/balances-and-allowances/src/types.ts b/libs/balances-and-allowances/src/types.ts new file mode 100644 index 0000000000..a1593ef071 --- /dev/null +++ b/libs/balances-and-allowances/src/types.ts @@ -0,0 +1,6 @@ +import { BigNumber } from '@ethersproject/bignumber' + +export interface Erc20MulticallState { + isLoading: boolean + values: { [address: string]: BigNumber | undefined } +} diff --git a/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts new file mode 100644 index 0000000000..b04cdeb896 --- /dev/null +++ b/libs/balances-and-allowances/src/updaters/BalancesAndAllowancesUpdater.ts @@ -0,0 +1,48 @@ +import { useSetAtom } from 'jotai/index' +import { useEffect, useMemo } from 'react' + +import { NATIVE_CURRENCY_BUY_TOKEN } from '@cowprotocol/common-const' +import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import { useAllTokens } from '@cowprotocol/tokens' + +import ms from 'ms.macro' + +import { useNativeTokenBalance } from '../hooks/useNativeTokenBalance' +import { usePersistBalancesAndAllowances } from '../hooks/usePersistBalancesAndAllowances' +import { balancesAtom } from '../state/balancesAtom' + +// 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`31s` } +const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`33s` } + +export interface BalancesAndAllowancesUpdaterProps { + account: string | undefined + chainId: SupportedChainId +} +export function BalancesAndAllowancesUpdater({ account, chainId }: BalancesAndAllowancesUpdaterProps) { + const setBalances = useSetAtom(balancesAtom) + + const allTokens = useAllTokens() + const { data: nativeTokenBalance } = useNativeTokenBalance() + + const tokenAddresses = useMemo(() => allTokens.map((token) => token.address), [allTokens]) + + usePersistBalancesAndAllowances({ + account, + chainId, + tokenAddresses, + setLoadingState: true, + balancesSwrConfig: BALANCES_SWR_CONFIG, + allowancesSwrConfig: ALLOWANCES_SWR_CONFIG, + }) + + // Add native token balance to the store as well + useEffect(() => { + const nativeToken = NATIVE_CURRENCY_BUY_TOKEN[chainId] + const nativeBalanceState = nativeTokenBalance ? { [nativeToken.address.toLowerCase()]: nativeTokenBalance } : {} + + 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 +} diff --git a/libs/balances-and-allowances/tsconfig.json b/libs/balances-and-allowances/tsconfig.json new file mode 100644 index 0000000000..c8f7ff0e4d --- /dev/null +++ b/libs/balances-and-allowances/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "types": [ + "vite/client" + ] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/libs/balances-and-allowances/tsconfig.lib.json b/libs/balances-and-allowances/tsconfig.lib.json new file mode 100644 index 0000000000..0998064300 --- /dev/null +++ b/libs/balances-and-allowances/tsconfig.lib.json @@ -0,0 +1,30 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "node", + "vite/client" + ] + }, + "files": [ + "../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../node_modules/@nx/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": [ + "**/*.js", + "**/*.jsx", + "**/*.ts", + "**/*.tsx" + ] +} diff --git a/libs/balances-and-allowances/tsconfig.spec.json b/libs/balances-and-allowances/tsconfig.spec.json new file mode 100644 index 0000000000..2ffaa67375 --- /dev/null +++ b/libs/balances-and-allowances/tsconfig.spec.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/balances-and-allowances/vite.config.ts b/libs/balances-and-allowances/vite.config.ts new file mode 100644 index 0000000000..52f37437be --- /dev/null +++ b/libs/balances-and-allowances/vite.config.ts @@ -0,0 +1,50 @@ +/// +import react from '@vitejs/plugin-react-swc' +import { defineConfig } from 'vite' +import dts from 'vite-plugin-dts' +import viteTsConfigPaths from 'vite-tsconfig-paths' + +import * as path from 'path' + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/balances-and-allowances', + + plugins: [ + dts({ + entryRoot: 'src', + tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'), + skipDiagnostics: true, + }), + react(), + viteTsConfigPaths({ + root: '../../', + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ + // viteTsConfigPaths({ + // root: '../../', + // }), + // ], + // }, + + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'balances-and-allowances', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es', 'cjs'], + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: ['react', 'react-dom', 'react/jsx-runtime'], + }, + }, +}) diff --git a/libs/common-const/src/chainInfo.ts b/libs/common-const/src/chainInfo.ts index bcebee3aa2..c29ae5a60e 100644 --- a/libs/common-const/src/chainInfo.ts +++ b/libs/common-const/src/chainInfo.ts @@ -12,7 +12,6 @@ export enum NetworkType { interface BaseChainInfo { readonly networkType: NetworkType - readonly blockWaitMsBeforeWarning?: number readonly docs: string readonly bridge?: string readonly explorer: string diff --git a/libs/common-utils/src/isEnoughAmount.ts b/libs/common-utils/src/isEnoughAmount.ts index ed29513919..cdb8b7b436 100644 --- a/libs/common-utils/src/isEnoughAmount.ts +++ b/libs/common-utils/src/isEnoughAmount.ts @@ -1,10 +1,13 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { BigNumber } from '@ethersproject/bignumber' export function isEnoughAmount( sellAmount: CurrencyAmount, - targetAmount: CurrencyAmount | undefined + _targetAmount: CurrencyAmount | BigNumber | undefined ): boolean | undefined { - if (!targetAmount) return undefined + if (!_targetAmount) return undefined + + const targetAmount = _targetAmount instanceof BigNumber ? _targetAmount.toHexString() : _targetAmount return sellAmount.equalTo(targetAmount) || sellAmount.lessThan(targetAmount) } diff --git a/libs/multicall/.babelrc b/libs/multicall/.babelrc new file mode 100644 index 0000000000..ef4889c1ab --- /dev/null +++ b/libs/multicall/.babelrc @@ -0,0 +1,20 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [ + [ + "styled-components", + { + "pure": true, + "ssr": true + } + ] + ] +} diff --git a/libs/multicall/.eslintrc.json b/libs/multicall/.eslintrc.json new file mode 100644 index 0000000000..8e5b0907e8 --- /dev/null +++ b/libs/multicall/.eslintrc.json @@ -0,0 +1,34 @@ +{ + "extends": [ + "plugin:@nx/react", + "../../.eslintrc-common.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} diff --git a/libs/multicall/README.md b/libs/multicall/README.md new file mode 100644 index 0000000000..a5fbcb6ff5 --- /dev/null +++ b/libs/multicall/README.md @@ -0,0 +1,84 @@ +# Multicall + +Multicall is a library that allows you to batch multiple calls into a single call to the Ethereum blockchain. + +There are two main cases where multicall is useful. + +## useMultipleContractSingleData() +>You want to get data from multiple contracts in a single call. +For example, you want to get the balance of 10 different ERC20 tokens. + +### Usage example +```ts +import { Interface } from '@ethersproject/abi' +import { Erc20Abi, Erc20Interface } from '@cowprotocol/abis' +import { useMultipleContractSingleData } from '@cowprotocol/multicall' + +const ACCOUNT = '0x0000000000000000000000000000000000000000' +const GP_VAULT_RELAYER = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' + +const TOKENS = [ + '0x5d30aD9C6374Bf925D0A75454fa327AACf778492', + '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0', + '0x418d75f65a02b3d53b2418fb8e1fe493759c7605' +] + +const ERC20_INTERFACE = new Interface(Erc20Abi) as Erc20Interface + +const results = useMultipleContractSingleData( + TOKENS, + ERC20_INTERFACE, + 'allowance', + [ACCOUNT, GP_VAULT_RELAYER] +) + +const allowancesPerToken = results.reduce((acc, allowance, i) => { + acc[TOKENS[i]] = allowance + return acc +}, {}) + +console.log(allowancesPerToken) +``` + +## useSingleContractMultipleData() +>You want to get data from a single contract, but you want to make multiple calls to it. +For example: you want to get the balance of 10 different users. + +### Usage example +```ts +import { Interface } from '@ethersproject/abi' +import { Erc20Abi, Erc20Interface } from '@cowprotocol/abis' +import { Contract } from '@ethersproject/contracts' +import { BigNumber } from '@ethersproject/bignumber' + +import { useSingleContractMultipleData } from '@cowprotocol/multicall' + +const { provider } = useWeb3React() + +const WETH_TOKEN_CONTRACT = new Contract( + '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + Erc20Abi, + provider +) + +const ACCOUNTS = [ + '0x5d30aD9C6374Bf925D0A75454fa327AACf778492', + '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0', + '0x418d75f65a02b3d53b2418fb8e1fe493759c7605' +] + +const ERC20_INTERFACE = new Interface(Erc20Abi) as Erc20Interface + +const results = useSingleContractMultipleData<[BigNumber]>( + WETH_TOKEN_CONTRACT, + 'balanceOf', + ACCOUNTS +) + +const balancesPerAddress = results.reduce((acc, balance, i) => { + acc[ACCOUNTS[i]] = balance + return acc +}, {}) + +console.log(balancesPerAddress) +``` diff --git a/libs/multicall/jest.config.ts b/libs/multicall/jest.config.ts new file mode 100644 index 0000000000..95ec4ef590 --- /dev/null +++ b/libs/multicall/jest.config.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +export default { + displayName: 'multicall', + preset: '../../jest.preset.js', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + setupFilesAfterEnv: ['../../jest.setup.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/multicall', +} diff --git a/libs/multicall/package.json b/libs/multicall/package.json new file mode 100644 index 0000000000..13a5e7a5a1 --- /dev/null +++ b/libs/multicall/package.json @@ -0,0 +1,12 @@ +{ + "name": "@cowprotocol/multicall", + "version": "0.0.1", + "main": "./index.js", + "types": "./index.d.ts", + "exports": { + ".": { + "import": "./index.mjs", + "require": "./index.js" + } + } +} diff --git a/libs/multicall/project.json b/libs/multicall/project.json new file mode 100644 index 0000000000..74a52bed29 --- /dev/null +++ b/libs/multicall/project.json @@ -0,0 +1,54 @@ +{ + "name": "@cowprotocol/multicall", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/multicall/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": [ + "{options.outputFile}" + ], + "options": { + "lintFilePatterns": [ + "libs/multicall/**/*.{ts,tsx,js,jsx}" + ] + } + }, + "build": { + "executor": "@nx/vite:build", + "outputs": [ + "{options.outputPath}" + ], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/libs/multicall" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], + "options": { + "jestConfig": "libs/multicall/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + } +} diff --git a/libs/multicall/src/const.ts b/libs/multicall/src/const.ts new file mode 100644 index 0000000000..c938d2243e --- /dev/null +++ b/libs/multicall/src/const.ts @@ -0,0 +1,4 @@ +// https://www.multicall3.com/ +export const MULTICALL_ADDRESS = '0xca11bde05977b3631167028862be2a173976ca11' + +export const DEFAULT_BATCH_SIZE = 200 diff --git a/libs/multicall/src/hooks/useMultipleContractSingleData.ts b/libs/multicall/src/hooks/useMultipleContractSingleData.ts new file mode 100644 index 0000000000..1708fa4885 --- /dev/null +++ b/libs/multicall/src/hooks/useMultipleContractSingleData.ts @@ -0,0 +1,54 @@ +import { useMemo } from 'react' + +import { Interface, Result } from '@ethersproject/abi' +import { useWeb3React } from '@web3-react/core' + +import useSWR, { SWRConfiguration, SWRResponse } from 'swr' + +import { multiCall, MultiCallOptions } from '../multiCall' + +export function useMultipleContractSingleData( + addresses: string[], + contractInterface: Interface, + methodName: string, + params: unknown[] | undefined, + multicallOptions: MultiCallOptions = {}, + swrConfig?: SWRConfiguration +): SWRResponse<(T | undefined)[] | null> { + const { provider } = useWeb3React() + + const callData = useMemo(() => { + if (!params) return null + + return contractInterface.encodeFunctionData(methodName, params) + }, [contractInterface, methodName, params]) + + const calls = useMemo(() => { + if (!callData) return null + + return addresses.map((address) => { + return { + target: address, + callData, + } + }) + }, [addresses, callData]) + + return useSWR<(T | undefined)[] | null>( + ['useMultipleContractSingleData', provider, calls, multicallOptions], + () => { + if (!calls || calls.length === 0 || !provider) return null + + return multiCall(provider, calls, multicallOptions).then((results) => { + return results.map((result) => { + try { + return contractInterface.decodeFunctionResult(methodName, result.returnData) as T + } catch (error) { + return undefined + } + }) + }) + }, + swrConfig + ) +} diff --git a/libs/multicall/src/hooks/useSingleContractMultipleData.ts b/libs/multicall/src/hooks/useSingleContractMultipleData.ts new file mode 100644 index 0000000000..d791db7202 --- /dev/null +++ b/libs/multicall/src/hooks/useSingleContractMultipleData.ts @@ -0,0 +1,48 @@ +import { useMemo } from 'react' + +import type { Result } from '@ethersproject/abi' +import type { BaseContract } from '@ethersproject/contracts' +import { useWeb3React } from '@web3-react/core' + +import useSWR, { SWRConfiguration, SWRResponse } from 'swr' + +import { multiCall, MultiCallOptions } from '../multiCall' + +export function useSingleContractMultipleData( + contract: BaseContract | undefined, + methodName: string, + params: P[] | undefined, + options: MultiCallOptions = {}, + swrConfig?: SWRConfiguration +): SWRResponse<(T | undefined)[] | null> { + const { provider } = useWeb3React() + + const calls = useMemo(() => { + if (!contract || !params) return null + + return params.map((param) => { + return { + target: contract.address, + callData: contract.interface.encodeFunctionData(methodName, param as unknown[]), + } + }) + }, [contract, methodName, params]) + + return useSWR<(T | undefined)[] | null>( + ['useSingleContractMultipleData', provider, calls, options], + async () => { + if (!contract || !calls || calls.length === 0 || !provider) return null + + return multiCall(provider, calls, options).then((results) => { + return results.map((result) => { + try { + return contract.interface.decodeFunctionResult(methodName, result.returnData) as T + } catch (error) { + return undefined + } + }) + }) + }, + swrConfig + ) +} diff --git a/libs/multicall/src/index.ts b/libs/multicall/src/index.ts new file mode 100644 index 0000000000..6402a5c103 --- /dev/null +++ b/libs/multicall/src/index.ts @@ -0,0 +1,5 @@ +export { useSingleContractMultipleData } from './hooks/useSingleContractMultipleData' +export { useMultipleContractSingleData } from './hooks/useMultipleContractSingleData' +export { getMulticallContract } from './utils/getMulticallContract' +export { multiCall } from './multiCall' +export type { MultiCallOptions } from './multiCall' diff --git a/libs/multicall/src/multiCall.ts b/libs/multicall/src/multiCall.ts new file mode 100644 index 0000000000..8aed7aff7b --- /dev/null +++ b/libs/multicall/src/multiCall.ts @@ -0,0 +1,44 @@ +import type { Multicall3 } from '@cowprotocol/abis' +import type { Web3Provider } from '@ethersproject/providers' + +import { DEFAULT_BATCH_SIZE } from './const' +import { getMulticallContract } from './utils/getMulticallContract' + +export interface MultiCallOptions { + batchSize?: number +} + +/** + * TODO: return results just after batch execution + * TODO: add fallback for failed calls + * TODO: add providers fallback + */ +export async function multiCall( + provider: Web3Provider, + calls: Multicall3.CallStruct[], + options: MultiCallOptions = {} +): Promise { + const { batchSize = DEFAULT_BATCH_SIZE } = options + + const multicall = getMulticallContract(provider) + + const batches = splitIntoBatches(calls, batchSize) + + const requests = batches.map((batch) => { + return multicall.callStatic.tryAggregate(false, batch) + }) + + return (await Promise.all(requests)).flat() +} + +function splitIntoBatches(calls: Multicall3.CallStruct[], batchSize: number): Multicall3.CallStruct[][] { + const results: Multicall3.CallStruct[][] = [] + + for (let i = 0; i < calls.length; i += batchSize) { + const batch = calls.slice(i, i + batchSize) + + results.push(batch) + } + + return results +} diff --git a/libs/multicall/src/multicall.ts b/libs/multicall/src/multicall.ts new file mode 100644 index 0000000000..8aed7aff7b --- /dev/null +++ b/libs/multicall/src/multicall.ts @@ -0,0 +1,44 @@ +import type { Multicall3 } from '@cowprotocol/abis' +import type { Web3Provider } from '@ethersproject/providers' + +import { DEFAULT_BATCH_SIZE } from './const' +import { getMulticallContract } from './utils/getMulticallContract' + +export interface MultiCallOptions { + batchSize?: number +} + +/** + * TODO: return results just after batch execution + * TODO: add fallback for failed calls + * TODO: add providers fallback + */ +export async function multiCall( + provider: Web3Provider, + calls: Multicall3.CallStruct[], + options: MultiCallOptions = {} +): Promise { + const { batchSize = DEFAULT_BATCH_SIZE } = options + + const multicall = getMulticallContract(provider) + + const batches = splitIntoBatches(calls, batchSize) + + const requests = batches.map((batch) => { + return multicall.callStatic.tryAggregate(false, batch) + }) + + return (await Promise.all(requests)).flat() +} + +function splitIntoBatches(calls: Multicall3.CallStruct[], batchSize: number): Multicall3.CallStruct[][] { + const results: Multicall3.CallStruct[][] = [] + + for (let i = 0; i < calls.length; i += batchSize) { + const batch = calls.slice(i, i + batchSize) + + results.push(batch) + } + + return results +} diff --git a/libs/multicall/src/utils/getMulticallContract.ts b/libs/multicall/src/utils/getMulticallContract.ts new file mode 100644 index 0000000000..147cb39900 --- /dev/null +++ b/libs/multicall/src/utils/getMulticallContract.ts @@ -0,0 +1,19 @@ +import { Multicall3, Multicall3Abi } from '@cowprotocol/abis' +import { Contract } from '@ethersproject/contracts' +import type { Web3Provider } from '@ethersproject/providers' + +import { MULTICALL_ADDRESS } from '../const' + +const multicallContractsCache = new Map() + +export function getMulticallContract(provider: Web3Provider): Multicall3 { + if (!multicallContractsCache.has(provider)) { + const multicall = new Contract(MULTICALL_ADDRESS, Multicall3Abi, provider) as Multicall3 + + multicallContractsCache.set(provider, multicall) + + return multicall + } + + return multicallContractsCache.get(provider) as Multicall3 +} diff --git a/libs/multicall/tsconfig.json b/libs/multicall/tsconfig.json new file mode 100644 index 0000000000..c8f7ff0e4d --- /dev/null +++ b/libs/multicall/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "types": [ + "vite/client" + ] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/libs/multicall/tsconfig.lib.json b/libs/multicall/tsconfig.lib.json new file mode 100644 index 0000000000..0998064300 --- /dev/null +++ b/libs/multicall/tsconfig.lib.json @@ -0,0 +1,30 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "node", + "vite/client" + ] + }, + "files": [ + "../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../node_modules/@nx/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": [ + "**/*.js", + "**/*.jsx", + "**/*.ts", + "**/*.tsx" + ] +} diff --git a/libs/multicall/tsconfig.spec.json b/libs/multicall/tsconfig.spec.json new file mode 100644 index 0000000000..2ffaa67375 --- /dev/null +++ b/libs/multicall/tsconfig.spec.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/multicall/vite.config.ts b/libs/multicall/vite.config.ts new file mode 100644 index 0000000000..5544ed70ef --- /dev/null +++ b/libs/multicall/vite.config.ts @@ -0,0 +1,50 @@ +/// +import react from '@vitejs/plugin-react-swc' +import { defineConfig } from 'vite' +import dts from 'vite-plugin-dts' +import viteTsConfigPaths from 'vite-tsconfig-paths' + +import * as path from 'path' + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/multicall', + + plugins: [ + dts({ + entryRoot: 'src', + tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'), + skipDiagnostics: true, + }), + react(), + viteTsConfigPaths({ + root: '../../', + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ + // viteTsConfigPaths({ + // root: '../../', + // }), + // ], + // }, + + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'multicall', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es', 'cjs'], + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: ['react', 'react-dom', 'react/jsx-runtime'], + }, + }, +}) diff --git a/libs/permit-utils/package.json b/libs/permit-utils/package.json index c9f38ccce8..4a27ce06db 100644 --- a/libs/permit-utils/package.json +++ b/libs/permit-utils/package.json @@ -1,6 +1,6 @@ { "name": "@cowprotocol/permit-utils", - "version": "0.0.1-RC.1", + "version": "0.0.1", "type": "module", "dependencies": { "ethers": "^5.7.2", diff --git a/libs/permit-utils/src/abi/erc20.json b/libs/permit-utils/src/abi/erc20.json new file mode 100644 index 0000000000..2433c82123 --- /dev/null +++ b/libs/permit-utils/src/abi/erc20.json @@ -0,0 +1,16 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/libs/permit-utils/src/index.ts b/libs/permit-utils/src/index.ts index 7c29fec237..91bf0357c9 100644 --- a/libs/permit-utils/src/index.ts +++ b/libs/permit-utils/src/index.ts @@ -4,12 +4,6 @@ export { checkIsCallDataAValidPermit } from './lib/checkIsCallDataAValidPermit' export { generatePermitHook } from './lib/generatePermitHook' export { getPermitUtilsInstance } from './lib/getPermitUtilsInstance' export { getTokenPermitInfo } from './lib/getTokenPermitInfo' +export { isSupportedPermitInfo } from './utils/isSupportedPermitInfo' -export type { - PermitHookData, - PermitHookParams, - PermitInfo, - PermitType, - SupportedPermitInfo, - GetTokenPermitIntoResult, -} from './types' +export type { PermitHookData, PermitHookParams, PermitInfo, PermitType, GetTokenPermitIntoResult } from './types' diff --git a/libs/permit-utils/src/lib/checkIsCallDataAValidPermit.ts b/libs/permit-utils/src/lib/checkIsCallDataAValidPermit.ts index 7c9c4cd48c..f249bfcfd1 100644 --- a/libs/permit-utils/src/lib/checkIsCallDataAValidPermit.ts +++ b/libs/permit-utils/src/lib/checkIsCallDataAValidPermit.ts @@ -1,6 +1,6 @@ import { DAI_PERMIT_SELECTOR, Eip2612PermitUtils, EIP_2612_PERMIT_SELECTOR } from '@1inch/permit-signed-approvals-utils' -import { SupportedPermitInfo } from '../types' +import { PermitInfo } from '../types' import { fixTokenName } from '../utils/fixTokenName' export async function checkIsCallDataAValidPermit( @@ -8,10 +8,21 @@ export async function checkIsCallDataAValidPermit( chainId: number, eip2162Utils: Eip2612PermitUtils, tokenAddress: string, - tokenName: string, + _tokenName: string | undefined, callData: string, - { version }: SupportedPermitInfo + { version, type, name }: PermitInfo ): Promise { + // TODO: take name only from PermitInfo + const tokenName = name || _tokenName + + if (type === 'unsupported') { + return false + } + + if (!tokenName) { + throw new Error(`No token name for ${tokenAddress}`) + } + const params = { chainId, tokenName: fixTokenName(tokenName), tokenAddress, callData, version } let recoverPermitOwnerPromise: Promise | undefined = undefined diff --git a/libs/permit-utils/src/lib/generatePermitHook.ts b/libs/permit-utils/src/lib/generatePermitHook.ts index 4299748a92..d8e36c8ab9 100644 --- a/libs/permit-utils/src/lib/generatePermitHook.ts +++ b/libs/permit-utils/src/lib/generatePermitHook.ts @@ -4,6 +4,7 @@ import { DEFAULT_PERMIT_GAS_LIMIT, DEFAULT_PERMIT_VALUE, PERMIT_SIGNER } from '. import { PermitHookData, PermitHookParams } from '../types' import { buildDaiLikePermitCallData, buildEip2162PermitCallData } from '../utils/buildPermitCallData' import { getPermitDeadline } from '../utils/getPermitDeadline' +import { isSupportedPermitInfo } from '../utils/isSupportedPermitInfo' const REQUESTS_CACHE: { [permitKey: string]: Promise } = {} @@ -35,8 +36,18 @@ export async function generatePermitHook(params: PermitHookParams): Promise { const { inputToken, spender, chainId, permitInfo, provider, account, eip2162Utils, nonce: preFetchedNonce } = params + const tokenAddress = inputToken.address - const tokenName = inputToken.name || tokenAddress + // TODO: remove the need for `name` from input token. Should come from permitInfo instead + const tokenName = permitInfo.name || inputToken.name + + if (!isSupportedPermitInfo(permitInfo)) { + throw new Error(`Trying to generate permit hook for unsupported token: ${tokenAddress}`) + } + + if (!tokenName) { + throw new Error(`No token name for token: ${tokenAddress}`) + } const owner = account || PERMIT_SIGNER.address diff --git a/libs/permit-utils/src/lib/getTokenPermitInfo.ts b/libs/permit-utils/src/lib/getTokenPermitInfo.ts index cb347da253..6ef486b717 100644 --- a/libs/permit-utils/src/lib/getTokenPermitInfo.ts +++ b/libs/permit-utils/src/lib/getTokenPermitInfo.ts @@ -6,9 +6,10 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { getPermitUtilsInstance } from './getPermitUtilsInstance' import { DEFAULT_PERMIT_VALUE, PERMIT_GAS_LIMIT_MIN, PERMIT_SIGNER, TOKENS_TO_SKIP_VERSION } from '../const' -import { GetTokenPermitInfoParams, GetTokenPermitIntoResult, PermitType } from '../types' +import { GetTokenPermitInfoParams, GetTokenPermitIntoResult, PermitInfo, PermitType } from '../types' import { buildDaiLikePermitCallData, buildEip2162PermitCallData } from '../utils/buildPermitCallData' import { getPermitDeadline } from '../utils/getPermitDeadline' +import { getTokenName } from '../utils/getTokenName' const EIP_2162_PERMIT_PARAMS = { value: DEFAULT_PERMIT_VALUE, @@ -24,6 +25,8 @@ const DAI_LIKE_PERMIT_PARAMS = { const REQUESTS_CACHE: Record> = {} +const UNSUPPORTED: PermitInfo = { type: 'unsupported' } + export async function getTokenPermitInfo(params: GetTokenPermitInfoParams): Promise { const { tokenAddress, chainId } = params @@ -43,25 +46,46 @@ export async function getTokenPermitInfo(params: GetTokenPermitInfoParams): Prom } async function actuallyCheckTokenIsPermittable(params: GetTokenPermitInfoParams): Promise { - const { spender, tokenAddress, tokenName, chainId, provider } = params + const { spender, tokenAddress, tokenName: _tokenName, chainId, provider } = params const eip2612PermitUtils = getPermitUtilsInstance(chainId, provider) const owner = PERMIT_SIGNER.address + // TODO: potentially remove the need for the name input + let tokenName = _tokenName + + try { + tokenName = await getTokenName(tokenAddress, chainId, provider) + } catch (e) { + if (/ETIMEDOUT/.test(e) && !tokenName) { + // Network issue or another temporary failure, return error + return { error: `Failed to fetch token name from contract. RPC connection error` } + } + console.debug( + `[checkTokenIsPermittable] Couldn't fetch token name from the contract for token ${tokenAddress}, using provided '${tokenName}'`, + e + ) + } + + if (!tokenName) { + const error = `Token name could not be determined for ${tokenAddress}` + return { error } + } + let nonce: number try { nonce = await eip2612PermitUtils.getTokenNonce(tokenAddress, owner) } catch (e) { if (e === 'nonce not supported' || e.message === 'nonce is NaN') { - console.debug(`[checkTokenIsPermittable] Not a permittable token ${tokenAddress}`, e?.message || e) - // Here we know it's not supported, return false + console.debug(`[checkTokenIsPermittable] Not a permittable token ${tokenAddress} - ${tokenName}`, e?.message || e) + // Here we know it's not supported, return unsupported // See https://github.com/1inch/permit-signed-approvals-utils/blob/b190197a45c3289867ee4e6da93f10dea51ef276/src/eip-2612-permit.utils.ts#L309 // and https://github.com/1inch/permit-signed-approvals-utils/blob/b190197a45c3289867ee4e6da93f10dea51ef276/src/eip-2612-permit.utils.ts#L325 - return false + return { ...UNSUPPORTED, name: tokenName } } - console.debug(`[checkTokenIsPermittable] Failed to get nonce for ${tokenAddress}`, e) + console.debug(`[checkTokenIsPermittable] Failed to get nonce for ${tokenAddress} - ${tokenName}`, e) // Otherwise, it might have been a network issue or another temporary failure, return error return { error: e.message || e.toString() } @@ -79,7 +103,7 @@ async function actuallyCheckTokenIsPermittable(params: GetTokenPermitInfoParams) version = await eip2612PermitUtils.getTokenVersion(tokenAddress) } catch (e) { // Not a problem, we can (try to) continue without it, and will default to `1` (part of the 1inch lib) - console.debug(`[checkTokenIsPermittable] Failed to get version for ${tokenAddress}`, e) + console.debug(`[checkTokenIsPermittable] Failed to get version for ${tokenAddress} - ${tokenName}`, e) } } @@ -99,12 +123,36 @@ async function actuallyCheckTokenIsPermittable(params: GetTokenPermitInfoParams) return await estimateTokenPermit({ ...baseParams, type: 'eip-2612', provider }) } catch (e) { // Not eip-2612, try dai-like - console.debug(`[checkTokenIsPermittable] Failed to estimate eip-2612 permit for ${tokenAddress}`, e) try { + const isDaiLike = await isDaiLikeTypeHash(tokenAddress, eip2612PermitUtils) + + if (!isDaiLike) { + // These might be supported, as they have nonces, but we don't know why the permit call fails + // TODO: further investigate this kind of token + // For now mark them as unsupported and don't check it again + if (/invalid signature/.test(e) || e?.code === 'UNPREDICTABLE_GAS_LIMIT') { + console.debug( + `[checkTokenIsPermittable] Token ${tokenAddress} - ${tokenName} might be permittable, but it's not supported for now. Reason:`, + e?.reason + ) + return { ...UNSUPPORTED, name: tokenName } + } + + // Maybe a temporary failure + console.debug( + `[checkTokenIsPermittable] Failed to estimate eip-2612 permit for ${tokenAddress} - ${tokenName}`, + e + ) + return { error: e.message || e.toString() } + } + return await estimateTokenPermit({ ...baseParams, type: 'dai-like', provider }) } catch (e) { // Not dai-like either, return error - console.debug(`[checkTokenIsPermittable] Failed to estimate dai-like permit for ${tokenAddress}`, e) + console.debug( + `[checkTokenIsPermittable] Failed to estimate dai-like permit for ${tokenAddress} - ${tokenName}`, + e + ) return { error: e.message || e.toString() } } } @@ -127,14 +175,14 @@ type EstimateParams = BaseParams & { } async function estimateTokenPermit(params: EstimateParams): Promise { - const { provider, chainId, walletAddress, tokenAddress, type, version } = params + const { provider, chainId, walletAddress, tokenAddress, tokenName, type, version } = params const getCallDataFn = type === 'eip-2612' ? getEip2612CallData : getDaiLikeCallData const data = await getCallDataFn(params) if (!data) { - return false + return { ...UNSUPPORTED, name: tokenName } } const estimatedGas = await provider.estimateGas({ @@ -149,8 +197,9 @@ async function estimateTokenPermit(params: EstimateParams): Promise { @@ -172,6 +221,12 @@ async function getEip2612CallData(params: BaseParams): Promise { }) } +async function isDaiLikeTypeHash(tokenAddress: string, eip2612PermitUtils: Eip2612PermitUtils): Promise { + const permitTypeHash = await eip2612PermitUtils.getPermitTypeHash(tokenAddress) + + return permitTypeHash === DAI_LIKE_PERMIT_TYPEHASH +} + async function getDaiLikeCallData(params: BaseParams): Promise { const { eip2612PermitUtils, tokenAddress, walletAddress, spender, nonce, chainId, tokenName, version } = params diff --git a/libs/permit-utils/src/types.ts b/libs/permit-utils/src/types.ts index 054aa5cef4..bb6d754549 100644 --- a/libs/permit-utils/src/types.ts +++ b/libs/permit-utils/src/types.ts @@ -2,18 +2,19 @@ import { Eip2612PermitUtils } from '@1inch/permit-signed-approvals-utils' import { latest } from '@cowprotocol/app-data' import { JsonRpcProvider } from '@ethersproject/providers' -export type PermitType = 'dai-like' | 'eip-2612' +export type PermitType = 'dai-like' | 'eip-2612' | 'unsupported' -export type SupportedPermitInfo = { +export type PermitInfo = { type: PermitType - version: string | undefined // Some tokens have it different than `1`, and won't work without it + // TODO: make it not optional once token-lists is migrated + name?: string + version?: string | undefined // Some tokens have it different than `1`, and won't work without it } -type UnsupportedPermitInfo = false -export type PermitInfo = SupportedPermitInfo | UnsupportedPermitInfo // Local TokenInfo definition to not depend on external libs just for this type TokenInfo = { address: string + // TODO: remove from token info name: string | undefined } @@ -21,7 +22,7 @@ export type PermitHookParams = { inputToken: TokenInfo spender: string chainId: number - permitInfo: SupportedPermitInfo + permitInfo: PermitInfo provider: JsonRpcProvider eip2162Utils: Eip2612PermitUtils account?: string | undefined @@ -34,11 +35,9 @@ type FailedToIdentify = { error: string } export type GetTokenPermitIntoResult = // When it's a permittable token: - | SupportedPermitInfo + | PermitInfo // When something failed: | FailedToIdentify - // When it's not permittable: - | UnsupportedPermitInfo type BasePermitCallDataParams = { eip2162Utils: Eip2612PermitUtils @@ -53,7 +52,7 @@ export type BuildDaiLikePermitCallDataParams = BasePermitCallDataParams & { export type GetTokenPermitInfoParams = { spender: string tokenAddress: string - tokenName: string + tokenName?: string | undefined chainId: number provider: JsonRpcProvider } diff --git a/libs/permit-utils/src/utils/PermitProviderConnector.ts b/libs/permit-utils/src/utils/PermitProviderConnector.ts index fa0715cb9e..a430744194 100644 --- a/libs/permit-utils/src/utils/PermitProviderConnector.ts +++ b/libs/permit-utils/src/utils/PermitProviderConnector.ts @@ -4,18 +4,15 @@ import { AbiInput, AbiItem, EIP712TypedData, ProviderConnector } from '@1inch/pe import { defaultAbiCoder, ParamType } from '@ethersproject/abi' import { TypedDataField } from '@ethersproject/abstract-signer' import { BigNumber } from '@ethersproject/bignumber' -import { Contract, ContractInterface } from '@ethersproject/contracts' import { Wallet } from '@ethersproject/wallet' +import { getContract } from './getContract' + export class PermitProviderConnector implements ProviderConnector { constructor(private provider: JsonRpcProvider, private walletSigner?: Wallet | undefined) {} - private getContract(address: string, abi: ContractInterface, provider: JsonRpcProvider): Contract { - return new Contract(address, abi, provider) - } - contractEncodeABI(abi: AbiItem[], address: string | null, methodName: string, methodParams: unknown[]): string { - const contract = this.getContract(address || '', abi, this.provider) + const contract = getContract(address || '', abi, this.provider) return contract.interface.encodeFunctionData(methodName, methodParams) } diff --git a/libs/permit-utils/src/utils/fixTokenName.ts b/libs/permit-utils/src/utils/fixTokenName.ts index 3b16d300d4..6b7a5bd8f1 100644 --- a/libs/permit-utils/src/utils/fixTokenName.ts +++ b/libs/permit-utils/src/utils/fixTokenName.ts @@ -1,3 +1,4 @@ +// TODO: remove this once permitInfo contains token names export function fixTokenName(tokenName: string): string { // TODO: this is ugly and I'm not happy with it either // It'll probably go away when the tokens overhaul is implemented diff --git a/libs/permit-utils/src/utils/getContract.ts b/libs/permit-utils/src/utils/getContract.ts new file mode 100644 index 0000000000..498f92b762 --- /dev/null +++ b/libs/permit-utils/src/utils/getContract.ts @@ -0,0 +1,7 @@ +import type { JsonRpcProvider } from '@ethersproject/providers' + +import { Contract, ContractInterface } from '@ethersproject/contracts' + +export function getContract(address: string, abi: ContractInterface, provider: JsonRpcProvider): Contract { + return new Contract(address, abi, provider) +} diff --git a/libs/permit-utils/src/utils/getTokenName.ts b/libs/permit-utils/src/utils/getTokenName.ts new file mode 100644 index 0000000000..3581b81bcb --- /dev/null +++ b/libs/permit-utils/src/utils/getTokenName.ts @@ -0,0 +1,14 @@ +import type { JsonRpcProvider } from '@ethersproject/providers' + +import { getAddress } from '@ethersproject/address' + +import { getContract } from './getContract' + +import Erc20Abi from '../abi/erc20.json' + +export async function getTokenName(tokenAddress: string, chainId: number, provider: JsonRpcProvider): Promise { + const formattedAddress = getAddress(tokenAddress) + const erc20Contract = getContract(formattedAddress, Erc20Abi, provider) + + return erc20Contract.callStatic['name']() +} diff --git a/libs/permit-utils/src/utils/isSupportedPermitInfo.ts b/libs/permit-utils/src/utils/isSupportedPermitInfo.ts new file mode 100644 index 0000000000..5396c038a2 --- /dev/null +++ b/libs/permit-utils/src/utils/isSupportedPermitInfo.ts @@ -0,0 +1,5 @@ +import { PermitInfo } from '../types' + +export function isSupportedPermitInfo(p: PermitInfo | undefined): p is PermitInfo { + return !!p && p.type !== 'unsupported' +} diff --git a/libs/tokens/src/state/tokens/allTokensAtom.ts b/libs/tokens/src/state/tokens/allTokensAtom.ts index 0451c5e3b6..e95e91f840 100644 --- a/libs/tokens/src/state/tokens/allTokensAtom.ts +++ b/libs/tokens/src/state/tokens/allTokensAtom.ts @@ -7,6 +7,7 @@ import { userAddedTokensAtom } from './userAddedTokensAtom' import { favouriteTokensAtom } from './favouriteTokensAtom' import { listsEnabledStateAtom, listsStatesListAtom } from '../tokenLists/tokenListsStateAtom' import { lowerCaseTokensMap } from '../../utils/lowerCaseTokensMap' +import type { TokenInfo } from '@uniswap/token-lists' export interface TokensByAddress { [address: string]: TokenWithLogo @@ -65,15 +66,12 @@ export const activeTokensAtom = atom((get) => { const tokensMap = get(tokensStateAtom) const nativeToken = NATIVE_CURRENCY_BUY_TOKEN[chainId] - const tokens = tokenMapToListWithLogo({ + return tokenMapToListWithLogo({ + [nativeToken.address.toLowerCase()]: nativeToken as TokenInfo, ...tokensMap.activeTokens, ...lowerCaseTokensMap(userAddedTokens[chainId]), ...lowerCaseTokensMap(favouriteTokensState[chainId]), }) - - tokens.unshift(nativeToken) - - return tokens }) export const inactiveTokensAtom = atom((get) => { diff --git a/package.json b/package.json index 2ba39bada4..8ef42cb886 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,6 @@ "@trezor/connect-plugin-ethereum": "^9.0.1", "@trezor/connect-web": "^9.0.11", "@types/hdkey": "^2.0.1", - "@uniswap/redux-multicall": "^1.1.5", "@uniswap/sdk-core": "^3.0.1", "@uniswap/token-lists": "^1.0.0-beta.30", "@use-gesture/react": "^10.2.23", @@ -259,6 +258,7 @@ "ts-jest": "^29.1.1", "ts-mockito": "^2.6.1", "ts-node": "^10.9.1", + "typechain": "^8.3.2", "typescript": "~5.1.3", "vite": "^4.4.11", "vite-plugin-babel-macros": "^1.0.6", diff --git a/tsconfig.base.json b/tsconfig.base.json index 6ba976d101..a214de565f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -46,7 +46,9 @@ "@cowprotocol/ui-utils": ["libs/ui-utils/src/index.ts"], "@cowprotocol/wallet": ["libs/wallet/src/index.ts"], "@cowprotocol/widget-lib": ["libs/widget-lib/src/index.ts"], - "@cowprotocol/widget-react": ["libs/widget-react/src/index.ts"] + "@cowprotocol/widget-react": ["libs/widget-react/src/index.ts"], + "@cowprotocol/multicall": ["libs/multicall/src/index.ts"], + "@cowprotocol/balances-and-allowances": ["libs/balances-and-allowances/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/yarn.lock b/yarn.lock index 31657349f5..e7a6e339eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6369,7 +6369,7 @@ dependencies: "@types/node" "*" -"@types/prettier@^2.1.5": +"@types/prettier@^2.1.1", "@types/prettier@^2.1.5": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== @@ -6866,11 +6866,6 @@ "@typescript-eslint/types" "6.2.0" eslint-visitor-keys "^3.4.1" -"@uniswap/redux-multicall@^1.1.5": - version "1.1.8" - resolved "https://registry.yarnpkg.com/@uniswap/redux-multicall/-/redux-multicall-1.1.8.tgz#9cc5090305b10df68fb6162eb1ba7c2c762f5e7f" - integrity sha512-LttOBVJuoRNC6N4MHsb5dF2GszLsj1ddPKKccEw1XOX17bGrFdm2A6GwKgES+v+Hj3lluDbQL6atcQtymP21iw== - "@uniswap/sdk-core@^3.0.1": version "3.2.6" resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-3.2.6.tgz#1a652516fab0c6bc1420c2226648da967a10f52a" @@ -8087,6 +8082,16 @@ aria-query@^5.1.3: dependencies: dequal "^2.0.3" +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1, array-back@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + array-buffer-byte-length@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" @@ -9763,6 +9768,26 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== +command-line-args@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^6.1.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== + dependencies: + array-back "^4.0.2" + chalk "^2.4.2" + table-layout "^1.0.2" + typical "^5.2.0" + commander@7, commander@^7.1.0, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" @@ -10904,7 +10929,7 @@ deep-equal@^2.0.5: which-collection "^1.0.1" which-typed-array "^1.1.9" -deep-extend@^0.6.0: +deep-extend@^0.6.0, deep-extend@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== @@ -12770,6 +12795,13 @@ find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -13007,6 +13039,15 @@ fs-extra@^4.0.2: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^7.0.0, fs-extra@~7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -13026,15 +13067,6 @@ fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@~7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-minipass@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" @@ -13234,6 +13266,18 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@9.3.2: version "9.3.2" resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.2.tgz#8528522e003819e63d11c979b30896e0eaf52eda" @@ -17350,7 +17394,7 @@ mkdirp@^0.5.5, mkdirp@^0.5.6, mkdirp@~0.5.1: dependencies: minimist "^1.2.6" -mkdirp@^1.0.3: +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -19291,7 +19335,7 @@ prettier@2.8.7: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450" integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw== -prettier@^2.6.2: +prettier@^2.3.1, prettier@^2.6.2: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -20282,6 +20326,11 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + redux-localstorage-simple@^2.3.1: version "2.5.1" resolved "https://registry.yarnpkg.com/redux-localstorage-simple/-/redux-localstorage-simple-2.5.1.tgz#d01b1a03786d010ccce0ae6808c64a62dc041ef8" @@ -21567,6 +21616,11 @@ string-argv@~0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== +string-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" + integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -21989,6 +22043,16 @@ tabbable@^5.3.3: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== +table-layout@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + tailwindcss@^3.0.2: version "3.3.3" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf" @@ -22421,6 +22485,16 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d" integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A== +ts-command-line-args@^2.2.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz#e64456b580d1d4f6d948824c274cf6fa5f45f7f0" + integrity sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw== + dependencies: + chalk "^4.1.0" + command-line-args "^5.1.1" + command-line-usage "^6.1.0" + string-format "^2.0.0" + ts-essentials@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" @@ -22622,6 +22696,22 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typechain@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/typechain/-/typechain-8.3.2.tgz#1090dd8d9c57b6ef2aed3640a516bdbf01b00d73" + integrity sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q== + dependencies: + "@types/prettier" "^2.1.1" + debug "^4.3.1" + fs-extra "^7.0.0" + glob "7.1.7" + js-sha3 "^0.8.0" + lodash "^4.17.15" + mkdirp "^1.0.4" + prettier "^2.3.1" + ts-command-line-args "^2.2.0" + ts-essentials "^7.0.1" + typed-array-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" @@ -22688,6 +22778,16 @@ typescript@~5.0.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + ua-parser-js@^0.7.24: version "0.7.35" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" @@ -24151,6 +24251,14 @@ wif@^2.0.6: dependencies: bs58check "<3.0.0" +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" + workbox-background-sync@6.6.1: version "6.6.1" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f"