Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ export function mapTwapOrderToStoreOrder(order: TwapOrderItem, tokensByAddress:
status,
apiAdditionalInfo: enrichedOrder,
isCancelling: order.status === TwapOrderStatus.Cancelling,
isUnfillable: order.isUnfillable,
}
}
5 changes: 5 additions & 0 deletions apps/cowswap-frontend/src/locales/en-US.po
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.constants'
import { OrderFillsAt } from '../../pure/OrderFillsAt/OrderFillsAt.pure'
import { OrderFillsAtWithDistance } from '../../pure/OrderFillsAtWithDistance/OrderFillsAtWithDistance.pure'
import { OrderMarketPrice } from '../../pure/OrderMarketPrice/OrderMarketPrice.pure'
Expand All @@ -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'
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -346,16 +359,20 @@ export function OrderRow({
<styledEl.StatusBox>
<OrderStatusBox
order={order}
withWarning={withWarning}
withWarning={withWarning || isFallbackHandlerUnfillable}
onClick={onClick}
WarningTooltip={
<WarningTooltip
hasEnoughBalance={hasEnoughBalance ?? false}
hasEnoughAllowance={hasEnoughAllowance ?? false}
inputTokenSymbol={inputTokenSymbol}
isOrderScheduled={isOrderScheduled}
onApprove={() => orderActions.approveOrderToken(order.inputToken)}
/>
isFallbackHandlerUnfillable ? (
<FallbackHandlerWarningTooltip />
) : (
<WarningTooltip
hasEnoughBalance={hasEnoughBalance ?? false}
hasEnoughAllowance={hasEnoughAllowance ?? false}
inputTokenSymbol={inputTokenSymbol}
isOrderScheduled={isOrderScheduled}
onApprove={() => orderActions.approveOrderToken(order.inputToken)}
/>
)
}
/>
</styledEl.StatusBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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 })
}
Comment on lines +91 to 95

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find other consumers of order.isUnfillable for composable/TWAP parent orders
# that might rely on it being persisted via setIsOrderUnfillable.
rg -n --type=ts --type=tsx 'isUnfillable' apps/cowswap-frontend/src -g '!**/*.test.*' -C2

Repository: cowprotocol/cowswap

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the hook around the persistence guard.
sed -n '1,180p' apps/cowswap-frontend/src/modules/ordersTable/hooks/useOrdersTableList.ts

echo
echo '--- setIsOrderUnfillable usage ---'
rg -n --glob '!**/*.test.*' 'setIsOrderUnfillable|isUnfillable' apps/cowswap-frontend/src

echo
echo '--- composable/TWAP order readers ---'
rg -n --glob '!**/*.test.*' 'order\.isUnfillable|isUnfillable.*order|TwapOrderStatus|Composable|TWAP' apps/cowswap-frontend/src/modules

Repository: cowprotocol/cowswap

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- reducer write path ---'
sed -n '470,515p' apps/cowswap-frontend/src/legacy/state/orders/reducer.ts

echo
echo '--- TWAP store mapping ---'
sed -n '1,120p' apps/cowswap-frontend/src/modules/twap/utils/mapPartOrderToStoreOrder.ts

echo
echo '--- TWAP items builder ---'
sed -n '1,120p' apps/cowswap-frontend/src/modules/twap/utils/buildTwapOrdersItems.ts

echo
echo '--- TWAP status consumers ---'
sed -n '1,120p' apps/cowswap-frontend/src/modules/ordersTable/pure/TwapOrderStatus/TwapOrderStatus.pure.tsx
sed -n '1,120p' apps/cowswap-frontend/src/modules/ordersTable/pure/TwapScheduledOrderStatus/TwapScheduledOrderStatus.pure.tsx
sed -n '1,120p' apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/getOrderStatusTitleAndColor.ts

echo
echo '--- hook consumer around activity state ---'
sed -n '90,125p' apps/cowswap-frontend/src/legacy/hooks/useActivityDerivedState.ts

Repository: cowprotocol/cowswap

Length of output: 15800


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- composable order flag consumers ---'
rg -n --glob '!**/*.test.*' 'composableCowInfo|parent\.isUnfillable|order\.isUnfillable' apps/cowswap-frontend/src/modules apps/cowswap-frontend/src/legacy apps/cowswap-frontend/src/utils -g '!**/node_modules/**' -C2

echo
echo '--- parsed order model ---'
sed -n '1,180p' apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts

echo
echo '--- order table item grouping ---'
sed -n '1,220p' apps/cowswap-frontend/src/modules/ordersTable/utils/orderTableGroupUtils.ts
sed -n '1,220p' apps/cowswap-frontend/src/modules/ordersTable/utils/groupOrdersTable.ts

Repository: cowprotocol/cowswap

Length of output: 21208


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- getOrderStatusTitleAndColor call sites ---'
rg -n --glob '!**/*.test.*' 'getOrderStatusTitleAndColor\(' apps/cowswap-frontend/src -C2

echo
echo '--- OrderRow / OrderStatusBox usage ---'
rg -n --glob '!**/*.test.*' 'OrderStatusBox|getOrderStatusTitleAndColor|isUnfillable' apps/cowswap-frontend/src/modules/ordersTable/containers apps/cowswap-frontend/src/modules/ordersTable/pure -C2

Repository: cowprotocol/cowswap

Length of output: 35652


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,180p' apps/cowswap-frontend/src/modules/ordersTable/pure/TwapStatusAndToggle/TwapStatusAndToggle.pure.tsx

echo
echo '--- TWAP row usage ---'
rg -n --glob '!**/*.test.*' 'TwapStatusAndToggle|TwapOrderStatus|TwapScheduledOrderStatus|OrderStatusBox order=\{parent\}' apps/cowswap-frontend/src/modules/ordersTable -C2

Repository: cowprotocol/cowswap

Length of output: 14496


TWAP parents no longer persist isUnfillable

OrderStatusBox and other ParsedOrder consumers still read order.isUnfillable, so skipping setIsOrderUnfillable for all composable orders means a TWAP parent can stop reflecting genuine balance/allowance-based unfillable state in the store. The table still uses the recomputed flag locally, but the persisted parent order stays stale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cowswap-frontend/src/modules/ordersTable/hooks/useOrdersTableList.ts`
around lines 91 - 95, The composable-order guard in useOrdersTableList is too
broad and now prevents TWAP parent orders from persisting a real isUnfillable
change into the store. Update the logic around the
recompute/setIsOrderUnfillable path so only child/computed composable orders are
skipped, while TWAP parent ParsedOrder entries still write back genuine
balance/allowance-based unfillable state. Use the existing isComposableOrder
check and the order.id / setIsOrderUnfillable flow to target the correct branch
without changing the local table recomputation behavior.


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ReactNode } from 'react'

import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { fireEvent, render, screen } from '@testing-library/react'

import { UPDATE_FALLBACK_HANDLER_WARNING } from './orderEstimatedExecutionPrice.constants'
import { OrderEstimatedExecutionPrice } from './OrderEstimatedExecutionPrice.pure'

jest.mock('react-inlinesvg', () => {
return function MockSvg() {
return <svg />
}
})

function renderWithI18n(ui: ReactNode): ReturnType<typeof render> {
return render(<I18nProvider i18n={i18n}>{ui}</I18nProvider>)
}

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(
<OrderEstimatedExecutionPrice
amount={undefined}
tokenSymbol={undefined}
isInverted={false}
isUnfillable={true}
canShowWarning={true}
warningText={UPDATE_FALLBACK_HANDLER_WARNING}
/>,
)

expect(screen.getByText('Update fallback handler')).not.toBeNull()
})

it('reveals the fallback handler explanation on hover', async () => {
renderWithI18n(
<OrderEstimatedExecutionPrice
amount={undefined}
tokenSymbol={undefined}
isInverted={false}
isUnfillable={true}
canShowWarning={true}
warningText={UPDATE_FALLBACK_HANDLER_WARNING}
/>,
)

fireEvent.mouseEnter(screen.getByText('Update fallback handler'))

expect(await screen.findByText(/Your Safe fallback handler was changed/i)).not.toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -101,15 +102,41 @@ 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`
: t`Unfillable`

const unfillableLabel = (
<styledEl.UnfillableLabel>
{isFallbackHandlerWarning && (
<HoverTooltip
content={
<warningTooltopEl.WarningContent>
<h3>{internationalizedWarningText}</h3>
<p>
<Trans>
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.
</Trans>
</p>
</warningTooltopEl.WarningContent>
}
bgColor={`var(${UI.COLOR_DANGER_BG})`}
color={`var(${UI.COLOR_DANGER_TEXT})`}
>
<styledEl.WarningContent>
<SVG src={svgAlertSrc} />
{internationalizedWarningText}
</styledEl.WarningContent>
</HoverTooltip>
)}
{(warningText === 'Insufficient allowance' || warningText === 'Insufficient balance') && (
<>
<HoverTooltip
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,38 @@ export function WarningTooltip({
</styledEl.WarningIndicator>
)
}

// 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 = (
<styledEl.WarningContent>
<styledEl.WarningParagraph>
<h3>
<Trans>Update fallback handler</Trans>
</h3>
<p>
<Trans>
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.
</Trans>
</p>
</styledEl.WarningParagraph>
</styledEl.WarningContent>
)

return (
<styledEl.WarningIndicator hasBackground={false}>
<styledEl.StyledQuestionHelper
text={tooltipContent}
placement="bottom"
bgColor={`var(${UI.COLOR_DANGER_BG})`}
color={`var(${UI.COLOR_DANGER_TEXT})`}
Icon={<SVG src={svgFilledInfoCircleSrc} description={t`warning`} width="14" height="14" />}
/>
{children}
</styledEl.WarningIndicator>
)
}
Original file line number Diff line number Diff line change
@@ -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 <svg />
}
})

beforeAll(() => {
window.matchMedia =
window.matchMedia ||
(() =>
({
matches: false,
addEventListener: () => undefined,
removeEventListener: () => undefined,
addListener: () => undefined,
removeListener: () => undefined,
}) as unknown as MediaQueryList)
})

function childOrder(overrides: Partial<ParsedOrder>): 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(
<TwapOrderStatus
orderStatus={OrderStatus.PENDING}
childOrders={[childOrder({ status: OrderStatus.PENDING, isUnfillable: true })]}
>
fallback children
</TwapOrderStatus>,
)

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(
<TwapOrderStatus
orderStatus={OrderStatus.PENDING}
childOrders={[childOrder({ status: OrderStatus.PENDING, isUnfillable: false })]}
>
fallback children
</TwapOrderStatus>,
)

expect(screen.queryByText('Update fallback handler')).toBeNull()
expect(screen.getByText('fallback children')).not.toBeNull()
})
})
Loading
Loading