Skip to content

Commit

Permalink
feat(fee=0): classify order types (#3559)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alfetopito authored Dec 27, 2023
1 parent 3d6dc3d commit a2f8def
Show file tree
Hide file tree
Showing 17 changed files with 169 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useSetAtom } from 'jotai'
import { useCallback, useEffect, useRef } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'

import {
getExplorerOrderLink,
isOrderInPendingTooLong,
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'
Expand All @@ -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'

Expand Down Expand Up @@ -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)
}
})
Expand All @@ -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({
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -333,6 +348,7 @@ export function PendingOrdersUpdater(): null {
}
},
[
updatersRefMap,
addOrUpdateOrders,
fulfillOrdersBatch,
expireOrdersBatch,
Expand All @@ -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])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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'

Expand All @@ -31,8 +33,8 @@ function useMarkets(chainId: SupportedChainId, account: string | undefined): Mar
return useSafeMemo(() => {
return pending.reduce<Record<string, { chainId: number; inputCurrency: Token; outputCurrency: Token }>>(
(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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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'
import { NATIVE_CURRENCY_BUY_ADDRESS, WRAPPED_NATIVE_CURRENCY } from '@cowprotocol/common-const'
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'

Expand All @@ -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'
Expand All @@ -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()

Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions apps/cowswap-frontend/src/legacy/hooks/useRecentActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ 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'
import { EnhancedTransactionDetails } from 'legacy/state/enhancedTransactions/reducer'
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
}
Expand Down Expand Up @@ -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<TransactionAndOrder[]>(() => {
return (
Expand Down
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/legacy/state/orders/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export type OrderInfoApi = Pick<
| 'ethflowData'
| 'onchainOrderData'
| 'class'
| 'fullAppData'
>

/**
Expand Down
23 changes: 15 additions & 8 deletions apps/cowswap-frontend/src/legacy/state/orders/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,7 +29,6 @@ import {
setOrderCancellationHash,
updatePresignGnosisSafeTx,
UpdatePresignGnosisSafeTxParams,
clearOrdersStorage,
} from './actions'
import { flatOrdersStateNetwork } from './flatOrdersStateNetwork'
import {
Expand Down Expand Up @@ -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()

Expand All @@ -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)
Expand All @@ -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 => {
Expand Down Expand Up @@ -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<AppState, PartialOrdersMap | undefined>(
(state) => chainId && state.orders?.[chainId]?.pending
)
Expand All @@ -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[] => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit a2f8def

Please sign in to comment.