@0xmonaco/types 1.0.50 → 1.0.53

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.
@@ -161,9 +161,26 @@ export interface TransferCollateralToIsolatedRiskBucketRequest extends TransferC
161
161
  */
162
162
  applyToPositionMargin?: boolean;
163
163
  }
164
+ /**
165
+ * Fund the account's cross risk bucket from the parent's unallocated
166
+ * collateral (the bucket is created if needed). A cross transfer names no
167
+ * pairs: cross scope is derived from trading — a pair joins the bucket when a
168
+ * cross order on it succeeds and leaves when its positions close — so funding
169
+ * has nothing to declare. Read the current scope from the cross bucket's row
170
+ * in `listMarginAccounts()` (`selectedTradingPairIds`) or from this call's
171
+ * response; the parent summary row carries no scope. Pairs declared before
172
+ * scope became derived stay recorded until they trade and their positions
173
+ * close.
174
+ */
164
175
  export interface TransferCollateralToCrossRiskBucketRequest extends TransferCollateralToParentMarginAccountRequest {
165
176
  marginMode: "CROSS";
166
- selectedTradingPairIds: string[];
177
+ /**
178
+ * Removed. This used to declare — and on every top-up REPLACE — the cross
179
+ * bucket's scope, which let a partial list silently evict every flat pair it
180
+ * forgot. The server now ignores the field; the SDK no longer sends it and
181
+ * rejects it here so a caller learns at compile time that it does nothing.
182
+ */
183
+ selectedTradingPairIds?: never;
167
184
  tradingPairId?: never;
168
185
  strategyKey?: never;
169
186
  /** Cross risk is pooled at bucket equity — there is no per-position margin; an explicit `true` is rejected. */
@@ -252,6 +269,28 @@ export interface SimulateOrderRiskResponse {
252
269
  initialMarginRequiredAfter: string;
253
270
  maintenanceMarginRequiredAfter: string;
254
271
  freeCollateralAfter: string;
272
+ /**
273
+ * What this order pays in fees once its WHOLE size has executed, priced by the
274
+ * role each part of it plays against the book right now: the taker rate (plus
275
+ * the application's additional taker fee) on the quantity that would cross
276
+ * immediately, and the maker rate on the quantity that would rest and be
277
+ * filled later. Both are the caller's own tier-resolved rates. One figure
278
+ * covers an order that crosses entirely, one that rests entirely, and every
279
+ * split in between.
280
+ *
281
+ * Signed: NEGATIVE means a net rebate, the honest answer for a resting order
282
+ * on a pair whose maker fee is negative. Populated even when `accepted` is
283
+ * false, and for reduce-only orders — closing a position pays fees like any
284
+ * other fill.
285
+ *
286
+ * Assumes the resting part eventually fills, so it states what the order costs
287
+ * if it runs to completion: cancel before the remainder fills and only the
288
+ * crossing part was ever charged.
289
+ *
290
+ * Absent — never zero — when the engine could not price the order: it was
291
+ * refused before it could be matched, or its resolved maker tier exceeds the
292
+ * pair's cap. Treat `undefined` as unknown, never as a free order.
293
+ */
255
294
  estimatedFee?: string;
256
295
  /**
257
296
  * Estimated liquidation mark-price. In ISOLATED mode, this is the
@@ -1,4 +1,5 @@
1
1
  import type { BaseAPI } from "../api";
2
+ import type { MarginMode } from "../margin-accounts";
2
3
  /** Closed rolling windows supported by the authoritative portfolio APIs. */
3
4
  export type PortfolioAnalyticsPeriod = "1d" | "1w" | "1m" | "all";
4
5
  export type PortfolioCoverage = "complete" | "partial" | "none";
@@ -215,6 +216,44 @@ export interface PortfolioRiskBucket {
215
216
  * (the key is omitted, never null) on persisted-fallback reads.
216
217
  */
217
218
  availableOrderCollateral?: string;
219
+ /**
220
+ * The mode this bucket is margined under.
221
+ *
222
+ * Always present — every row here IS a bucket, and its mode is a non-null
223
+ * column (unlike `MarginAccountSummary.marginMode`, which is optional
224
+ * because that shape also models a parent row belonging to no bucket). It
225
+ * lets you attribute bucket margin to cross or isolated from this response
226
+ * alone, instead of resolving the opaque `riskBucketId` against the
227
+ * margin-accounts surface.
228
+ *
229
+ * It does NOT reconcile with the `positions.cross` / `positions.isolated`
230
+ * counts beside it, and grouping these rows by mode will not reproduce
231
+ * them. Those counts come from a separate query with a wider scope — every
232
+ * margin account for your user/application pair, while these rows come from
233
+ * the single oldest (parent) account — and they attribute a legacy position
234
+ * carrying no risk-bucket reference to cross whether or not a cross bucket
235
+ * row exists. Treat the counts as account-wide totals, not as a checksum
236
+ * over `riskBuckets`.
237
+ */
238
+ marginMode: MarginMode;
239
+ /**
240
+ * The trading pair this bucket is scoped to. Present for `ISOLATED`
241
+ * buckets; absent (the key is omitted, never null) for `CROSS`, which spans
242
+ * the pairs selected into it and is scoped to none of them individually.
243
+ *
244
+ * For a cross bucket's membership read
245
+ * `MarginAccountSummary.selectedTradingPairIds` — it is not carried here
246
+ * because it would cost this endpoint an extra query per request.
247
+ *
248
+ * These labels do not make this response row-for-row joinable with the
249
+ * margin-accounts surface on `(marginMode, tradingPairId)`. That surface
250
+ * emits rows this one has no counterpart for (a provisioned-but-unfunded
251
+ * auto bucket, and a parent row with no mode at all), and on a
252
+ * persisted-fallback read the two surfaces attribute a legacy position
253
+ * carrying no risk-bucket reference to opposite buckets. Use these fields
254
+ * to label and group this response, not to cross-foot it against another.
255
+ */
256
+ tradingPairId?: string;
218
257
  }
219
258
  export interface PortfolioMargin {
220
259
  asOf: string;
@@ -32,10 +32,16 @@ export interface Position {
32
32
  unrealizedPnl: string;
33
33
  realizedPnl: string;
34
34
  /**
35
- * Open rows: current isolated collateral for this position's risk bucket.
36
- * Terminal rows: total initial margin allocated across the position's
37
- * reducing executions when available — the margin basis for lifetime
38
- * realized return.
35
+ * Open ISOLATED rows: the position's current backing — its risk bucket's
36
+ * allocated collateral adjusted by settled realized PnL and funding paid,
37
+ * net of collateral reserved for the bucket's open orders, clamped at zero
38
+ * (0XM-2558). Partial closes shrink the posted margin pro-rata while the
39
+ * realized loss stays in the bucket, so this is the unrealized-return
40
+ * denominator. Not a hard -100% bound: liquidation measures the bucket's
41
+ * whole equity, order reserves included, and a mark can gap past the
42
+ * backing. Open CROSS rows: the position's posted margin. Terminal rows:
43
+ * total initial margin allocated across the position's reducing executions
44
+ * when available — the margin basis for lifetime realized return.
39
45
  */
40
46
  isolatedMargin: string;
41
47
  leverage?: string;
@@ -55,6 +55,8 @@ export interface TradingAPI extends BaseAPI {
55
55
  * replacement re-states it and inherits nothing from the original order.
56
56
  */
57
57
  clientOrderId?: string;
58
+ /** The SDK generates a key once per call when omitted. Supply the same key to retry a logical order within 24 hours. */
59
+ idempotencyKey?: string;
58
60
  takeProfit?: ParentTpSlLegParams;
59
61
  stopLoss?: ParentTpSlLegParams;
60
62
  }): Promise<CreateOrderResponse>;
@@ -90,6 +92,8 @@ export interface TradingAPI extends BaseAPI {
90
92
  * replacement re-states it and inherits nothing from the original order.
91
93
  */
92
94
  clientOrderId?: string;
95
+ /** The SDK generates a key once per call when omitted. Supply the same key to retry a logical order within 24 hours. */
96
+ idempotencyKey?: string;
93
97
  takeProfit?: ParentTpSlLegParams;
94
98
  stopLoss?: ParentTpSlLegParams;
95
99
  }): Promise<CreateOrderResponse>;
@@ -84,6 +84,51 @@ export interface Order {
84
84
  positionId?: string;
85
85
  /** Stable cause when status is CANCELLED */
86
86
  terminalReason?: OrderTerminalReason;
87
+ /**
88
+ * Opaque, compare-only revision counter for this order's state: a strictly
89
+ * higher value is strictly newer state for that order, and equal values
90
+ * mean identical state.
91
+ *
92
+ * Compare two REST reads of the same order with it — whether a re-poll
93
+ * advanced, or which of `getOrder` and a list row is fresher.
94
+ *
95
+ * The SAME counter is on every WebSocket order event and on the `orders`
96
+ * snapshot frame, so it is also the snapshot-versus-stream reconciliation
97
+ * key: after a disconnect, keep applying events as they arrive and merge a
98
+ * REST read per order — take the REST row when
99
+ * `snapshot.version >= local.version`, else keep local. No buffering, so no
100
+ * quote blackout on reconnect. The rule is `>=` rather than `>` because one
101
+ * sequencer step emits several events for one order (an `OrderPlaced` and
102
+ * then its fills) and a batch shares one step across every item, so equal
103
+ * means "same step" on the stream — and a REST row is that step's final
104
+ * state, so it wins the tie. Between two REST reads, equal does mean
105
+ * identical state.
106
+ *
107
+ * The snapshot frame alone does not close a reconnect gap: an order that
108
+ * filled or was cancelled while the socket was down is in neither the
109
+ * resting snapshot nor the stream. Recover those explicitly — `listUserTrades`
110
+ * for fills, and a targeted `getOrder` for anything local state believes is
111
+ * resting that the snapshot no longer lists.
112
+ *
113
+ * Read nothing else into it. It is not a count of updates; consecutive
114
+ * values are not expected (it is drawn from a global sequence shared by
115
+ * every order, so per-order values are sparse and gaps are normal); and it
116
+ * is not comparable across different orders.
117
+ *
118
+ * Absent means unknown — an order last written before this field existed.
119
+ * Fall back to a full field comparison for those rather than treating
120
+ * absent as version zero. Note that a comparison against an absent value
121
+ * cannot be true either way, so an order held locally without a version is
122
+ * never refreshed by the merge above: take the REST row unconditionally for
123
+ * those rather than letting them go stale.
124
+ *
125
+ * Prefer this over `updatedAt`: `version` is identical on every endpoint
126
+ * that serves the same order state and never advances without a state
127
+ * change, whereas `updatedAt` carries a different clock per endpoint (order
128
+ * detail reports the matching-engine event time, order lists report the
129
+ * database commit time) and can move without the order changing.
130
+ */
131
+ version?: number;
87
132
  }
88
133
  /** Stable terminal causes for accepted orders that end as CANCELLED. */
89
134
  export type OrderTerminalReason = "USER_REQUESTED" | "REPLACED" | "LIQUIDATION" | "INSUFFICIENT_MARGIN" | "REDUCE_ONLY_EXHAUSTED" | "POSITION_CLOSED" | "OCO_SIBLING_TRIGGERED" | "SYSTEM";
@@ -168,6 +213,11 @@ export type ConditionalOrderState = "PENDING_PARENT" | "ACTIVE" | "TRIGGERING" |
168
213
  * - ACTIVE: inside the execution window; child slices are firing
169
214
  * - COMPLETED: window ended (fully filled or hard-stopped with a shortfall)
170
215
  * - CANCELLED: cancelled by the user before the window ended
216
+ *
217
+ * These are the values the server sends today, not a runtime guarantee:
218
+ * `@0xmonaco/core` parses the `twap_orders` WebSocket frame leniently, so a
219
+ * state added server-side after this version ships is delivered as-is rather
220
+ * than dropping the update (0XM-2628). Match it with a default branch.
171
221
  */
172
222
  export type TwapOrderState = "PENDING" | "ACTIVE" | "COMPLETED" | "CANCELLED";
173
223
  /**
@@ -385,6 +385,8 @@ export interface BatchCreateOrderParams {
385
385
  * execute a second time.
386
386
  */
387
387
  clientOrderId?: string;
388
+ /** The SDK generates a key once per item when omitted. Supply the same key for batch or single-create retries within 24 hours. */
389
+ idempotencyKey?: string;
388
390
  }
389
391
  /**
390
392
  * Parameters for a single order in a batch replace request
@@ -456,6 +458,28 @@ export interface ConditionalOrder {
456
458
  failureReason?: string;
457
459
  createdAt: string;
458
460
  updatedAt: string;
461
+ /**
462
+ * Opaque, compare-only revision counter for this conditional order's state —
463
+ * the sequencer step that wrote it (0XM-2440). A strictly higher value is
464
+ * strictly newer state for that conditional order.
465
+ *
466
+ * Present on REST reads and on the subscribe-time `conditional_orders`
467
+ * snapshot frame. **Absent on live conditional events**, by design: those are
468
+ * emitted from a model captured before the sequencer acted, so the only value
469
+ * available there is the one the row held *before* the change being
470
+ * announced — a client merging on `>=` would let a snapshot read at that
471
+ * version win the tie and undo the transition.
472
+ *
473
+ * So seed conditional state from the snapshot and REST, comparing with
474
+ * `snapshot.version >= local.version`, and apply live events as state without
475
+ * ranking them. `>=` rather than `>`: one sequencer step can write several
476
+ * conditional rows (a trigger writes the triggered row and cancels its OCO
477
+ * sibling), so equal means "same step", not identical state.
478
+ *
479
+ * Absent means unknown — a row last written before the field existed — never
480
+ * zero. Wire: `version`.
481
+ */
482
+ version?: number;
459
483
  }
460
484
  export interface ListConditionalOrdersResponse {
461
485
  orders: ConditionalOrder[];
@@ -38,7 +38,13 @@ export declare const TransferCollateralToParentMarginAccountSchema: z.ZodObject<
38
38
  }, z.core.$strip>;
39
39
  }, z.core.$strip>;
40
40
  export declare const TransferCollateralToRiskBucketSchema: z.ZodObject<{
41
- request: z.ZodUnion<readonly [z.ZodObject<{
41
+ request: z.ZodPipe<z.ZodObject<{
42
+ marginMode: z.ZodOptional<z.ZodEnum<{
43
+ ISOLATED: "ISOLATED";
44
+ CROSS: "CROSS";
45
+ }>>;
46
+ selectedTradingPairIds: z.ZodOptional<z.ZodUnknown>;
47
+ }, z.core.$loose>, z.ZodUnion<readonly [z.ZodObject<{
42
48
  asset: z.ZodString;
43
49
  amount: z.ZodString;
44
50
  tradingPairId: z.ZodUUID;
@@ -50,10 +56,10 @@ export declare const TransferCollateralToRiskBucketSchema: z.ZodObject<{
50
56
  asset: z.ZodString;
51
57
  amount: z.ZodString;
52
58
  marginMode: z.ZodLiteral<"CROSS">;
53
- selectedTradingPairIds: z.ZodArray<z.ZodUUID>;
59
+ selectedTradingPairIds: z.ZodOptional<z.ZodNever>;
54
60
  strategyKey: z.ZodOptional<z.ZodNever>;
55
61
  tradingPairId: z.ZodOptional<z.ZodNever>;
56
- }, z.core.$strict>]>;
62
+ }, z.core.$strict>]>>;
57
63
  }, z.core.$strip>;
58
64
  export declare const TransferCollateralFromParentMarginAccountSchema: z.ZodObject<{
59
65
  request: z.ZodObject<{
@@ -56,12 +56,22 @@ const IsolatedRiskBucketRequestSchema = RiskBucketRequestSchema.extend({
56
56
  // stays strict-rejected (no per-position margin to commit to).
57
57
  applyToPositionMargin: z.boolean().optional(),
58
58
  });
59
+ // Simulation still declares the scope it wants previewed: a preview of a
60
+ // bucket that may not exist yet has nothing recorded to read from.
59
61
  const CrossRiskBucketScopeSchema = z.object({
60
62
  marginMode: z.literal("CROSS"),
61
63
  selectedTradingPairIds: SelectedCrossTradingPairIdsSchema,
62
64
  strategyKey: z.never().optional(),
63
65
  });
64
- const CrossRiskBucketRequestSchema = CrossRiskBucketScopeSchema.extend({
66
+ // A cross transfer-in names no pairs. Cross scope is derived from trading, so
67
+ // funding has nothing to declare; `selectedTradingPairIds` used to REPLACE the
68
+ // scope on every top-up (silently evicting any flat pair a partial list
69
+ // forgot) and is now ignored by the server. Rejected here so the caller learns
70
+ // it does nothing rather than believing it took effect.
71
+ const CrossRiskBucketRequestSchema = z.object({
72
+ marginMode: z.literal("CROSS"),
73
+ selectedTradingPairIds: z.never().optional(),
74
+ strategyKey: z.never().optional(),
65
75
  tradingPairId: z.never().optional(),
66
76
  });
67
77
  export const TransferCollateralSchema = z.object({
@@ -71,11 +81,29 @@ export const TransferCollateralSchema = z.object({
71
81
  export const TransferCollateralToParentMarginAccountSchema = z.object({
72
82
  request: ParentMarginAccountCollateralRequestSchema,
73
83
  });
84
+ // Named ahead of the union: a union of two strict members collapses every
85
+ // mismatch into a single "Invalid input" on `request`, which would hide the one
86
+ // mistake this schema most needs to name.
87
+ const CrossTransferNamesNoPairsSchema = z
88
+ .object({
89
+ marginMode: z.enum(["ISOLATED", "CROSS"]).optional(),
90
+ selectedTradingPairIds: z.unknown().optional(),
91
+ })
92
+ .loose()
93
+ .superRefine((request, ctx) => {
94
+ if (request.marginMode === "CROSS" && request.selectedTradingPairIds !== undefined) {
95
+ ctx.addIssue({
96
+ code: "custom",
97
+ path: ["selectedTradingPairIds"],
98
+ message: "selectedTradingPairIds is not accepted on a CROSS transfer: cross scope is derived from trading, so a funding call has nothing to declare",
99
+ });
100
+ }
101
+ });
74
102
  export const TransferCollateralToRiskBucketSchema = z.object({
75
- request: z.union([
103
+ request: CrossTransferNamesNoPairsSchema.pipe(z.union([
76
104
  ParentMarginAccountCollateralRequestSchema.merge(IsolatedRiskBucketRequestSchema).strict(),
77
105
  ParentMarginAccountCollateralRequestSchema.merge(CrossRiskBucketRequestSchema).strict(),
78
- ]),
106
+ ])),
79
107
  });
80
108
  export const TransferCollateralFromParentMarginAccountSchema = z.object({
81
109
  request: ParentMarginAccountCollateralRequestSchema,
@@ -88,6 +88,7 @@ export declare const ParentTpSlLegSchema: z.ZodObject<{
88
88
  /**
89
89
  * Place Limit Order validation schema
90
90
  */
91
+ export declare const OrderIdempotencyKeySchema: z.ZodString;
91
92
  export declare const PlaceLimitOrderSchema: z.ZodObject<{
92
93
  tradingPairId: z.ZodUUID;
93
94
  side: z.ZodEnum<{
@@ -97,6 +98,7 @@ export declare const PlaceLimitOrderSchema: z.ZodObject<{
97
98
  quantity: z.ZodString;
98
99
  price: z.ZodString;
99
100
  options: z.ZodOptional<z.ZodObject<{
101
+ idempotencyKey: z.ZodOptional<z.ZodString>;
100
102
  tradingMode: z.ZodOptional<z.ZodEnum<{
101
103
  SPOT: "SPOT";
102
104
  MARGIN: "MARGIN";
@@ -169,6 +171,7 @@ export declare const PlaceMarketOrderSchema: z.ZodObject<{
169
171
  }>;
170
172
  quantity: z.ZodString;
171
173
  options: z.ZodOptional<z.ZodObject<{
174
+ idempotencyKey: z.ZodOptional<z.ZodString>;
172
175
  tradingMode: z.ZodOptional<z.ZodEnum<{
173
176
  SPOT: "SPOT";
174
177
  MARGIN: "MARGIN";
@@ -405,6 +408,7 @@ export declare const BatchCreateOrderItemSchema: z.ZodObject<{
405
408
  }>;
406
409
  price: z.ZodOptional<z.ZodString>;
407
410
  quantity: z.ZodString;
411
+ idempotencyKey: z.ZodOptional<z.ZodString>;
408
412
  tradingMode: z.ZodOptional<z.ZodEnum<{
409
413
  SPOT: "SPOT";
410
414
  MARGIN: "MARGIN";
@@ -458,6 +462,7 @@ export declare const BatchCreateOrdersSchema: z.ZodObject<{
458
462
  }>;
459
463
  price: z.ZodOptional<z.ZodString>;
460
464
  quantity: z.ZodString;
465
+ idempotencyKey: z.ZodOptional<z.ZodString>;
461
466
  tradingMode: z.ZodOptional<z.ZodEnum<{
462
467
  SPOT: "SPOT";
463
468
  MARGIN: "MARGIN";
@@ -126,6 +126,11 @@ const legacyPositionSideMatchesOrderSide = (side, positionSide) => positionSide
126
126
  /**
127
127
  * Place Limit Order validation schema
128
128
  */
129
+ export const OrderIdempotencyKeySchema = z
130
+ .string()
131
+ .min(1)
132
+ .max(64)
133
+ .regex(/^[A-Za-z0-9._:-]+$/, "idempotencyKey must contain only letters, digits, or . _ : -");
129
134
  export const PlaceLimitOrderSchema = z
130
135
  .object({
131
136
  tradingPairId: UUIDSchema,
@@ -134,6 +139,7 @@ export const PlaceLimitOrderSchema = z
134
139
  price: PositiveDecimalStringSchema,
135
140
  options: z
136
141
  .object({
142
+ idempotencyKey: OrderIdempotencyKeySchema.optional(),
137
143
  tradingMode: TradingModeSchema.optional(),
138
144
  useMasterBalance: z.boolean().optional(),
139
145
  expirationDate: ISO8601DateSchema.optional(),
@@ -214,6 +220,7 @@ export const PlaceMarketOrderSchema = z
214
220
  quantity: PositiveDecimalStringSchema,
215
221
  options: z
216
222
  .object({
223
+ idempotencyKey: OrderIdempotencyKeySchema.optional(),
217
224
  tradingMode: TradingModeSchema.optional(),
218
225
  slippageTolerance: SlippageToleranceSchema,
219
226
  marginAccountId: UUIDSchema.optional(),
@@ -508,6 +515,7 @@ export const BatchCreateOrderItemSchema = z
508
515
  side: OrderSideSchema,
509
516
  price: PositiveDecimalStringSchema.optional(),
510
517
  quantity: PositiveDecimalStringSchema,
518
+ idempotencyKey: OrderIdempotencyKeySchema.optional(),
511
519
  tradingMode: TradingModeSchema.optional(),
512
520
  slippageTolerance: SlippageToleranceSchema,
513
521
  useMasterBalance: z.boolean().optional(),
@@ -29,6 +29,55 @@ export interface WebSocketResyncInfo {
29
29
  }
30
30
  /** Handler invoked after a reconnect resumes the live stream. */
31
31
  export type ResyncHandler = (info: WebSocketResyncInfo) => void;
32
+ /**
33
+ * Machine-readable classification the server puts on every `Error` frame so a
34
+ * client can branch on the failure without string-matching the human message.
35
+ * Mirrors `ws-api`'s `WsErrorCode` enum, whose SCREAMING_SNAKE_CASE wire
36
+ * strings are pinned on the Rust side in `ws-api/src/websocket/protocol.rs`
37
+ * and cross-checked against this union by `@0xmonaco/core`'s
38
+ * `websocket-enum-parity` test.
39
+ */
40
+ export type WebSocketErrorCode =
41
+ /** A user-specific channel was subscribed before authenticating. */
42
+ "AUTH_REQUIRED"
43
+ /** The `Authenticate` handshake was rejected; the connection is closed after this frame. */
44
+ | "AUTH_FAILED"
45
+ /** The per-user concurrent-connection cap was reached; this connection is closed after the frame. */
46
+ | "CONNECTION_LIMIT"
47
+ /** The per-connection subscription-cardinality cap was reached. */
48
+ | "SUBSCRIPTION_LIMIT"
49
+ /** A subscription request was malformed or failed parameter validation. */
50
+ | "INVALID_SUBSCRIPTION"
51
+ /** An inbound frame could not be parsed, or an unsupported frame type was sent. */
52
+ | "INVALID_MESSAGE"
53
+ /**
54
+ * A previously-authenticated session became invalid after the handshake
55
+ * (revoked, expired, or the account was banned/deactivated). Re-authenticate
56
+ * rather than blindly reconnecting into the same rejection.
57
+ */
58
+ | "SESSION_INVALID"
59
+ /**
60
+ * The subscribe-time snapshot could not be read. The subscription is still
61
+ * active and live diffs flow, but the baseline is missing — refetch it (e.g.
62
+ * from REST) rather than assuming the stream started from a coherent state.
63
+ */
64
+ | "SNAPSHOT_UNAVAILABLE";
65
+ /**
66
+ * An `Error` frame the server sent on this connection.
67
+ *
68
+ * `code` is widened past {@link WebSocketErrorCode} on purpose: a code added
69
+ * server-side after this SDK version shipped is delivered as-is rather than
70
+ * dropped, so an older client still sees that something failed and can log it.
71
+ * Narrow with a `switch` on the known values and treat the default as unknown.
72
+ */
73
+ export interface WebSocketErrorInfo {
74
+ /** Stable classification of the failure. */
75
+ code: WebSocketErrorCode | (string & {});
76
+ /** Human-readable description; for logging and display, never for branching. */
77
+ message: string;
78
+ }
79
+ /** Handler invoked for every `Error` frame the server sends. */
80
+ export type ErrorHandler = (info: WebSocketErrorInfo) => void;
32
81
  /**
33
82
  * Reconnect/resync knobs and callbacks for the Monaco WebSocket client.
34
83
  *
@@ -62,6 +111,26 @@ export interface MonacoWebSocketConfig {
62
111
  * the snapshot was in flight are newer than the snapshot rows they duplicate.
63
112
  */
64
113
  onResync?: ResyncHandler;
114
+ /**
115
+ * Called for every `Error` frame the server sends on this connection.
116
+ *
117
+ * `Error` frames are connection-scoped, not channel-scoped, so they reach no
118
+ * subscription handler — without this callback a failed subscribe, an
119
+ * invalidated session, or an unreadable subscribe-time snapshot is invisible
120
+ * to the app. Branch on `code`:
121
+ *
122
+ * - `SNAPSHOT_UNAVAILABLE` — live diffs flow but the baseline is missing;
123
+ * refetch it. The subscription itself is fine.
124
+ * - `SESSION_INVALID` — re-authenticate rather than reconnect into the same
125
+ * rejection.
126
+ * - `AUTH_REQUIRED` — a rejected SUBSCRIBE, not an invalidated session: the
127
+ * channel was never registered, and this client still holds your handler,
128
+ * so it treats the channel as subscribed and sends no new `Subscribe`.
129
+ * Re-authenticating alone leaves it dead. Release the last handler for the
130
+ * channel and subscribe again after authenticating (0XM-2644 tracks doing
131
+ * this automatically).
132
+ */
133
+ onError?: ErrorHandler;
65
134
  }
66
135
  /**
67
136
  * Raw event message interface
@@ -7,13 +7,53 @@
7
7
  * surface — the channel-coverage lock in
8
8
  * `tests/websocket/channel-coverage.test.ts` parses this file as the source
9
9
  * of truth for the SDK's channel surface.
10
+ *
11
+ * ## Subscribe-time snapshots
12
+ *
13
+ * Every subscription whose channel has a server-side baseline takes an
14
+ * optional trailing `onSnapshot`. It receives that channel's COMPLETE current
15
+ * state as one array, once, before the first live event — so a consumer can
16
+ * seed state from the socket instead of a REST read, and replace it wholesale
17
+ * after a reconnect.
18
+ *
19
+ * The argument is deliberately trailing and optional on every method: adding a
20
+ * required parameter, or inserting one ahead of an existing optional filter,
21
+ * would break every existing call site.
22
+ *
23
+ * Snapshot rows go to `onSnapshot` and nowhere else. They are the channel's
24
+ * terminal-state payload rather than its event envelope (see
25
+ * {@link SnapshotHandler}), so a consumer that reduced them through the event
26
+ * handler too would apply the same baseline twice.
27
+ *
28
+ * Snapshot frames are routed by the FULL canonical channel string, unlike live
29
+ * events, which are prefix-matched. That is what keeps two pair-scoped
30
+ * subscriptions' baselines on their own callbacks.
31
+ *
32
+ * **One baseline per wire subscription, not per call.** The client sends one
33
+ * `Subscribe` for a channel however many local handlers it has, and the server
34
+ * snapshots only genuinely new subscriptions — so on a channel this client
35
+ * already holds, a second subscriber's `onSnapshot` is not invoked until the
36
+ * next reconnect resubscribes. `instruments` is the exception: it caches its
37
+ * baseline locally and replays it to a late subscriber. The other channels
38
+ * cannot reuse that cache — `orders` snapshot rows are not the shape its live
39
+ * events carry, so there is nothing to keep the cache current with — and a
40
+ * stale replayed baseline would be worse than none. Subscribe once per channel
41
+ * and fan out in application code (a React hook already does this), or read the
42
+ * baseline over REST for the extra consumer.
43
+ *
44
+ * Where the server has no snapshot arm for a channel yet, the callback is
45
+ * simply never invoked — a silent no-op, not an error. `orderbook`, `ohlcv`,
46
+ * `trades` and `movements` take no `onSnapshot` at all: they stream events
47
+ * rather than state, and the channel-coverage lock pins that classification
48
+ * against the server's own snapshot dispatch.
10
49
  */
11
50
  import type { SessionCredentials } from "../auth/index";
12
51
  import type { Interval } from "../market";
13
52
  import type { TradingMode } from "../trading";
14
53
  import type { WebSocketStatus } from "./base";
15
54
  import type { OrderbookQuotationMode } from "./clients/orderbook-client";
16
- import type { AccountEvent, ConditionalOrderEvent, InstrumentEvent, LiquidationEvent, MarketStatsAllEvent, MarketStatsEvent, OHLCVEvent, OHLCVPriceType, OrderbookEvent, OrderEvent, PositionEvent, TradeEvent, TwapOrderEvent, UserBalanceEvent, UserMovementEvent } from "./events";
55
+ import type { AccountEvent, AccountEventData, ConditionalOrderEvent, ConditionalOrderEventData, InstrumentEvent, InstrumentEventData, LiquidationEvent, LiquidationEventData, MarketStatsAllEvent, MarketStatsData, MarketStatsEvent, MarketStatsSummary, OHLCVEvent, OHLCVPriceType, OrderbookEvent, OrderEvent, PositionEvent, PositionEventData, TradeEvent, TwapOrderEvent, TwapOrderEventData, UserBalanceEvent, UserBalanceEventData, UserMovementEvent } from "./events";
56
+ import type { OrderSnapshotItem, SnapshotHandler } from "./snapshots";
17
57
  export type MessageHandler<T> = (data: T) => void;
18
58
  export interface MonacoWebSocket {
19
59
  /** Connect to the WebSocket server */
@@ -26,8 +66,8 @@ export interface MonacoWebSocket {
26
66
  getStatus: () => WebSocketStatus;
27
67
  /** Set (or clear) the session keypair used to authenticate the connection */
28
68
  setSessionKeypair: (credentials: SessionCredentials | undefined) => void;
29
- /** Subscribe to order events (requires authentication) */
30
- orders: (tradingPairId: string, tradingMode: TradingMode, handler: MessageHandler<OrderEvent>) => () => void;
69
+ /** Subscribe to order events (requires authentication); `onSnapshot` receives every resting order on the pair */
70
+ orders: (tradingPairId: string, tradingMode: TradingMode, handler: MessageHandler<OrderEvent>, onSnapshot?: SnapshotHandler<OrderSnapshotItem>) => () => void;
31
71
  /** Subscribe to orderbook events (public) */
32
72
  orderbook: (tradingPairId: string, tradingMode: TradingMode, magnitude: number, quotationMode: OrderbookQuotationMode, handler: MessageHandler<OrderbookEvent>) => () => void;
33
73
  /** Subscribe to OHLCV candlestick events (public) */
@@ -36,24 +76,33 @@ export interface MonacoWebSocket {
36
76
  trades: (tradingPairId: string, handler: MessageHandler<TradeEvent>) => () => void;
37
77
  /** Subscribe to user movement events (requires authentication) */
38
78
  movements: (handler: MessageHandler<UserMovementEvent>) => () => void;
39
- /** Subscribe to user order events for all trading pairs (requires authentication) */
40
- userOrders: (handler: MessageHandler<OrderEvent>) => () => void;
41
- /** Subscribe to user balance events (requires authentication) */
42
- balances: (handler: MessageHandler<UserBalanceEvent>) => () => void;
43
- /** Subscribe to conditional TP/SL lifecycle events (requires authentication) */
44
- conditionalOrders: (handler: MessageHandler<ConditionalOrderEvent>, tradingPairId?: string) => () => void;
79
+ /** Subscribe to user order events for all trading pairs (requires authentication); `onSnapshot` receives every resting order */
80
+ userOrders: (handler: MessageHandler<OrderEvent>, onSnapshot?: SnapshotHandler<OrderSnapshotItem>) => () => void;
81
+ /** Subscribe to user balance events (requires authentication); `onSnapshot` receives the balance of every asset held */
82
+ balances: (handler: MessageHandler<UserBalanceEvent>, onSnapshot?: SnapshotHandler<UserBalanceEventData>) => () => void;
83
+ /** Subscribe to conditional TP/SL lifecycle events (requires authentication); `onSnapshot` receives every live conditional order */
84
+ conditionalOrders: (handler: MessageHandler<ConditionalOrderEvent>, tradingPairId?: string, onSnapshot?: SnapshotHandler<ConditionalOrderEventData>) => () => void;
45
85
  /** Subscribe to TWAP parent-order lifecycle events (requires authentication) */
46
- twapOrders: (handler: MessageHandler<TwapOrderEvent>, tradingPairId?: string) => () => void;
47
- /** Subscribe to position state updates (requires authentication); omit `tradingPairId` for all pairs */
48
- positions: (handler: MessageHandler<PositionEvent>, tradingPairId?: string) => () => void;
86
+ twapOrders: (handler: MessageHandler<TwapOrderEvent>, tradingPairId?: string, onSnapshot?: SnapshotHandler<TwapOrderEventData>) => () => void;
87
+ /** Subscribe to position state updates (requires authentication); omit `tradingPairId` for all pairs. `onSnapshot` receives every open position */
88
+ positions: (handler: MessageHandler<PositionEvent>, tradingPairId?: string, onSnapshot?: SnapshotHandler<PositionEventData>) => () => void;
49
89
  /** Subscribe to liquidation alerts (requires authentication); omit `tradingPairId` for all pairs */
50
- liquidations: (handler: MessageHandler<LiquidationEvent>, tradingPairId?: string) => () => void;
90
+ liquidations: (handler: MessageHandler<LiquidationEvent>, tradingPairId?: string, onSnapshot?: SnapshotHandler<LiquidationEventData>) => () => void;
51
91
  /** Subscribe to account-level margin health updates for the authenticated user (requires authentication) */
52
- account: (handler: MessageHandler<AccountEvent>) => () => void;
53
- /** Subscribe to market lifecycle + config changes (public); omit `tradingPairId` for all markets */
54
- instruments: (handler: MessageHandler<InstrumentEvent>, tradingPairId?: string) => () => void;
92
+ account: (handler: MessageHandler<AccountEvent>, onSnapshot?: SnapshotHandler<AccountEventData>) => () => void;
93
+ /**
94
+ * Subscribe to market lifecycle + config changes (public); omit
95
+ * `tradingPairId` for all markets. `onSnapshot` receives the full current
96
+ * config of every active market.
97
+ *
98
+ * Opting in also changes how the baseline is delivered: without
99
+ * `onSnapshot`, the snapshot is fanned out to `handler` as one synthetic
100
+ * per-market event (the channel's original behaviour, kept for source and
101
+ * behaviour compatibility). With it, the rows arrive only as the array.
102
+ */
103
+ instruments: (handler: MessageHandler<InstrumentEvent>, tradingPairId?: string, onSnapshot?: SnapshotHandler<InstrumentEventData>) => () => void;
55
104
  /** Subscribe to per-market statistics for one trading pair (public) */
56
- marketStats: (tradingPairId: string, handler: MessageHandler<MarketStatsEvent>) => () => void;
105
+ marketStats: (tradingPairId: string, handler: MessageHandler<MarketStatsEvent>, onSnapshot?: SnapshotHandler<MarketStatsData>) => () => void;
57
106
  /** Subscribe to the all-markets statistics summary (public) */
58
- marketStatsAll: (handler: MessageHandler<MarketStatsAllEvent>) => () => void;
107
+ marketStatsAll: (handler: MessageHandler<MarketStatsAllEvent>, onSnapshot?: SnapshotHandler<MarketStatsSummary>) => () => void;
59
108
  }