Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: only show twap market price for non final states #5302

Merged
merged 7 commits into from
Jan 16, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import orderPresignaturePending from '@cowprotocol/assets/cow-swap/order-presignature-pending.svg'
import { ZERO_FRACTION } from '@cowprotocol/common-const'
import { useTimeAgo } from '@cowprotocol/common-hooks'
import { formatDateWithTimezone, getAddress, getEtherscanLink } from '@cowprotocol/common-utils'
import { getAddress, getEtherscanLink, formatDateWithTimezone } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { TokenLogo } from '@cowprotocol/tokens'
import { Command, UiOrderType } from '@cowprotocol/types'
import { HoverTooltip, Loader, PercentDisplay, percentIsAlmostHundred, TokenAmount, UI } from '@cowprotocol/ui'
import { UI, TokenAmount, Loader, HoverTooltip } from '@cowprotocol/ui'
import { PercentDisplay, percentIsAlmostHundred } from '@cowprotocol/ui'
import { useIsSafeWallet } from '@cowprotocol/wallet'
import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core'

import { Check, Clock, X, Zap } from 'react-feather'
import { Clock, Zap, Check, X } from 'react-feather'
import SVG from 'react-inlinesvg'

import { OrderStatus } from 'legacy/state/orders/actions'
Expand All @@ -20,9 +21,9 @@ import { PendingOrderPrices } from 'modules/orders/state/pendingOrdersPricesAtom
import { getIsEthFlowOrder } from 'modules/swap/containers/EthFlowStepper'

import {
FAIR_PRICE_THRESHOLD_PERCENTAGE,
GOOD_PRICE_THRESHOLD_PERCENTAGE,
PENDING_EXECUTION_THRESHOLD_PERCENTAGE,
GOOD_PRICE_THRESHOLD_PERCENTAGE,
FAIR_PRICE_THRESHOLD_PERCENTAGE,
} from 'common/constants/common'
import { useSafeMemo } from 'common/hooks/useSafeMemo'
import { RateInfo } from 'common/pure/RateInfo'
Expand Down Expand Up @@ -88,6 +89,7 @@ export interface OrderRowProps {
onClick: Command
orderActions: OrderActions
children?: React.ReactNode
childOrders?: ParsedOrder[]
isTwapTable?: boolean
}

Expand All @@ -105,6 +107,7 @@ export function OrderRow({
prices,
spotPrice,
children,
childOrders,
isTwapTable,
}: OrderRowProps) {
const { buyAmount, rateInfoParams, hasEnoughAllowance, hasEnoughBalance, chainId } = orderParams
Expand Down Expand Up @@ -170,19 +173,17 @@ export function OrderRow({
return 'Unfillable'
}

const renderWarningTooltip =
(showIcon?: boolean) =>
({ children }: { children: React.ReactNode }) => (
<WarningTooltip
hasEnoughBalance={hasEnoughBalance ?? false}
hasEnoughAllowance={hasEnoughAllowance ?? false}
inputTokenSymbol={inputTokenSymbol}
isOrderScheduled={isOrderScheduled}
onApprove={() => orderActions.approveOrderToken(order.inputToken)}
showIcon={showIcon}
children={children}
/>
)
const renderWarningTooltip = (showIcon?: boolean) => (props: { children: React.ReactNode }) => (
<WarningTooltip
hasEnoughBalance={hasEnoughBalance ?? false}
hasEnoughAllowance={hasEnoughAllowance ?? false}
inputTokenSymbol={inputTokenSymbol}
isOrderScheduled={isOrderScheduled}
onApprove={() => orderActions.approveOrderToken(order.inputToken)}
showIcon={showIcon}
{...props}
/>
)

const renderLimitPrice = () => (
<styledEl.RateValue onClick={toggleIsInverted}>
Expand All @@ -198,19 +199,37 @@ export function OrderRow({
</styledEl.RateValue>
)

const areAllChildOrdersCancelled = (orders: ParsedOrder[] | undefined): boolean => {
if (!orders || orders.length === 0) return false
return orders.every((order) => order.status === OrderStatus.CANCELLED)
}

const renderFillsAt = () => (
<>
{getIsFinalizedOrder(order) ? (
order.status === OrderStatus.CANCELLED ? (
<styledEl.CancelledDisplay>
<X size={14} strokeWidth={2.5} />
Order cancelled
</styledEl.CancelledDisplay>
) : order.status === OrderStatus.FULFILLED ? (
order.executionData.partiallyFilled || order.status === OrderStatus.FULFILLED ? (
<styledEl.FilledDisplay>
<Check size={14} strokeWidth={3.5} />
Order {order.partiallyFillable && Number(filledPercentDisplay) < 100 ? 'partially ' : ''}filled
</styledEl.FilledDisplay>
) : order.status === OrderStatus.CANCELLED ? (
// For TWAP parent orders, show cancelled only when ALL child orders are cancelled
children ? (
childOrders && areAllChildOrdersCancelled(childOrders) ? (
<styledEl.CancelledDisplay>
<X size={14} strokeWidth={2.5} />
Order cancelled
</styledEl.CancelledDisplay>
) : (
'-'
)
) : (
// For non-TWAP orders and TWAP child orders, show cancelled normally
<styledEl.CancelledDisplay>
<X size={14} strokeWidth={2.5} />
Order cancelled
</styledEl.CancelledDisplay>
)
) : order.status === OrderStatus.EXPIRED ? (
<styledEl.ExpiredDisplay>
<Clock size={14} strokeWidth={2.5} />
Expand Down Expand Up @@ -291,24 +310,56 @@ export function OrderRow({
return renderFillsAt()
}

// For TWAP parent orders, show the next scheduled child order's fills at price
if (children) {
// Get the next scheduled order from the children prop
const childrenArray = React.Children.toArray(children) as React.ReactElement<{ order: ParsedOrder }>[]
const nextScheduledOrder = childrenArray
.map((child) => child.props.order)
.find((childOrder) => {
return childOrder && childOrder.status === OrderStatus.SCHEDULED && !getIsFinalizedOrder(childOrder)
})
// Handle warning states first, regardless of order type
if (withWarning) {
return (
<styledEl.ExecuteCellWrapper>
<EstimatedExecutionPrice
amount={undefined}
tokenSymbol={undefined}
isInverted={isInverted}
isUnfillable={withWarning}
canShowWarning={true}
warningText={getWarningText()}
WarningTooltip={renderWarningTooltip(true)}
onApprove={withAllowanceWarning ? () => orderActions.approveOrderToken(order.inputToken) : undefined}
/>
</styledEl.ExecuteCellWrapper>
)
}

// For TWAP parent orders
if (children && childOrders) {
// Check if all child orders are cancelled first
if (areAllChildOrdersCancelled(childOrders)) {
return (
<styledEl.CellElement doubleRow>
<b>
<styledEl.CancelledDisplay>
<X size={14} strokeWidth={2.5} />
Order cancelled
</styledEl.CancelledDisplay>
</b>
<i></i>
</styledEl.CellElement>
)
}

const nextScheduledOrder = childOrders.find(
(childOrder) => childOrder.status === OrderStatus.SCHEDULED && !getIsFinalizedOrder(childOrder),
)

if (nextScheduledOrder) {
// Get the execution price from the next scheduled order
const nextOrderExecutionPrice = nextScheduledOrder.executionData.executedPrice
const nextOrderPriceDiffs = calculatePriceDifference({
referencePrice: spotPrice,
targetPrice: nextOrderExecutionPrice,
isInverted: false,
})
// For scheduled orders, use the execution price if available, otherwise use the estimated price from props
const nextOrderExecutionPrice =
nextScheduledOrder.executionData.executedPrice || prices?.estimatedExecutionPrice
const nextOrderPriceDiffs = nextOrderExecutionPrice
? calculatePriceDifference({
referencePrice: spotPrice,
targetPrice: nextOrderExecutionPrice,
isInverted: false,
})
: null

// Show the execution price for the next scheduled order
let nextOrderFillsAtContent
Expand All @@ -319,7 +370,7 @@ export function OrderRow({
} else {
nextOrderFillsAtContent = (
<TokenAmount
amount={nextOrderExecutionPrice}
amount={isInverted ? nextOrderExecutionPrice.invert() : nextOrderExecutionPrice}
tokenSymbol={nextOrderExecutionPrice?.quoteCurrency}
opacitySymbol
/>
Expand Down Expand Up @@ -381,27 +432,43 @@ export function OrderRow({
)
}

const renderMarketPrice = () => (
<>
{children ? (
'-'
) : order.status === OrderStatus.CANCELLED || withWarning || order.status === OrderStatus.PRESIGNATURE_PENDING ? (
'-'
) : spotPrice ? (
const renderMarketPrice = () => {
// Early return for warning states and non-active orders
if (
withWarning ||
order.status === OrderStatus.CREATING ||
order.status === OrderStatus.PRESIGNATURE_PENDING ||
getIsFinalizedOrder(order)
) {
return '-'
}

// Check children finalization status
if (children && childOrders) {
if (childOrders.every((childOrder) => getIsFinalizedOrder(childOrder))) {
return '-'
}
}

// Handle spot price cases
if (spotPrice === null) {
return '-'
}

if (spotPrice) {
return (
<TokenAmount
amount={spotPriceInverted}
tokenSymbol={spotPriceInverted?.quoteCurrency}
opacitySymbol
clickable
noTitle
/>
) : spotPrice === null ? (
'-'
) : (
<Loader size="14px" style={{ margin: '0 0 -2px 7px' }} />
)}
</>
)
)
}

return <Loader size="14px" style={{ margin: '0 0 -2px 7px' }} />
}

return (
<TableRow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export function TableGroup(props: TableGroupProps) {
orderParams={getOrderParams(chainId, balancesAndAllowances, parent)}
onClick={() => orderActions.selectReceiptOrder(parent)}
isExpanded={!isCollapsed}
childOrders={children}
>
{isParentSigning ? undefined : (
<TwapStatusAndToggle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,35 @@ import { WIDGET_MAX_WIDTH } from 'theme'

const DEFAULT_MAX_WIDTH = '1500px'

export const PageWrapper = styled.div<{ isUnlocked: boolean; secondaryOnLeft?: boolean; maxWidth?: string }>`
export const PageWrapper = styled.div<{
isUnlocked: boolean
secondaryOnLeft?: boolean
maxWidth?: string
hideOrdersTable?: boolean
}>`
width: 100%;
display: grid;
max-width: ${({ maxWidth = DEFAULT_MAX_WIDTH }) => maxWidth};
margin: 0 auto;
grid-template-columns: 1fr;
grid-template-rows: auto auto;
grid-template-areas: 'primary' 'secondary';
grid-template-rows: auto;
grid-template-areas: ${({ hideOrdersTable }) => (hideOrdersTable ? '"primary"' : '"primary" "secondary"')};
gap: 20px;

${Media.LargeAndUp()} {
grid-template-columns: ${({ isUnlocked, secondaryOnLeft }) =>
isUnlocked
grid-template-columns: ${({ isUnlocked, hideOrdersTable, secondaryOnLeft }) =>
isUnlocked && !hideOrdersTable
? secondaryOnLeft
? '1fr minmax(auto, ' + WIDGET_MAX_WIDTH.swap.replace('px', '') + 'px)'
: 'minmax(auto, ' + WIDGET_MAX_WIDTH.swap.replace('px', '') + 'px) 1fr'
: '1fr'};
grid-template-rows: 1fr;
grid-template-areas: ${({ secondaryOnLeft }) => (secondaryOnLeft ? '"secondary primary"' : '"primary secondary"')};
grid-template-areas: ${({ secondaryOnLeft, hideOrdersTable }) =>
hideOrdersTable ? '"primary"' : secondaryOnLeft ? '"secondary primary"' : '"primary secondary"'};
}

> div:last-child {
display: ${({ isUnlocked }) => (isUnlocked ? '' : 'none')};
display: ${({ isUnlocked }) => (!isUnlocked ? 'none' : '')};
}
`

Expand Down
9 changes: 5 additions & 4 deletions apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default function AdvancedOrdersPage() {
isUnlocked={isUnlocked}
maxWidth={ADVANCED_ORDERS_MAX_WIDTH}
secondaryOnLeft={ordersTableOnLeft}
hideOrdersTable={hideOrdersTable}
>
<styledEl.PrimaryWrapper>
{isFallbackHandlerRequired && pendingOrders.length > 0 && <SetupFallbackHandlerWarning />}
Expand All @@ -69,15 +70,15 @@ export default function AdvancedOrdersPage() {
</AdvancedOrdersWidget>
</styledEl.PrimaryWrapper>

<styledEl.SecondaryWrapper>
{!hideOrdersTable && (
{!hideOrdersTable && (
<styledEl.SecondaryWrapper>
<OrdersTableWidget
displayOrdersOnlyForSafeApp={true}
orderType={TabOrderTypes.ADVANCED}
orders={allEmulatedOrders}
/>
)}
</styledEl.SecondaryWrapper>
</styledEl.SecondaryWrapper>
)}
</styledEl.PageWrapper>
</>
)
Expand Down
15 changes: 10 additions & 5 deletions apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,25 @@ export function RegularLimitOrders() {
const { ordersTableOnLeft } = useAtomValue(limitOrdersSettingsAtom)

return (
<styledEl.PageWrapper isUnlocked={isUnlocked} secondaryOnLeft={ordersTableOnLeft} maxWidth={LIMIT_ORDERS_MAX_WIDTH}>
<styledEl.PageWrapper
isUnlocked={isUnlocked}
secondaryOnLeft={ordersTableOnLeft}
maxWidth={LIMIT_ORDERS_MAX_WIDTH}
hideOrdersTable={hideOrdersTable}
>
<styledEl.PrimaryWrapper>
<LimitOrdersWidget />
</styledEl.PrimaryWrapper>

<styledEl.SecondaryWrapper>
{!hideOrdersTable && (
{!hideOrdersTable && (
<styledEl.SecondaryWrapper>
<OrdersTableWidget
displayOrdersOnlyForSafeApp={false}
orderType={TabOrderTypes.LIMIT}
orders={allLimitOrders}
/>
)}
</styledEl.SecondaryWrapper>
</styledEl.SecondaryWrapper>
)}
</styledEl.PageWrapper>
)
}
Loading