From f0a7af12f1a53f36580e4a0e8b608baffc9811aa Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Thu, 2 Jul 2026 19:47:47 +0530 Subject: [PATCH 1/4] fix(twap): label open orders as unfillable when Safe fallback handler is reset When a Safe's ComposableCoW fallback handler is removed or reset, open TWAP orders can no longer be executed by the watchtower, but the orders table still showed them as pending. Thread the existing fallback-handler verification into the order status build so open orders (and their still-open parts) surface the existing Unfillable badge. Closes #5426 --- .../entities/twap/services/mapTwapOrderToStoreOrder.ts | 1 + apps/cowswap-frontend/src/modules/twap/types.ts | 3 +++ .../src/modules/twap/updaters/TwapOrdersUpdater.tsx | 8 ++++++++ .../src/modules/twap/utils/buildTwapOrdersItems.ts | 9 +++++++++ .../src/modules/twap/utils/mapPartOrderToStoreOrder.ts | 6 +++++- 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/entities/twap/services/mapTwapOrderToStoreOrder.ts b/apps/cowswap-frontend/src/entities/twap/services/mapTwapOrderToStoreOrder.ts index 3403b150b6e..da6cea2c08b 100644 --- a/apps/cowswap-frontend/src/entities/twap/services/mapTwapOrderToStoreOrder.ts +++ b/apps/cowswap-frontend/src/entities/twap/services/mapTwapOrderToStoreOrder.ts @@ -37,5 +37,6 @@ export function mapTwapOrderToStoreOrder(order: TwapOrderItem, tokensByAddress: status, apiAdditionalInfo: enrichedOrder, isCancelling: order.status === TwapOrderStatus.Cancelling, + isUnfillable: order.isUnfillable, } } diff --git a/apps/cowswap-frontend/src/modules/twap/types.ts b/apps/cowswap-frontend/src/modules/twap/types.ts index ab9a157c1b3..c389878ff82 100644 --- a/apps/cowswap-frontend/src/modules/twap/types.ts +++ b/apps/cowswap-frontend/src/modules/twap/types.ts @@ -43,6 +43,9 @@ export interface TwapOrderItem { id: string safeTxParams?: SafeTransactionParams executionInfo: TwapOrdersExecution + // True when an open order cannot be executed because the Safe's ComposableCoW + // fallback handler is missing/reset by the user (see issue #5426) + isUnfillable?: boolean } export type TwapOrdersAuthResult = { [key: string]: boolean | undefined } diff --git a/apps/cowswap-frontend/src/modules/twap/updaters/TwapOrdersUpdater.tsx b/apps/cowswap-frontend/src/modules/twap/updaters/TwapOrdersUpdater.tsx index 6c98b0fe730..90e93daf7e4 100644 --- a/apps/cowswap-frontend/src/modules/twap/updaters/TwapOrdersUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/twap/updaters/TwapOrdersUpdater.tsx @@ -12,6 +12,7 @@ import type { ComposableCowContractData } from 'modules/advancedOrders' import { TWAP_PENDING_STATUSES } from '../const' import { useAllTwapOrdersInfo } from '../hooks/useAllTwapOrdersInfo' +import { useIsFallbackHandlerRequired } from '../hooks/useFallbackHandlerVerification' import { useFetchTwapOrdersFromSafe } from '../hooks/useFetchTwapOrdersFromSafe' import { useTwapOrdersAuthMulticall } from '../hooks/useTwapOrdersAuthMulticall' import { useTwapOrdersExecutions } from '../hooks/useTwapOrdersExecutions' @@ -52,6 +53,12 @@ export function TwapOrdersUpdater(props: { // eslint-disable-next-line react-hooks/refs twapOrderExecutions.current = _twapOrderExecutions + // When the Safe's fallback handler is missing/reset, open orders will never execute + const isFallbackHandlerRequired = useIsFallbackHandlerRequired() + const isFallbackHandlerRequiredRef = useRef(isFallbackHandlerRequired) + // eslint-disable-next-line react-hooks/refs + isFallbackHandlerRequiredRef.current = isFallbackHandlerRequired + // Here we can split all orders in two groups: 1. Not signed + expired, 2. Open + cancelled const pendingTwapOrderIds = useMemo(() => { // eslint-disable-next-line react-hooks/refs @@ -96,6 +103,7 @@ export function TwapOrdersUpdater(props: { allOrdersInfo, ordersAuthResult, twapOrderExecutions.current, + isFallbackHandlerRequiredRef.current, ) /** diff --git a/apps/cowswap-frontend/src/modules/twap/utils/buildTwapOrdersItems.ts b/apps/cowswap-frontend/src/modules/twap/utils/buildTwapOrdersItems.ts index 2439960ff8a..fec41232b9a 100644 --- a/apps/cowswap-frontend/src/modules/twap/utils/buildTwapOrdersItems.ts +++ b/apps/cowswap-frontend/src/modules/twap/utils/buildTwapOrdersItems.ts @@ -13,6 +13,7 @@ import { type TwapOrdersAuthResult, type TwapOrdersExecution, type TwapOrdersSafeData, + TwapOrderStatus, } from '../types' import type { Hex } from 'viem' @@ -23,6 +24,7 @@ export function buildTwapOrdersItems( ordersInfo: TwapOrderInfo[], ordersAuthResult: TwapOrdersAuthResult, twapOrderExecutions: TwapOrdersExecutionMap, + isFallbackHandlerBroken: boolean, ): TwapOrdersList { return ordersInfo.reduce((acc, { safeData, id }) => { acc[id] = getTwapOrderItem( @@ -32,6 +34,7 @@ export function buildTwapOrdersItems( id as `0x${string}`, ordersAuthResult[id], twapOrderExecutions[id] ?? DEFAULT_TWAP_EXECUTION, + isFallbackHandlerBroken, ) return acc }, {}) @@ -44,6 +47,7 @@ function getTwapOrderItem( id: Hex, authorized: boolean | undefined, executionInfo: TwapOrdersExecution, + isFallbackHandlerBroken: boolean, ): TwapOrderItem { const { conditionalOrderParams, safeTxParams } = safeData const { isExecuted, submissionDate, executionDate: _executionDate } = safeTxParams @@ -52,6 +56,10 @@ function getTwapOrderItem( const order = parseTwapOrderStruct(conditionalOrderParams.staticInput as `0x${string}`) const status = getTwapOrderStatus(order, isExecuted, executionDate, authorized, executionInfo) + // An open (Pending) order will never be picked up by watchtower when the Safe's + // ComposableCoW fallback handler has been reset/removed, so surface it as Unfillable. + const isUnfillable = isFallbackHandlerBroken && status === TwapOrderStatus.Pending + return { order, status, @@ -62,5 +70,6 @@ function getTwapOrderItem( executedDate: _executionDate || undefined, safeTxParams, executionInfo, + isUnfillable, } } diff --git a/apps/cowswap-frontend/src/modules/twap/utils/mapPartOrderToStoreOrder.ts b/apps/cowswap-frontend/src/modules/twap/utils/mapPartOrderToStoreOrder.ts index 6ddbcdfb760..239663353bb 100644 --- a/apps/cowswap-frontend/src/modules/twap/utils/mapPartOrderToStoreOrder.ts +++ b/apps/cowswap-frontend/src/modules/twap/utils/mapPartOrderToStoreOrder.ts @@ -1,7 +1,7 @@ import { EnrichedOrder, getAddressKey } from '@cowprotocol/cow-sdk' import { TokensByAddress } from '@cowprotocol/tokens' -import { Order } from 'legacy/state/orders/actions' +import { Order, OrderStatus } from 'legacy/state/orders/actions' import { getIsLastPartOrder } from './getIsLastPartOrder' import { getPartOrderStatus } from './getPartOrderStatus' @@ -19,6 +19,9 @@ export function mapPartOrderToStoreOrder( const isCancelling = item.isCancelling || parent.status === TwapOrderStatus.Cancelling const status = getPartOrderStatus(enrichedOrder, parent, isVirtualPart) + // A broken fallback handler blocks the whole order, so mark still-open parts as unfillable too + const isUnfillable = !!parent.isUnfillable && (status === OrderStatus.PENDING || status === OrderStatus.SCHEDULED) + const inputToken = tokensByAddress[getAddressKey(enrichedOrder.sellToken)] const outputToken = tokensByAddress[getAddressKey(enrichedOrder.buyToken)] @@ -39,5 +42,6 @@ export function mapPartOrderToStoreOrder( status, apiAdditionalInfo: enrichedOrder, isCancelling, + isUnfillable, } } From 4b3f47f39e90aba23d3379188a45bccffa3bb46b Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Thu, 2 Jul 2026 23:01:36 +0530 Subject: [PATCH 2/4] fix(twap): show unfillable order in Unfillable tab and Fills-at column Addresses @elena-zh's review feedback: - Categorize composable (TWAP) orders into the Unfillable tab by respecting their derived isUnfillable flag in useOrdersTableList, instead of recomputing it purely from balance/allowance (which also overwrote the flag). Regular orders keep their existing balance-based behaviour. - Show an 'Unfillable' indicator with an explanatory tooltip in the 'Fills at' column for affected TWAP orders instead of 'pending execution'. - Rename the threaded flag to isFallbackHandlerBroken for consistency across layers. --- .../containers/OrderRow/OrderRow.styled.tsx | 10 ++++++ .../ordersTable/hooks/useOrdersTableList.ts | 13 ++++++-- .../TwapOrderStatus/TwapOrderStatus.pure.tsx | 32 ++++++++++++++++++- .../twap/updaters/TwapOrdersUpdater.tsx | 10 +++--- 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.styled.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.styled.tsx index 41c19b8d514..6ad76330f66 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.styled.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.styled.tsx @@ -195,6 +195,16 @@ export const CancelledDisplay = styled.div` font-size: 12px; ` +export const UnfillableDisplay = styled.div` + display: flex; + align-items: center; + gap: 4px; + cursor: help; + color: var(${UI.COLOR_DANGER}); + font-weight: 500; + font-size: 12px; +` + export const ExpiredDisplay = styled.div` display: flex; align-items: center; diff --git a/apps/cowswap-frontend/src/modules/ordersTable/hooks/useOrdersTableList.ts b/apps/cowswap-frontend/src/modules/ordersTable/hooks/useOrdersTableList.ts index 0e32cf06d09..84eecf5da10 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/hooks/useOrdersTableList.ts +++ b/apps/cowswap-frontend/src/modules/ordersTable/hooks/useOrdersTableList.ts @@ -57,11 +57,19 @@ export function useOrdersTableList( const isPending = PENDING_STATES.includes(order.status) const isSigning = order.status === OrderStatus.PRESIGNATURE_PENDING + // Composable (TWAP) orders are emulated and carry a derived `isUnfillable` flag + // (e.g. Safe fallback handler reset). Regular orders derive it from balance/allowance here. + const isComposableOrder = getIsComposableCowOrder(order) // Check if order is unfillable (insufficient balance or allowance) const params = getOrderParams(chainId, balancesAndAllowances, order, pendingOrdersPermitValidityState) let isUnfillable = params.hasEnoughBalance === false || params.hasEnoughAllowance === false + // Respect the derived unfillable flag already computed for composable orders + if (isComposableOrder && order.isUnfillable) { + isUnfillable = true + } + // For TWAP orders, also check child orders if (!isParsedOrder(item) && item.children) { const hasUnfillableChild = item.children.some((childOrder) => { @@ -80,8 +88,9 @@ export function useOrdersTableList( isUnfillable = isUnfillable || hasUnfillableChild } - // Update the unfillable flag whenever the state changes, not just when becoming unfillable - if (isPending && order.isUnfillable !== isUnfillable) { + // Persist the recomputed flag for regular orders only; composable orders derive it from + // their emulated state, so writing it back here would fight that source. + if (isPending && !isComposableOrder && order.isUnfillable !== isUnfillable) { setIsOrderUnfillable({ chainId, id: order.id, isUnfillable }) } diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx index 49aeedc2e03..3926acef706 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx @@ -1,7 +1,9 @@ import React, { ReactNode } from 'react' +import { HoverTooltip } from '@cowprotocol/ui' + import { Trans } from '@lingui/react/macro' -import { Check, Clock, X } from 'react-feather' +import { AlertTriangle, Check, Clock, X } from 'react-feather' import { OrderStatus } from 'legacy/state/orders/actions' @@ -39,6 +41,34 @@ export function TwapOrderStatus({ childOrders, orderStatus, children }: FillsAtS ) } + // An open order is unfillable when it can no longer be executed (e.g. the Safe's ComposableCoW + // fallback handler was reset). Surface it instead of the default "pending execution" display. + const isUnfillable = childOrders.some((childOrder) => childOrder.isUnfillable) + + if (isUnfillable) { + return ( + <> + + + This order can’t be executed at the moment (for example, the Safe’s fallback handler was reset), so it + won’t be filled until the issue is resolved. + + } + > + + + Unfillable + + + + + + ) + } + // Third priority: Check for scheduled orders const hasScheduledOrder = childOrders.some((childOrder) => childOrder.status === OrderStatus.SCHEDULED) diff --git a/apps/cowswap-frontend/src/modules/twap/updaters/TwapOrdersUpdater.tsx b/apps/cowswap-frontend/src/modules/twap/updaters/TwapOrdersUpdater.tsx index 90e93daf7e4..a59736c809a 100644 --- a/apps/cowswap-frontend/src/modules/twap/updaters/TwapOrdersUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/twap/updaters/TwapOrdersUpdater.tsx @@ -53,11 +53,11 @@ export function TwapOrdersUpdater(props: { // eslint-disable-next-line react-hooks/refs twapOrderExecutions.current = _twapOrderExecutions - // When the Safe's fallback handler is missing/reset, open orders will never execute - const isFallbackHandlerRequired = useIsFallbackHandlerRequired() - const isFallbackHandlerRequiredRef = useRef(isFallbackHandlerRequired) + // When the Safe's fallback handler is missing/reset, open orders will never execute (they are "broken") + const isFallbackHandlerBroken = useIsFallbackHandlerRequired() + const isFallbackHandlerBrokenRef = useRef(isFallbackHandlerBroken) // eslint-disable-next-line react-hooks/refs - isFallbackHandlerRequiredRef.current = isFallbackHandlerRequired + isFallbackHandlerBrokenRef.current = isFallbackHandlerBroken // Here we can split all orders in two groups: 1. Not signed + expired, 2. Open + cancelled const pendingTwapOrderIds = useMemo(() => { @@ -103,7 +103,7 @@ export function TwapOrdersUpdater(props: { allOrdersInfo, ordersAuthResult, twapOrderExecutions.current, - isFallbackHandlerRequiredRef.current, + isFallbackHandlerBrokenRef.current, ) /** From e44a66c333ab5932ca2c961a500b12d60cd07618 Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Mon, 6 Jul 2026 12:29:37 +0530 Subject: [PATCH 3/4] fix(twap): show unfillable reason and failure design for reset fallback handler Reuse the existing balance/allowance unfillable design for open TWAP orders blocked by a reset Safe ComposableCoW fallback handler: - Fills-at column now shows an "Update fallback handler" reason (via the shared OrderEstimatedExecutionPrice label) instead of "Pending execution"/"Unfillable", for the parent order and its still-open parts. - Status badge shows the danger warning icon with a tooltip explaining the reset fallback handler (FallbackHandlerWarningTooltip), matching the other warnings. - Thread the fallback-handler reason through OrderRow so parent and parts render the same reason and tooltip. Copy matches the existing SetupFallbackHandlerWarning strings, so no new catalog entries are added. Adds unit tests for the reason label and tooltip content. --- apps/cowswap-frontend/src/locales/en-US.po | 5 ++ .../OrderRow/OrderRow.container.tsx | 41 ++++++++---- .../containers/OrderRow/OrderRow.styled.tsx | 10 --- ...OrderEstimatedExecutionPrice.pure.test.tsx | 64 ++++++++++++++++++ .../OrderEstimatedExecutionPrice.pure.tsx | 34 +++++++++- .../WarningTooltip/WarningTooltip.pure.tsx | 35 ++++++++++ .../TwapOrderStatus.pure.test.tsx | 65 +++++++++++++++++++ .../TwapOrderStatus/TwapOrderStatus.pure.tsx | 30 ++++----- 8 files changed, 243 insertions(+), 41 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx create mode 100644 apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.test.tsx diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index 1334d7e91b5..1757d5a2423 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -84,6 +84,8 @@ msgstr "Because you are using a smart contract wallet, you will pay a separate g #~ msgid "cancelled. Too many order cancellations" #~ msgstr "cancelled. Too many order cancellations" +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx #: apps/cowswap-frontend/src/modules/twap/containers/SetupFallbackHandlerWarning/index.tsx msgid "Your Safe fallback handler was changed after TWAP orders were placed. All open TWAP orders are not getting created because of that. Please, update the fallback handler in order to make the orders work again." msgstr "Your Safe fallback handler was changed after TWAP orders were placed. All open TWAP orders are not getting created because of that. Please, update the fallback handler in order to make the orders work again." @@ -1162,6 +1164,7 @@ msgstr "Switch Network" #~ msgid "Invalid code" #~ msgstr "Invalid code" +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx #: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTabs/OrdersTabs.pure.tsx msgid "warning" @@ -1716,6 +1719,8 @@ msgstr "Deprecated Network" #~ msgid "Limit price (incl.costs)" #~ msgstr "Limit price (incl.costs)" +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx +#: apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure.tsx #: apps/cowswap-frontend/src/modules/twap/containers/SetupFallbackHandlerWarning/index.tsx msgid "Update fallback handler" msgstr "Update fallback handler" diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx index 46be608facd..7adcf0b5301 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx @@ -30,6 +30,7 @@ import { TableRow } from './OrderRow.styled' import { usePricesDifference } from '../../hooks/usePricesDifference' import { OrderContextMenu } from '../../pure/ContextMenu/OrderContextMenu.pure' import { CurrencyAmountItem } from '../../pure/CurrencyAmountItem/CurrencyAmountItem.pure' +import { UPDATE_FALLBACK_HANDLER_WARNING } from '../../pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' import { OrderFillsAt } from '../../pure/OrderFillsAt/OrderFillsAt.pure' import { OrderFillsAtWithDistance } from '../../pure/OrderFillsAtWithDistance/OrderFillsAtWithDistance.pure' import { OrderMarketPrice } from '../../pure/OrderMarketPrice/OrderMarketPrice.pure' @@ -39,7 +40,10 @@ import { TableRowCheckboxWrapper, } from '../../pure/OrdersTable/Row/Checkbox/Checkbox.styled' import { OrderRowWarningEstimatedPrice } from '../../pure/OrdersTable/Row/WarningEstimatedPrice/OrderRowWarningEstimatedPrice.pure' -import { WarningTooltip } from '../../pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure' +import { + FallbackHandlerWarningTooltip, + WarningTooltip, +} from '../../pure/OrdersTable/Row/WarningTooltip/WarningTooltip.pure' import { OrderStatusBox } from '../../pure/OrderStatusBox/OrderStatusBox.pure' import { OrderActions } from '../../state/ordersTable.types' import { OrderParams } from '../../utils/getOrderParams' @@ -148,13 +152,22 @@ export function OrderRow({ const isExecutedPriceZero = executedPriceInverted !== undefined && executedPriceInverted?.equalTo(ZERO_FRACTION) - const isUnfillable = !percentIsAlmostHundred(filledPercentDisplay) && (isExecutedPriceZero || withWarning) + // A still-open order (or open part) carrying the derived `isUnfillable` flag is blocked by a reset + // Safe ComposableCoW fallback handler (see issue #5426); surface the "Update fallback handler" + // reason with the same danger design as the balance/allowance warnings. + const isFallbackHandlerUnfillable = + order.isUnfillable === true && (status === OrderStatus.PENDING || status === OrderStatus.SCHEDULED) + + const isUnfillable = + isFallbackHandlerUnfillable || + (!percentIsAlmostHundred(filledPercentDisplay) && (isExecutedPriceZero || withWarning)) const inputTokenSymbol = order.inputToken.symbol || '' // NOTE: Don't internationalize this, the text is being used as a flag... - const warningText = - hasEnoughBalance === false + const warningText = isFallbackHandlerUnfillable + ? UPDATE_FALLBACK_HANDLER_WARNING + : hasEnoughBalance === false ? `Insufficient balance` : hasEnoughAllowance === false ? `Insufficient allowance` @@ -346,16 +359,20 @@ export function OrderRow({ orderActions.approveOrderToken(order.inputToken)} - /> + isFallbackHandlerUnfillable ? ( + + ) : ( + orderActions.approveOrderToken(order.inputToken)} + /> + ) } /> diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.styled.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.styled.tsx index 6ad76330f66..41c19b8d514 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.styled.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.styled.tsx @@ -195,16 +195,6 @@ export const CancelledDisplay = styled.div` font-size: 12px; ` -export const UnfillableDisplay = styled.div` - display: flex; - align-items: center; - gap: 4px; - cursor: help; - color: var(${UI.COLOR_DANGER}); - font-weight: 500; - font-size: 12px; -` - export const ExpiredDisplay = styled.div` display: flex; align-items: center; diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx new file mode 100644 index 00000000000..998176b1ede --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx @@ -0,0 +1,64 @@ +import { ReactNode } from 'react' + +import { i18n } from '@lingui/core' +import { I18nProvider } from '@lingui/react' +import { fireEvent, render, screen } from '@testing-library/react' + +import { OrderEstimatedExecutionPrice, UPDATE_FALLBACK_HANDLER_WARNING } from './OrderEstimatedExecutionPrice.pure' + +jest.mock('react-inlinesvg', () => { + return function MockSvg() { + return + } +}) + +function renderWithI18n(ui: ReactNode): ReturnType { + return render({ui}) +} + +beforeAll(() => { + window.matchMedia = + window.matchMedia || + (() => + ({ + matches: false, + addEventListener: () => undefined, + removeEventListener: () => undefined, + addListener: () => undefined, + removeListener: () => undefined, + }) as unknown as MediaQueryList) +}) + +describe('OrderEstimatedExecutionPrice() – fallback handler warning', () => { + it('renders the "Update fallback handler" reason label', () => { + renderWithI18n( + , + ) + + expect(screen.getByText('Update fallback handler')).not.toBeNull() + }) + + it('reveals the fallback handler explanation on hover', async () => { + renderWithI18n( + , + ) + + fireEvent.mouseEnter(screen.getByText('Update fallback handler')) + + expect(await screen.findByText(/Your Safe fallback handler was changed/i)).not.toBeNull() + }) +}) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx index b7a54e3bf5c..93bc2320949 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx @@ -21,6 +21,10 @@ import * as warningTooltopEl from '../OrdersTable/Row/WarningTooltip/WarningTool const MINUS_ONE_FRACTION = new Fraction(-1) +// warningText flag (not internationalized on purpose, matches the other flags): an open TWAP order +// whose Safe ComposableCoW fallback handler was reset can no longer be created (see issue #5426). +export const UPDATE_FALLBACK_HANDLER_WARNING = 'Update fallback handler' + export interface OrderEstimatedExecutionPriceProps extends TokenAmountProps { amountDifference?: CurrencyAmount amountFee?: CurrencyAmount @@ -101,8 +105,11 @@ export function OrderEstimatedExecutionPrice({ return () => clearTimeout(timeout) }, [approveClicked]) - const internationalizedWarningText = - warningText === 'Insufficient balance' + const isFallbackHandlerWarning = warningText === UPDATE_FALLBACK_HANDLER_WARNING + + const internationalizedWarningText = isFallbackHandlerWarning + ? t`Update fallback handler` + : warningText === 'Insufficient balance' ? t`Insufficient balance` : warningText === 'Insufficient allowance' ? t`Insufficient allowance` @@ -110,6 +117,29 @@ export function OrderEstimatedExecutionPrice({ const unfillableLabel = ( + {isFallbackHandlerWarning && ( + +

{internationalizedWarningText}

+

+ + Your Safe fallback handler was changed after TWAP orders were placed. All open TWAP orders are not + getting created because of that. Please, update the fallback handler in order to make the orders work + again. + +

+ + } + bgColor={`var(${UI.COLOR_DANGER_BG})`} + color={`var(${UI.COLOR_DANGER_TEXT})`} + > + + + {internationalizedWarningText} + +
+ )} {(warningText === 'Insufficient allowance' || warningText === 'Insufficient balance') && ( <> ) } + +// Shown on the status badge of a TWAP order (and its open parts) whose Safe ComposableCoW fallback +// handler was reset, so open orders can no longer be created (see issue #5426). +// TODO: Add proper return type annotation +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function FallbackHandlerWarningTooltip({ children }: { children?: ReactNode }) { + const tooltipContent = ( + + +

+ Update fallback handler +

+

+ + Your Safe fallback handler was changed after TWAP orders were placed. All open TWAP orders are not getting + created because of that. Please, update the fallback handler in order to make the orders work again. + +

+
+
+ ) + + return ( + + } + /> + {children} + + ) +} diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.test.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.test.tsx new file mode 100644 index 00000000000..6b89e3ae07b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.test.tsx @@ -0,0 +1,65 @@ +import { render, screen } from '@testing-library/react' + +import { OrderStatus } from 'legacy/state/orders/actions' + +import { ParsedOrder } from 'utils/orderUtils/parseOrder' + +import { TwapOrderStatus } from './TwapOrderStatus.pure' + +jest.mock('react-inlinesvg', () => { + return function MockSvg() { + return + } +}) + +beforeAll(() => { + window.matchMedia = + window.matchMedia || + (() => + ({ + matches: false, + addEventListener: () => undefined, + removeEventListener: () => undefined, + addListener: () => undefined, + removeListener: () => undefined, + }) as unknown as MediaQueryList) +}) + +function childOrder(overrides: Partial): ParsedOrder { + return { + status: OrderStatus.SCHEDULED, + isUnfillable: false, + executionData: { filledPercentDisplay: '0' }, + ...overrides, + } as unknown as ParsedOrder +} + +describe('TwapOrderStatus()', () => { + it('shows the "Update fallback handler" reason when an open part is unfillable', () => { + render( + + fallback children + , + ) + + expect(screen.getByText('Update fallback handler')).not.toBeNull() + expect(screen.queryByText('fallback children')).toBeNull() + }) + + it('renders the passed children when no part is unfillable', () => { + render( + + fallback children + , + ) + + expect(screen.queryByText('Update fallback handler')).toBeNull() + expect(screen.getByText('fallback children')).not.toBeNull() + }) +}) diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx index 3926acef706..28302b17d4f 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx @@ -1,9 +1,7 @@ import React, { ReactNode } from 'react' -import { HoverTooltip } from '@cowprotocol/ui' - import { Trans } from '@lingui/react/macro' -import { AlertTriangle, Check, Clock, X } from 'react-feather' +import { Check, Clock, X } from 'react-feather' import { OrderStatus } from 'legacy/state/orders/actions' @@ -11,6 +9,10 @@ import { ParsedOrder } from 'utils/orderUtils/parseOrder' // TODO: make CancelledDisplay, FilledDisplay, ExpiredDisplay common import * as styledEl from '../../containers/OrderRow/OrderRow.styled' +import { + OrderEstimatedExecutionPrice, + UPDATE_FALLBACK_HANDLER_WARNING, +} from '../OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' export interface FillsAtStatusProps { childOrders?: ParsedOrder[] @@ -49,20 +51,14 @@ export function TwapOrderStatus({ childOrders, orderStatus, children }: FillsAtS return ( <> - - This order can’t be executed at the moment (for example, the Safe’s fallback handler was reset), so it - won’t be filled until the issue is resolved. - - } - > - - - Unfillable - - + From 6db4785479cdf98c0690b7bf39e8a451e617f347 Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Tue, 7 Jul 2026 17:59:26 +0530 Subject: [PATCH 4/4] fix(twap): surface fallback-handler warning on parent TWAP status badge The parent TWAP status badge only reflected child balance/allowance warnings, so an order blocked by a reset Safe ComposableCoW fallback handler still showed a plain status with no reason. Derive the fallback-handler-unfillable state from the parent or any still-open part and render the same danger design and FallbackHandlerWarningTooltip already used on the parts and Fills-at column. Also move UPDATE_FALLBACK_HANDLER_WARNING into a dedicated constants module so the component file no longer exports a non-component value. --- .../containers/OrderRow/OrderRow.container.tsx | 2 +- .../OrderEstimatedExecutionPrice.pure.test.tsx | 3 ++- .../OrderEstimatedExecutionPrice.pure.tsx | 5 +---- .../orderEstimatedExecutionPrice.constants.ts | 3 +++ .../TwapOrderStatus/TwapOrderStatus.pure.tsx | 6 ++---- .../TwapStatusAndToggle.pure.tsx | 17 ++++++++++++++--- 6 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants.ts diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx index 7adcf0b5301..b61cc66ec40 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrderRow/OrderRow.container.tsx @@ -30,7 +30,7 @@ import { TableRow } from './OrderRow.styled' import { usePricesDifference } from '../../hooks/usePricesDifference' import { OrderContextMenu } from '../../pure/ContextMenu/OrderContextMenu.pure' import { CurrencyAmountItem } from '../../pure/CurrencyAmountItem/CurrencyAmountItem.pure' -import { UPDATE_FALLBACK_HANDLER_WARNING } from '../../pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' +import { UPDATE_FALLBACK_HANDLER_WARNING } from '../../pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants' import { OrderFillsAt } from '../../pure/OrderFillsAt/OrderFillsAt.pure' import { OrderFillsAtWithDistance } from '../../pure/OrderFillsAtWithDistance/OrderFillsAtWithDistance.pure' import { OrderMarketPrice } from '../../pure/OrderMarketPrice/OrderMarketPrice.pure' diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx index 998176b1ede..21ef199e9ad 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.test.tsx @@ -4,7 +4,8 @@ import { i18n } from '@lingui/core' import { I18nProvider } from '@lingui/react' import { fireEvent, render, screen } from '@testing-library/react' -import { OrderEstimatedExecutionPrice, UPDATE_FALLBACK_HANDLER_WARNING } from './OrderEstimatedExecutionPrice.pure' +import { UPDATE_FALLBACK_HANDLER_WARNING } from './orderEstimatedExecutionPrice.constants' +import { OrderEstimatedExecutionPrice } from './OrderEstimatedExecutionPrice.pure' jest.mock('react-inlinesvg', () => { return function MockSvg() { diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx index 93bc2320949..1093b0d77d4 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure.tsx @@ -14,6 +14,7 @@ import { Nullish } from 'types' import { HIGH_FEE_WARNING_PERCENTAGE, PENDING_EXECUTION_THRESHOLD_PERCENTAGE } from 'common/constants/common' +import { UPDATE_FALLBACK_HANDLER_WARNING } from './orderEstimatedExecutionPrice.constants' import * as styledEl from './OrderEstimatedExecutionPrice.styled' import * as orderRowEl from '../../containers/OrderRow/OrderRow.styled' @@ -21,10 +22,6 @@ import * as warningTooltopEl from '../OrdersTable/Row/WarningTooltip/WarningTool const MINUS_ONE_FRACTION = new Fraction(-1) -// warningText flag (not internationalized on purpose, matches the other flags): an open TWAP order -// whose Safe ComposableCoW fallback handler was reset can no longer be created (see issue #5426). -export const UPDATE_FALLBACK_HANDLER_WARNING = 'Update fallback handler' - export interface OrderEstimatedExecutionPriceProps extends TokenAmountProps { amountDifference?: CurrencyAmount amountFee?: CurrencyAmount diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants.ts b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants.ts new file mode 100644 index 00000000000..abf86e9bd37 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants.ts @@ -0,0 +1,3 @@ +// warningText flag (not internationalized on purpose, matches the other flags): an open TWAP order +// whose Safe ComposableCoW fallback handler was reset can no longer be created (see issue #5426). +export const UPDATE_FALLBACK_HANDLER_WARNING = 'Update fallback handler' diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx index 28302b17d4f..900f716dde6 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx @@ -9,10 +9,8 @@ import { ParsedOrder } from 'utils/orderUtils/parseOrder' // TODO: make CancelledDisplay, FilledDisplay, ExpiredDisplay common import * as styledEl from '../../containers/OrderRow/OrderRow.styled' -import { - OrderEstimatedExecutionPrice, - UPDATE_FALLBACK_HANDLER_WARNING, -} from '../OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' +import { UPDATE_FALLBACK_HANDLER_WARNING } from '../OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants' +import { OrderEstimatedExecutionPrice } from '../OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure' export interface FillsAtStatusProps { childOrders?: ParsedOrder[] diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx index fd98836a17f..1f71be7ea37 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx @@ -10,7 +10,7 @@ import type { ParsedOrder } from 'utils/orderUtils/parseOrder' import * as styledEl from './TwapStatusAndToggle.styled' -import { WarningTooltip } from '../OrdersTable/Row/WarningTooltip/WarningTooltip.pure' +import { FallbackHandlerWarningTooltip, WarningTooltip } from '../OrdersTable/Row/WarningTooltip/WarningTooltip.pure' import { OrderStatusBox } from '../OrderStatusBox/OrderStatusBox.pure' import type { OrderParams } from '../../utils/getOrderParams' @@ -56,14 +56,25 @@ export function TwapStatusAndToggle({ const warningChild = childWithAllowanceWarning || childWithBalanceWarning + // A still-open parent or part carrying `isUnfillable` is blocked by a reset Safe ComposableCoW + // fallback handler (see issue #5426). Surface the same danger design on the parent status badge + // so it matches the reason shown in the Fills-at column and on the individual parts. + const isFallbackHandlerUnfillable = (order: ParsedOrder): boolean => + order.isUnfillable === true && (order.status === OrderStatus.PENDING || order.status === OrderStatus.SCHEDULED) + + const isFallbackHandlerBlocked = + isFallbackHandlerUnfillable(parent) || childOrders.some((child) => isFallbackHandlerUnfillable(child.order)) + return ( <> + ) : warningChild ? (