@0xmonaco/core 1.0.45 → 1.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/perp/routes.d.ts +1 -0
- package/dist/api/perp/routes.js +1 -0
- package/dist/api/trading/api.d.ts +23 -1
- package/dist/api/trading/api.js +31 -0
- package/dist/api/websocket/types.d.ts +2 -43
- package/dist/api/websocket/websocket.js +215 -2
- package/dist/coverage.d.ts +1 -0
- package/dist/coverage.js +1 -0
- package/package.json +3 -3
|
@@ -34,6 +34,7 @@ export declare const perpRoutes: {
|
|
|
34
34
|
pageSize?: number;
|
|
35
35
|
}) => string;
|
|
36
36
|
readonly getTwap: (twapOrderId: string) => string;
|
|
37
|
+
readonly getTwapTca: (twapOrderId: string) => string;
|
|
37
38
|
readonly cancelTwap: (twapOrderId: string) => string;
|
|
38
39
|
};
|
|
39
40
|
readonly delegatedAgents: {
|
package/dist/api/perp/routes.js
CHANGED
|
@@ -32,6 +32,7 @@ export const perpRoutes = {
|
|
|
32
32
|
createTwap: () => `${API_V1}/orders/twap`,
|
|
33
33
|
listTwap: (params) => withQuery(`${API_V1}/orders/twap`, params),
|
|
34
34
|
getTwap: (twapOrderId) => `${API_V1}/orders/twap/${encodeSegment(twapOrderId)}`,
|
|
35
|
+
getTwapTca: (twapOrderId) => `${API_V1}/orders/twap/${encodeSegment(twapOrderId)}/tca`,
|
|
35
36
|
cancelTwap: (twapOrderId) => `${API_V1}/orders/twap/${encodeSegment(twapOrderId)}/cancel`,
|
|
36
37
|
},
|
|
37
38
|
delegatedAgents: {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* );
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
import type { BatchCancelOrdersResponse, BatchCreateOrderParams, BatchCreateOrdersResponse, BatchReplaceOrderParams, BatchReplaceOrdersResponse, CancelConditionalOrderResponse, CancelOrderResponse, CancelTwapOrderResponse, ConditionalOrder, CreateOrderResponse, CreateTwapOrderOptions, CreateTwapOrderResponse, GetOrderResponse, GetPaginatedOrdersParams, GetPaginatedOrdersResponse, ListConditionalOrdersParams, ListConditionalOrdersResponse, ListTwapOrdersParams, ListTwapOrdersResponse, MarginMode, OrderSide, ParentTpSlLegParams, PositionSide, ReplaceOrderResponse, TimeInForce, TradingAPI, TradingMode, TwapOrder } from "@0xmonaco/types";
|
|
24
|
+
import type { BatchCancelOrdersResponse, BatchCreateOrderParams, BatchCreateOrdersResponse, BatchReplaceOrderParams, BatchReplaceOrdersResponse, CancelConditionalOrderResponse, CancelOrderResponse, CancelTwapOrderResponse, ConditionalOrder, CreateOrderResponse, CreateTwapOrderOptions, CreateTwapOrderResponse, GetOrderResponse, GetPaginatedOrdersParams, GetPaginatedOrdersResponse, GetTwapOrderTcaResponse, ListConditionalOrdersParams, ListConditionalOrdersResponse, ListTwapOrdersParams, ListTwapOrdersResponse, MarginMode, OrderSide, ParentTpSlLegParams, PositionSide, ReplaceOrderResponse, TimeInForce, TradingAPI, TradingMode, TwapOrder } from "@0xmonaco/types";
|
|
25
25
|
import { BaseAPI } from "../base";
|
|
26
26
|
export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
|
|
27
27
|
/**
|
|
@@ -444,6 +444,28 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
|
|
|
444
444
|
* ```
|
|
445
445
|
*/
|
|
446
446
|
getTwapOrder(twapOrderId: string): Promise<TwapOrder>;
|
|
447
|
+
/**
|
|
448
|
+
* Gets the post-execution TCA (transaction cost analysis) report for a
|
|
449
|
+
* TERMINAL TWAP parent order.
|
|
450
|
+
*
|
|
451
|
+
* Available once the parent is COMPLETED or CANCELLED (a non-terminal
|
|
452
|
+
* parent returns 400). Reports realized average price against the arrival
|
|
453
|
+
* price and the window-TWAP benchmark over the parent's active window.
|
|
454
|
+
* Sign convention: positive bps = execution beat the reference. Benchmark
|
|
455
|
+
* fields are null when no price series covered the window sufficiently.
|
|
456
|
+
*
|
|
457
|
+
* @param twapOrderId - The TWAP parent order UUID
|
|
458
|
+
* @returns Promise resolving to the TCA report
|
|
459
|
+
* @throws {APIError} When API communication fails, the order is not found,
|
|
460
|
+
* or the order has not reached a terminal state
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* ```typescript
|
|
464
|
+
* const tca = await tradingAPI.getTwapOrderTca(twapOrderId);
|
|
465
|
+
* console.log(`vs benchmark: ${tca.performanceVsBenchmarkBps} bps (${tca.benchmarkSource})`);
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
getTwapOrderTca(twapOrderId: string): Promise<GetTwapOrderTcaResponse>;
|
|
447
469
|
/**
|
|
448
470
|
* Lists TWAP parent orders for the authenticated user.
|
|
449
471
|
*
|
package/dist/api/trading/api.js
CHANGED
|
@@ -668,6 +668,10 @@ export class TradingAPIImpl extends BaseAPI {
|
|
|
668
668
|
strategyKey: options.strategyKey,
|
|
669
669
|
useMasterBalance: options.useMasterBalance,
|
|
670
670
|
clientTwapId: options.clientTwapId,
|
|
671
|
+
executionStyle: options.executionStyle,
|
|
672
|
+
triggerPrice: options.triggerPrice,
|
|
673
|
+
triggerDirection: options.triggerDirection,
|
|
674
|
+
conditionalExpiry: options.conditionalExpiry,
|
|
671
675
|
};
|
|
672
676
|
return await this.makeAuthenticatedRequest(perpRoutes.orders.createTwap(), {
|
|
673
677
|
method: "POST",
|
|
@@ -720,6 +724,33 @@ export class TradingAPIImpl extends BaseAPI {
|
|
|
720
724
|
method: "GET",
|
|
721
725
|
});
|
|
722
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* Gets the post-execution TCA (transaction cost analysis) report for a
|
|
729
|
+
* TERMINAL TWAP parent order.
|
|
730
|
+
*
|
|
731
|
+
* Available once the parent is COMPLETED or CANCELLED (a non-terminal
|
|
732
|
+
* parent returns 400). Reports realized average price against the arrival
|
|
733
|
+
* price and the window-TWAP benchmark over the parent's active window.
|
|
734
|
+
* Sign convention: positive bps = execution beat the reference. Benchmark
|
|
735
|
+
* fields are null when no price series covered the window sufficiently.
|
|
736
|
+
*
|
|
737
|
+
* @param twapOrderId - The TWAP parent order UUID
|
|
738
|
+
* @returns Promise resolving to the TCA report
|
|
739
|
+
* @throws {APIError} When API communication fails, the order is not found,
|
|
740
|
+
* or the order has not reached a terminal state
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```typescript
|
|
744
|
+
* const tca = await tradingAPI.getTwapOrderTca(twapOrderId);
|
|
745
|
+
* console.log(`vs benchmark: ${tca.performanceVsBenchmarkBps} bps (${tca.benchmarkSource})`);
|
|
746
|
+
* ```
|
|
747
|
+
*/
|
|
748
|
+
async getTwapOrderTca(twapOrderId) {
|
|
749
|
+
validate(CancelTwapOrderSchema, { twapOrderId });
|
|
750
|
+
return await this.makeAuthenticatedRequest(perpRoutes.orders.getTwapTca(twapOrderId), {
|
|
751
|
+
method: "GET",
|
|
752
|
+
});
|
|
753
|
+
}
|
|
723
754
|
/**
|
|
724
755
|
* Lists TWAP parent orders for the authenticated user.
|
|
725
756
|
*
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export type { ResyncHandler, StatusHandler, WebSocketResyncInfo } from "@0xmonaco/types";
|
|
3
|
-
export type MessageHandler<T> = (data: T) => void;
|
|
1
|
+
import type { MonacoWebSocketConfig, SessionCredentials } from "@0xmonaco/types";
|
|
2
|
+
export type { MessageHandler, MonacoWebSocket, ResyncHandler, StatusHandler, WebSocketResyncInfo } from "@0xmonaco/types";
|
|
4
3
|
/**
|
|
5
4
|
* Options for {@link createMonacoWebSocket}. The reconnect/resync knobs and
|
|
6
5
|
* callbacks are shared with the main SDK config via {@link MonacoWebSocketConfig}
|
|
@@ -11,43 +10,3 @@ export interface MonacoWebSocketOptions extends MonacoWebSocketConfig {
|
|
|
11
10
|
/** Session keypair for authenticated channels (orders, balances, etc.) */
|
|
12
11
|
session?: SessionCredentials;
|
|
13
12
|
}
|
|
14
|
-
export interface MonacoWebSocket {
|
|
15
|
-
/** Connect to the WebSocket server */
|
|
16
|
-
connect: () => Promise<void>;
|
|
17
|
-
/** Disconnect from the WebSocket server */
|
|
18
|
-
disconnect: () => void;
|
|
19
|
-
/** Check if currently connected */
|
|
20
|
-
isConnected: () => boolean;
|
|
21
|
-
/** Get current websocket connection status */
|
|
22
|
-
getStatus: () => WebSocketStatus;
|
|
23
|
-
/** Set (or clear) the session keypair used to authenticate the connection */
|
|
24
|
-
setSessionKeypair: (credentials: SessionCredentials | undefined) => void;
|
|
25
|
-
/** Subscribe to order events (requires authentication) */
|
|
26
|
-
orders: (tradingPairId: string, tradingMode: TradingMode, handler: MessageHandler<OrderEvent>) => () => void;
|
|
27
|
-
/** Subscribe to orderbook events (public) */
|
|
28
|
-
orderbook: (tradingPairId: string, tradingMode: TradingMode, magnitude: number, quotationMode: OrderbookQuotationMode, handler: MessageHandler<OrderbookEvent>) => () => void;
|
|
29
|
-
/** Subscribe to OHLCV candlestick events (public) */
|
|
30
|
-
ohlcv: (tradingPairId: string, tradingMode: TradingMode, interval: Interval, handler: MessageHandler<OHLCVEvent>, priceType?: OHLCVPriceType) => () => void;
|
|
31
|
-
/** Subscribe to trade events (public) */
|
|
32
|
-
trades: (tradingPairId: string, handler: MessageHandler<TradeEvent>) => () => void;
|
|
33
|
-
/** Subscribe to user movement events (requires authentication) */
|
|
34
|
-
movements: (handler: MessageHandler<UserMovementEvent>) => () => void;
|
|
35
|
-
/** Subscribe to user order events for all trading pairs (requires authentication) */
|
|
36
|
-
userOrders: (handler: MessageHandler<OrderEvent>) => () => void;
|
|
37
|
-
/** Subscribe to user balance events (requires authentication) */
|
|
38
|
-
balances: (handler: MessageHandler<UserBalanceEvent>) => () => void;
|
|
39
|
-
/** Subscribe to conditional TP/SL lifecycle events (requires authentication) */
|
|
40
|
-
conditionalOrders: (handler: MessageHandler<ConditionalOrderEvent>, tradingPairId?: string) => () => void;
|
|
41
|
-
/** Subscribe to TWAP parent-order lifecycle events (requires authentication) */
|
|
42
|
-
twapOrders: (handler: MessageHandler<TwapOrderEvent>, tradingPairId?: string) => () => void;
|
|
43
|
-
/** Subscribe to position state updates (requires authentication); omit `tradingPairId` for all pairs */
|
|
44
|
-
positions: (handler: MessageHandler<PositionEvent>, tradingPairId?: string) => () => void;
|
|
45
|
-
/** Subscribe to liquidation alerts (requires authentication); omit `tradingPairId` for all pairs */
|
|
46
|
-
liquidations: (handler: MessageHandler<LiquidationEvent>, tradingPairId?: string) => () => void;
|
|
47
|
-
/** Subscribe to account-level margin health updates for the authenticated user (requires authentication) */
|
|
48
|
-
account: (handler: MessageHandler<AccountEvent>) => () => void;
|
|
49
|
-
/** Subscribe to per-market statistics for one trading pair (public) */
|
|
50
|
-
marketStats: (tradingPairId: string, handler: MessageHandler<MarketStatsEvent>) => () => void;
|
|
51
|
-
/** Subscribe to the all-markets statistics summary (public) */
|
|
52
|
-
marketStatsAll: (handler: MessageHandler<MarketStatsAllEvent>) => () => void;
|
|
53
|
-
}
|
|
@@ -13,6 +13,7 @@ const CONDITIONAL_ORDER_TRIGGER_SOURCES = ["MARK_PRICE"];
|
|
|
13
13
|
const CONDITIONAL_ORDER_STATES = ["PENDING_PARENT", "ACTIVE", "TRIGGERING", "TRIGGERED", "CANCELLED", "EXPIRED", "FAILED"];
|
|
14
14
|
const CONDITIONAL_ORDER_ASSOCIATION_TYPES = ["POSITION", "PARENT_ORDER"];
|
|
15
15
|
const TWAP_ORDER_STATES = ["PENDING", "ACTIVE", "COMPLETED", "CANCELLED"];
|
|
16
|
+
const INSTRUMENT_CHANGE_KINDS = ["listing", "delisting", "halt", "unhalt", "config_change"];
|
|
16
17
|
function isRecord(value) {
|
|
17
18
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
19
|
}
|
|
@@ -39,6 +40,19 @@ function readBoolean(source, field) {
|
|
|
39
40
|
}
|
|
40
41
|
return value;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Read a wire sequence counter: a non-negative safe integer. The server
|
|
45
|
+
* serializes Rust `u64` sequence fields as plain JSON numbers, so a negative,
|
|
46
|
+
* fractional, or unsafe (> 2^53 - 1) value is a malformed frame that would
|
|
47
|
+
* corrupt ordering/dedup logic — reject it rather than pass it through.
|
|
48
|
+
*/
|
|
49
|
+
function readSequence(source, field) {
|
|
50
|
+
const value = source[field];
|
|
51
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
52
|
+
throw new Error(`Invalid WebSocket event: ${field} must be a non-negative safe integer`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
42
56
|
function readOptionalNumber(source, field) {
|
|
43
57
|
const value = source[field];
|
|
44
58
|
if (value === undefined || value === null)
|
|
@@ -48,6 +62,13 @@ function readOptionalNumber(source, field) {
|
|
|
48
62
|
}
|
|
49
63
|
return value;
|
|
50
64
|
}
|
|
65
|
+
function readStringArray(source, field) {
|
|
66
|
+
const value = source[field];
|
|
67
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
68
|
+
throw new Error(`Invalid WebSocket event: ${field} must be an array of strings`);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
51
72
|
function readEnum(source, field, allowed) {
|
|
52
73
|
const value = readString(source, field);
|
|
53
74
|
if (!allowed.includes(value)) {
|
|
@@ -186,6 +207,10 @@ function parseAccountEvent(rawData) {
|
|
|
186
207
|
userId: readString(rawData, "user_id"),
|
|
187
208
|
data: {
|
|
188
209
|
marginAccountId: readString(data, "marginAccountId"),
|
|
210
|
+
// Monotonic per-account publish counter (0XM-2498): a plain JSON number on
|
|
211
|
+
// the wire, used to order/dedupe frames across reconnect races. Must be a
|
|
212
|
+
// non-negative safe integer — anything else is a malformed frame.
|
|
213
|
+
sequence: readSequence(data, "sequence"),
|
|
189
214
|
equity: readString(data, "equity"),
|
|
190
215
|
collateral: readString(data, "collateral"),
|
|
191
216
|
freeCollateral: readString(data, "freeCollateral"),
|
|
@@ -269,6 +294,42 @@ function parseMarketStatsAllEvent(rawData) {
|
|
|
269
294
|
markets: rawData.markets.map(parseMarketStatsSummary),
|
|
270
295
|
};
|
|
271
296
|
}
|
|
297
|
+
function parseInstrumentEventData(raw) {
|
|
298
|
+
if (!isRecord(raw)) {
|
|
299
|
+
throw new Error("Invalid instrument event: data must be an object");
|
|
300
|
+
}
|
|
301
|
+
const data = keysToCamelCase(raw);
|
|
302
|
+
return {
|
|
303
|
+
change: readEnum(data, "change", INSTRUMENT_CHANGE_KINDS),
|
|
304
|
+
tradingPairId: readString(data, "tradingPairId"),
|
|
305
|
+
symbol: readString(data, "symbol"),
|
|
306
|
+
isActive: readBoolean(data, "isActive"),
|
|
307
|
+
tickSize: readString(data, "tickSize"),
|
|
308
|
+
quantityStepSize: readString(data, "quantityStepSize"),
|
|
309
|
+
minOrderSize: readString(data, "minOrderSize"),
|
|
310
|
+
maxOrderSize: readString(data, "maxOrderSize"),
|
|
311
|
+
marketType: readString(data, "marketType"),
|
|
312
|
+
category: readString(data, "category"),
|
|
313
|
+
maxLeverage: readOptionalString(data, "maxLeverage"),
|
|
314
|
+
initialMarginRate: readOptionalString(data, "initialMarginRate"),
|
|
315
|
+
maintenanceMarginRate: readOptionalString(data, "maintenanceMarginRate"),
|
|
316
|
+
// Values are snake_case wire field NAMES (e.g. "tick_size"); only object
|
|
317
|
+
// keys are camelCased, so they pass through untouched.
|
|
318
|
+
changedFields: readStringArray(data, "changedFields"),
|
|
319
|
+
updatedAt: readString(data, "updatedAt"),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function parseInstrumentEvent(rawData) {
|
|
323
|
+
if (!isRecord(rawData)) {
|
|
324
|
+
throw new Error("Invalid instrument event: payload must be an object");
|
|
325
|
+
}
|
|
326
|
+
// The frame's top-level `trading_pair_id` is not inside `data`, so it stays
|
|
327
|
+
// snake_case (only `data` is camelCased by parseInstrumentEventData).
|
|
328
|
+
return {
|
|
329
|
+
tradingPairId: readString(rawData, "trading_pair_id"),
|
|
330
|
+
data: parseInstrumentEventData(rawData.data),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
272
333
|
/**
|
|
273
334
|
* Parse a `twap_orders` channel frame into a typed event. Only the envelope
|
|
274
335
|
* and the snapshot's identity/state fields are validated strictly — the rest
|
|
@@ -337,6 +398,23 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
337
398
|
let pendingResyncCode = null;
|
|
338
399
|
// Handler storage: channel -> set of handlers (supports multiple subscribers per channel)
|
|
339
400
|
const handlers = new Map();
|
|
401
|
+
// Subscribe-time Snapshot frame handlers, keyed by the RAW subscribed
|
|
402
|
+
// channel: the server echoes that exact channel on the Snapshot frame
|
|
403
|
+
// (unlike live Event frames, which can arrive on a more specific channel and
|
|
404
|
+
// are prefix-matched). Only subscriptions that opt in receive them — a
|
|
405
|
+
// snapshot's data shape differs from the channel's Event shape, so blindly
|
|
406
|
+
// reusing the Event handlers would feed every other channel's parser a frame
|
|
407
|
+
// it must reject.
|
|
408
|
+
const snapshotHandlers = new Map();
|
|
409
|
+
// Latest known full-config frame per market for each subscribed instrument
|
|
410
|
+
// channel, seeded by the subscribe-time Snapshot and superseded by every
|
|
411
|
+
// live frame (instrument frames are terminal-state, never deltas). The
|
|
412
|
+
// server sends a snapshot only for a genuinely new wire subscription, so a
|
|
413
|
+
// late local subscriber on an already-held channel bootstraps from this
|
|
414
|
+
// cache instead of starting blind. Dropped when the channel's last local
|
|
415
|
+
// subscriber unsubscribes — the next first subscriber triggers a fresh wire
|
|
416
|
+
// Subscribe whose snapshot is authoritative, never the stale cache.
|
|
417
|
+
const instrumentBaseline = new Map();
|
|
340
418
|
const getStatus = () => {
|
|
341
419
|
if (!ws)
|
|
342
420
|
return reconnectTimer ? "reconnecting" : "disconnected";
|
|
@@ -431,6 +509,11 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
431
509
|
}
|
|
432
510
|
};
|
|
433
511
|
const resubscribeAll = () => {
|
|
512
|
+
// Every re-sent Subscribe is genuinely new to the server (new
|
|
513
|
+
// connection), so fresh authoritative snapshots are inbound: the
|
|
514
|
+
// pre-outage instrument cache is stale — markets may have been delisted
|
|
515
|
+
// during the gap — and must not be replayed to late subscribers.
|
|
516
|
+
instrumentBaseline.clear();
|
|
434
517
|
const channels = Array.from(handlers.keys());
|
|
435
518
|
if (channels.length > 0) {
|
|
436
519
|
send({ type: "Subscribe", channels });
|
|
@@ -460,7 +543,17 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
460
543
|
return frameChannel === subscriptionChannel || frameChannel.startsWith(`${subscriptionChannel}:`);
|
|
461
544
|
};
|
|
462
545
|
const routeMessage = (msg) => {
|
|
463
|
-
if (
|
|
546
|
+
if (!msg.channel)
|
|
547
|
+
return;
|
|
548
|
+
if (msg.type === "Snapshot") {
|
|
549
|
+
// Exact match only: the Snapshot frame's channel echoes the raw
|
|
550
|
+
// subscribed channel, never a more specific one.
|
|
551
|
+
for (const handler of snapshotHandlers.get(msg.channel) ?? []) {
|
|
552
|
+
handler(msg.data);
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (msg.type !== "Event")
|
|
464
557
|
return;
|
|
465
558
|
// Find matching handlers and call all of them
|
|
466
559
|
for (const [subscriptionChannel, channelHandlers] of handlers) {
|
|
@@ -579,23 +672,38 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
579
672
|
stopHeartbeat();
|
|
580
673
|
pendingResyncCode = null;
|
|
581
674
|
handlers.clear();
|
|
675
|
+
snapshotHandlers.clear();
|
|
676
|
+
instrumentBaseline.clear();
|
|
582
677
|
ws?.close(1000, "Client disconnect");
|
|
583
678
|
ws = null;
|
|
584
679
|
options.onStatusChange?.("disconnected");
|
|
585
680
|
};
|
|
586
|
-
const subscribe = (channel, handler) => {
|
|
681
|
+
const subscribe = (channel, handler, snapshotHandler) => {
|
|
587
682
|
const isFirstSubscriber = !handlers.has(channel);
|
|
588
683
|
// Add handler to the set for this channel
|
|
589
684
|
if (!handlers.has(channel)) {
|
|
590
685
|
handlers.set(channel, new Set());
|
|
591
686
|
}
|
|
592
687
|
handlers.get(channel)?.add(handler);
|
|
688
|
+
if (snapshotHandler) {
|
|
689
|
+
if (!snapshotHandlers.has(channel)) {
|
|
690
|
+
snapshotHandlers.set(channel, new Set());
|
|
691
|
+
}
|
|
692
|
+
snapshotHandlers.get(channel)?.add(snapshotHandler);
|
|
693
|
+
}
|
|
593
694
|
// Only send Subscribe message on first subscriber for this channel
|
|
594
695
|
if (isFirstSubscriber && ws?.readyState === WebSocket.OPEN) {
|
|
595
696
|
send({ type: "Subscribe", channels: [channel] });
|
|
596
697
|
}
|
|
597
698
|
// Return unsubscribe function
|
|
598
699
|
return () => {
|
|
700
|
+
if (snapshotHandler) {
|
|
701
|
+
const channelSnapshotHandlers = snapshotHandlers.get(channel);
|
|
702
|
+
channelSnapshotHandlers?.delete(snapshotHandler);
|
|
703
|
+
if (channelSnapshotHandlers?.size === 0) {
|
|
704
|
+
snapshotHandlers.delete(channel);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
599
707
|
const channelHandlers = handlers.get(channel);
|
|
600
708
|
if (channelHandlers) {
|
|
601
709
|
channelHandlers.delete(handler);
|
|
@@ -952,6 +1060,110 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
952
1060
|
}
|
|
953
1061
|
});
|
|
954
1062
|
};
|
|
1063
|
+
/**
|
|
1064
|
+
* Subscribe to market lifecycle + configuration changes (public): listings,
|
|
1065
|
+
* delistings, and edits to tick size, step size, order-size bounds, or (for
|
|
1066
|
+
* perp markets) leverage/margin-rate bounds. Omit `tradingPairId` for all
|
|
1067
|
+
* markets, or pass one to filter. Every frame carries the market's FULL
|
|
1068
|
+
* current config as a terminal-state snapshot (never an additive delta);
|
|
1069
|
+
* `changedFields` names what the mutation touched. A subscribe-time snapshot
|
|
1070
|
+
* of the current active-pair list arrives before the live diffs; a
|
|
1071
|
+
* subscriber joining a channel this client already holds is replayed the
|
|
1072
|
+
* current baseline from a local cache instead (the server snapshots only
|
|
1073
|
+
* genuinely new wire subscriptions). Lag policy is Conflate: a lagging
|
|
1074
|
+
* subscriber is fast-forwarded, no gap signal.
|
|
1075
|
+
*/
|
|
1076
|
+
const subscribeInstruments = (handler, tradingPairId) => {
|
|
1077
|
+
if (!handler) {
|
|
1078
|
+
throw new Error("instruments subscription requires a handler");
|
|
1079
|
+
}
|
|
1080
|
+
if (tradingPairId !== undefined && tradingPairId.length === 0) {
|
|
1081
|
+
throw new Error("instruments tradingPairId cannot be empty");
|
|
1082
|
+
}
|
|
1083
|
+
// Every live frame is published on instrument:<pairId>; a bare
|
|
1084
|
+
// `instrument` subscription receives all of them via routeMessage's prefix
|
|
1085
|
+
// match, so unlike market_stats there is no distinct all-markets frame
|
|
1086
|
+
// shape to guard against.
|
|
1087
|
+
const channel = tradingPairId ? `instrument:${tradingPairId}` : "instrument";
|
|
1088
|
+
const cacheEvent = (event) => {
|
|
1089
|
+
let byPair = instrumentBaseline.get(channel);
|
|
1090
|
+
if (!byPair) {
|
|
1091
|
+
byPair = new Map();
|
|
1092
|
+
instrumentBaseline.set(channel, byPair);
|
|
1093
|
+
}
|
|
1094
|
+
byPair.set(event.tradingPairId, event);
|
|
1095
|
+
};
|
|
1096
|
+
// A subscriber joining a channel this connection already holds gets no
|
|
1097
|
+
// fresh server snapshot (the wire Subscribe is suppressed, and the server
|
|
1098
|
+
// snapshots only genuinely new subscriptions), so replay the cached
|
|
1099
|
+
// baseline to it instead. Asynchronously, matching how a real snapshot
|
|
1100
|
+
// arrives — never during the subscribe call itself.
|
|
1101
|
+
const isLateSubscriber = handlers.has(channel);
|
|
1102
|
+
let unsubscribed = false;
|
|
1103
|
+
const unsubscribe = subscribe(channel, (rawData) => {
|
|
1104
|
+
try {
|
|
1105
|
+
const event = parseInstrumentEvent(rawData);
|
|
1106
|
+
cacheEvent(event);
|
|
1107
|
+
handler(event);
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
console.error("WebSocket: Error processing instrument event", err);
|
|
1111
|
+
}
|
|
1112
|
+
},
|
|
1113
|
+
// The subscribe-time baseline arrives as one Snapshot frame whose data is
|
|
1114
|
+
// a flat InstrumentEventData[] (a `listing` per active market, no
|
|
1115
|
+
// per-item envelope). Fan it out as one event per market so the handler
|
|
1116
|
+
// sees the same shape for baseline and live frames.
|
|
1117
|
+
(rawData) => {
|
|
1118
|
+
try {
|
|
1119
|
+
if (!Array.isArray(rawData)) {
|
|
1120
|
+
throw new Error("Invalid instrument snapshot: data must be an array");
|
|
1121
|
+
}
|
|
1122
|
+
const events = rawData.map(parseInstrumentEventData).map((item) => ({ tradingPairId: item.tradingPairId, data: item }));
|
|
1123
|
+
// A snapshot is the authoritative active-market set: replace the
|
|
1124
|
+
// channel's cache wholesale rather than upserting, so markets absent
|
|
1125
|
+
// from it (delisted while disconnected) don't linger.
|
|
1126
|
+
instrumentBaseline.set(channel, new Map(events.map((event) => [event.tradingPairId, event])));
|
|
1127
|
+
for (const event of events) {
|
|
1128
|
+
handler(event);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
console.error("WebSocket: Error processing instrument snapshot", err);
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
// Replay only while the socket is open: during abnormal-close backoff the
|
|
1136
|
+
// handlers and cache survive but the state is pre-outage — the reconnect's
|
|
1137
|
+
// resubscribe clears it and a fresh authoritative snapshot follows, so a
|
|
1138
|
+
// backoff-time subscriber must wait for that instead.
|
|
1139
|
+
if (isLateSubscriber && ws?.readyState === WebSocket.OPEN) {
|
|
1140
|
+
const cached = instrumentBaseline.get(channel);
|
|
1141
|
+
if (cached && cached.size > 0) {
|
|
1142
|
+
const replay = [...cached.values()];
|
|
1143
|
+
queueMicrotask(() => {
|
|
1144
|
+
if (unsubscribed)
|
|
1145
|
+
return;
|
|
1146
|
+
for (const event of replay) {
|
|
1147
|
+
try {
|
|
1148
|
+
handler(event);
|
|
1149
|
+
}
|
|
1150
|
+
catch (err) {
|
|
1151
|
+
console.error("WebSocket: Error processing instrument baseline replay", err);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return () => {
|
|
1158
|
+
unsubscribed = true;
|
|
1159
|
+
unsubscribe();
|
|
1160
|
+
// Last local subscriber gone: the wire Unsubscribe was sent, so the
|
|
1161
|
+
// cache would only go stale from here — drop it.
|
|
1162
|
+
if (!handlers.has(channel)) {
|
|
1163
|
+
instrumentBaseline.delete(channel);
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
};
|
|
955
1167
|
/**
|
|
956
1168
|
* Subscribe to per-market statistics for a single trading pair (public). Each
|
|
957
1169
|
* frame carries the market's full current stats as a terminal-state snapshot.
|
|
@@ -1029,6 +1241,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1029
1241
|
positions: subscribePositions,
|
|
1030
1242
|
liquidations: subscribeLiquidations,
|
|
1031
1243
|
account: subscribeAccount,
|
|
1244
|
+
instruments: subscribeInstruments,
|
|
1032
1245
|
marketStats: subscribeMarketStats,
|
|
1033
1246
|
marketStatsAll: subscribeMarketStatsAll,
|
|
1034
1247
|
};
|
package/dist/coverage.d.ts
CHANGED
package/dist/coverage.js
CHANGED
|
@@ -13,6 +13,7 @@ export const COVERED = {
|
|
|
13
13
|
cancel_twap_order: "cancelTwapOrder",
|
|
14
14
|
create_twap_order: "createTwapOrder",
|
|
15
15
|
get_twap_order: "getTwapOrder",
|
|
16
|
+
get_twap_order_tca: "getTwapOrderTca",
|
|
16
17
|
list_twap_orders: "listTwapOrders",
|
|
17
18
|
close_position: "closePosition",
|
|
18
19
|
create_challenge: "createChallenge",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xmonaco/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.47",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"homepage": "https://docs.0xmonaco.com/sdk/typescript",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"viem": "^2.45.2"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@0xmonaco/contracts": "1.0.
|
|
23
|
-
"@0xmonaco/types": "1.0.
|
|
22
|
+
"@0xmonaco/contracts": "1.0.47",
|
|
23
|
+
"@0xmonaco/types": "1.0.47",
|
|
24
24
|
"@noble/curves": "^1.9.1",
|
|
25
25
|
"@noble/hashes": "^1.8.0",
|
|
26
26
|
"http-status-codes": "^2.3.0"
|