Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -1181,6 +1183,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 @@ -1741,6 +1744,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 { WarningReason } 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,17 +154,27 @@ 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 isFallbackHandlerRequired = useIsFallbackHandlerRequired()
const isFallbackHandlerUnfillable =
isTwapTable === true && getIsFallbackHandlerUnfillable(status, isFallbackHandlerRequired)

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
? `Insufficient balance`
const warningReason = isFallbackHandlerUnfillable
? WarningReason.FallbackHandler
: hasEnoughBalance === false
? WarningReason.Balance
: hasEnoughAllowance === false
? `Insufficient allowance`
: `Unfillable`
? WarningReason.Allowance
: undefined

const onApprove = withAllowanceWarning ? () => orderActions.approveOrderToken(order.inputToken) : undefined

Expand All @@ -182,6 +198,7 @@ export function OrderRow({
estimatedExecutionPrice={estimatedExecutionPrice}
estimatedPriceWarning={estimatedPriceWarning}
isChild={isChild}
isFallbackHandlerRequired={isFallbackHandlerRequired}
isInverted={isInverted}
isSafeWallet={isSafeWallet}
isTwapTable={isTwapTable}
Expand All @@ -192,7 +209,7 @@ export function OrderRow({
prices={prices}
rateInfoParams={rateInfoParams}
spotPrice={spotPrice}
warningText={warningText}
warningReason={warningReason}
withWarning={withWarning}
/>
)
Expand Down Expand Up @@ -252,10 +269,11 @@ export function OrderRow({
<OrderFillsAtWithDistance
order={order}
withWarning={withWarning}
warningText={warningText}
warningReason={warningReason}
onApprove={onApprove}
isInverted={isInverted}
isUnfillable={isUnfillable}
isFallbackHandlerRequired={isFallbackHandlerRequired}
estimatedExecutionPrice={estimatedExecutionPrice}
spotPrice={spotPrice}
estimatedPriceWarning={estimatedPriceWarning}
Expand Down Expand Up @@ -346,16 +364,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
@@ -0,0 +1,66 @@
import { ReactNode } from 'react'

import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'

import { fireEvent, render, screen } from '@testing-library/react'

import { WarningReason } 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}
warningReason={WarningReason.FallbackHandler}
/>,
)

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}
warningReason={WarningReason.FallbackHandler}
/>,
)

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 { WarningReason } from './orderEstimatedExecutionPrice.constants'
import * as styledEl from './OrderEstimatedExecutionPrice.styled'

import * as orderRowEl from '../../containers/OrderRow/OrderRow.styled'
Expand All @@ -32,7 +33,7 @@ export interface OrderEstimatedExecutionPriceProps extends TokenAmountProps {
onApprove?: Command
percentageDifference?: Percent
percentageFee?: Percent
warningText?: string
warningReason?: WarningReason
}

type UnlikelyToExecuteWarningProps = {
Expand All @@ -57,7 +58,7 @@ export function OrderEstimatedExecutionPrice({
percentageDifference,
percentageFee,
tokenSymbol,
warningText,
warningReason,
...rest
}: OrderEstimatedExecutionPriceProps) {
const [approveClicked, setApproveClicked] = useState(true)
Expand Down Expand Up @@ -101,27 +102,55 @@ export function OrderEstimatedExecutionPrice({
return () => clearTimeout(timeout)
}, [approveClicked])

const internationalizedWarningText =
warningText === 'Insufficient balance'
const isFallbackHandlerWarning = warningReason === WarningReason.FallbackHandler
const isAllowanceWarning = warningReason === WarningReason.Allowance
const isBalanceWarning = warningReason === WarningReason.Balance

const warningLabel = isFallbackHandlerWarning
? t`Update fallback handler`
: isBalanceWarning
? t`Insufficient balance`
: warningText === 'Insufficient allowance'
: isAllowanceWarning
? t`Insufficient allowance`
: t`Unfillable`

const unfillableLabel = (
<styledEl.UnfillableLabel>
{(warningText === 'Insufficient allowance' || warningText === 'Insufficient balance') && (
{isFallbackHandlerWarning && (
<HoverTooltip
content={
<warningTooltopEl.WarningContent>
<h3>{warningLabel}</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} />
{warningLabel}
</styledEl.WarningContent>
</HoverTooltip>
)}
{(isAllowanceWarning || isBalanceWarning) && (
<>
<HoverTooltip
content={
<warningTooltopEl.WarningContent>
<h3>{internationalizedWarningText}</h3>
<h3>{warningLabel}</h3>
<p>
{warningText === 'Insufficient allowance'
{isAllowanceWarning
? t`The order remains open. Execution requires adequate allowance. Approve the token to proceed.`
: t`The order remains open. Execution requires sufficient balance.`}
</p>
{warningText === 'Insufficient allowance' && handleApproveClick && (
{isAllowanceWarning && handleApproveClick && (
<warningTooltopEl.WarningActionBox>
{approveClicked ? (
<styledEl.ApproveLoaderWrapper>
Expand All @@ -141,10 +170,10 @@ export function OrderEstimatedExecutionPrice({
>
<styledEl.WarningContent>
<SVG src={iconAllowanceSrc} />
{internationalizedWarningText}
{warningLabel}
</styledEl.WarningContent>
</HoverTooltip>
{warningText === 'Insufficient allowance' &&
{isAllowanceWarning &&
handleApproveClick &&
(approveClicked ? (
<Loader />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Reason an open order is surfaced as unfillable. Used as a flag to pick the danger tooltip/label;
* the human-readable, internationalized text is derived from it in `OrderEstimatedExecutionPrice`.
*
* `FallbackHandler`: an open TWAP order whose Safe ComposableCoW fallback handler was reset can no
* longer be created (see issue #5426).
*/
export enum WarningReason {
Balance = 'balance',
Allowance = 'allowance',
FallbackHandler = 'fallback-handler',
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ParsedOrder } from 'utils/orderUtils/parseOrder'

import * as styledEl from '../../containers/OrderRow/OrderRow.styled'
import { useFeeAmountDifference } from '../../hooks/useFeeAmountDifference'
import { WarningReason } from '../OrderEstimatedExecutionPrice/orderEstimatedExecutionPrice.constants'
import { OrderEstimatedExecutionPrice } from '../OrderEstimatedExecutionPrice/OrderEstimatedExecutionPrice.pure'
import { TwapOrderStatus } from '../TwapOrderStatus/TwapOrderStatus.pure'

Expand All @@ -33,6 +34,7 @@ export interface OrderFillsAtProps {
isTwapTable?: boolean
isChild?: boolean
isUnfillable: boolean
isFallbackHandlerRequired?: boolean
isInverted: boolean
withWarning: boolean
estimatedPriceWarning: ReactNode | undefined
Expand All @@ -42,7 +44,7 @@ export interface OrderFillsAtProps {
rateInfoParams: RateInfoParams
prices: PendingOrderPrices | undefined | null
spotPrice: Nullish<Price<Currency, Currency>>
warningText: string
warningReason?: WarningReason
onApprove?: Command
}

Expand All @@ -57,14 +59,15 @@ export function OrderFillsAt({
isTwapTable,
isChild,
isUnfillable,
isFallbackHandlerRequired,
estimatedPriceWarning,
childOrders,
estimatedExecutionPrice,
priceDiffs,
rateInfoParams,
prices,
spotPrice,
warningText,
warningReason,
withWarning,
onApprove,
}: OrderFillsAtProps) {
Expand Down Expand Up @@ -103,7 +106,11 @@ export function OrderFillsAt({
return (
estimatedPriceWarning || (
<styledEl.CellElement doubleRow>
<TwapOrderStatus orderStatus={order.status} childOrders={childOrders}>
<TwapOrderStatus
orderStatus={order.status}
childOrders={childOrders}
isFallbackHandlerRequired={isFallbackHandlerRequired}
>
-
</TwapOrderStatus>
</styledEl.CellElement>
Expand Down Expand Up @@ -199,7 +206,7 @@ export function OrderFillsAt({
amountFee={feeAmount}
canShowWarning={getUiOrderType(order) !== UiOrderType.SWAP && !isUnfillable}
isUnfillable={withWarning}
warningText={warningText}
warningReason={warningReason}
onApprove={onApprove}
/>
)}
Expand Down
Loading
Loading