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 {