diff --git a/package.json b/package.json index 3720b8a354..c716c7dd61 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@cosmjs/tendermint-rpc": "^0.32.1", "@datadog/browser-logs": "^5.23.3", "@dydxprotocol/v4-client-js": "2.1.1", - "@dydxprotocol/v4-localization": "1.1.312", + "@dydxprotocol/v4-localization": "^1.1.313", "@dydxprotocol/v4-proto": "^7.0.0-dev.0", "@emotion/is-prop-valid": "^1.3.0", "@hugocxl/react-to-image": "^0.0.9", @@ -217,7 +217,9 @@ }, "pnpm": { "overrides": { - "follow-redirects": "1.15.3" + "follow-redirects": "1.15.3", + "@injectivelabs/sdk-ts": "1.16.10", + "@injectivelabs/ts-types": "1.16.10" } }, "babelMacros": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55c09a4f8c..485fd6e10b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,8 @@ settings: overrides: follow-redirects: 1.15.3 + '@injectivelabs/sdk-ts': 1.16.10 + '@injectivelabs/ts-types': 1.16.10 dependencies: '@cosmjs/amino': @@ -33,8 +35,8 @@ dependencies: specifier: 2.1.1 version: 2.1.1 '@dydxprotocol/v4-localization': - specifier: 1.1.312 - version: 1.1.312 + specifier: ^1.1.313 + version: 1.1.313 '@dydxprotocol/v4-proto': specifier: ^7.0.0-dev.0 version: 7.0.0-dev.0 @@ -1499,8 +1501,8 @@ packages: - utf-8-validate dev: false - /@dydxprotocol/v4-localization@1.1.312: - resolution: {integrity: sha512-jg/mLRjawRzBD9VLOlZTDUgdRJBmIBLkps42E9uIfxW0ZrSt9mEpC1iX1v/qkkSr79dbwqNZsIBYG8gKMP5dyA==} + /@dydxprotocol/v4-localization@1.1.313: + resolution: {integrity: sha512-VMBGj7/1RUeRAFa0qxew8V4e2VUh188qa7tVudJqdo7CLLZz8su+A0GRmADv7/ntnYBBQhHwMWQFrJkT4kQphA==} dev: false /@dydxprotocol/v4-proto@7.0.0-dev.0: @@ -2711,20 +2713,20 @@ packages: '@injectivelabs/grpc-web': 0.0.1(google-protobuf@3.21.4) google-protobuf: 3.21.4 protobufjs: 7.5.1 - rxjs: 7.8.1 + rxjs: 7.8.2 dev: false - /@injectivelabs/core-proto-ts@1.14.3: - resolution: {integrity: sha512-V45Pr3hFD09Rlkai2bWKjntfvgDFvGEBWh5Wy1iRpjnYzeiwnizvaJtyLrAEfZ87d5AD5qtPCNJN5fd27iFa5w==} + /@injectivelabs/core-proto-ts@1.16.1: + resolution: {integrity: sha512-/6LWwZ/ZcC6/sI21s92cf3/8mgma6xRnBXtsF+708g2SX87WatJuXL8DjZsY1agCCnyw6bLVLtjKAfLQopNkxw==} dependencies: '@injectivelabs/grpc-web': 0.0.1(google-protobuf@3.21.4) google-protobuf: 3.21.4 protobufjs: 7.5.1 - rxjs: 7.8.1 + rxjs: 7.8.2 dev: false - /@injectivelabs/exceptions@1.15.2: - resolution: {integrity: sha512-sAlJfLMH9HxlABKrlnjNX/O/gg49ao2r/yuuFbQci1vVNeF2TSoaa3f0OzUckakyPBslDJCK24Ddi+mft0qQqg==} + /@injectivelabs/exceptions@1.16.10: + resolution: {integrity: sha512-Tv0yM1JGSRzqtHp5fQlJ+B16Xej78WJrOqPrHC1H1MB9wr0NL3krAo86sm04IZI1RCq0Mv3hqCPPWqpZ6z+pRQ==} dependencies: http-status-codes: 2.3.0 dev: false @@ -2754,13 +2756,13 @@ packages: google-protobuf: 3.21.4 dev: false - /@injectivelabs/indexer-proto-ts@1.13.9: - resolution: {integrity: sha512-05goWVmXpwiHDVPK/p2fr9xUrslHrKSUdsu5N2AYbQoXMs0Txnke8vFrLtIbKV0BMOxteqlOunAClJ75Wt6hTA==} + /@injectivelabs/indexer-proto-ts@1.13.14: + resolution: {integrity: sha512-tOXIvmKbsotMWz1w3ajUbzwQOenNbgk3mHjvbiJltldr9QYCaKH/++CHZ4/O+uuW7rb3HtAStjXbXC4lyIizEQ==} dependencies: '@injectivelabs/grpc-web': 0.0.1(google-protobuf@3.21.4) google-protobuf: 3.21.4 protobufjs: 7.5.1 - rxjs: 7.8.1 + rxjs: 7.8.2 dev: false /@injectivelabs/mito-proto-ts@1.13.2: @@ -2769,13 +2771,13 @@ packages: '@injectivelabs/grpc-web': 0.0.1(google-protobuf@3.21.4) google-protobuf: 3.21.4 protobufjs: 7.5.1 - rxjs: 7.8.1 + rxjs: 7.8.2 dev: false - /@injectivelabs/networks@1.15.3: - resolution: {integrity: sha512-WZ4E5TLUgHudVXpU7AGlLppx1NUmk9mtOOsDJQcpRaKXTQ520AXMov6DsqMY5aLUJ7QCkDz5HuoK0D7/SSrh6Q==} + /@injectivelabs/networks@1.16.10: + resolution: {integrity: sha512-vdAKoMqJ7O3zEMaPzTcskDJfgS7zz6eSCQ7tJE7kIfStGEoK0aGJI7RdgWxfm4CApW4PKqb4u/LSmNkYHmMrww==} dependencies: - '@injectivelabs/ts-types': 1.15.3 + '@injectivelabs/ts-types': 1.16.10 dev: false /@injectivelabs/olp-proto-ts@1.13.4: @@ -2784,34 +2786,33 @@ packages: '@injectivelabs/grpc-web': 0.0.1(google-protobuf@3.21.4) google-protobuf: 3.21.4 protobufjs: 7.5.1 - rxjs: 7.8.1 + rxjs: 7.8.2 dev: false - /@injectivelabs/sdk-ts@1.15.3(@types/react@18.3.3)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-R6iTABejgO2LVJqeHS2BYFjutIbMwxMVMqFCoowedIBUiUbnKA3Lqn/4fKDSgHJngOmI1RGafqS2SVvA8tSh1w==} + /@injectivelabs/sdk-ts@1.16.10(@types/react@18.3.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-krRmlDzeKjJWZjIerVoIzN2ULgVVFvVlqbzOUfbv59KCJCU17e/6idadYlzBBxVa4r4Ybjs8A0fat7CjvVj0pw==} dependencies: '@apollo/client': 3.13.8(@types/react@18.3.3)(graphql@16.11.0)(react-dom@18.2.0)(react@18.2.0) '@cosmjs/amino': 0.33.1 '@cosmjs/proto-signing': 0.33.1 '@cosmjs/stargate': 0.33.1 - '@ethersproject/bytes': 5.8.0 '@injectivelabs/abacus-proto-ts': 1.14.0 - '@injectivelabs/core-proto-ts': 1.14.3 - '@injectivelabs/exceptions': 1.15.2 + '@injectivelabs/core-proto-ts': 1.16.1 + '@injectivelabs/exceptions': 1.16.10 '@injectivelabs/grpc-web': 0.0.1(google-protobuf@3.21.4) '@injectivelabs/grpc-web-node-http-transport': 0.0.2(@injectivelabs/grpc-web@0.0.1) '@injectivelabs/grpc-web-react-native-transport': 0.0.2(@injectivelabs/grpc-web@0.0.1) - '@injectivelabs/indexer-proto-ts': 1.13.9 + '@injectivelabs/indexer-proto-ts': 1.13.14 '@injectivelabs/mito-proto-ts': 1.13.2 - '@injectivelabs/networks': 1.15.3 + '@injectivelabs/networks': 1.16.10 '@injectivelabs/olp-proto-ts': 1.13.4 - '@injectivelabs/ts-types': 1.15.3 - '@injectivelabs/utils': 1.15.3 + '@injectivelabs/ts-types': 1.16.10 + '@injectivelabs/utils': 1.16.10 '@metamask/eth-sig-util': 8.2.0 '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 axios: 1.9.0 - bech32: 2.0.0 bip39: 3.1.0 cosmjs-types: 0.9.0 crypto-js: 4.2.0 @@ -2821,6 +2822,7 @@ packages: graphql: 16.11.0 http-status-codes: 2.3.0 keccak256: 1.0.6 + rxjs: 7.8.2 secp256k1: 4.0.3 shx: 0.3.4 snakecase-keys: 5.5.0 @@ -2836,17 +2838,17 @@ packages: - utf-8-validate dev: false - /@injectivelabs/ts-types@1.15.3: - resolution: {integrity: sha512-bMLLP6WQAK8/x8DrPWKov+czHYj0newP6hSJLGYxThlJEFHMu3bHMPj9nfReLTUKzfkbG2zg2g/bcNMKWgImew==} + /@injectivelabs/ts-types@1.16.10: + resolution: {integrity: sha512-ZkECbMz5zD2+pp3tHQzFnezuK1r23IgJc8SQ/Rbu+jogwGkQmHurQTLut0MfYFLKWaqfJPQhqndBs2Fm17NBAA==} dev: false - /@injectivelabs/utils@1.15.3: - resolution: {integrity: sha512-S15az5c4uY+YCqC19yKkT9cEUrwhrKYdd1TGVta1EbnjDEU0tTAUMYJNhzW7/gyivd2gdjrO9Ie0o2rtViniMQ==} + /@injectivelabs/utils@1.16.10: + resolution: {integrity: sha512-0eOcKlQ9Uly9e3zgEmWlCA3uA2mPtmBH6qC94JQB/H2+dy+/3/OsksEuRTc5HJ8WIGKus/EtDbHygae/wkKz4w==} dependencies: '@bangjelkoski/store2': 2.14.3 - '@injectivelabs/exceptions': 1.15.2 - '@injectivelabs/networks': 1.15.3 - '@injectivelabs/ts-types': 1.15.3 + '@injectivelabs/exceptions': 1.16.10 + '@injectivelabs/networks': 1.16.10 + '@injectivelabs/ts-types': 1.16.10 axios: 1.9.0 bignumber.js: 9.3.0 http-status-codes: 2.3.0 @@ -3053,7 +3055,7 @@ packages: bip39: 3.1.0 bs58check: 2.1.2 buffer: 6.0.3 - crypto-js: 4.1.1 + crypto-js: 4.2.0 elliptic: 6.6.1 sha.js: 2.4.11 dev: false @@ -3535,7 +3537,7 @@ packages: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.2.1 '@noble/hashes': 1.7.1 - '@scure/base': 1.2.4 + '@scure/base': 1.2.6 '@types/debug': 4.1.12 debug: 4.3.4(supports-color@5.5.0) pony-cause: 2.1.10 @@ -7033,6 +7035,10 @@ packages: resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==} dev: false + /@scure/base@1.2.6: + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + dev: false + /@scure/bip32@1.3.1: resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} dependencies: @@ -7106,7 +7112,7 @@ packages: '@cosmjs/math': 0.33.1 '@cosmjs/proto-signing': 0.33.1 '@cosmjs/stargate': 0.33.1 - '@injectivelabs/sdk-ts': 1.15.3(@types/react@18.3.3)(react-dom@18.2.0)(react@18.2.0) + '@injectivelabs/sdk-ts': 1.16.10(@types/react@18.3.3)(react-dom@18.2.0)(react@18.2.0) '@keplr-wallet/unit': 0.12.162(starknet@6.11.0) '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.93.2) '@solana/web3.js': 1.93.2 @@ -7573,13 +7579,13 @@ packages: resolution: {integrity: sha512-5lxnyLEYFskErRPenYItLRSge5DjrJngYKdVjRSrWfza9G6KkgHEXi0vUZiyUeMU5JfXH1YnvXZzSp8ul88o2Q==} dependencies: legacy-swc-helpers: /@swc/helpers@0.4.14 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@swc/helpers@0.5.11: resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@swc/types@0.1.5: @@ -17308,7 +17314,7 @@ packages: resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==} dependencies: destr: 2.0.3 - node-fetch-native: 1.6.6 + node-fetch-native: 1.6.7 ufo: 1.5.4 dev: false @@ -18469,7 +18475,7 @@ packages: '@types/react': 18.3.3 react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.2.0) - tslib: 2.6.2 + tslib: 2.7.0 dev: false /react-remove-scroll@2.5.4(@types/react@18.3.3)(react@18.2.0): @@ -18577,7 +18583,7 @@ packages: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /react-use-measure@2.1.1(react-dom@18.2.0)(react@18.2.0): @@ -19137,6 +19143,12 @@ packages: dependencies: tslib: 2.7.0 + /rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + dependencies: + tslib: 2.7.0 + dev: false + /safaridriver@0.1.2: resolution: {integrity: sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==} dev: true @@ -20822,7 +20834,7 @@ packages: dependencies: '@types/react': 18.3.3 react: 18.2.0 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@18.2.0): @@ -20865,7 +20877,7 @@ packages: '@types/react': 18.3.3 detect-node-es: 1.1.0 react: 18.2.0 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /use-sync-external-store@1.2.0(react@18.2.0): diff --git a/src/bonsai/calculators/orders.ts b/src/bonsai/calculators/orders.ts index 695b0fda00..13afaf8807 100644 --- a/src/bonsai/calculators/orders.ts +++ b/src/bonsai/calculators/orders.ts @@ -76,6 +76,10 @@ function calculateSubaccountOrder( reduceOnly: !!base.reduceOnly, remainingSize: MustBigNumber(base.size).minus(MustBigNumber(base.totalFilled)), removalReason: base.removalReason, + // TWAP order parameters + duration: base.duration || undefined, + interval: base.interval || undefined, + priceTolerance: base.priceTolerance || undefined, }; order = maybeUpdateOrderIfExpired(order, protocolHeight); return order; @@ -232,6 +236,8 @@ function calculateBaseOrderStatus( return OrderStatus.Canceling; case IndexerOrderStatus.UNTRIGGERED: return OrderStatus.Untriggered; + case IndexerOrderStatus.ERROR: + return OrderStatus.Canceled; // Treat ERROR status as canceled default: assertNever(status); return undefined; diff --git a/src/bonsai/selectors/account.ts b/src/bonsai/selectors/account.ts index 0c25c8b1cc..a748ec7847 100644 --- a/src/bonsai/selectors/account.ts +++ b/src/bonsai/selectors/account.ts @@ -11,6 +11,7 @@ import { getCurrentMarketIdIfTradeable } from '@/state/currentMarketSelectors'; import { convertBech32Address } from '@/lib/addressUtils'; import { BIG_NUMBERS } from '@/lib/numbers'; +import { IndexerOrderType } from '@/types/indexer/indexerApiGen'; import { calculateBlockRewards } from '../calculators/blockRewards'; import { calculateFills } from '../calculators/fills'; import { getMarketEffectiveInitialMarginForMarket } from '../calculators/markets'; @@ -29,7 +30,7 @@ import { import { calculateTransfers } from '../calculators/transfers'; import { mergeLoadableStatus } from '../lib/mapLoadable'; import { selectParentSubaccountInfo } from '../socketSelectors'; -import { SubaccountTransfer } from '../types/summaryTypes'; +import { OrderFlags, SubaccountTransfer } from '../types/summaryTypes'; import { selectLatestIndexerHeight, selectLatestValidatorHeight } from './apiStatus'; import { selectRawBlockTradingRewardsLiveData, @@ -138,6 +139,16 @@ export const selectOrderHistory = createAppSelector([selectAccountOrders], (orde return calculateOrderHistory(orders); }); +export const selectTWAPOrders = createAppSelector([selectAccountOrders], (orders) => { + return orders.filter((order) => + order.orderFlags === OrderFlags.TWAP && order.type === IndexerOrderType.TWAP + ); +}); + +export const selectActiveTWAPOrders = createAppSelector([selectTWAPOrders], (twapOrders) => { + return calculateOpenOrders(twapOrders); +}); + export const selectCurrentMarketOpenOrders = createAppSelector( [getCurrentMarketIdIfTradeable, selectOpenOrders], (currentMarketId, orders) => diff --git a/src/bonsai/types/summaryTypes.ts b/src/bonsai/types/summaryTypes.ts index b0eec4d6dd..cadbfb8ccf 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -145,6 +145,8 @@ export enum OrderFlags { SHORT_TERM = '0', LONG_TERM = '64', CONDITIONAL = '32', + TWAP = '128', + TWAP_SUBORDER = '256', } export type SubaccountOrder = { @@ -176,6 +178,9 @@ export type SubaccountOrder = { reduceOnly: boolean; removalReason: string | undefined; marginMode: MarginMode | undefined; + duration: string | undefined; + interval: string | undefined; + priceTolerance: string | undefined; }; export enum SubaccountFillType { diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index 535bc9a257..24ac6005e1 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -47,10 +47,12 @@ import { shortenNumberForDisplay } from '@/lib/numbers'; import { TradeTableSettings } from './TradeTableSettings'; import { MaybeUnopenedIsolatedPositionsDrawer } from './UnopenedIsolatedPositions'; import { MarketTypeFilter, PanelView } from './types'; +import { TWAPTable } from '../twap/TWAPTable'; enum InfoSection { Position = 'Position', Orders = 'Orders', + Twap = 'Twap', OrderHistory = 'OrderHistory', Fills = 'Fills', Payments = 'Payments', @@ -381,9 +383,22 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: [stringGetter, showCurrentMarket, isTablet, initialPageSize, currentMarketId] ); + const twapTabItem = useMemo( + () => ({ + asChild: true, + value: InfoSection.Twap, + label: stringGetter({ key: STRING_KEYS.TWAP }), + + content: ( + + ), + }), + [stringGetter] + ); + const tabItems = useMemo( - () => [positionTabItem, ordersTabItem, fillsTabItem, orderHistoryTabItem, paymentsTabItem], - [positionTabItem, fillsTabItem, ordersTabItem, orderHistoryTabItem, paymentsTabItem] + () => [positionTabItem, ordersTabItem, fillsTabItem, orderHistoryTabItem, paymentsTabItem, twapTabItem], + [positionTabItem, fillsTabItem, ordersTabItem, orderHistoryTabItem, paymentsTabItem, twapTabItem] ); const slotBottom = { @@ -394,6 +409,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: [InfoSection.OrderHistory]: null, [InfoSection.Fills]: null, [InfoSection.Payments]: null, + [InfoSection.Twap]: null, }[tab]; return isTablet ? ( diff --git a/src/pages/twap/ActiveTWAPTable.tsx b/src/pages/twap/ActiveTWAPTable.tsx new file mode 100644 index 0000000000..05ba8b39c9 --- /dev/null +++ b/src/pages/twap/ActiveTWAPTable.tsx @@ -0,0 +1,209 @@ +import { BonsaiCore } from '@/bonsai/ontology'; +import { selectActiveTWAPOrders } from '@/bonsai/selectors/account'; +import { OrderSideTag } from '@/components/OrderSideTag'; +import { Table, type BaseTableRowData, type ColumnDef } from '@/components/Table'; +import { PageSize } from '@/components/Table/TablePaginationRow'; +import { TagSize } from '@/components/Tag'; +import { orEmptyRecord } from '@/lib/typeUtils'; +import { MarketTypeFilter } from '@/pages/trade/types'; +import { useAppSelector } from '@/state/appTypes'; +import { tradeViewMixins } from '@/styles/tradeViewMixins'; +import { IndexerOrderSide } from '@/types/indexer/indexerApiGen'; +import type { ColumnSize } from '@react-types/table'; +import { forwardRef } from 'react'; +import styled from 'styled-components'; + +type ActiveTWAPOrder = BaseTableRowData & { + uniqueId: string; + market: string; + side: IndexerOrderSide; + quantity: number; + price: number; + status: 'Active' | 'Filled' | 'Cancelled'; + reduceOnly: boolean; + orderTime: string; + runtime: string; + // Additional fields for proper rendering + totalFilled?: string; + size?: string; + createdAtMilliseconds?: number; + duration?: string; +}; + +export enum ActiveTWAPTableColumnKey { + Market = 'Market', + Side = 'Side', + Execution = 'Executed / Total Size', + AveragePrice = 'Average Price', + Runtime = "Runtime / Total", + ReduceOnly = 'Reduce Only', + OrderTime = 'Order Time', + Terminate = '', + } + +type ElementProps = { + columnKeys: ActiveTWAPTableColumnKey[]; + columnWidths?: Partial>; + currentRoute?: string; + currentMarket?: string; + marketTypeFilter?: MarketTypeFilter; + showClosePositionAction: boolean; + initialPageSize?: PageSize; + onNavigate?: () => void; + navigateToOrders: (market: string) => void; +}; + +const getActiveTWAPTableColumnDef = ({ + key, + width, +}: { + key: ActiveTWAPTableColumnKey; + width?: ColumnSize; +}): ColumnDef => ({ + width, + ...( + { + [ActiveTWAPTableColumnKey.Market]: { + columnKey: 'market', + getCellValue: (row) => row.market, + label: 'Market', + allowsSorting: true, + renderCell: ({ market }) => ( +
+ {market} +
+ ), + }, + [ActiveTWAPTableColumnKey.Side]: { + columnKey: 'side', + getCellValue: (row) => row.side, + label: 'Side', + allowsSorting: true, + renderCell: ({ side }) => side && , + }, + [ActiveTWAPTableColumnKey.Execution]: { + columnKey: 'execution', + getCellValue: (row) => row.quantity, + label: 'Executed / Total Size', + allowsSorting: true, + renderCell: (row) => ( + {row.totalFilled} / {row.size} + ), + }, + [ActiveTWAPTableColumnKey.AveragePrice]: { + columnKey: 'averagePrice', + getCellValue: (row) => row.price, + label: 'Average Price', + allowsSorting: true, + renderCell: ({ price }) => ( + {price} + ), + }, + [ActiveTWAPTableColumnKey.Runtime]: { + columnKey: 'runtime', + label: 'Runtime / Total', + allowsSorting: false, + renderCell: ({ runtime }) => ( + {runtime} + ), + }, + [ActiveTWAPTableColumnKey.ReduceOnly]: { + columnKey: 'reduceOnly', + label: 'Reduce Only', + allowsSorting: false, + renderCell: ({ reduceOnly }) => ( + {reduceOnly ? 'Yes' : 'No'} + ), + }, + [ActiveTWAPTableColumnKey.OrderTime]: { + columnKey: 'orderTime', + label: 'Order Time', + allowsSorting: false, + renderCell: ({ orderTime }) => ( + {orderTime} + ), + }, + [ActiveTWAPTableColumnKey.Terminate]: { + columnKey: 'terminate', + label: '', + allowsSorting: false, + isActionable: true, + renderCell: ({ status }) => ( + status === 'Active' ? ( + + ) : null + ), + }, + } satisfies Record> + )[key], +}); + + + +export const ActiveTWAPTable = forwardRef( + ( + { + columnKeys, + columnWidths, + initialPageSize, + }: ElementProps, + _ref + ) => { + const activeTWAPOrders = useAppSelector(selectActiveTWAPOrders); + const marketSummaries = orEmptyRecord(useAppSelector(BonsaiCore.markets.markets.data)); + + const formatExecutionDisplay = (totalFilled: string, size: string) => { + return `${totalFilled} / ${size}`; + }; + + const formatRuntimeDisplay = (createdAtMilliseconds: number | undefined, duration: string | undefined) => { + if (!createdAtMilliseconds || !duration) return 'N/A'; + + const now = Date.now(); + const elapsed = now - createdAtMilliseconds; + const elapsedSeconds = Math.floor(elapsed / 1000); + const durationSeconds = parseInt(duration, 10) || 0; + + return `${elapsedSeconds}s / ${durationSeconds}s`; + }; + + const twapOrdersData: ActiveTWAPOrder[] = activeTWAPOrders.map((order) => ({ + uniqueId: order.id, + market: marketSummaries[order.marketId]?.displayableTicker || order.displayId, + side: order.side, + quantity: parseFloat(order.totalFilled?.toString() || '0'), // For sorting by execution amount + price: parseFloat(order.price.toString()), + status: 'Active', // All active TWAP orders are active + reduceOnly: order.reduceOnly, + orderTime: new Date(order.createdAtHeight || 0).toLocaleString(), + runtime: formatRuntimeDisplay(order.updatedAtMilliseconds, order.duration), + // Store original values for column rendering + totalFilled: order.totalFilled?.toString() || '0', + size: order.size.toString(), + createdAtMilliseconds: order.updatedAtMilliseconds, + duration: order.duration, + })); + return ( + <$Table + key={'active-twap-positions'} + label="Active TWAP Orders" + columns={columnKeys.map((key) => getActiveTWAPTableColumnDef({ key }))} + data={twapOrdersData} + tableId="active-twap-table" + getRowKey={(row) => row?.uniqueId} + slotEmpty={
No active TWAP orders
} + initialPageSize={initialPageSize} + withInnerBorders + withScrollSnapColumns + withScrollSnapRows + withFocusStickyRows + /> + ) + } +) + +const $Table = styled(Table)` + ${tradeViewMixins.horizontalTable} +` as typeof Table; \ No newline at end of file diff --git a/src/pages/twap/TWAPTable.tsx b/src/pages/twap/TWAPTable.tsx new file mode 100644 index 0000000000..6bfb351ee0 --- /dev/null +++ b/src/pages/twap/TWAPTable.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { Tabs, type TabItem } from '@/components/Tabs'; +import { ActiveTWAPTable, ActiveTWAPTableColumnKey } from './ActiveTWAPTable'; + +const tabItems: TabItem[] = [ + { + value: 'Active', + label: 'Active', + content: ( + {}} /> + ), + }, + { + value: 'OrderHistory', + label: 'Order History', + content: ( +
+

Order History

+
+ ), + }, + { + value: 'Fills', + label: 'Fills', + content: ( +
+

Fills

+
+ ), + }, +]; + +export const TWAPTable: React.FC = () => { + const [tab, setTab] = useState('Active'); + + return ( + + ); +}; + diff --git a/src/types/indexer/indexerManual.ts b/src/types/indexer/indexerManual.ts index 9c0d892abd..56ee21704b 100644 --- a/src/types/indexer/indexerManual.ts +++ b/src/types/indexer/indexerManual.ts @@ -56,6 +56,10 @@ export interface IndexerCompositeOrderObject { subaccountNumber: number; removalReason?: string; totalOptimisticFilled?: string; + // TWAP order parameters + duration?: string | null; + interval?: string | null; + priceTolerance?: string | null; } export interface IndexerCompositeMarketObject {