Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
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 @@ -14,6 +14,7 @@ import { OrderStatus } from 'legacy/state/orders/actions'
import { getEstimatedExecutionPrice } from 'legacy/state/orders/utils'

import { PendingOrderPrices } from 'modules/orders'
import { useIsFallbackHandlerRequired } from 'modules/twap'

import { useIsProviderNetworkDeprecated } from 'common/hooks/useIsProviderNetworkDeprecated'
import { useSafeMemo } from 'common/hooks/useSafeMemo'
Expand All @@ -30,6 +31,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,9 +41,13 @@ 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 { getIsFallbackHandlerUnfillable } from '../../utils/getIsFallbackHandlerUnfillable'
import { OrderParams } from '../../utils/getOrderParams'
import { shouldShowDashForExpiration } from '../../utils/shouldShowDashForExpiration'
import { getActivityUrl } from '../../utils/url/getActivityUrl'
Expand Down Expand Up @@ -148,13 +154,24 @@ export function OrderRow({

const isExecutedPriceZero = executedPriceInverted !== undefined && executedPriceInverted?.equalTo(ZERO_FRACTION)

const isUnfillable = !percentIsAlmostHundred(filledPercentDisplay) && (isExecutedPriceZero || withWarning)
// The Safe ComposableCoW fallback handler being reset/removed is a per-account condition, so it is
// resolved here in the view (not persisted onto the order) and combined with the per-order status.
// It only applies to composable (TWAP) orders; surface it with the same danger design as the
// balance/allowance warnings (see issue #5426).
const isFallbackHandlerBroken = useIsFallbackHandlerRequired()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Uber nitpick, I wouldn't substitute concepts handler required and handler broken

const isFallbackHandlerUnfillable =
isTwapTable === true && getIsFallbackHandlerUnfillable(status, isFallbackHandlerBroken)

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 @@ -182,6 +199,7 @@ export function OrderRow({
estimatedExecutionPrice={estimatedExecutionPrice}
estimatedPriceWarning={estimatedPriceWarning}
isChild={isChild}
isFallbackHandlerBroken={isFallbackHandlerBroken}
isInverted={isInverted}
isSafeWallet={isSafeWallet}
isTwapTable={isTwapTable}
Expand Down Expand Up @@ -256,6 +274,7 @@ export function OrderRow({
onApprove={onApprove}
isInverted={isInverted}
isUnfillable={isUnfillable}
isFallbackHandlerBroken={isFallbackHandlerBroken}
estimatedExecutionPrice={estimatedExecutionPrice}
spotPrice={spotPrice}
estimatedPriceWarning={estimatedPriceWarning}
Expand Down Expand Up @@ -346,16 +365,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 thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's actually weird.
We don't need both warningText and internationalizedWarningText.
I would convert warningText into a enum like WarningReason { Balance, Allowance, FallbackHandler }.

? 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 @@ -33,6 +33,7 @@ export interface OrderFillsAtProps {
isTwapTable?: boolean
isChild?: boolean
isUnfillable: boolean
isFallbackHandlerBroken?: boolean
isInverted: boolean
withWarning: boolean
estimatedPriceWarning: ReactNode | undefined
Expand All @@ -57,6 +58,7 @@ export function OrderFillsAt({
isTwapTable,
isChild,
isUnfillable,
isFallbackHandlerBroken,
estimatedPriceWarning,
childOrders,
estimatedExecutionPrice,
Expand Down Expand Up @@ -103,7 +105,11 @@ export function OrderFillsAt({
return (
estimatedPriceWarning || (
<styledEl.CellElement doubleRow>
<TwapOrderStatus orderStatus={order.status} childOrders={childOrders}>
<TwapOrderStatus
orderStatus={order.status}
childOrders={childOrders}
isFallbackHandlerBroken={isFallbackHandlerBroken}
>
-
</TwapOrderStatus>
</styledEl.CellElement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface OrderFillsAtWithDistanceProps {
order: ParsedOrder
isInverted: boolean
isUnfillable: boolean
isFallbackHandlerBroken?: boolean
withWarning: boolean
warningText: string
onApprove?: Command
Expand All @@ -49,6 +50,7 @@ export function OrderFillsAtWithDistance({
order,
isInverted,
isUnfillable,
isFallbackHandlerBroken,
withWarning,
warningText,
onApprove,
Expand All @@ -60,7 +62,11 @@ export function OrderFillsAtWithDistance({
return (
estimatedPriceWarning || (
<styledEl.CellElement doubleRow>
<TwapOrderStatus orderStatus={order.status} childOrders={childOrders}>
<TwapOrderStatus
orderStatus={order.status}
childOrders={childOrders}
isFallbackHandlerBroken={isFallbackHandlerBroken}
>
<TwapScheduledOrderStatus
childOrders={childOrders}
isInverted={isInverted}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import styled from 'styled-components/macro'
import { OrderStatus } from 'legacy/state/orders/actions'

import type { PendingOrderPrices } from 'modules/orders'
import { useIsFallbackHandlerRequired } from 'modules/twap'

import { OrderRow } from '../../../../containers/OrderRow/OrderRow.container'
import { OrderActions, OrderTableGroup } from '../../../../state/ordersTable.types'
Expand Down Expand Up @@ -58,6 +59,10 @@ export function OrdersTableRowGroup({
}: OrdersTableRowGroupProps) {
const { parent, children } = item

// Per-account condition (the Safe's ComposableCoW fallback handler was reset); resolved here in the
// view and passed to the status badge rather than persisted onto the order (see issue #5426).
const isFallbackHandlerBroken = useIsFallbackHandlerRequired()

const [isCollapsed, setIsCollapsed] = useState<boolean>(true)
const [currentPage, setCurrentPage] = useState<number>(1)

Expand Down Expand Up @@ -103,6 +108,7 @@ export function OrdersTableRowGroup({
parent={parent}
childrenLength={childrenLength}
isCollapsed={isCollapsed}
isFallbackHandlerBroken={isFallbackHandlerBroken}
onToggle={() => setIsCollapsed((state) => !state)}
onClick={() => orderActions.selectReceiptOrder(parent)}
childOrders={childrenWithParams}
Expand Down
Loading
Loading