Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
87162b6
refactor: extract QuoteErrorsButton pure component
shoom3301 Jun 4, 2026
9de8ff6
refactor: extract QuoteApiErrorButton pure component
shoom3301 Jun 4, 2026
b52b891
refactor: clean up OperatorError
shoom3301 Jun 4, 2026
73814e7
fix: get rid of default export OperatorError
shoom3301 Jun 4, 2026
b755ca9
refactor: clean up QuoteApiError
shoom3301 Jun 4, 2026
d52bdd7
chore(i18n): extract i18n strings [automatic]
github-actions[bot] Jun 4, 2026
49cb52d
feat: support new quote api errors
shoom3301 Jun 4, 2026
b47736a
Merge branch 'feat/new-quote-errors' of https://github.com/cowprotoco…
shoom3301 Jun 4, 2026
383c211
chore(i18n): extract i18n strings [automatic]
github-actions[bot] Jun 4, 2026
e0c0c9c
chore: fix SellAmountDoesNotCoverFee
shoom3301 Jun 4, 2026
16d2add
fix: improve errors displaying
shoom3301 Jun 5, 2026
dbdbd2f
chore(i18n): extract i18n strings [automatic]
github-actions[bot] Jun 5, 2026
e261db5
Merge branch 'develop' into feat/new-quote-errors
fairlighteth Jun 5, 2026
1a02f1b
chore: fix error tooltip
shoom3301 Jun 5, 2026
61d51b0
Merge remote-tracking branch 'origin/feat/new-quote-errors-1' into fe…
shoom3301 Jun 5, 2026
f8774d7
chore: fix SellAmountDoesNotCoverFee text
shoom3301 Jun 5, 2026
a5ac2d1
chore(i18n): extract i18n strings [automatic]
github-actions[bot] Jun 5, 2026
39696c2
Merge branch 'feat/new-quote-errors' into feat/new-quote-errors-1
fairlighteth Jun 5, 2026
7bf1028
Merge branch 'develop' into feat/new-quote-errors
fairlighteth Jun 6, 2026
c624faf
chore: add NoLiquidity
shoom3301 Jun 8, 2026
84ee94b
Merge branch 'feat/new-quote-errors' into feat/new-quote-errors-1
fairlighteth Jun 8, 2026
61b6af2
Merge branch 'develop' into feat/new-quote-errors
shoom3301 Jun 9, 2026
afc6a13
Merge branch 'feat/new-quote-errors' into feat/new-quote-errors-1
shoom3301 Jun 9, 2026
dc6ffbb
Merge branch 'develop' of https://github.com/cowprotocol/cowswap into…
shoom3301 Jun 15, 2026
da5625a
chore: fix code style
shoom3301 Jun 15, 2026
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
112 changes: 45 additions & 67 deletions apps/cowswap-frontend/src/api/cowProtocol/errors/QuoteError.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,68 @@
import { ApiErrorCodes, ApiErrorObject } from './OperatorError'

interface QuoteApiErrorObject {
export interface QuoteApiErrorObject {
errorType: QuoteApiErrorCodes
description: string
data?: unknown
}

// Conforms to backend API
// https://github.com/cowprotocol/services/blob/main/crates/orderbook/openapi.yml
// TODO: import from SDK `PriceEstimationError.errorType`

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export enum QuoteApiErrorCodes {
UnsupportedToken = 'UnsupportedToken',
AppDataHashMismatch = 'AppDataHashMismatch',
CustomSolverError = 'CustomSolverError',
ExcessiveValidTo = 'ExcessiveValidTo',
Forbidden = 'Forbidden',
InsufficientLiquidity = 'InsufficientLiquidity',
FeeExceedsFrom = 'FeeExceedsFrom',
ZeroPrice = 'ZeroPrice',
TransferEthToContract = 'TransferEthToContract',
InsufficientValidTo = 'InsufficientValidTo',
InternalServerError = 'InternalServerError',
InvalidAppData = 'InvalidAppData',
InvalidNativeSellToken = 'InvalidNativeSellToken',
NoLiquidity = 'NoLiquidity',
QuoteNotVerified = 'QuoteNotVerified',
SameBuyAndSellToken = 'SameBuyAndSellToken',
UNHANDLED_ERROR = 'UNHANDLED_ERROR',
}

export const SENTRY_IGNORED_QUOTE_ERRORS = [QuoteApiErrorCodes.FeeExceedsFrom]

export enum QuoteApiErrorDetails {
UnsupportedToken = 'One of the tokens you are trading is unsupported. Please read the FAQ for more info.',
InsufficientLiquidity = 'Token pair selected has insufficient liquidity.',
FeeExceedsFrom = 'Current fee exceeds entered "from" amount.',
ZeroPrice = 'Quoted price is zero. This is likely due to a significant price difference between the two tokens. Please try increasing amounts.',
TransferEthToContract = 'Buying native currencies using smart contract wallets is not currently supported.',
SameBuyAndSellToken = 'You are trying to buy and sell the same token.',
SellAmountDoesNotCoverFee = 'The selling amount for the order is lower than the fee.',
UNHANDLED_ERROR = 'Quote fetch failed. This may be due to a server or network connectivity issue. Please try again later.',
SellAmountDoesNotCoverFee = 'SellAmountDoesNotCoverFee',
TokenTemporarilySuspended = 'TokenTemporarilySuspended',
TradingOutsideAllowedWindow = 'TradingOutsideAllowedWindow',
UnsupportedBuyTokenDestination = 'UnsupportedBuyTokenDestination',
UnsupportedOrderType = 'UnsupportedOrderType',
UnsupportedSellTokenSource = 'UnsupportedSellTokenSource',
UnsupportedToken = 'UnsupportedToken',
}

export function mapOperatorErrorToQuoteError(error?: ApiErrorObject): QuoteApiErrorObject {
switch (error?.errorType) {
case ApiErrorCodes.NotFound:
case ApiErrorCodes.NoLiquidity:
return {
errorType: QuoteApiErrorCodes.InsufficientLiquidity,
description: QuoteApiErrorDetails.InsufficientLiquidity,
}

case ApiErrorCodes.SellAmountDoesNotCoverFee:
return {
errorType: QuoteApiErrorCodes.FeeExceedsFrom,
description: QuoteApiErrorDetails.FeeExceedsFrom,
data: error?.data,
}
/**
* Errors that are expected to happen on regular basis
*/
export const SENTRY_IGNORED_QUOTE_ERRORS = [
QuoteApiErrorCodes.InsufficientLiquidity,
QuoteApiErrorCodes.SellAmountDoesNotCoverFee,
QuoteApiErrorCodes.TokenTemporarilySuspended,
QuoteApiErrorCodes.TradingOutsideAllowedWindow,
QuoteApiErrorCodes.UnsupportedToken,
QuoteApiErrorCodes.NoLiquidity,
]

case ApiErrorCodes.UnsupportedToken:
return {
errorType: QuoteApiErrorCodes.UnsupportedToken,
description: error.description,
}
case ApiErrorCodes.TransferEthToContract:
return {
errorType: QuoteApiErrorCodes.TransferEthToContract,
description: error.description,
}
export const UNHANDLED_ERROR_CODE = 'UNHANDLED_ERROR' as const

case ApiErrorCodes.SameBuyAndSellToken:
return {
errorType: QuoteApiErrorCodes.SameBuyAndSellToken,
description: QuoteApiErrorDetails.SameBuyAndSellToken,
}

default:
return { errorType: QuoteApiErrorCodes.UNHANDLED_ERROR, description: QuoteApiErrorDetails.UNHANDLED_ERROR }
}
}
const UNHANDLED_ERROR_DESC =
'Quote fetch failed. This may be due to a server or network connectivity issue. Please try again later.'

export class QuoteApiError<Data = unknown> extends Error {
name = 'QuoteErrorObject'
type: QuoteApiErrorCodes
name = 'QuoteApiError'
type: QuoteApiErrorCodes | typeof UNHANDLED_ERROR_CODE
description: string
data?: Data

constructor(quoteError: QuoteApiErrorObject) {
super(quoteError.description)
constructor(quoteError: QuoteApiErrorObject | string) {
super(typeof quoteError === 'string' ? quoteError : quoteError.description)

if (typeof quoteError === 'string') {
this.type = UNHANDLED_ERROR_CODE
this.description = UNHANDLED_ERROR_DESC
this.message = quoteError
return
}

this.type = quoteError.errorType
this.description = quoteError.description
this.message = QuoteApiErrorDetails[quoteError.errorType]
this.message = quoteError.description
this.data = quoteError?.data as Data
}
}

export function isValidQuoteError(error: unknown): error is QuoteApiError {
return error instanceof QuoteApiError
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { OrderBookApiError } from '@cowprotocol/cow-sdk'
import type { OrderBookApiError } from '@cowprotocol/cow-sdk'

import { ApiErrorObject } from './errors/OperatorError'
import type { ApiErrorObject } from './errors/OperatorError'
import type { QuoteApiErrorObject } from './errors/QuoteError'

export type OrderBookTypedError = OrderBookApiError<ApiErrorObject>
export type QuoteApiTypedError = OrderBookApiError<QuoteApiErrorObject>

// TODO: Replace any with proper type definitions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getIsOrderBookTypedError(e: any): e is OrderBookTypedError {
const error = e as OrderBookTypedError
export function getIsOrderBookTypedError(err: unknown): err is OrderBookTypedError {
const error = err as OrderBookTypedError

if (!error?.body) return false

return error.body.errorType !== undefined && error.body.description !== undefined
}

export function getIsQuoteApiTypedError(err: unknown): err is QuoteApiTypedError {
const error = err as OrderBookTypedError

if (!error?.body) return false

Expand Down
38 changes: 33 additions & 5 deletions apps/cowswap-frontend/src/locales/en-US.po
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,10 @@ msgstr "Swaps not supported"
msgid "Some orders can not be cancelled!"
msgstr "Some orders can not be cancelled!"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
#~ msgid "The selling amount for the order is lower than the fee"
#~ msgstr "The selling amount for the order is lower than the fee"

#: apps/cowswap-frontend/src/modules/ordersTable/state/tabs/ordersTableTabs.constants.ts
#: apps/cowswap-frontend/src/modules/ordersTable/test/ordersTable.cosmos.tsx
msgid "Orders history"
Expand Down Expand Up @@ -2532,10 +2536,19 @@ msgstr "Waiting for confirmation."
#~ msgid "Aave Debt Swap"
#~ msgstr "Aave Debt Swap"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Order metadata is invalid"
msgstr "Order metadata is invalid"

#: apps/cowswap-frontend/src/pages/Account/LockedGnoVesting/index.tsx
msgid "COW token"
msgstr "COW token"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Order validity is too long"
msgstr "Order validity is too long"

#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/steps/ExpiredStep.tsx
msgid "so we can investigate the problem."
msgstr "so we can investigate the problem."
Expand Down Expand Up @@ -3246,6 +3259,10 @@ msgstr "Liquidity pools on CoW AMM grow faster than on other AMMs because they d
msgid "TWAP"
msgstr "TWAP"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Insufficient liquidity for this trade"
msgstr "Insufficient liquidity for this trade"

#: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx
#: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx
msgid "Will be updated {approxNextUpdateTimeAgo}"
Expand Down Expand Up @@ -4140,6 +4157,10 @@ msgstr "CoW Swap's robust solver competition protects your slippage from being e
msgid "Details"
msgstr "Details"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Token is temporarily suspended from trading"
msgstr "Token is temporarily suspended from trading"

#: apps/cowswap-frontend/src/legacy/state/orders/helpers.tsx
#: apps/cowswap-frontend/src/modules/orderProgressBar/pure/TransactionSubmittedContent/index.tsx
#: apps/cowswap-frontend/src/modules/orders/pure/OrderNotificationContent/index.tsx
Expand Down Expand Up @@ -6031,7 +6052,6 @@ msgid "on {chainLabel}"
msgstr "on {chainLabel}"

#: apps/cowswap-frontend/src/modules/limitOrders/containers/TradeButtons/limitOrdersTradeButtonsMap.tsx
#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Invalid price. Try increasing input/output amount."
msgstr "Invalid price. Try increasing input/output amount."

Expand Down Expand Up @@ -6149,6 +6169,10 @@ msgstr "View"
msgid "Execution price"
msgstr "Execution price"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Token can only be traded during specific time windows"
msgstr "Token can only be traded during specific time windows"

#: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx
msgid "Linked"
msgstr "Linked"
Expand Down Expand Up @@ -6560,6 +6584,10 @@ msgstr "Valid"
msgid "For this order, network costs would be"
msgstr "For this order, network costs would be"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Token pair selected has insufficient liquidity"
msgstr "Token pair selected has insufficient liquidity"

#: apps/cowswap-frontend/src/common/containers/MultipleOrdersCancellationModal/index.tsx
#: apps/cowswap-frontend/src/common/pure/CancellationModal/index.tsx
msgid "Canceling your order"
Expand Down Expand Up @@ -7143,12 +7171,12 @@ msgid "turn on expert mode"
msgstr "turn on expert mode"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Insufficient liquidity for this trade."
msgstr "Insufficient liquidity for this trade."
#~ msgid "Insufficient liquidity for this trade."
#~ msgstr "Insufficient liquidity for this trade."

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/QuoteErrorsButton/quoteErrors.utils.ts
msgid "Buying native currency with smart contract wallets is not currently supported"
msgstr "Buying native currency with smart contract wallets is not currently supported"
#~ msgid "Buying native currency with smart contract wallets is not currently supported"
#~ msgstr "Buying native currency with smart contract wallets is not currently supported"

#: apps/cowswap-frontend/src/modules/swap/containers/SwapConfirmModal/useLabelsAndTooltips.tsx
msgid "Expected to sell"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ export function SwapWidget({ topContent, bottomContent, allowSwapSameToken }: Sw
const hooksEnabledState = useHooksEnabledManager()
const isNonEvmBridging = useIsNonEvmBridging()
const { isLoading: isRateLoading, bridgeQuote, error: quoteError } = useTradeQuote()
const isFeeExceedsError = quoteError instanceof QuoteApiError && quoteError.type === QuoteApiErrorCodes.FeeExceedsFrom
const isFeeExceedsError =
quoteError instanceof QuoteApiError && quoteError.type === QuoteApiErrorCodes.SellAmountDoesNotCoverFee
const hideQuoteAmount = useShouldHideTradeRateDetails()
const priceImpact = useTradePriceImpact()
const widgetActions = useSwapWidgetActions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useIsAnyOfTokensRWA } from '@cowprotocol/tokens'

import { TradeQuoteState } from 'modules/tradeQuote'

import { isValidQuoteError, QuoteApiErrorCodes } from 'api/cowProtocol/errors/QuoteError'
import { QuoteApiError, QuoteApiErrorCodes } from 'api/cowProtocol/errors/QuoteError'

export function useTokenCustomTradeError(
inputCurrency: Currency | undefined | null,
Expand Down Expand Up @@ -34,3 +34,7 @@ function isWeekend(): boolean {
// 0 - Sunday, 6 - Saturday
return utcDay === 0 || utcDay === 6
}

function isValidQuoteError(error: unknown): error is QuoteApiError {
return error instanceof QuoteApiError
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HelpTooltip } from '@cowprotocol/ui'

import { t } from '@lingui/core/macro'

import { QuoteApiError, QuoteApiErrorCodes } from 'api/cowProtocol/errors/QuoteError'
import { QuoteApiError, QuoteApiErrorCodes, UNHANDLED_ERROR_CODE } from 'api/cowProtocol/errors/QuoteError'

import {
getBridgeQuoteErrorTexts,
Expand All @@ -22,7 +22,7 @@ export function QuoteApiErrorButton(props: TradeFormButtonContext): ReactNode {

if (!quote || !(quote.error instanceof QuoteApiError)) return null

const DEFAULT_QUOTE_ERROR = getDefaultQuoteError()
const DEFAULT_ERROR_TEXT = getDefaultQuoteError()

const quoteErrorTexts = getQuoteErrorTexts()

Expand All @@ -40,11 +40,20 @@ export function QuoteApiErrorButton(props: TradeFormButtonContext): ReactNode {
const bridgeQuoteErrorTexts = getBridgeQuoteErrorTexts()

const errorType = quote.error.type
const errorDescription = quote.error.description

if (errorType === QuoteApiErrorCodes.UnsupportedToken) {
return <UnsupportedTokenButton {...props} />
}

if (errorType === UNHANDLED_ERROR_CODE) {
return (
<TradeFormBlankButton disabled>
<>{DEFAULT_ERROR_TEXT}</>
</TradeFormBlankButton>
)
}

const isBridge = quote.isBridgeQuote
const errorText = (() => {
const quoteErrorText = quoteErrorTexts[errorType]
Expand All @@ -66,13 +75,15 @@ export function QuoteApiErrorButton(props: TradeFormButtonContext): ReactNode {
return bridgeQuoteErrorText
}

return quoteErrorText || DEFAULT_QUOTE_ERROR
return quoteErrorText ?? DEFAULT_ERROR_TEXT
})()

const errorTooltipText = isBridge && errorTooltipContentForBridges[errorType]
const errorTooltipText =
(isBridge ? errorTooltipContentForBridges[errorType] : null) ??
(errorText === DEFAULT_ERROR_TEXT ? errorDescription : null)

return (
<TradeFormBlankButton disabled={true}>
<TradeFormBlankButton disabled>
<>
{errorText}
{errorTooltipText && <HelpTooltip text={errorTooltipText} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ export function getDefaultQuoteError(): string {
return t`Error loading price. Try again later.`
}

export function getQuoteErrorTexts(): Record<QuoteApiErrorCodes, string> {
export function getQuoteErrorTexts(): Partial<Record<QuoteApiErrorCodes, string>> {
return {
[QuoteApiErrorCodes.UNHANDLED_ERROR]: getDefaultQuoteError(),
[QuoteApiErrorCodes.TransferEthToContract]: t`Buying native currency with smart contract wallets is not currently supported`,
[QuoteApiErrorCodes.AppDataHashMismatch]: t`Order metadata is invalid`,
[QuoteApiErrorCodes.InvalidAppData]: t`Order metadata is invalid`,
[QuoteApiErrorCodes.ExcessiveValidTo]: t`Order validity is too long`,
[QuoteApiErrorCodes.UnsupportedToken]: t`Unsupported token`,
[QuoteApiErrorCodes.InsufficientLiquidity]: t`Insufficient liquidity for this trade.`,
[QuoteApiErrorCodes.FeeExceedsFrom]: t`Sell amount is too small`,
[QuoteApiErrorCodes.ZeroPrice]: t`Invalid price. Try increasing input/output amount.`,
[QuoteApiErrorCodes.NoLiquidity]: t`Token pair selected has insufficient liquidity`,
[QuoteApiErrorCodes.InsufficientLiquidity]: t`Insufficient liquidity for this trade`,
[QuoteApiErrorCodes.SellAmountDoesNotCoverFee]: t`Sell amount is too small`,
[QuoteApiErrorCodes.SameBuyAndSellToken]: t`Tokens must be different`,
[QuoteApiErrorCodes.TokenTemporarilySuspended]: t`Token is temporarily suspended from trading`,
[QuoteApiErrorCodes.TradingOutsideAllowedWindow]: t`Token can only be traded during specific time windows`,
}
}
Loading
Loading