@0xmonaco/core 1.0.37 → 1.0.38-develop.ebf8e8c

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.
@@ -1,51 +1,15 @@
1
- import type { ConditionalOrderEvent, Interval, OHLCVEvent, OrderbookEvent, OrderbookQuotationMode, OrderEvent, SessionCredentials, TradeEvent, TradingMode, UserBalanceEvent, UserMovementEvent, WebSocketStatus } from "@0xmonaco/types";
2
- export type StatusHandler = (status: WebSocketStatus) => void;
1
+ import type { ConditionalOrderEvent, Interval, LiquidationEvent, MonacoWebSocketConfig, OHLCVEvent, OrderbookEvent, OrderbookQuotationMode, OrderEvent, PositionEvent, SessionCredentials, TradeEvent, TradingMode, UserBalanceEvent, UserMovementEvent, WebSocketStatus } from "@0xmonaco/types";
2
+ export type { ResyncHandler, StatusHandler, WebSocketResyncInfo } from "@0xmonaco/types";
3
3
  export type MessageHandler<T> = (data: T) => void;
4
4
  /**
5
- * Why the connection was re-established. Delivered by
6
- * {@link MonacoWebSocketOptions.onResync} after the client has reconnected,
7
- * re-authenticated, and re-subscribed — the point where the app should
8
- * refetch REST snapshots and reconcile by `(id, updatedAt)`.
5
+ * Options for {@link createMonacoWebSocket}. The reconnect/resync knobs and
6
+ * callbacks are shared with the main SDK config via {@link MonacoWebSocketConfig}
7
+ * (settable as `SDKConfig.ws`); the session keypair is factory-only because the
8
+ * SDK propagates it after {@link login} rather than at construction.
9
9
  */
10
- export interface WebSocketResyncInfo {
11
- /** WebSocket close code of the connection that was lost. */
12
- code: number;
13
- /**
14
- * True when the server closed with 1013 (try again later): the client fell
15
- * behind a sequential stream (orders, conditional orders, liquidations,
16
- * trades, movements) and the stream has a gap. There is no wire sequence
17
- * number — the 1013 close is the gap signal, and a snapshot refetch is the
18
- * only safe repair.
19
- */
20
- slowClient: boolean;
21
- }
22
- export type ResyncHandler = (info: WebSocketResyncInfo) => void;
23
- export interface MonacoWebSocketOptions {
10
+ export interface MonacoWebSocketOptions extends MonacoWebSocketConfig {
24
11
  /** Session keypair for authenticated channels (orders, balances, etc.) */
25
12
  session?: SessionCredentials;
26
- /** Enable auto-reconnect on disconnect (default: true) */
27
- autoReconnect?: boolean;
28
- /**
29
- * Maximum reconnection attempts (default: unlimited). Retry delay is
30
- * full-jitter exponential backoff capped at 30s, so unlimited retries are
31
- * capped-rate, not a tight loop.
32
- */
33
- maxReconnectAttempts?: number;
34
- /** Base delay for the exponential backoff in milliseconds (default: 1000) */
35
- reconnectBaseDelayMs?: number;
36
- /** How long a connection attempt may stay CONNECTING before it is failed
37
- * and retried, in milliseconds (default: 10000) */
38
- connectionTimeoutMs?: number;
39
- /** Callback for connection status changes */
40
- onStatusChange?: StatusHandler;
41
- /**
42
- * Called after every automatic reconnect once the live stream is resumed
43
- * (re-authenticated and re-subscribed). Refetch REST snapshots here and
44
- * reconcile by `(id, updatedAt)`, last-writer-wins; events that arrived
45
- * while the snapshot was in flight are newer than the snapshot rows they
46
- * duplicate.
47
- */
48
- onResync?: ResyncHandler;
49
13
  }
50
14
  export interface MonacoWebSocket {
51
15
  /** Connect to the WebSocket server */
@@ -74,4 +38,8 @@ export interface MonacoWebSocket {
74
38
  balances: (handler: MessageHandler<UserBalanceEvent>) => () => void;
75
39
  /** Subscribe to conditional TP/SL lifecycle events (requires authentication) */
76
40
  conditionalOrders: (handler: MessageHandler<ConditionalOrderEvent>, tradingPairId?: string) => () => void;
41
+ /** Subscribe to position state updates (requires authentication); omit `tradingPairId` for all pairs */
42
+ positions: (handler: MessageHandler<PositionEvent>, tradingPairId?: string) => () => void;
43
+ /** Subscribe to liquidation alerts (requires authentication); omit `tradingPairId` for all pairs */
44
+ liquidations: (handler: MessageHandler<LiquidationEvent>, tradingPairId?: string) => () => void;
77
45
  }
@@ -18,7 +18,7 @@ function isRecord(value) {
18
18
  function readString(source, field) {
19
19
  const value = source[field];
20
20
  if (typeof value !== "string" || value.length === 0) {
21
- throw new Error(`Invalid conditional order event: ${field} must be a non-empty string`);
21
+ throw new Error(`Invalid WebSocket event: ${field} must be a non-empty string`);
22
22
  }
23
23
  return value;
24
24
  }
@@ -27,14 +27,14 @@ function readOptionalString(source, field) {
27
27
  if (value === undefined || value === null)
28
28
  return undefined;
29
29
  if (typeof value !== "string") {
30
- throw new Error(`Invalid conditional order event: ${field} must be a string`);
30
+ throw new Error(`Invalid WebSocket event: ${field} must be a string`);
31
31
  }
32
32
  return value;
33
33
  }
34
34
  function readBoolean(source, field) {
35
35
  const value = source[field];
36
36
  if (typeof value !== "boolean") {
37
- throw new Error(`Invalid conditional order event: ${field} must be a boolean`);
37
+ throw new Error(`Invalid WebSocket event: ${field} must be a boolean`);
38
38
  }
39
39
  return value;
40
40
  }
@@ -43,14 +43,14 @@ function readOptionalNumber(source, field) {
43
43
  if (value === undefined || value === null)
44
44
  return undefined;
45
45
  if (typeof value !== "number" || !Number.isFinite(value)) {
46
- throw new Error(`Invalid conditional order event: ${field} must be a finite number`);
46
+ throw new Error(`Invalid WebSocket event: ${field} must be a finite number`);
47
47
  }
48
48
  return value;
49
49
  }
50
50
  function readEnum(source, field, allowed) {
51
51
  const value = readString(source, field);
52
52
  if (!allowed.includes(value)) {
53
- throw new Error(`Invalid conditional order event: ${field} must be one of ${allowed.join(", ")}`);
53
+ throw new Error(`Invalid WebSocket event: ${field} must be one of ${allowed.join(", ")}`);
54
54
  }
55
55
  return value;
56
56
  }
@@ -59,7 +59,7 @@ function readOptionalEnum(source, field, allowed) {
59
59
  if (value === undefined || value === null)
60
60
  return undefined;
61
61
  if (typeof value !== "string" || !allowed.includes(value)) {
62
- throw new Error(`Invalid conditional order event: ${field} must be one of ${allowed.join(", ")}`);
62
+ throw new Error(`Invalid WebSocket event: ${field} must be one of ${allowed.join(", ")}`);
63
63
  }
64
64
  return value;
65
65
  }
@@ -105,6 +105,69 @@ function parseConditionalOrderEvent(rawData) {
105
105
  },
106
106
  };
107
107
  }
108
+ function parsePositionEvent(rawData) {
109
+ if (!isRecord(rawData)) {
110
+ throw new Error("Invalid position event: payload must be an object");
111
+ }
112
+ if (!isRecord(rawData.data)) {
113
+ throw new Error("Invalid position event: data must be an object");
114
+ }
115
+ const data = keysToCamelCase(rawData.data);
116
+ return {
117
+ // event_type is the event-class discriminator, validated strictly so a
118
+ // mis-routed frame is rejected rather than parsed as a position. The value
119
+ // fields below (side/status/reason) stay lenient so an unexpected *value*
120
+ // never drops a risk-critical update.
121
+ eventType: readEnum(rawData, "event_type", ["position_update"]),
122
+ userId: readString(rawData, "user_id"),
123
+ data: {
124
+ positionId: readString(data, "positionId"),
125
+ marginAccountId: readString(data, "marginAccountId"),
126
+ tradingPairId: readString(data, "tradingPairId"),
127
+ side: readString(data, "side"),
128
+ status: readString(data, "status"),
129
+ size: readString(data, "size"),
130
+ entryPrice: readString(data, "entryPrice"),
131
+ markPrice: readOptionalString(data, "markPrice"),
132
+ unrealizedPnl: readOptionalString(data, "unrealizedPnl"),
133
+ liquidationPrice: readOptionalString(data, "liquidationPrice"),
134
+ isolatedMargin: readString(data, "isolatedMargin"),
135
+ leverage: readString(data, "leverage"),
136
+ reason: readString(data, "reason"),
137
+ updatedAt: readString(data, "updatedAt"),
138
+ },
139
+ };
140
+ }
141
+ function parseLiquidationEvent(rawData) {
142
+ if (!isRecord(rawData)) {
143
+ throw new Error("Invalid liquidation event: payload must be an object");
144
+ }
145
+ if (!isRecord(rawData.data)) {
146
+ throw new Error("Invalid liquidation event: data must be an object");
147
+ }
148
+ const data = keysToCamelCase(rawData.data);
149
+ return {
150
+ // event_type is the event-class discriminator, validated strictly so a
151
+ // mis-routed frame is rejected rather than parsed as a liquidation. The
152
+ // free-form value fields below (status/accountState) stay lenient so an
153
+ // unexpected *value* never drops a risk-critical alert.
154
+ eventType: readEnum(rawData, "event_type", ["liquidation_alert"]),
155
+ userId: readString(rawData, "user_id"),
156
+ data: {
157
+ liquidationId: readString(data, "liquidationId"),
158
+ marginAccountId: readString(data, "marginAccountId"),
159
+ positionId: readOptionalString(data, "positionId"),
160
+ tradingPairId: readOptionalString(data, "tradingPairId"),
161
+ status: readString(data, "status"),
162
+ accountState: readString(data, "accountState"),
163
+ liquidationPrice: readOptionalString(data, "liquidationPrice"),
164
+ markPrice: readOptionalString(data, "markPrice"),
165
+ estimatedShortfall: readString(data, "estimatedShortfall"),
166
+ message: readString(data, "message"),
167
+ triggeredAt: readString(data, "triggeredAt"),
168
+ },
169
+ };
170
+ }
108
171
  /**
109
172
  * Create a Monaco WebSocket client
110
173
  */
@@ -618,6 +681,54 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
618
681
  }
619
682
  });
620
683
  };
684
+ /**
685
+ * Subscribe to authenticated user's position updates.
686
+ * Requires authentication - user is identified from the signed session-key
687
+ * handshake. Omit `tradingPairId` for all pairs, or pass one to filter.
688
+ * Lag policy is Conflate: a lagging subscriber is fast-forwarded, no gap
689
+ * signal.
690
+ */
691
+ const subscribePositions = (handler, tradingPairId) => {
692
+ if (!handler) {
693
+ throw new Error("positions subscription requires a handler");
694
+ }
695
+ if (tradingPairId !== undefined && tradingPairId.length === 0) {
696
+ throw new Error("positions tradingPairId cannot be empty");
697
+ }
698
+ const channel = tradingPairId ? `positions:${tradingPairId}` : "positions";
699
+ return subscribe(channel, (rawData) => {
700
+ try {
701
+ handler(parsePositionEvent(rawData));
702
+ }
703
+ catch (err) {
704
+ console.error("WebSocket: Error processing position event", err);
705
+ }
706
+ });
707
+ };
708
+ /**
709
+ * Subscribe to authenticated user's liquidation alerts.
710
+ * Requires authentication - user is identified from the signed session-key
711
+ * handshake. Omit `tradingPairId` for all pairs, or pass one to filter.
712
+ * Lag policy is Disconnect: a subscriber that falls behind is closed with
713
+ * `1013`; resync by refetching a snapshot (no wire sequence number).
714
+ */
715
+ const subscribeLiquidations = (handler, tradingPairId) => {
716
+ if (!handler) {
717
+ throw new Error("liquidations subscription requires a handler");
718
+ }
719
+ if (tradingPairId !== undefined && tradingPairId.length === 0) {
720
+ throw new Error("liquidations tradingPairId cannot be empty");
721
+ }
722
+ const channel = tradingPairId ? `liquidations:${tradingPairId}` : "liquidations";
723
+ return subscribe(channel, (rawData) => {
724
+ try {
725
+ handler(parseLiquidationEvent(rawData));
726
+ }
727
+ catch (err) {
728
+ console.error("WebSocket: Error processing liquidation event", err);
729
+ }
730
+ });
731
+ };
621
732
  return {
622
733
  connect,
623
734
  disconnect,
@@ -646,5 +757,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
646
757
  userOrders: subscribeUserOrders,
647
758
  balances: subscribeBalances,
648
759
  conditionalOrders: subscribeConditionalOrders,
760
+ positions: subscribePositions,
761
+ liquidations: subscribeLiquidations,
649
762
  };
650
763
  }
package/dist/sdk.js CHANGED
@@ -201,7 +201,10 @@ export class MonacoSDKImpl {
201
201
  this.trading = new TradingAPIImpl(apiUrl);
202
202
  this.orderbook = new OrderbookAPIImpl(apiUrl);
203
203
  this.trades = new TradesAPIImpl(apiUrl);
204
- this.ws = createMonacoWebSocket(wsUrl);
204
+ // Thread the reconnect/resync knobs and callbacks from SDK config into the
205
+ // internally-built ws client. The session keypair is not passed here — it
206
+ // is propagated on login via propagateSession/setSessionKeypair.
207
+ this.ws = createMonacoWebSocket(wsUrl, cfg.ws);
205
208
  // Auto-connect WebSocket, log errors
206
209
  this.ws.connect().catch(console.error);
207
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmonaco/core",
3
- "version": "1.0.37",
3
+ "version": "1.0.38-develop.ebf8e8c",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,8 +23,8 @@
23
23
  "viem": "^2.45.2"
24
24
  },
25
25
  "dependencies": {
26
- "@0xmonaco/contracts": "1.0.37",
27
- "@0xmonaco/types": "1.0.37",
26
+ "@0xmonaco/contracts": "1.0.38-develop.ebf8e8c",
27
+ "@0xmonaco/types": "1.0.38-develop.ebf8e8c",
28
28
  "@noble/curves": "^1.9.1",
29
29
  "@noble/hashes": "^1.8.0",
30
30
  "http-status-codes": "^2.3.0"