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"