Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 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
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { t } from '@lingui/core/macro'

type ApiActionType = 'get' | 'create' | 'delete'

export interface ApiErrorObject {
errorType: ApiErrorCodes
description: string
Expand Down Expand Up @@ -96,85 +92,11 @@ export enum ApiErrorCodeDetails {
UNHANDLED_DELETE_ERROR = 'The order cancellation was not accepted by the network.',
}

// TODO: Add proper return type annotation
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function _mapActionToErrorDetail(action?: ApiActionType) {
switch (action) {
case 'get':
return ApiErrorCodeDetails.UNHANDLED_GET_ERROR
case 'create':
return ApiErrorCodeDetails.UNHANDLED_CREATE_ERROR
case 'delete':
return ApiErrorCodeDetails.UNHANDLED_DELETE_ERROR
default:
console.error(
'[OperatorError::_mapActionToErrorDetails] Uncaught error mapping error action type to server error. Please try again later.',
)
return `Something failed. Please try again later.`
}
}

export default class OperatorError extends Error {
export class OperatorError extends Error {
name = 'OperatorError'
type: ApiErrorCodes
description: ApiErrorObject['description']

// Status 400 errors
// https://github.com/cowprotocol/services/blob/9014ae55412a356e46343e051aefeb683cc69c41/orderbook/openapi.yml#L563
static apiErrorDetails = ApiErrorCodeDetails

public static getErrorMessage(orderPostError: ApiErrorObject, action: ApiActionType): string {
try {
if (orderPostError.errorType) {
const errorMessage = OperatorError.apiErrorDetails[orderPostError.errorType]
// shouldn't fall through as this error constructor expects the error code to exist but just in case
return errorMessage || orderPostError.errorType
} else {
console.error('Unknown reason for bad order submission', orderPostError)
return orderPostError.description
}
} catch {
console.error('Error handling a 400 error. Likely a problem deserialising the JSON response')
return _mapActionToErrorDetail(action)
}
}
// TODO: Reduce function complexity by extracting logic
// eslint-disable-next-line complexity
static getErrorFromStatusCode(statusCode: number, errorObject: ApiErrorObject, action: 'create' | 'delete'): string {
switch (statusCode) {
case 400:
case 404:
return this.getErrorMessage(errorObject, action)

case 403: {
const acceptedText = t`accepted`
const cancelledText = t`cancelled`
const statusText = action === 'create' ? acceptedText : cancelledText
return t`The order cannot be ${statusText}. Your account is deny-listed.`
}

case 429: {
const placementsText = t`accepted. Too many order placements`
const cancellationsText = t`cancelled. Too many order cancellations`
const msg = action === 'create' ? placementsText : cancellationsText
return t`The order cannot be ${msg}. Please, retry in a minute`
}

case 500:
default: {
console.error(
`[OperatorError::getErrorFromStatusCode] Error ${
action === 'create' ? 'creating' : 'cancelling'
} the order, status code:`,
statusCode || 'unknown',
)
const creatingText = t`creating`
const cancellingText = t`cancelling`
const verb = action === 'create' ? creatingText : cancellingText
return t`Error ${verb} the order`
}
}
}
constructor(apiError: ApiErrorObject) {
super(apiError.description)

Expand All @@ -185,7 +107,3 @@ export default class OperatorError extends Error {
this.message = message === this.type.toString() ? this.description : message
}
}

export function isValidOperatorError(error: unknown): error is OperatorError {
return error instanceof OperatorError
}
167 changes: 49 additions & 118 deletions apps/cowswap-frontend/src/api/cowProtocol/errors/QuoteError.ts
Original file line number Diff line number Diff line change
@@ -1,137 +1,68 @@
import { ApiErrorCodes, ApiErrorObject } from './OperatorError'

export interface QuoteApiErrorObject {
errorType: QuoteApiErrorCodes
description: string
// TODO: Replace any with proper type definitions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any
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.',
}

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,
}

case ApiErrorCodes.UnsupportedToken:
return {
errorType: QuoteApiErrorCodes.UnsupportedToken,
description: error.description,
}
case ApiErrorCodes.TransferEthToContract:
return {
errorType: QuoteApiErrorCodes.TransferEthToContract,
description: error.description,
}

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

default:
return { errorType: QuoteApiErrorCodes.UNHANDLED_ERROR, description: QuoteApiErrorDetails.UNHANDLED_ERROR }
}
SellAmountDoesNotCoverFee = 'SellAmountDoesNotCoverFee',
TokenTemporarilySuspended = 'TokenTemporarilySuspended',
TradingOutsideAllowedWindow = 'TradingOutsideAllowedWindow',
UnsupportedBuyTokenDestination = 'UnsupportedBuyTokenDestination',
UnsupportedOrderType = 'UnsupportedOrderType',
UnsupportedSellTokenSource = 'UnsupportedSellTokenSource',
UnsupportedToken = 'UnsupportedToken',
}

export class QuoteApiError extends Error {
name = 'QuoteErrorObject'
type: QuoteApiErrorCodes
/**
* 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,
]

const UNHANDLED_ERROR_CODE = 'UNHANDLED_ERROR' as const

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 = 'QuoteApiError'
type: QuoteApiErrorCodes | typeof UNHANDLED_ERROR_CODE
description: string
// any data attached
// TODO: Replace any with proper type definitions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any

// Status 400 errors
// https://github.com/cowprotocol/services/blob/9014ae55412a356e46343e051aefeb683cc69c41/orderbook/openapi.yml#L563
static quoteErrorDetails = QuoteApiErrorDetails
data?: Data

// TODO: Add proper return type annotation
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
public static async getErrorMessage(response: Response) {
try {
const orderPostError: QuoteApiErrorObject = await response.json()
constructor(quoteError: QuoteApiErrorObject | string) {
super(typeof quoteError === 'string' ? quoteError : quoteError.description)

if (orderPostError.errorType) {
const errorMessage = QuoteApiError.quoteErrorDetails[orderPostError.errorType]
// shouldn't fall through as this error constructor expects the error code to exist but just in case
return errorMessage || orderPostError.errorType
} else {
console.error('Unknown reason for bad quote fetch', orderPostError)
return orderPostError.description
}
} catch {
console.error('Error handling 400/404 error. Likely a problem deserialising the JSON response')
return QuoteApiError.quoteErrorDetails.UNHANDLED_ERROR
if (typeof quoteError === 'string') {
this.type = UNHANDLED_ERROR_CODE
this.description = UNHANDLED_ERROR_DESC
this.message = quoteError
return
}
}

// TODO: Add proper return type annotation
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
static async getErrorFromStatusCode(response: Response) {
switch (response.status) {
case 400:
case 404:
return this.getErrorMessage(response)

case 500:
default:
console.error(
'[QuoteError::getErrorFromStatusCode] Error fetching quote, status code:',
response.status || 'unknown',
)
return 'Error fetching quote'
}
}

constructor(quoteError: QuoteApiErrorObject) {
super(quoteError.description)

this.type = quoteError.errorType
this.description = quoteError.description
this.message = QuoteApiError.quoteErrorDetails[quoteError.errorType]
this.data = quoteError?.data
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,12 +1,20 @@
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 {
export function getIsOrderBookTypedError(e: unknown): e is OrderBookTypedError {
Comment thread
shoom3301 marked this conversation as resolved.
Outdated
const error = e as OrderBookTypedError

if (!error?.body) return false

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

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

if (!error?.body) return false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { capitalizeFirstLetter, getProviderErrorMessage, isRejectRequestProviderError } from '@cowprotocol/common-utils'

import { isValidOperatorError } from 'api/cowProtocol/errors/OperatorError'
import { OperatorError } from 'api/cowProtocol/errors/OperatorError'

export const USER_SWAP_REJECTED_ERROR = 'User rejected signing the order'

Expand All @@ -17,3 +17,7 @@ export function getSwapErrorMessage(error: Error): string {
return defaultErrorMessage
}
}

function isValidOperatorError(error: unknown): error is OperatorError {
return error instanceof OperatorError
}
2 changes: 1 addition & 1 deletion apps/cowswap-frontend/src/legacy/utils/trade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ChangeOrderStatusParams, Order, OrderStatus } from 'legacy/state/orders
import { AppDataInfo } from 'modules/appData'

import { getIsOrderBookTypedError } from 'api/cowProtocol'
import OperatorError, { ApiErrorObject } from 'api/cowProtocol/errors/OperatorError'
import { ApiErrorObject, OperatorError } from 'api/cowProtocol/errors/OperatorError'

import type { WalletClient } from 'viem'

Expand Down
Loading
Loading