diff --git a/src/abacus-ts/lib/mapLoadable.ts b/src/abacus-ts/lib/mapLoadable.ts index 27cdfa162..92954d453 100644 --- a/src/abacus-ts/lib/mapLoadable.ts +++ b/src/abacus-ts/lib/mapLoadable.ts @@ -19,18 +19,15 @@ export function mergeLoadableData( } as any; } -// converts idle to pending and if a status has valid data is counts as success -export function mergeLoadableStatus( - ...status: Array> -): Exclude['status'], 'idle'> { +export function mergeLoadableStatus(...status: Array>): Loadable['status'] { if (status.some((s) => s.status === 'error' && s.data == null)) { return 'error'; } - if (status.some((s) => s.status === 'idle')) { - return 'pending'; - } if (status.some((s) => s.status === 'pending' && s.data == null)) { return 'pending'; } + if (status.some((s) => s.status === 'idle')) { + return 'idle'; + } return 'success'; } diff --git a/src/components/OrderSideTag.tsx b/src/components/OrderSideTag.tsx index def4293cd..cb20386ef 100644 --- a/src/components/OrderSideTag.tsx +++ b/src/components/OrderSideTag.tsx @@ -1,3 +1,4 @@ +import { IndexerOrderSide } from '@/types/indexer/indexerApiGen'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import { STRING_KEYS } from '@/constants/localization'; @@ -7,7 +8,7 @@ import { useStringGetter } from '@/hooks/useStringGetter'; import { Tag, TagSign, TagSize, TagType } from './Tag'; type ElementProps = { - orderSide: OrderSide; + orderSide: OrderSide | IndexerOrderSide; }; type StyleProps = { diff --git a/src/lib/orders.ts b/src/lib/orders.ts index 3d74fc882..68731d806 100644 --- a/src/lib/orders.ts +++ b/src/lib/orders.ts @@ -1,3 +1,5 @@ +import { AssetInfo, AssetInfos, MarketsData } from '@/abacus-ts/rawTypes'; +import { IndexerCompositeFillObject } from '@/types/indexer/indexerManual'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import BigNumber from 'bignumber.js'; @@ -8,12 +10,12 @@ import { AbacusOrderTypes, KotlinIrEnumValues, Nullable, + SubaccountFill, SubaccountFills, TRADE_TYPES, type Asset, type OrderStatus, type PerpetualMarket, - type SubaccountFill, type SubaccountFundingPayment, type SubaccountOrder, } from '@/constants/abacus'; @@ -22,6 +24,9 @@ import { IconName } from '@/components/Icon'; import { convertAbacusOrderSide } from '@/lib/abacus/conversions'; +import { getAssetFromMarketId } from './assetUtils'; +import { MaybeBigNumber } from './numbers'; + export const getOrderStatusInfo = ({ status }: { status: string }) => { switch (status) { case AbacusOrderStatus.Open.rawValue: { @@ -145,6 +150,33 @@ export const getHydratedTradingData = < ...('side' in data && { orderSide: convertAbacusOrderSide(data.side) }), }); +type NewAddedProps = { + asset: AssetInfo | undefined; + stepSizeDecimals: Nullable; + tickSizeDecimals: Nullable; +}; + +export const getHydratedFill = ({ + data, + assets, + perpetualMarkets, +}: { + data: IndexerCompositeFillObject; + assets: AssetInfos; + perpetualMarkets: MarketsData; +}): IndexerCompositeFillObject & NewAddedProps => { + return { + ...data, + asset: assets[getAssetFromMarketId(data.market ?? '')], + stepSizeDecimals: MaybeBigNumber( + perpetualMarkets[data.market ?? '']?.stepSize + )?.decimalPlaces(), + tickSizeDecimals: MaybeBigNumber( + perpetualMarkets[data.market ?? '']?.tickSize + )?.decimalPlaces(), + }; +}; + export const getTradeType = (orderType: string) => TRADE_TYPES[orderType as KotlinIrEnumValues]; diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index 1694de597..53b661be1 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; +import { selectAccountFillsLoading } from '@/abacus-ts/selectors/account'; import { shallowEqual } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -90,6 +91,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { ); const hasUnseenOrderUpdates = unseenOrders > 0; + const areFillsLoading = useAppSelector(selectAccountFillsLoading) === 'pending'; const numUnseenFills = useParameterizedSelector( createGetUnseenFillsCount, showCurrentMarket ? currentMarketId : undefined @@ -227,10 +229,14 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { value: InfoSection.Fills, label: stringGetter({ key: STRING_KEYS.FILLS }), - slotRight: fillsTagNumber && ( - - {fillsTagNumber} - + slotRight: areFillsLoading ? ( + + ) : ( + fillsTagNumber && ( + + {fillsTagNumber} + + ) ), content: ( @@ -264,11 +270,12 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => { }), [ stringGetter, - currentMarketId, - showCurrentMarket, - isTablet, + areFillsLoading, fillsTagNumber, hasUnseenFillUpdates, + showCurrentMarket, + currentMarketId, + isTablet, ] ); diff --git a/src/state/_store.ts b/src/state/_store.ts index dc0ae05ef..4d7601a4a 100644 --- a/src/state/_store.ts +++ b/src/state/_store.ts @@ -5,7 +5,6 @@ import storage from 'redux-persist/lib/storage'; import abacusStateManager from '@/lib/abacus'; import { runFn } from '@/lib/do'; -import { testFlags } from '@/lib/testFlags'; import { accountSlice } from './account'; import { accountUiMemorySlice } from './accountUiMemory'; @@ -98,13 +97,11 @@ export const persistor = persistStore(store); // Set store so (Abacus & v4-Client) classes can getState and dispatch abacusStateManager.setStore(store); -if (testFlags.useAbacusTs) { - runFn(async () => { - const { storeLifecycles } = await import('@/abacus-ts/storeLifecycles'); - // we ignore the cleanups for now since we want these running forever - storeLifecycles.forEach((fn) => fn(store)); - }); -} +runFn(async () => { + const { storeLifecycles } = await import('@/abacus-ts/storeLifecycles'); + // we ignore the cleanups for now since we want these running forever + storeLifecycles.forEach((fn) => fn(store)); +}); export type RootStore = typeof store; export type RootState = ReturnType; diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts index 49337c144..646aea047 100644 --- a/src/state/accountSelectors.ts +++ b/src/state/accountSelectors.ts @@ -1,3 +1,4 @@ +import { selectAccountFills } from '@/abacus-ts/selectors/account'; import { selectRawIndexerHeightData } from '@/abacus-ts/selectors/base'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import BigNumber from 'bignumber.js'; @@ -735,15 +736,14 @@ export const createGetUnseenFillsCount = () => [ getCurrentAccountMemory, selectRawIndexerHeightData, - getSubaccountFills, + selectAccountFills, (state, market: string | undefined) => market, ], (memory, height, fills, market) => { if (height == null) { return 0; } - const ourFills = - (market == null ? fills : fills?.filter((o) => o.marketId === market)) ?? EMPTY_ARR; + const ourFills = market == null ? fills : fills.filter((o) => o.market === market); if (ourFills.length === 0) { return 0; } @@ -752,9 +752,9 @@ export const createGetUnseenFillsCount = () => } const unseen = ourFills.filter( (o) => - o.createdAtMilliseconds > + (mapIfPresent(o.createdAt, (c) => new Date(c).valueOf()) ?? 0) > (mapIfPresent( - (memory.seenFills[o.marketId] ?? memory.seenFills[ALL_MARKETS_STRING])?.time, + (memory.seenFills[o.market ?? ''] ?? memory.seenFills[ALL_MARKETS_STRING])?.time, (t) => new Date(t).valueOf() ) ?? 0) ); diff --git a/src/views/tables/FillsTable.tsx b/src/views/tables/FillsTable.tsx index f24ae9551..fb040a0d2 100644 --- a/src/views/tables/FillsTable.tsx +++ b/src/views/tables/FillsTable.tsx @@ -1,16 +1,18 @@ import { forwardRef, Key, useMemo } from 'react'; +import { AssetInfo } from '@/abacus-ts/rawTypes'; +import { getCurrentMarketAccountFills, selectAccountFills } from '@/abacus-ts/selectors/account'; +import { selectRawAssetsData, selectRawMarketsData } from '@/abacus-ts/selectors/base'; +import { IndexerFillType, IndexerLiquidity, IndexerOrderSide } from '@/types/indexer/indexerApiGen'; +import { IndexerCompositeFillObject } from '@/types/indexer/indexerManual'; import { Nullable } from '@dydxprotocol/v4-abacus'; -import { OrderSide } from '@dydxprotocol/v4-client-js'; import type { ColumnSize } from '@react-types/table'; -import { shallowEqual } from 'react-redux'; import styled, { css } from 'styled-components'; import tw from 'twin.macro'; -import { type Asset, type SubaccountFill } from '@/constants/abacus'; +import { Asset } from '@/constants/abacus'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; -import { EMPTY_ARR } from '@/constants/objects'; import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useViewPanel } from '@/hooks/useSeen'; @@ -29,15 +31,14 @@ import { TableColumnHeader } from '@/components/Table/TableColumnHeader'; import { PageSize } from '@/components/Table/TablePaginationRow'; import { TagSize } from '@/components/Tag'; -import { getCurrentMarketFills, getSubaccountFills } from '@/state/accountSelectors'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; -import { getAssets } from '@/state/assetsSelectors'; import { openDialog } from '@/state/dialogs'; -import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; +import { assertNever } from '@/lib/assertNever'; +import { getAssetFromMarketId } from '@/lib/assetUtils'; import { mapIfPresent } from '@/lib/do'; import { MustBigNumber } from '@/lib/numbers'; -import { getHydratedTradingData } from '@/lib/orders'; +import { getHydratedFill } from '@/lib/orders'; import { orEmptyRecord } from '@/lib/typeUtils'; const MOBILE_FILLS_PER_PAGE = 50; @@ -47,7 +48,6 @@ export enum FillsTableColumnKey { Market = 'Market', Action = 'Action', Side = 'Side', - SideLongShort = 'Side-LongShort', Type = 'Type', Price = 'Price', Liquidity = 'Liquidity', @@ -55,10 +55,6 @@ export enum FillsTableColumnKey { AmountTag = 'Amount-Tag', Total = 'Total', Fee = 'Fee', - TypeLiquidity = 'Type-Liquidity', - - // TODO: CT-1292 remove deprecated fields - TotalFee = 'Total-Fee', // Tablet Only TypeAmount = 'Type-Amount', @@ -66,11 +62,10 @@ export enum FillsTableColumnKey { } export type FillTableRow = { - asset: Nullable; + asset: Nullable; stepSizeDecimals: Nullable; tickSizeDecimals: Nullable; - orderSide?: Nullable; -} & SubaccountFill; +} & IndexerCompositeFillObject; const getFillsTableColumnDef = ({ key, @@ -95,19 +90,13 @@ const getFillsTableColumnDef = ({ {stringGetter({ key: STRING_KEYS.AMOUNT })} ), - renderCell: ({ resources, size, stepSizeDecimals, asset }) => ( + renderCell: ({ size, type, stepSizeDecimals, asset }) => ( - } + slotLeft={} > - {resources.typeStringKey ? stringGetter({ key: resources.typeStringKey }) : null} + {type != null ? stringGetter({ key: getIndexerFillTypeStringKey(type) }) : null} {stringGetter({ key: STRING_KEYS.FEE })} ), - renderCell: ({ fee, orderSide, price, resources, tickSizeDecimals }) => ( + renderCell: ({ fee, side, price, tickSizeDecimals, liquidity }) => ( <$InlineRow> - <$Side side={orderSide}> - {resources.sideStringKey ? stringGetter({ key: resources.sideStringKey }) : null} + <$Side side={side}> + {side != null ? stringGetter({ key: getIndexerOrderSideStringKey(side) }) : null} @ <$InlineRow> - {resources.liquidityStringKey - ? stringGetter({ key: resources.liquidityStringKey }) + {liquidity != null + ? stringGetter({ key: getIndexerLiquidityStringKey(liquidity) }) : null} @@ -154,36 +143,47 @@ const getFillsTableColumnDef = ({ }, [FillsTableColumnKey.Time]: { columnKey: 'time', - getCellValue: (row) => row.createdAtMilliseconds, + getCellValue: (row) => row.createdAt, label: stringGetter({ key: STRING_KEYS.TIME }), - renderCell: ({ createdAtMilliseconds }) => ( + renderCell: ({ createdAt }) => ( ), }, [FillsTableColumnKey.Market]: { columnKey: 'market', - getCellValue: (row) => row.marketId, + getCellValue: (row) => row.market, label: stringGetter({ key: STRING_KEYS.MARKET }), - renderCell: ({ asset }) => , + renderCell: ({ asset }) => ( + + ), }, [FillsTableColumnKey.Action]: { columnKey: 'market-simple', - getCellValue: (row) => row.marketId, + getCellValue: (row) => row.market, label: stringGetter({ key: STRING_KEYS.ACTION }), - renderCell: ({ asset, orderSide }) => ( + renderCell: ({ asset, side }) => ( - {orderSide && ( - <$Side side={orderSide}> + {side != null && ( + <$Side side={side}> {stringGetter({ - key: { - [OrderSide.BUY]: STRING_KEYS.BUY, - [OrderSide.SELL]: STRING_KEYS.SELL, - }[orderSide], + key: getIndexerOrderSideStringKey(side), })} )} @@ -193,34 +193,21 @@ const getFillsTableColumnDef = ({ }, [FillsTableColumnKey.Liquidity]: { columnKey: 'liquidity', - getCellValue: (row) => row.liquidity.rawValue, + getCellValue: (row) => row.liquidity, label: stringGetter({ key: STRING_KEYS.LIQUIDITY }), - renderCell: ({ resources }) => - resources.liquidityStringKey ? stringGetter({ key: resources.liquidityStringKey }) : null, - }, - [FillsTableColumnKey.TotalFee]: { - columnKey: 'totalFee', - getCellValue: (row) => MustBigNumber(row.price).times(row.size).toNumber(), - label: ( - - {stringGetter({ key: STRING_KEYS.TOTAL })} - {stringGetter({ key: STRING_KEYS.FEE })} - - ), - renderCell: ({ size, fee, price }) => ( - - - - - ), + renderCell: ({ liquidity }) => + liquidity != null ? stringGetter({ key: getIndexerLiquidityStringKey(liquidity) }) : null, }, [FillsTableColumnKey.Total]: { columnKey: 'total', - getCellValue: (row) => MustBigNumber(row.price).times(row.size).toNumber(), + getCellValue: (row) => + MustBigNumber(row.price) + .times(row.size ?? 0) + .toNumber(), label: stringGetter({ key: STRING_KEYS.TOTAL }), renderCell: ({ size, price }) => ( - + ), }, @@ -236,10 +223,10 @@ const getFillsTableColumnDef = ({ }, [FillsTableColumnKey.Type]: { columnKey: 'type', - getCellValue: (row) => row.type.rawValue, + getCellValue: (row) => row.type, label: stringGetter({ key: STRING_KEYS.TYPE }), - renderCell: ({ resources }) => - resources.typeStringKey ? stringGetter({ key: resources.typeStringKey }) : null, + renderCell: ({ type }) => + type != null ? stringGetter({ key: getIndexerFillTypeStringKey(type) }) : null, }, [FillsTableColumnKey.Price]: { columnKey: 'price', @@ -265,27 +252,11 @@ const getFillsTableColumnDef = ({ }, [FillsTableColumnKey.Side]: { columnKey: 'side', - getCellValue: (row) => row.orderSide, + getCellValue: (row) => row.side, label: stringGetter({ key: STRING_KEYS.SIDE }), - renderCell: ({ orderSide }) => - orderSide && , - }, - [FillsTableColumnKey.SideLongShort]: { - columnKey: 'side', - getCellValue: (row) => row.orderSide, - label: stringGetter({ key: STRING_KEYS.SIDE }), - renderCell: ({ orderSide }) => ( - - ), + renderCell: ({ side }) => side && , }, + [FillsTableColumnKey.AmountPrice]: { columnKey: 'sizePrice', getCellValue: (row) => row.size, @@ -307,32 +278,47 @@ const getFillsTableColumnDef = ({ ), }, - [FillsTableColumnKey.TypeLiquidity]: { - columnKey: 'typeLiquidity', - getCellValue: (row) => row.type.rawValue, - label: ( - - {stringGetter({ key: STRING_KEYS.TYPE })} - {stringGetter({ key: STRING_KEYS.LIQUIDITY })} - - ), - renderCell: ({ resources }) => ( - - - {resources.typeStringKey ? stringGetter({ key: resources.typeStringKey }) : null} - - - {resources.liquidityStringKey - ? stringGetter({ key: resources.liquidityStringKey }) - : null} - - - ), - }, } satisfies Record> )[key], }); +function getIndexerFillTypeStringKey(fillType: IndexerFillType): string { + switch (fillType) { + case IndexerFillType.LIMIT: + return STRING_KEYS.LIMIT_ORDER_SHORT; + case IndexerFillType.LIQUIDATED: + return STRING_KEYS.LIQUIDATED; + case IndexerFillType.LIQUIDATION: + return STRING_KEYS.LIQUIDATION; + case IndexerFillType.DELEVERAGED: + return STRING_KEYS.DELEVERAGED; + case IndexerFillType.OFFSETTING: + return STRING_KEYS.OFFSETTING; + default: + assertNever(fillType); + return STRING_KEYS.LIMIT_ORDER_SHORT; + } +} + +function getIndexerLiquidityStringKey(liquidity: IndexerLiquidity): string { + switch (liquidity) { + case IndexerLiquidity.MAKER: + return STRING_KEYS.MAKER; + case IndexerLiquidity.TAKER: + return STRING_KEYS.TAKER; + default: + assertNever(liquidity); + return STRING_KEYS.MAKER; + } +} + +function getIndexerOrderSideStringKey(side: IndexerOrderSide) { + if (side === IndexerOrderSide.BUY) { + return STRING_KEYS.BUY; + } + return STRING_KEYS.SELL; +} + type ElementProps = { columnKeys: FillsTableColumnKey[]; columnWidths?: Partial>; @@ -363,24 +349,27 @@ export const FillsTable = forwardRef( const dispatch = useAppDispatch(); const { isMobile } = useBreakpoints(); - const marketFills = useAppSelector(getCurrentMarketFills, shallowEqual) ?? EMPTY_ARR; - const allFills = useAppSelector(getSubaccountFills, shallowEqual) ?? EMPTY_ARR; + const marketFills = useAppSelector(getCurrentMarketAccountFills); + const allFills = useAppSelector(selectAccountFills); const fills = currentMarket ? marketFills : allFills; - const allPerpetualMarkets = orEmptyRecord(useAppSelector(getPerpetualMarkets, shallowEqual)); - const allAssets = orEmptyRecord(useAppSelector(getAssets, shallowEqual)); + const allPerpetualMarkets = orEmptyRecord(useAppSelector(selectRawMarketsData)); + const allAssets = orEmptyRecord(useAppSelector(selectRawAssetsData)); useViewPanel(currentMarket, 'fills'); const symbol = mapIfPresent(currentMarket, (market) => - mapIfPresent(allPerpetualMarkets[market]?.assetId, (assetId) => allAssets[assetId]?.id) + mapIfPresent( + allPerpetualMarkets[market]?.ticker, + (ticker) => allAssets[getAssetFromMarketId(ticker)]?.id + ) ); const fillsData = useMemo( () => fills.map( - (fill: SubaccountFill): FillTableRow => - getHydratedTradingData({ + (fill: IndexerCompositeFillObject): FillTableRow => + getHydratedFill({ data: fill, assets: allAssets, perpetualMarkets: allPerpetualMarkets, @@ -396,7 +385,7 @@ export const FillsTable = forwardRef( data={ isMobile && withGradientCardRows ? fillsData.slice(0, MOBILE_FILLS_PER_PAGE) : fillsData } - getRowKey={(row: FillTableRow) => row.id} + getRowKey={(row: FillTableRow) => row.id ?? ''} onRowAction={(key: Key) => dispatch(openDialog(DialogTypes.FillDetails({ fillId: `${key}` }))) } @@ -428,14 +417,14 @@ const $Table = styled(Table)` ${tradeViewMixins.horizontalTable} ` as typeof Table; const $InlineRow = tw.div`inlineRow`; -const $Side = styled.span<{ side: Nullable }>` +const $Side = styled.span<{ side: Nullable }>` ${({ side }) => side && { - [OrderSide.BUY]: css` + [IndexerOrderSide.BUY]: css` color: var(--color-positive); `, - [OrderSide.SELL]: css` + [IndexerOrderSide.SELL]: css` color: var(--color-negative); `, }[side]};