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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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
@@ -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'

Expand Down Expand Up @@ -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 (
<>
<b>
<HoverTooltip
wrapInContainer
content={
<Trans>
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.
</Trans>
}
>
<styledEl.UnfillableDisplay>
<AlertTriangle size={14} strokeWidth={2.5} />
<Trans>Unfillable</Trans>
</styledEl.UnfillableDisplay>
</HoverTooltip>
</b>
<i></i>
</>
)
}

// Third priority: Check for scheduled orders
const hasScheduledOrder = childOrders.some((childOrder) => childOrder.status === OrderStatus.SCHEDULED)

Expand Down
3 changes: 3 additions & 0 deletions apps/cowswap-frontend/src/modules/twap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 (they are "broken")
const isFallbackHandlerBroken = useIsFallbackHandlerRequired()
const isFallbackHandlerBrokenRef = useRef(isFallbackHandlerBroken)
// eslint-disable-next-line react-hooks/refs
isFallbackHandlerBrokenRef.current = isFallbackHandlerBroken

// 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
Expand Down Expand Up @@ -96,6 +103,7 @@ export function TwapOrdersUpdater(props: {
allOrdersInfo,
ordersAuthResult,
twapOrderExecutions.current,
isFallbackHandlerBrokenRef.current,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type TwapOrdersAuthResult,
type TwapOrdersExecution,
type TwapOrdersSafeData,
TwapOrderStatus,
} from '../types'

import type { Hex } from 'viem'
Expand All @@ -23,6 +24,7 @@ export function buildTwapOrdersItems(
ordersInfo: TwapOrderInfo[],
ordersAuthResult: TwapOrdersAuthResult,
twapOrderExecutions: TwapOrdersExecutionMap,
isFallbackHandlerBroken: boolean,
): TwapOrdersList {
return ordersInfo.reduce<TwapOrdersList>((acc, { safeData, id }) => {
acc[id] = getTwapOrderItem(
Expand All @@ -32,6 +34,7 @@ export function buildTwapOrdersItems(
id as `0x${string}`,
ordersAuthResult[id],
twapOrderExecutions[id] ?? DEFAULT_TWAP_EXECUTION,
isFallbackHandlerBroken,
)
return acc
}, {})
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -62,5 +70,6 @@ function getTwapOrderItem(
executedDate: _executionDate || undefined,
safeTxParams,
executionInfo,
isUnfillable,
}
}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)]

Expand All @@ -39,5 +42,6 @@ export function mapPartOrderToStoreOrder(
status,
apiAdditionalInfo: enrichedOrder,
isCancelling,
isUnfillable,
}
}
Loading