From a2f8def2f203301885e4548b49f73336abbabe21 Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 27 Dec 2023 08:53:22 -0800 Subject: [PATCH] feat(fee=0): classify order types (#3559) * feat: add getUiOrderType utils fn * feat: use getUiOrderType to filter orders on recent activity * feat: update PendingOrders and SpotPrices updaters to use UiOrderType * feat: use UiOrderType on appziMiddleware * feat: use UiOrderType on UnfillableOrdersUpdater * feat: use UiOrderType on getEstiamtedExecutionPrice * chore: add todo note about analytics type * feat: update OrderRow with UiOrderType * feat: update usePriorityTokenAddresses with uiOrderType * chore: remove unused property picked from Order * refactor: rename const to twapOrders to be in line with its content * chore: memoize array concatenation * refactor: organize imports and remove redundant variables * feat: show progress bar for fee=0 swap orders --- .../hooks/useCategorizeRecentActivity.ts | 7 +-- .../updaters/orders/PendingOrdersUpdater.ts | 46 +++++++++++---- .../updaters/orders/SpotPricesUpdater.ts | 8 ++- .../orders/UnfillableOrdersUpdater.ts | 16 ++++-- .../src/legacy/hooks/useRecentActivity.ts | 6 +- .../src/legacy/state/orders/actions.ts | 1 + .../src/legacy/state/orders/hooks.ts | 23 +++++--- .../orders/middleware/appziMiddleware.ts | 9 ++- .../src/legacy/state/orders/utils.ts | 5 +- .../Transaction/ActivityDetails.tsx | 17 +++--- .../modules/appData/{types.tsx => types.ts} | 1 + .../OrdersTableContainer/OrderRow/index.tsx | 8 +-- .../trade/hooks/usePriorityTokenAddresses.ts | 5 +- .../twap/hooks/useAllEmulatedOrders.ts | 13 ++--- .../src/pages/LimitOrders/index.tsx | 16 ++---- .../src/utils/orderUtils/getUiOrderType.ts | 56 +++++++++++++++++++ libs/analytics/src/types.ts | 1 + 17 files changed, 169 insertions(+), 69 deletions(-) rename apps/cowswap-frontend/src/modules/appData/{types.tsx => types.ts} (95%) create mode 100644 apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts diff --git a/apps/cowswap-frontend/src/common/hooks/useCategorizeRecentActivity.ts b/apps/cowswap-frontend/src/common/hooks/useCategorizeRecentActivity.ts index 14a67c4b22..09e5a93fb5 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCategorizeRecentActivity.ts +++ b/apps/cowswap-frontend/src/common/hooks/useCategorizeRecentActivity.ts @@ -1,11 +1,10 @@ import { useMemo } from 'react' -import { OrderClass } from '@cowprotocol/cow-sdk' - import { useRecentActivity } from 'legacy/hooks/useRecentActivity' -import { OrderStatus, PENDING_STATES } from 'legacy/state/orders/actions' +import { Order, OrderStatus, PENDING_STATES } from 'legacy/state/orders/actions' import { getIsFinalizedOrder } from 'utils/orderUtils/getIsFinalizedOrder' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' export const isPending = ({ status }: { status: OrderStatus }) => PENDING_STATES.includes(status) @@ -19,7 +18,7 @@ export function useCategorizeRecentActivity() { allRecentActivity.reduce<[string[], string[]]>( (acc, activity) => { // Only display regular on-chain transactions (wrap, approval, etc) OR MARKET orders - if (!activity.class || activity.class === OrderClass.MARKET) { + if (!activity.class || getUiOrderType(activity as Order) === UiOrderType.SWAP) { if (isPending(activity)) { acc[0].push(activity.id) } else if (getIsFinalizedOrder(activity)) { diff --git a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts index ded8820af3..00e3148515 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/PendingOrdersUpdater.ts @@ -1,5 +1,5 @@ import { useSetAtom } from 'jotai' -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { getExplorerOrderLink, @@ -7,7 +7,7 @@ import { openNpsAppziSometimes, timeSinceInSeconds, } from '@cowprotocol/common-utils' -import { EthflowData, OrderClass, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { EthflowData, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' import { GetSafeInfo, useGetSafeInfo } from 'legacy/hooks/useGetSafeInfo' @@ -33,6 +33,7 @@ import { OrderTransitionStatus } from 'legacy/state/orders/utils' import { useAddOrderToSurplusQueue } from 'modules/swap/state/surplusModal' import { getOrder } from 'api/gnosisProtocol' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' import { fetchOrderPopupData, OrderLogPopupMixData } from './utils' @@ -234,7 +235,11 @@ async function _updateOrders({ }) // add to surplus queue fulfilledOrders.forEach(({ id, apiAdditionalInfo }) => { - if (!apiAdditionalInfo || apiAdditionalInfo.class === OrderClass.MARKET) { + if ( + !apiAdditionalInfo || + getUiOrderType({ fullAppData: apiAdditionalInfo.fullAppData, class: apiAdditionalInfo.class }) === + UiOrderType.SWAP + ) { addOrderToSurplusQueue(id) } }) @@ -253,8 +258,8 @@ async function _updateOrders({ function _triggerNps(pending: Order[], chainId: ChainId) { for (const order of pending) { const { openSince, id: orderId } = order - // Check if there's any MARKET pending for more than `PENDING_TOO_LONG_TIME` - if (order.class === OrderClass.MARKET && isOrderInPendingTooLong(openSince)) { + // Check if there's any SWAP pending for more than `PENDING_TOO_LONG_TIME` + if (getUiOrderType(order) === UiOrderType.SWAP && isOrderInPendingTooLong(openSince)) { const explorerUrl = getExplorerOrderLink(chainId, orderId) // Trigger NPS display, controlled by Appzi openNpsAppziSometimes({ @@ -277,6 +282,16 @@ export function PendingOrdersUpdater(): null { // TODO: Implement using SWR or retry/cancellable promises const isUpdatingMarket = useRef(false) const isUpdatingLimit = useRef(false) + const isUpdatingTwap = useRef(false) + + const updatersRefMap = useMemo( + () => ({ + [UiOrderType.SWAP]: isUpdatingMarket, + [UiOrderType.LIMIT]: isUpdatingLimit, + [UiOrderType.TWAP]: isUpdatingTwap, + }), + [] + ) // Ref, so we don't rerun useEffect const pendingRef = useRef(pending) @@ -302,12 +317,12 @@ export function PendingOrdersUpdater(): null { ) const updateOrders = useCallback( - async (chainId: ChainId, account: string, orderClass: OrderClass) => { + async (chainId: ChainId, account: string, uiOrderType: UiOrderType) => { if (!account) { return [] } - const isUpdating = orderClass === OrderClass.LIMIT ? isUpdatingLimit : isUpdatingMarket + const isUpdating = updatersRefMap[uiOrderType] if (!isUpdating.current) { isUpdating.current = true @@ -316,7 +331,7 @@ export function PendingOrdersUpdater(): null { return _updateOrders({ account, chainId, - orders: pendingRef.current.filter((order) => order.class === orderClass), + orders: pendingRef.current.filter((order) => getUiOrderType(order) === uiOrderType), addOrUpdateOrders, fulfillOrdersBatch, expireOrdersBatch, @@ -333,6 +348,7 @@ export function PendingOrdersUpdater(): null { } }, [ + updatersRefMap, addOrUpdateOrders, fulfillOrdersBatch, expireOrdersBatch, @@ -351,20 +367,26 @@ export function PendingOrdersUpdater(): null { } const marketInterval = setInterval( - () => updateOrders(chainId, account, OrderClass.MARKET), + () => updateOrders(chainId, account, UiOrderType.SWAP), MARKET_OPERATOR_API_POLL_INTERVAL ) const limitInterval = setInterval( - () => updateOrders(chainId, account, OrderClass.LIMIT), + () => updateOrders(chainId, account, UiOrderType.LIMIT), + LIMIT_OPERATOR_API_POLL_INTERVAL + ) + const twapInterval = setInterval( + () => updateOrders(chainId, account, UiOrderType.TWAP), LIMIT_OPERATOR_API_POLL_INTERVAL ) - updateOrders(chainId, account, OrderClass.MARKET) - updateOrders(chainId, account, OrderClass.LIMIT) + updateOrders(chainId, account, UiOrderType.SWAP) + updateOrders(chainId, account, UiOrderType.LIMIT) + updateOrders(chainId, account, UiOrderType.TWAP) return () => { clearInterval(marketInterval) clearInterval(limitInterval) + clearInterval(twapInterval) } }, [account, chainId, updateOrders]) diff --git a/apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts index 9f09e4af74..36ace36176 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef } from 'react' import { useIsWindowVisible } from '@cowprotocol/common-hooks' import { FractionUtils } from '@cowprotocol/common-utils' -import { OrderClass, SupportedChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' import { Token } from '@uniswap/sdk-core' @@ -13,6 +13,8 @@ import { useCombinedPendingOrders } from 'legacy/state/orders/hooks' import { requestPrice } from 'modules/limitOrders/hooks/useGetInitialPrice' import { UpdateSpotPriceAtom, updateSpotPricesAtom } from 'modules/orders/state/spotPricesAtom' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' + import { useSafeMemo } from '../../hooks/useSafeMemo' import { getCanonicalMarketChainKey } from '../../utils/markets' @@ -31,8 +33,8 @@ function useMarkets(chainId: SupportedChainId, account: string | undefined): Mar return useSafeMemo(() => { return pending.reduce>( (acc, order) => { - // Query spot prices only for Limit orders - if (order.class !== OrderClass.LIMIT) return acc + // Do not query spot prices for SWAP + if (getUiOrderType(order) === UiOrderType.SWAP) return acc // Aggregating pending orders per market. No need to query multiple times same market const { marketInverted, marketKey } = getCanonicalMarketChainKey(chainId, order.sellToken, order.buyToken) diff --git a/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts index 5b1dcda134..fc62427ed2 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts @@ -1,5 +1,5 @@ import { useSetAtom } from 'jotai' -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { priceOutOfRangeAnalytics } from '@cowprotocol/analytics' import { useTokensBalances } from '@cowprotocol/balances-and-allowances' @@ -7,7 +7,7 @@ import { NATIVE_CURRENCY_BUY_ADDRESS, WRAPPED_NATIVE_CURRENCY } from '@cowprotoc import { useIsWindowVisible } from '@cowprotocol/common-hooks' import { getPromiseFulfilledValue } from '@cowprotocol/common-utils' import { timestamp } from '@cowprotocol/contracts' -import { OrderClass, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' import { Currency, CurrencyAmount, Price } from '@uniswap/sdk-core' @@ -30,6 +30,7 @@ import { updatePendingOrderPricesAtom } from 'modules/orders/state/pendingOrders import { hasEnoughBalanceAndAllowance } from 'modules/tokens' import { getPriceQuality } from 'api/gnosisProtocol/api' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' import { PRICE_QUOTE_VALID_TO_TIME } from '../../constants/quote' import { useVerifiedQuotesEnabled } from '../../hooks/featureFlags/useVerifiedQuotesEnabled' @@ -43,7 +44,10 @@ export function UnfillableOrdersUpdater(): null { const updatePendingOrderPrices = useSetAtom(updatePendingOrderPricesAtom) const isWindowVisible = useIsWindowVisible() - const pending = useOnlyPendingOrders(chainId, OrderClass.LIMIT) + const pendingLimit = useOnlyPendingOrders(chainId, UiOrderType.LIMIT) + const pendingTwap = useOnlyPendingOrders(chainId, UiOrderType.TWAP) + const pending = useMemo(() => pendingLimit.concat(pendingTwap), [pendingLimit, pendingTwap]) + const setIsOrderUnfillable = useSetIsOrderUnfillable() const strategy = useGetGpPriceStrategy() @@ -94,10 +98,12 @@ export function UnfillableOrdersUpdater(): null { const marketPrice = getOrderMarketPrice(order, price.amount, fee.amount) const estimatedExecutionPrice = getEstimatedExecutionPrice(order, marketPrice, fee.amount) - const isUnfillable = order.class === OrderClass.MARKET && isOrderUnfillable(order, orderPrice, marketPrice) + + const isSwap = getUiOrderType(order) === UiOrderType.SWAP + const isUnfillable = isSwap && isOrderUnfillable(order, orderPrice, marketPrice) // Only trigger state update if flag changed - if (order.isUnfillable !== isUnfillable && order.class === OrderClass.MARKET) { + if (order.isUnfillable !== isUnfillable && isSwap) { setIsOrderUnfillable({ chainId, id: order.id, isUnfillable }) // order.isUnfillable by default is undefined, so we don't want to dispatch this in that case diff --git a/apps/cowswap-frontend/src/legacy/hooks/useRecentActivity.ts b/apps/cowswap-frontend/src/legacy/hooks/useRecentActivity.ts index 1f653b49e0..ef17992437 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/useRecentActivity.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/useRecentActivity.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { MAXIMUM_ORDERS_TO_DISPLAY } from '@cowprotocol/common-const' import { getDateTimestamp } from '@cowprotocol/common-utils' -import { OrderClass, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' import { isTransactionRecent, useAllTransactions, useTransactionsByHash } from 'legacy/state/enhancedTransactions/hooks' @@ -10,6 +10,8 @@ import { EnhancedTransactionDetails } from 'legacy/state/enhancedTransactions/re import { Order, OrderStatus } from 'legacy/state/orders/actions' import { useCombinedPendingOrders, useOrder, useOrders, useOrdersById } from 'legacy/state/orders/hooks' +import { UiOrderType } from 'utils/orderUtils/getUiOrderType' + export interface AddedOrder extends Order { addedTime: number } @@ -48,7 +50,7 @@ enum TxReceiptStatus { export function useRecentActivity(): TransactionAndOrder[] { const { chainId, account } = useWalletInfo() const allTransactions = useAllTransactions() - const allNonEmptyOrders = useOrders(chainId, account, OrderClass.MARKET) + const allNonEmptyOrders = useOrders(chainId, account, UiOrderType.SWAP) const recentOrdersAdjusted = useMemo(() => { return ( diff --git a/apps/cowswap-frontend/src/legacy/state/orders/actions.ts b/apps/cowswap-frontend/src/legacy/state/orders/actions.ts index 162055f7c8..4c16aa6e0f 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/actions.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/actions.ts @@ -113,6 +113,7 @@ export type OrderInfoApi = Pick< | 'ethflowData' | 'onchainOrderData' | 'class' + | 'fullAppData' > /** diff --git a/apps/cowswap-frontend/src/legacy/state/orders/hooks.ts b/apps/cowswap-frontend/src/legacy/state/orders/hooks.ts index f8306f4195..e18ee5f682 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/hooks.ts @@ -2,15 +2,18 @@ import { useCallback, useMemo } from 'react' import { TokenWithLogo } from '@cowprotocol/common-const' import { isTruthy } from '@cowprotocol/common-utils' -import { OrderClass, SupportedChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useDispatch, useSelector } from 'react-redux' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' + import { addOrUpdateOrders, AddOrUpdateOrdersParams, addPendingOrder, cancelOrdersBatch, + clearOrdersStorage, expireOrdersBatch, fulfillOrdersBatch, FulfillOrdersBatchParams, @@ -26,7 +29,6 @@ import { setOrderCancellationHash, updatePresignGnosisSafeTx, UpdatePresignGnosisSafeTxParams, - clearOrdersStorage, } from './actions' import { flatOrdersStateNetwork } from './flatOrdersStateNetwork' import { @@ -181,7 +183,11 @@ function useOrdersStateNetwork(chainId: SupportedChainId | undefined): OrdersSta }, [JSON.stringify(ordersState), chainId]) } -export const useOrders = (chainId: SupportedChainId, account: string | undefined, orderClass: OrderClass): Order[] => { +export const useOrders = ( + chainId: SupportedChainId, + account: string | undefined, + uiOrderType: UiOrderType +): Order[] => { const state = useOrdersStateNetwork(chainId) const accountLowerCase = account?.toLowerCase() @@ -192,7 +198,8 @@ export const useOrders = (chainId: SupportedChainId, account: string | undefined if (!order) return acc const doesBelongToAccount = order.order.owner.toLowerCase() === accountLowerCase - const doesMatchClass = order.order.class === orderClass + const orderType = getUiOrderType(order.order) + const doesMatchClass = orderType === uiOrderType if (doesBelongToAccount && doesMatchClass) { const mappedOrder = _deserializeOrder(order) @@ -204,7 +211,7 @@ export const useOrders = (chainId: SupportedChainId, account: string | undefined return acc }, []) - }, [state, accountLowerCase, orderClass]) + }, [state, accountLowerCase, uiOrderType]) } const useAllOrdersMap = ({ chainId }: GetOrdersParams): PartialOrdersMap => { @@ -287,7 +294,7 @@ export const useCombinedPendingOrders = ({ * The difference is that this hook returns only orders that have the status PENDING * while usePendingOrders aggregates all pending states */ -export const useOnlyPendingOrders = (chainId: SupportedChainId, orderClass: OrderClass): Order[] => { +export const useOnlyPendingOrders = (chainId: SupportedChainId, uiOrderType: UiOrderType): Order[] => { const state = useSelector( (state) => chainId && state.orders?.[chainId]?.pending ) @@ -296,10 +303,10 @@ export const useOnlyPendingOrders = (chainId: SupportedChainId, orderClass: Orde if (!state) return [] return Object.values(state) - .filter((order) => order?.order.class === orderClass) + .filter((order) => order && getUiOrderType(order.order) === uiOrderType) .map(_deserializeOrder) .filter(isTruthy) - }, [state, orderClass]) + }, [state, uiOrderType]) } export const useCancelledOrders = ({ chainId }: GetOrdersParams): Order[] => { diff --git a/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.ts b/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.ts index 69a95df740..2bad277960 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/middleware/appziMiddleware.ts @@ -4,11 +4,13 @@ import { openNpsAppziSometimes, timeSinceInSeconds, } from '@cowprotocol/common-utils' -import { OrderClass, SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { isAnyOf } from '@reduxjs/toolkit' import { AnyAction, Dispatch, Middleware, MiddlewareAPI } from 'redux' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' + import { AppState } from '../../index' import * as OrderActions from '../actions' import { getOrderByIdFromState } from '../helpers' @@ -49,9 +51,12 @@ function _triggerNps( const openSince = order?.openSince const explorerUrl = getExplorerOrderLink(chainId, orderId) + const uiOrderType = order && getUiOrderType(order) + + // TODO: should we show NPS for TWAP orders as well? // Open Appzi NPS for limit orders only if they were filled before `PENDING_TOO_LONG_TIME` since creating const isLimitOrderRecentlyTraded = - order?.class === OrderClass.LIMIT && npsParams?.traded && isOrderInPendingTooLong(openSince) + uiOrderType === UiOrderType.LIMIT && npsParams?.traded && isOrderInPendingTooLong(openSince) // Do not show NPS if the order is hidden and expired const isHiddenAndExpired = order?.isHidden && npsParams?.expired diff --git a/apps/cowswap-frontend/src/legacy/state/orders/utils.ts b/apps/cowswap-frontend/src/legacy/state/orders/utils.ts index 1619550723..e012047740 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/utils.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/utils.ts @@ -1,12 +1,13 @@ import { ONE_HUNDRED_PERCENT, PENDING_ORDERS_BUFFER, ZERO_FRACTION } from '@cowprotocol/common-const' import { buildPriceFromCurrencyAmounts } from '@cowprotocol/common-utils' -import { EnrichedOrder, OrderClass, OrderKind, OrderStatus } from '@cowprotocol/cow-sdk' +import { EnrichedOrder, OrderKind, OrderStatus } from '@cowprotocol/cow-sdk' import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' import JSBI from 'jsbi' import { getIsComposableCowParentOrder } from 'utils/orderUtils/getIsComposableCowParentOrder' import { getOrderSurplus } from 'utils/orderUtils/getOrderSurplus' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' import { Order, updateOrder, UpdateOrderParams as UpdateOrderParamsAction } from './actions' import { OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE } from './consts' @@ -238,7 +239,7 @@ export function getEstimatedExecutionPrice( const outputAmount = CurrencyAmount.fromRawAmount(order.outputToken, order.buyAmount.toString()) const limitPrice = buildPriceFromCurrencyAmounts(inputAmount, outputAmount) - if (order.class === OrderClass.MARKET) { + if (getUiOrderType(order) === UiOrderType.SWAP) { return limitPrice } diff --git a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx index 02fe5c795e..5b7da2640d 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx @@ -1,13 +1,11 @@ import { ReactNode } from 'react' -import { V_COW_CONTRACT_ADDRESS, V_COW, COW } from '@cowprotocol/common-const' +import { COW, V_COW, V_COW_CONTRACT_ADDRESS } from '@cowprotocol/common-const' import { ExplorerDataType, getExplorerLink, shortenAddress } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useENS } from '@cowprotocol/ens' -import { useTokenBySymbolOrAddress } from '@cowprotocol/tokens' -import { TokenLogo } from '@cowprotocol/tokens' -import { ExternalLink, TokenAmount } from '@cowprotocol/ui' -import { UI } from '@cowprotocol/ui' +import { TokenLogo, useTokenBySymbolOrAddress } from '@cowprotocol/tokens' +import { ExternalLink, TokenAmount, UI } from '@cowprotocol/ui' import { CurrencyAmount } from '@uniswap/sdk-core' import { OrderProgressBar } from 'legacy/components/OrderProgressBar' @@ -22,12 +20,13 @@ import { isPending } from 'common/hooks/useCategorizeRecentActivity' import { useGetSurplusData } from 'common/hooks/useGetSurplusFiatValue' import { Icon } from 'common/pure/Icon' import { BannerOrientation, CustomRecipientWarningBanner } from 'common/pure/InlineBanner/banners' -import { RateInfoParams, RateInfo } from 'common/pure/RateInfo' +import { RateInfo, RateInfoParams } from 'common/pure/RateInfo' import { SafeWalletLink } from 'common/pure/SafeWalletLink' import { useHideReceiverWalletBanner, useIsReceiverWalletBannerHidden, } from 'common/state/receiverWalletBannerVisibility' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' import { StatusDetails } from './StatusDetails' import { @@ -177,8 +176,10 @@ export function ActivityDetails(props: { const getShowCancellationModal = useCancelOrder() - const showProgressBar = (activityState === 'open' || activityState === 'filled') && order?.class !== 'limit' - const showCancellationModal = activityDerivedState.order ? getShowCancellationModal(activityDerivedState.order) : null + const isSwap = order && getUiOrderType(order) === UiOrderType.SWAP + + const showProgressBar = (activityState === 'open' || activityState === 'filled') && isSwap + const showCancellationModal = order ? getShowCancellationModal(order) : null const { surplusFiatValue, showFiatValue, surplusToken, surplusAmount } = useGetSurplusData(order) diff --git a/apps/cowswap-frontend/src/modules/appData/types.tsx b/apps/cowswap-frontend/src/modules/appData/types.ts similarity index 95% rename from apps/cowswap-frontend/src/modules/appData/types.tsx rename to apps/cowswap-frontend/src/modules/appData/types.ts index c25a9f6592..3b46e6991f 100644 --- a/apps/cowswap-frontend/src/modules/appData/types.tsx +++ b/apps/cowswap-frontend/src/modules/appData/types.ts @@ -21,6 +21,7 @@ export type AppDataKeyParams = { export type AppDataRecord = AppDataInfo & AppDataUploadStatus & AppDataKeyParams +export type AppDataMetadataOrderClass = latest.OrderClass export type AppDataOrderClass = latest.OrderClass['orderClass'] export type AppDataPendingToUpload = Array diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx index 2ba5b51239..16c0625997 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx @@ -4,10 +4,9 @@ import AlertTriangle from '@cowprotocol/assets/cow-swap/alert.svg' import { ZERO_FRACTION } from '@cowprotocol/common-const' import { useTimeAgo } from '@cowprotocol/common-hooks' import { getAddress, getEtherscanLink } from '@cowprotocol/common-utils' -import { OrderClass, SupportedChainId } from '@cowprotocol/cow-sdk' +import { SupportedChainId } from '@cowprotocol/cow-sdk' import { TokenLogo } from '@cowprotocol/tokens' -import { UI } from '@cowprotocol/ui' -import { Loader, TokenAmount, TokenSymbol } from '@cowprotocol/ui' +import { Loader, TokenAmount, TokenSymbol, UI } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' import SVG from 'react-inlinesvg' @@ -36,6 +35,7 @@ import { calculatePercentageInRelationToReference } from 'utils/orderUtils/calcu import { calculatePriceDifference, PriceDifference } from 'utils/orderUtils/calculatePriceDifference' import { getIsComposableCowParentOrder } from 'utils/orderUtils/getIsComposableCowParentOrder' import { getSellAmountWithFee } from 'utils/orderUtils/getSellAmountWithFee' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' import { ParsedOrder } from 'utils/orderUtils/parseOrder' import * as styledEl from './styled' @@ -307,7 +307,7 @@ export function OrderRow({ amountDifference={priceDiffs?.amount} percentageFee={feeDifference} amountFee={feeAmount} - canShowWarning={order.class !== OrderClass.MARKET && !isUnfillable} + canShowWarning={getUiOrderType(order) !== UiOrderType.SWAP && !isUnfillable} isUnfillable={isUnfillable} /> diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/usePriorityTokenAddresses.ts b/apps/cowswap-frontend/src/modules/trade/hooks/usePriorityTokenAddresses.ts index 204f749915..f2a98980a0 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/usePriorityTokenAddresses.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/usePriorityTokenAddresses.ts @@ -1,7 +1,6 @@ import { useMemo } from 'react' import { getAddress, isTruthy } from '@cowprotocol/common-utils' -import { OrderClass } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' import { useSelector } from 'react-redux' @@ -9,6 +8,8 @@ import { useSelector } from 'react-redux' import { AppState } from 'legacy/state' import { PartialOrdersMap } from 'legacy/state/orders/reducer' +import { getUiOrderType, UiOrderType } from 'utils/orderUtils/getUiOrderType' + import { useDerivedTradeState } from './useDerivedTradeState' export function usePriorityTokenAddresses(): string[] { @@ -24,7 +25,7 @@ export function usePriorityTokenAddresses(): string[] { return Object.values(pending) .filter(isTruthy) - .filter(({ order }) => order.class === OrderClass.MARKET) + .filter(({ order }) => getUiOrderType(order) === UiOrderType.SWAP) .map(({ order }) => { return [order.inputToken.address, order.outputToken.address] }) diff --git a/apps/cowswap-frontend/src/modules/twap/hooks/useAllEmulatedOrders.ts b/apps/cowswap-frontend/src/modules/twap/hooks/useAllEmulatedOrders.ts index b02315f559..ca0d1a8bff 100644 --- a/apps/cowswap-frontend/src/modules/twap/hooks/useAllEmulatedOrders.ts +++ b/apps/cowswap-frontend/src/modules/twap/hooks/useAllEmulatedOrders.ts @@ -1,11 +1,12 @@ import { useMemo } from 'react' -import { OrderClass } from '@cowprotocol/cow-sdk' import { useIsSafeApp, useWalletInfo } from '@cowprotocol/wallet' import { Order } from 'legacy/state/orders/actions' import { useOrders } from 'legacy/state/orders/hooks' +import { UiOrderType } from 'utils/orderUtils/getUiOrderType' + import { useEmulatedPartOrders } from './useEmulatedPartOrders' import { useEmulatedTwapOrders } from './useEmulatedTwapOrders' import { useTwapOrdersTokens } from './useTwapOrdersTokens' @@ -17,16 +18,14 @@ export function useAllEmulatedOrders(): Order[] { const emulatedPartOrders = useEmulatedPartOrders(twapOrdersTokens) const isSafeApp = useIsSafeApp() - const limitOrders = useOrders(chainId, account, OrderClass.LIMIT) + const twapOrders = useOrders(chainId, account, UiOrderType.TWAP) const discreteTwapOrders = useMemo(() => { - return limitOrders.filter((order) => order.composableCowInfo?.isVirtualPart === false) - }, [limitOrders]) + return twapOrders.filter((order) => order.composableCowInfo?.isVirtualPart === false) + }, [twapOrders]) - const allEmulatedOrders = useMemo(() => { + return useMemo(() => { if (!isSafeApp) return [] return emulatedTwapOrders.concat(emulatedPartOrders).concat(discreteTwapOrders) }, [emulatedTwapOrders, emulatedPartOrders, discreteTwapOrders, isSafeApp]) - - return allEmulatedOrders } diff --git a/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx b/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx index 1415d5e0c3..1ccec9792c 100644 --- a/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/LimitOrders/index.tsx @@ -1,18 +1,15 @@ -import { useMemo } from 'react' - -import { OrderClass } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' import { useOrders } from 'legacy/state/orders/hooks' import { AppDataUpdater } from 'modules/appData' import { - LimitOrdersWidget, - QuoteObserverUpdater, - InitialPriceUpdater, ExecutionPriceUpdater, FillLimitOrdersDerivedStateUpdater, + InitialPriceUpdater, LIMIT_ORDER_SLIPPAGE, + LimitOrdersWidget, + QuoteObserverUpdater, SetupLimitOrderAmountsFromUrlUpdater, useIsWidgetUnlocked, } from 'modules/limitOrders' @@ -20,12 +17,11 @@ import { OrdersTableWidget } from 'modules/ordersTable' import { TabOrderTypes } from 'modules/ordersTable/pure/OrdersTableContainer' import * as styledEl from 'modules/trade/pure/TradePageLayout' -import { getIsNotComposableCowOrder } from 'utils/orderUtils/getIsNotComposableCowOrder' +import { UiOrderType } from 'utils/orderUtils/getUiOrderType' export default function LimitOrderPage() { const { chainId, account } = useWalletInfo() - const allLimitOrders = useOrders(chainId, account, OrderClass.LIMIT) - const onlyPlainLimitOrders = useMemo(() => allLimitOrders.filter(getIsNotComposableCowOrder), [allLimitOrders]) + const allLimitOrders = useOrders(chainId, account, UiOrderType.LIMIT) const isUnlocked = useIsWidgetUnlocked() @@ -46,7 +42,7 @@ export default function LimitOrderPage() { diff --git a/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts b/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts new file mode 100644 index 0000000000..d7ae7f1db0 --- /dev/null +++ b/apps/cowswap-frontend/src/utils/orderUtils/getUiOrderType.ts @@ -0,0 +1,56 @@ +import { OrderClass } from '@cowprotocol/cow-sdk' + +import { Order } from 'legacy/state/orders/actions' + +import { AppDataMetadataOrderClass } from 'modules/appData/types' +import { decodeAppData } from 'modules/appData/utils/decodeAppData' + +/** + * UI order type that is different from existing types or classes + * + * This concept doesn't match what the API returns, as it has no notion of advanced/twap orders + * It uses order appData if available, otherwise fallback to less reliable ways + */ +export enum UiOrderType { + SWAP = 'SWAP', + LIMIT = 'LIMIT', + TWAP = 'TWAP', +} + +const APPDATA_ORDER_CLASS_TO_UI_ORDER_TYPE_MAP: Record = { + market: UiOrderType.SWAP, + limit: UiOrderType.LIMIT, + liquidity: UiOrderType.LIMIT, + twap: UiOrderType.TWAP, +} + +const API_ORDER_CLASS_TO_UI_ORDER_TYPE_MAP: Record = { + [OrderClass.MARKET]: UiOrderType.SWAP, + [OrderClass.LIMIT]: UiOrderType.LIMIT, + [OrderClass.LIQUIDITY]: UiOrderType.LIMIT, +} + +export function getUiOrderType({ + fullAppData, + composableCowInfo, + class: orderClass, +}: Pick): UiOrderType { + const parsedAppData = decodeAppData(fullAppData) + + const appDataOrderClass = parsedAppData?.metadata?.orderClass as AppDataMetadataOrderClass | undefined + const typeFromAppData = APPDATA_ORDER_CLASS_TO_UI_ORDER_TYPE_MAP[appDataOrderClass?.orderClass || ''] + + // 1. AppData info has priority as it's what's more precise + if (typeFromAppData) { + return typeFromAppData + } + + // 2. If composableCowInfo is available, we know it to be a twap + if (composableCowInfo) { + return UiOrderType.TWAP + } + + // 3. As a last resort, map it to API classification. + // Least precise as it doesn't distinguish twap type and uses backend logic which doesn't match frontend's classification + return API_ORDER_CLASS_TO_UI_ORDER_TYPE_MAP[orderClass] +} diff --git a/libs/analytics/src/types.ts b/libs/analytics/src/types.ts index 51e3c45c3d..364bd3e168 100644 --- a/libs/analytics/src/types.ts +++ b/libs/analytics/src/types.ts @@ -37,4 +37,5 @@ export enum Dimensions { injectedWidgetAppId = 'injectedWidgetAppId', } +// TODO: use UiOrderType instead export type AnalyticsOrderType = OrderClass | 'TWAP'