Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/cowswap-frontend/src/locales/en-US.po
Original file line number Diff line number Diff line change
Expand Up @@ -5626,6 +5626,10 @@ msgstr "Order cannot be filled due to insufficient balance on the current accoun
msgid "required"
msgstr "required"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx
msgid "Fetching balances"
msgstr "Fetching balances"

#: apps/cowswap-frontend/src/modules/wallet/pure/Web3StatusInner/index.tsx
msgid "Wallet"
msgstr "Wallet"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { TradingSdkUpdater } from 'tradingSdk/TradingSdkUpdater'

import { UploadToIpfsUpdater } from 'modules/appData/updater/UploadToIpfsUpdater'
import { CommonPriorityBalancesAndAllowancesUpdater } from 'modules/balancesAndAllowances'
import { BalancesDevtools } from 'modules/balancesAndAllowances/updaters/BalancesDevtools'
import { PendingBridgeOrdersUpdater, BridgingEnabledUpdater } from 'modules/bridge'
import { BalancesCombinedUpdater } from 'modules/combinedBalances/updater/BalancesCombinedUpdater'
import { InFlightOrderFinalizeUpdater } from 'modules/ethFlow'
Expand Down Expand Up @@ -126,6 +127,7 @@ export function Updaters(): ReactNode {
<LpTokensWithBalancesUpdater />
<VampireAttackUpdater />
<BalancesCombinedUpdater />
<BalancesDevtools />
<CorrelatedTokensUpdater />
<BridgeOrdersCleanUpdater />
<PendingBridgeOrdersUpdater />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { JSX } from 'react'

import { DevTools } from 'jotai-devtools'

export function BalancesDevtools(): JSX.Element | null {
if (process.env.NODE_ENV !== 'development') return null

return <DevTools position="bottom-left" />
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,7 @@ function applyBalanceDiffs(
values: normalizedValues,
chainId,
fromCache: false,
hasFirstLoad: currentBalances.hasFirstLoad,
error: currentBalances.error,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const defaultProps: SelectTokenModalProps = {
isLoading: false,
chainId: SupportedChainId.SEPOLIA,
fromCache: false,
hasFirstLoad: true,
error: null,
},
selectedToken,
isRouteAvailable: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useWalletDetails } from '@cowprotocol/wallet'

import { useToggleWalletModal } from 'legacy/state/application/hooks'

import { useTokensBalancesCombined } from 'modules/combinedBalances'
import { useGetAmountToSignApprove } from 'modules/erc20Approve'
import { useInjectedWidgetParams } from 'modules/injectedWidget'
import { useAmountsToSignFromQuote, useDerivedTradeState, useWrapNativeFlow } from 'modules/trade'
Expand Down Expand Up @@ -31,6 +32,7 @@ export function useTradeFormButtonContext(
derivedState?.outputCurrency,
quote.error,
)
const { error: balancesError } = useTokensBalancesCombined()

return useMemo(() => {
if (!derivedState) return null
Expand All @@ -48,6 +50,7 @@ export function useTradeFormButtonContext(
enablePartialApprove,
customTokenError,
minAmountToSignForSwap,
balancesError,
}
}, [
defaultText,
Expand All @@ -62,5 +65,6 @@ export function useTradeFormButtonContext(
enablePartialApprove,
customTokenError,
minAmountToSignForSwap,
balancesError,
])
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useGnosisSafeInfo, useIsTxBundlingSupported, useWalletDetails, useWalle
import { useHasHookBridgeProvidersEnabled } from 'entities/bridgeProvider'

import { useCurrentAccountProxy } from 'modules/accountProxy/hooks/useCurrentAccountProxy'
import { useTokensBalancesCombined } from 'modules/combinedBalances'
import { useApproveState, useGetAmountToSignApprove, useIsApprovalOrPermitRequired } from 'modules/erc20Approve'
import { RwaTokenStatus, useRwaTokenStatus } from 'modules/rwa'
import { TradeType, useDerivedTradeState, useIsWrapOrUnwrap } from 'modules/trade'
Expand All @@ -21,12 +22,14 @@ import { useTokenCustomTradeError } from './useTokenCustomTradeError'

import { TradeFormValidationCommonContext } from '../types'

// eslint-disable-next-line max-lines-per-function
export function useTradeFormValidationContext(): TradeFormValidationCommonContext | null {
const { account } = useWalletInfo()
const derivedTradeState = useDerivedTradeState()
const tradeQuote = useTradeQuote()
const isProviderNetworkUnsupported = useIsProviderNetworkUnsupported()
const isOnline = useIsOnline()
const { isLoading: isBalancesLoading, hasFirstLoad, error: balancesError } = useTokensBalancesCombined()

const { inputCurrency, outputCurrency, recipient, tradeType } = derivedTradeState || {}
const customTokenError = useTokenCustomTradeError(inputCurrency, outputCurrency, tradeQuote.error)
Expand Down Expand Up @@ -86,8 +89,11 @@ export function useTradeFormValidationContext(): TradeFormValidationCommonContex
isProxySetupValid,
customTokenError,
isRestrictedForCountry,
isBalancesLoading: !hasFirstLoad || isBalancesLoading,
balancesError,
}
}, [
hasFirstLoad,
account,
approvalState,
customTokenError,
Expand All @@ -102,12 +108,14 @@ export function useTradeFormValidationContext(): TradeFormValidationCommonContex
isRestrictedForCountry,
isSafeReadonlyUser,
isSupportedWallet,
isBalancesLoading,
isSwapUnsupported,
isWrapUnwrap,
isProxySetupValid,
recipientEnsAddress,
toBeImported,
tradeQuote,
balancesError,
])
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ReactElement, ReactNode } from 'react'

import { getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils'
import { BridgeProviderQuoteError, BridgeQuoteErrors } from '@cowprotocol/sdk-bridging'
import { HelpTooltip, InfoTooltip, TokenSymbol } from '@cowprotocol/ui'
import { CenteredDots, HelpTooltip, InfoTooltip, TokenSymbol } from '@cowprotocol/ui'

import { t } from '@lingui/core/macro'
import { Trans } from '@lingui/react/macro'
Expand Down Expand Up @@ -254,8 +254,23 @@ export const tradeButtonsMap: Record<TradeFormValidation, ButtonErrorConfig | Bu
[TradeFormValidation.QuoteLoading]: {
text: <TradeLoadingButton />,
},
[TradeFormValidation.BalancesNotLoaded]: {
text: <Trans>Couldn't load balances</Trans>,
[TradeFormValidation.BalancesLoading]: {
text: (
<>
<Trans>Fetching balances</Trans>
<CenteredDots smaller />
</>
),
},
[TradeFormValidation.BalancesNotLoaded]: (context) => {
return (
<TradeFormBlankButton disabled={true}>
<>
<Trans>Couldn't load balances</Trans>
{context.balancesError ? <HelpTooltip text={<div>{context.balancesError}</div>} /> : null}
</>
</TradeFormBlankButton>
)
},
[TradeFormValidation.BalanceInsufficient]: (context) => {
const inputCurrency = context.derivedState.inputCurrency
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor
isProxySetupValid,
customTokenError,
isRestrictedForCountry,
isBalancesLoading,
balancesError,
} = context

const {
Expand Down Expand Up @@ -118,6 +120,20 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor
validations.push(TradeFormValidation.CurrencyNotSupported)
}

if (!canPlaceOrderWithoutBalance && !!account) {
if (isBalancesLoading) {
validations.push(TradeFormValidation.BalancesLoading)
} else {
if (!inputCurrencyBalance && balancesError) {
validations.push(TradeFormValidation.BalancesNotLoaded)
}

if (inputCurrencyBalance && inputCurrencyAmount && inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
validations.push(TradeFormValidation.BalanceInsufficient)
}
}
}

if (isFastQuote || !tradeQuote.quote || (isBridging && tradeQuote.isLoading)) {
validations.push(TradeFormValidation.QuoteLoading)
}
Expand All @@ -142,16 +158,6 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor
}
}

if (!canPlaceOrderWithoutBalance) {
if (!inputCurrencyBalance) {
validations.push(TradeFormValidation.BalancesNotLoaded)
}

if (inputCurrencyBalance && inputCurrencyAmount && inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
validations.push(TradeFormValidation.BalanceInsufficient)
}
}

if (isWrapUnwrap) {
validations.push(TradeFormValidation.WrapUnwrapFlow)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum TradeFormValidation {
QuoteExpired,

// Balances
BalancesLoading,
BalancesNotLoaded,
BalanceInsufficient,

Expand Down Expand Up @@ -72,6 +73,8 @@ export interface TradeFormValidationCommonContext {
isProxySetupValid: boolean | null | undefined
customTokenError?: string
isRestrictedForCountry: boolean
isBalancesLoading: boolean
balancesError: string | null
}

export interface TradeFormValidationContext extends TradeFormValidationCommonContext {}
Expand All @@ -86,6 +89,7 @@ export interface TradeFormButtonContext {
enablePartialApprove?: boolean
customTokenError?: string
minAmountToSignForSwap?: CurrencyAmount<Currency>
balancesError: string | null

confirmTrade(): void

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ describe('usePersistBalancesFromBff - invalidateCacheTrigger', () => {
chainId: SupportedChainId.MAINNET,
values: {},
fromCache: false,
hasFirstLoad: false,
error: null,
} as BalancesState,
],
[balancesUpdateAtom, mockBalancesUpdate],
Expand Down Expand Up @@ -188,5 +190,4 @@ describe('usePersistBalancesFromBff - invalidateCacheTrigger', () => {
)
})
})

})
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export function usePersistBalancesFromBff(params: PersistBalancesFromBffParams):
setBalances((state) => ({ ...state, isLoading: isBalancesLoading, chainId: targetChainId }))
}, [setBalances, isBalancesLoading, targetChainId, targetAccount])

useEffect(() => {
if (!error) return

const message = error instanceof Error ? error.message : String(error)

setBalances((state) => ({ ...state, error: message, isLoading: false }))
}, [error, setBalances])

useEffect(() => {
setIsBffFailed(!!error)
}, [error, setIsBffFailed])
Expand All @@ -77,6 +85,8 @@ export function usePersistBalancesFromBff(params: PersistBalancesFromBffParams):
...state,
chainId: targetChainId,
fromCache: false,
hasFirstLoad: true,
error: null,
values: balancesState,
isLoading: false,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export function usePersistBalancesViaWebCalls(params: PersistBalancesAndAllowanc

const balanceOfParams = useMemo(() => (account ? [account] : undefined), [account])

const { isLoading: isBalancesLoading, data } = useMultipleContractSingleData<{ balance: BigNumber }>(
const {
isLoading: isBalancesLoading,
data,
error,
} = useMultipleContractSingleData<{ balance: BigNumber }>(
chainId,
tokenAddresses,
ERC_20_INTERFACE,
Expand All @@ -65,6 +69,17 @@ export function usePersistBalancesViaWebCalls(params: PersistBalancesAndAllowanc
setBalances((state) => ({ ...state, isLoading: isBalancesLoading, chainId }))
}, [setBalances, isBalancesLoading, setLoadingState, chainId])

// Set balances error state for full balances fetches only
useEffect(() => {
if (!setLoadingState) return

if (!error) return

const message = error instanceof Error ? error.message : String(error)

setBalances((state) => ({ ...state, error: message, isLoading: false }))
}, [setBalances, error, setLoadingState])

// Set balances to the store
useEffect(() => {
if (!account || !balances?.length || !isNewBlockNumber) return
Expand All @@ -83,6 +98,8 @@ export function usePersistBalancesViaWebCalls(params: PersistBalancesAndAllowanc
...state,
chainId,
fromCache: false,
hasFirstLoad: true,
error: null,
values: { ...state.values, ...balancesState },
...(setLoadingState ? { isLoading: false } : {}),
}
Expand Down
4 changes: 4 additions & 0 deletions libs/balances-and-allowances/src/state/balancesAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ type BalancesCache = PersistentStateByChain<Record<Account, Record<TokenAddress,
export interface BalancesState extends Erc20MulticallState {
chainId: SupportedChainId | null
fromCache: boolean
hasFirstLoad: boolean
error: string | null
}

export const DEFAULT_BALANCES_STATE: BalancesState = {
isLoading: false,
values: {},
chainId: null,
fromCache: false,
hasFirstLoad: false,
error: null,
}

export const balancesCacheAtom = atomWithStorage<BalancesCache>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export function BalancesCacheUpdater({ chainId, account, excludedTokens }: Balan
fromCache: true,
chainId,
isLoading: state.isLoading,
hasFirstLoad: state.hasFirstLoad,
error: state.error,
values: {
...state.values,
...cacheKeys.reduce(
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@
"jest-environment-jsdom": "^30.2.0",
"jest-fetch-mock": "^3.0.3",
"jest-styled-components": "^7.1.1",
"jotai-devtools": "0.8.0",
"jsdom": "~27.0.0",
"lint-staged": "^16.2.7",
"node-stdlib-browser": "^1.2.0",
Expand Down
Loading
Loading