Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
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 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
? 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