@0xmonaco/core 1.0.51 → 1.0.54
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.
|
@@ -69,7 +69,9 @@ export class MarginAccountsAPIImpl extends BaseAPI {
|
|
|
69
69
|
tradingPairId: request.tradingPairId,
|
|
70
70
|
strategyKey: request.strategyKey,
|
|
71
71
|
marginMode: request.marginMode,
|
|
72
|
-
selectedTradingPairIds:
|
|
72
|
+
// No selectedTradingPairIds: cross scope is derived from trading, and
|
|
73
|
+
// the server ignores the deprecated field. Not sending it keeps a
|
|
74
|
+
// pre-upgrade caller's stale list from reaching the wire at all.
|
|
73
75
|
// JSON.stringify drops it when omitted; an explicit false MUST reach
|
|
74
76
|
// the wire or the server applies its Adjust Margin default and every
|
|
75
77
|
// funding-loop top-up converts into initial margin requirement.
|
|
@@ -89,6 +89,8 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
|
|
|
89
89
|
reduceOnly?: boolean;
|
|
90
90
|
postOnly?: boolean;
|
|
91
91
|
clientOrderId?: string;
|
|
92
|
+
/** Generated once per call when omitted. Supply the same key to retry a logical order within 24 hours. */
|
|
93
|
+
idempotencyKey?: string;
|
|
92
94
|
takeProfit?: ParentTpSlLegParams;
|
|
93
95
|
stopLoss?: ParentTpSlLegParams;
|
|
94
96
|
}): Promise<CreateOrderResponse>;
|
|
@@ -149,6 +151,8 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
|
|
|
149
151
|
takeProfit?: ParentTpSlLegParams;
|
|
150
152
|
stopLoss?: ParentTpSlLegParams;
|
|
151
153
|
clientOrderId?: string;
|
|
154
|
+
/** Generated once per call when omitted. Supply the same key to retry a logical order within 24 hours. */
|
|
155
|
+
idempotencyKey?: string;
|
|
152
156
|
}): Promise<CreateOrderResponse>;
|
|
153
157
|
/**
|
|
154
158
|
* Cancels an existing order.
|
package/dist/api/trading/api.js
CHANGED
|
@@ -121,6 +121,7 @@ export class TradingAPIImpl extends BaseAPI {
|
|
|
121
121
|
reduceOnly: options?.reduceOnly,
|
|
122
122
|
postOnly: options?.postOnly,
|
|
123
123
|
clientOrderId: options?.clientOrderId,
|
|
124
|
+
idempotencyKey: options?.idempotencyKey === undefined ? crypto.randomUUID() : options.idempotencyKey,
|
|
124
125
|
takeProfit: parentTpSlLegToRequest(options?.takeProfit),
|
|
125
126
|
stopLoss: parentTpSlLegToRequest(options?.stopLoss),
|
|
126
127
|
};
|
|
@@ -199,6 +200,7 @@ export class TradingAPIImpl extends BaseAPI {
|
|
|
199
200
|
takeProfit: parentTpSlLegToRequest(options?.takeProfit),
|
|
200
201
|
stopLoss: parentTpSlLegToRequest(options?.stopLoss),
|
|
201
202
|
clientOrderId: options?.clientOrderId,
|
|
203
|
+
idempotencyKey: options?.idempotencyKey === undefined ? crypto.randomUUID() : options.idempotencyKey,
|
|
202
204
|
};
|
|
203
205
|
return await this.makeAuthenticatedRequest(perpRoutes.orders.create(), {
|
|
204
206
|
method: "POST",
|
|
@@ -387,6 +389,7 @@ export class TradingAPIImpl extends BaseAPI {
|
|
|
387
389
|
reduceOnly: order.reduceOnly,
|
|
388
390
|
postOnly: order.postOnly,
|
|
389
391
|
clientOrderId: order.clientOrderId,
|
|
392
|
+
idempotencyKey: order.idempotencyKey === undefined ? crypto.randomUUID() : order.idempotencyKey,
|
|
390
393
|
})),
|
|
391
394
|
};
|
|
392
395
|
return this.makeAuthenticatedRequest(perpRoutes.orders.batchCreate(), {
|
|
@@ -26,6 +26,33 @@ function readOptionalString(source, field) {
|
|
|
26
26
|
}
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Read a field the server serializes as an explicit `null` when unset.
|
|
31
|
+
*
|
|
32
|
+
* Distinct from {@link readOptionalString}, which normalizes absent to
|
|
33
|
+
* `undefined`: a Rust `Option` WITHOUT `skip_serializing_if` always puts the key
|
|
34
|
+
* on the wire, so the declared type is `string | null` and `null` has to survive
|
|
35
|
+
* the parse rather than being laundered into `undefined`.
|
|
36
|
+
*
|
|
37
|
+
* An ABSENT key is a malformed frame, not "unset", and is rejected. The
|
|
38
|
+
* distinction matters because these readers feed the snapshot parsers, whose
|
|
39
|
+
* output is an authoritative complete baseline: reading a missing key as `null`
|
|
40
|
+
* would let a truncated row through as though the server had said the field was
|
|
41
|
+
* empty, and a consumer would clear real state on it. Rejecting withholds the
|
|
42
|
+
* whole snapshot instead, which is the delivery contract's failure mode.
|
|
43
|
+
*/
|
|
44
|
+
function readNullableString(source, field) {
|
|
45
|
+
if (!Object.hasOwn(source, field)) {
|
|
46
|
+
throw new Error(`Invalid WebSocket event: ${field} is missing`);
|
|
47
|
+
}
|
|
48
|
+
const value = source[field];
|
|
49
|
+
if (value === null || value === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
if (typeof value !== "string") {
|
|
52
|
+
throw new Error(`Invalid WebSocket event: ${field} must be a string`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
29
56
|
function readBoolean(source, field) {
|
|
30
57
|
const value = source[field];
|
|
31
58
|
if (typeof value !== "boolean") {
|
|
@@ -84,152 +111,183 @@ function readOptionalEnumValue(source, field) {
|
|
|
84
111
|
const value = readOptionalString(source, field);
|
|
85
112
|
return value === undefined || value.length === 0 ? undefined : value;
|
|
86
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Parse a conditional order's terminal-state payload.
|
|
116
|
+
*
|
|
117
|
+
* Shared by the live event and the subscribe-time snapshot: a snapshot row IS
|
|
118
|
+
* this payload with no envelope around it, so parsing both through one function
|
|
119
|
+
* is what stops the baseline and the diffs describing state differently.
|
|
120
|
+
*/
|
|
121
|
+
function parseConditionalOrderEventData(raw) {
|
|
122
|
+
if (!isRecord(raw)) {
|
|
123
|
+
throw new Error("Invalid conditional order event: data must be an object");
|
|
124
|
+
}
|
|
125
|
+
const data = keysToCamelCase(raw);
|
|
126
|
+
return {
|
|
127
|
+
conditionalOrderId: readString(data, "conditionalOrderId"),
|
|
128
|
+
tradingPairId: readString(data, "tradingPairId"),
|
|
129
|
+
marginAccountId: readString(data, "marginAccountId"),
|
|
130
|
+
positionId: readOptionalString(data, "positionId"),
|
|
131
|
+
parentOrderId: readOptionalString(data, "parentOrderId"),
|
|
132
|
+
associationType: readOptionalEnumValue(data, "associationType"),
|
|
133
|
+
linkedGroupId: readOptionalString(data, "linkedGroupId"),
|
|
134
|
+
conditionType: readString(data, "conditionType"),
|
|
135
|
+
triggerSource: readString(data, "triggerSource"),
|
|
136
|
+
triggerPrice: readString(data, "triggerPrice"),
|
|
137
|
+
side: readString(data, "side"),
|
|
138
|
+
positionSide: readString(data, "positionSide"),
|
|
139
|
+
orderType: readString(data, "orderType"),
|
|
140
|
+
limitPrice: readOptionalString(data, "limitPrice"),
|
|
141
|
+
quantity: readOptionalString(data, "quantity"),
|
|
142
|
+
slippageToleranceBps: readOptionalNumber(data, "slippageToleranceBps"),
|
|
143
|
+
reduceOnly: readBoolean(data, "reduceOnly"),
|
|
144
|
+
timeInForce: readOptionalEnumValue(data, "timeInForce"),
|
|
145
|
+
state: readString(data, "state"),
|
|
146
|
+
triggeredOrderId: readOptionalString(data, "triggeredOrderId"),
|
|
147
|
+
triggeredAt: readOptionalString(data, "triggeredAt"),
|
|
148
|
+
activatedAt: readOptionalString(data, "activatedAt"),
|
|
149
|
+
cancelledAt: readOptionalString(data, "cancelledAt"),
|
|
150
|
+
expiresAt: readOptionalString(data, "expiresAt"),
|
|
151
|
+
failureReason: readOptionalString(data, "failureReason"),
|
|
152
|
+
// Free-form on the wire (the server field is a plain string), so it is
|
|
153
|
+
// passed through untouched rather than matched against a value list.
|
|
154
|
+
reason: readString(data, "reason"),
|
|
155
|
+
updatedAt: readString(data, "updatedAt"),
|
|
156
|
+
// Populated on the subscribe-time snapshot frame and absent on live
|
|
157
|
+
// updates (0XM-2440). Reading it optionally is what keeps the snapshot's
|
|
158
|
+
// value from being silently dropped before the subscriber.
|
|
159
|
+
version: readOptionalNumber(data, "version"),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
87
162
|
function parseConditionalOrderEvent(rawData) {
|
|
88
163
|
if (!isRecord(rawData)) {
|
|
89
164
|
throw new Error("Invalid conditional order event: payload must be an object");
|
|
90
165
|
}
|
|
91
|
-
if (!isRecord(rawData.data)) {
|
|
92
|
-
throw new Error("Invalid conditional order event: data must be an object");
|
|
93
|
-
}
|
|
94
|
-
const data = keysToCamelCase(rawData.data);
|
|
95
166
|
return {
|
|
96
167
|
// event_type is the event-class discriminator, validated strictly so a
|
|
97
168
|
// mis-routed frame is rejected rather than parsed as a conditional order.
|
|
98
|
-
// The value fields
|
|
99
|
-
// checked against an allow-list — so an unexpected *value* never
|
|
100
|
-
// whole update (0XM-2627). The parse throw is swallowed by the
|
|
169
|
+
// The value fields in the payload stay lenient — read as strings and cast,
|
|
170
|
+
// never checked against an allow-list — so an unexpected *value* never
|
|
171
|
+
// drops the whole update (0XM-2627). The parse throw is swallowed by the
|
|
101
172
|
// message-handler catch, so a strict enum here means a silently lost frame
|
|
102
173
|
// with nothing surfaced to the consumer.
|
|
103
174
|
eventType: readEnum(rawData, "event_type", ["conditional_order_update"]),
|
|
104
175
|
userId: readString(rawData, "user_id"),
|
|
105
|
-
data:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
updatedAt: readString(data, "updatedAt"),
|
|
135
|
-
// Populated on the subscribe-time snapshot frame and absent on live
|
|
136
|
-
// updates (0XM-2440). Reading it optionally is what keeps the
|
|
137
|
-
// snapshot's value from being silently dropped before the subscriber.
|
|
138
|
-
version: readOptionalNumber(data, "version"),
|
|
139
|
-
},
|
|
176
|
+
data: parseConditionalOrderEventData(rawData.data),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Parse a position's terminal-state payload. Shared by the live event and the
|
|
181
|
+
* subscribe-time snapshot (see {@link parseConditionalOrderEventData}).
|
|
182
|
+
*/
|
|
183
|
+
function parsePositionEventData(raw) {
|
|
184
|
+
if (!isRecord(raw)) {
|
|
185
|
+
throw new Error("Invalid position event: data must be an object");
|
|
186
|
+
}
|
|
187
|
+
const data = keysToCamelCase(raw);
|
|
188
|
+
return {
|
|
189
|
+
positionId: readString(data, "positionId"),
|
|
190
|
+
marginAccountId: readString(data, "marginAccountId"),
|
|
191
|
+
tradingPairId: readString(data, "tradingPairId"),
|
|
192
|
+
side: readString(data, "side"),
|
|
193
|
+
status: readString(data, "status"),
|
|
194
|
+
size: readString(data, "size"),
|
|
195
|
+
entryPrice: readString(data, "entryPrice"),
|
|
196
|
+
markPrice: readOptionalString(data, "markPrice"),
|
|
197
|
+
unrealizedPnl: readOptionalString(data, "unrealizedPnl"),
|
|
198
|
+
liquidationPrice: readOptionalString(data, "liquidationPrice"),
|
|
199
|
+
isolatedMargin: readString(data, "isolatedMargin"),
|
|
200
|
+
leverage: readString(data, "leverage"),
|
|
201
|
+
tradeId: readOptionalString(data, "tradeId"),
|
|
202
|
+
orderId: readOptionalString(data, "orderId"),
|
|
203
|
+
reason: readString(data, "reason"),
|
|
204
|
+
updatedAt: readString(data, "updatedAt"),
|
|
140
205
|
};
|
|
141
206
|
}
|
|
142
207
|
function parsePositionEvent(rawData) {
|
|
143
208
|
if (!isRecord(rawData)) {
|
|
144
209
|
throw new Error("Invalid position event: payload must be an object");
|
|
145
210
|
}
|
|
146
|
-
if (!isRecord(rawData.data)) {
|
|
147
|
-
throw new Error("Invalid position event: data must be an object");
|
|
148
|
-
}
|
|
149
|
-
const data = keysToCamelCase(rawData.data);
|
|
150
211
|
return {
|
|
151
212
|
// event_type is the event-class discriminator, validated strictly so a
|
|
152
213
|
// mis-routed frame is rejected rather than parsed as a position. The value
|
|
153
|
-
// fields
|
|
154
|
-
// never drops a risk-critical update.
|
|
214
|
+
// fields in the payload (side/status/reason) stay lenient so an unexpected
|
|
215
|
+
// *value* never drops a risk-critical update.
|
|
155
216
|
eventType: readEnum(rawData, "event_type", ["position_update"]),
|
|
156
217
|
userId: readString(rawData, "user_id"),
|
|
157
|
-
data:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
218
|
+
data: parsePositionEventData(rawData.data),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Parse a liquidation alert's payload. Shared by the live event and the
|
|
223
|
+
* subscribe-time snapshot (see {@link parseConditionalOrderEventData}).
|
|
224
|
+
*/
|
|
225
|
+
function parseLiquidationEventData(raw) {
|
|
226
|
+
if (!isRecord(raw)) {
|
|
227
|
+
throw new Error("Invalid liquidation event: data must be an object");
|
|
228
|
+
}
|
|
229
|
+
const data = keysToCamelCase(raw);
|
|
230
|
+
return {
|
|
231
|
+
liquidationId: readString(data, "liquidationId"),
|
|
232
|
+
marginAccountId: readString(data, "marginAccountId"),
|
|
233
|
+
positionId: readOptionalString(data, "positionId"),
|
|
234
|
+
tradingPairId: readOptionalString(data, "tradingPairId"),
|
|
235
|
+
status: readString(data, "status"),
|
|
236
|
+
accountState: readString(data, "accountState"),
|
|
237
|
+
liquidationPrice: readOptionalString(data, "liquidationPrice"),
|
|
238
|
+
markPrice: readOptionalString(data, "markPrice"),
|
|
239
|
+
estimatedShortfall: readString(data, "estimatedShortfall"),
|
|
240
|
+
message: readString(data, "message"),
|
|
241
|
+
triggeredAt: readString(data, "triggeredAt"),
|
|
175
242
|
};
|
|
176
243
|
}
|
|
177
244
|
function parseLiquidationEvent(rawData) {
|
|
178
245
|
if (!isRecord(rawData)) {
|
|
179
246
|
throw new Error("Invalid liquidation event: payload must be an object");
|
|
180
247
|
}
|
|
181
|
-
if (!isRecord(rawData.data)) {
|
|
182
|
-
throw new Error("Invalid liquidation event: data must be an object");
|
|
183
|
-
}
|
|
184
|
-
const data = keysToCamelCase(rawData.data);
|
|
185
248
|
return {
|
|
186
249
|
// event_type is the event-class discriminator, validated strictly so a
|
|
187
250
|
// mis-routed frame is rejected rather than parsed as a liquidation. The
|
|
188
|
-
// free-form value fields
|
|
189
|
-
// unexpected *value* never drops a risk-critical alert.
|
|
251
|
+
// free-form value fields in the payload (status/accountState) stay lenient
|
|
252
|
+
// so an unexpected *value* never drops a risk-critical alert.
|
|
190
253
|
eventType: readEnum(rawData, "event_type", ["liquidation_alert"]),
|
|
191
254
|
userId: readString(rawData, "user_id"),
|
|
192
|
-
data:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
255
|
+
data: parseLiquidationEventData(rawData.data),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Parse an account's margin-health payload. Shared by the live event and the
|
|
260
|
+
* subscribe-time snapshot (see {@link parseConditionalOrderEventData}).
|
|
261
|
+
*/
|
|
262
|
+
function parseAccountEventData(raw) {
|
|
263
|
+
if (!isRecord(raw)) {
|
|
264
|
+
throw new Error("Invalid account event: data must be an object");
|
|
265
|
+
}
|
|
266
|
+
const data = keysToCamelCase(raw);
|
|
267
|
+
return {
|
|
268
|
+
marginAccountId: readString(data, "marginAccountId"),
|
|
269
|
+
// Monotonic per-account publish counter (0XM-2498): a plain JSON number on
|
|
270
|
+
// the wire, used to order/dedupe frames across reconnect races. Must be a
|
|
271
|
+
// non-negative safe integer — anything else is a malformed frame.
|
|
272
|
+
sequence: readSequence(data, "sequence"),
|
|
273
|
+
equity: readString(data, "equity"),
|
|
274
|
+
collateral: readString(data, "collateral"),
|
|
275
|
+
freeCollateral: readString(data, "freeCollateral"),
|
|
276
|
+
marginRatio: readString(data, "marginRatio"),
|
|
277
|
+
maintenanceRequirement: readString(data, "maintenanceRequirement"),
|
|
278
|
+
updatedAt: readString(data, "updatedAt"),
|
|
205
279
|
};
|
|
206
280
|
}
|
|
207
281
|
function parseAccountEvent(rawData) {
|
|
208
282
|
if (!isRecord(rawData)) {
|
|
209
283
|
throw new Error("Invalid account event: payload must be an object");
|
|
210
284
|
}
|
|
211
|
-
if (!isRecord(rawData.data)) {
|
|
212
|
-
throw new Error("Invalid account event: data must be an object");
|
|
213
|
-
}
|
|
214
|
-
const data = keysToCamelCase(rawData.data);
|
|
215
285
|
return {
|
|
216
286
|
// event_type is the event-class discriminator, validated strictly so a
|
|
217
287
|
// mis-routed frame is rejected rather than parsed as an account update.
|
|
218
288
|
eventType: readEnum(rawData, "event_type", ["account_update"]),
|
|
219
289
|
userId: readString(rawData, "user_id"),
|
|
220
|
-
data:
|
|
221
|
-
marginAccountId: readString(data, "marginAccountId"),
|
|
222
|
-
// Monotonic per-account publish counter (0XM-2498): a plain JSON number on
|
|
223
|
-
// the wire, used to order/dedupe frames across reconnect races. Must be a
|
|
224
|
-
// non-negative safe integer — anything else is a malformed frame.
|
|
225
|
-
sequence: readSequence(data, "sequence"),
|
|
226
|
-
equity: readString(data, "equity"),
|
|
227
|
-
collateral: readString(data, "collateral"),
|
|
228
|
-
freeCollateral: readString(data, "freeCollateral"),
|
|
229
|
-
marginRatio: readString(data, "marginRatio"),
|
|
230
|
-
maintenanceRequirement: readString(data, "maintenanceRequirement"),
|
|
231
|
-
updatedAt: readString(data, "updatedAt"),
|
|
232
|
-
},
|
|
290
|
+
data: parseAccountEventData(rawData.data),
|
|
233
291
|
};
|
|
234
292
|
}
|
|
235
293
|
function parseMarketStatsData(raw) {
|
|
@@ -350,12 +408,9 @@ function parseInstrumentEvent(rawData) {
|
|
|
350
408
|
};
|
|
351
409
|
}
|
|
352
410
|
/**
|
|
353
|
-
* Parse a `twap_orders` channel frame into a typed event.
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* fields never break older clients. The server serializes absent optional
|
|
357
|
-
* fields as explicit `null`; those are dropped so the parsed snapshot matches
|
|
358
|
-
* the optional (`?:`) fields of `TwapOrderEventData`.
|
|
411
|
+
* Parse a `twap_orders` channel frame into a typed event. The envelope is
|
|
412
|
+
* validated strictly here; the payload's own leniency contract lives in
|
|
413
|
+
* {@link parseTwapOrderEventData}.
|
|
359
414
|
*/
|
|
360
415
|
function parseTwapOrderEvent(rawData) {
|
|
361
416
|
if (!isRecord(rawData)) {
|
|
@@ -372,7 +427,27 @@ function parseTwapOrderEvent(rawData) {
|
|
|
372
427
|
if (typeof userId !== "string" || userId.length === 0) {
|
|
373
428
|
throw new Error("Invalid TWAP order event: user_id must be a non-empty string");
|
|
374
429
|
}
|
|
375
|
-
|
|
430
|
+
return {
|
|
431
|
+
eventType: "twap_order_update",
|
|
432
|
+
userId,
|
|
433
|
+
data: parseTwapOrderEventData(rawData.data),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Parse a TWAP parent order's terminal-state payload. Shared by the live event
|
|
438
|
+
* and the subscribe-time snapshot (see {@link parseConditionalOrderEventData}).
|
|
439
|
+
*
|
|
440
|
+
* Only the identity and state fields are validated; the rest of the snapshot is
|
|
441
|
+
* passed through camelCased, so additive server fields never break older
|
|
442
|
+
* clients. The server serializes absent optional fields as explicit `null`;
|
|
443
|
+
* those are dropped so the parsed payload matches the optional (`?:`) fields of
|
|
444
|
+
* `TwapOrderEventData`.
|
|
445
|
+
*/
|
|
446
|
+
function parseTwapOrderEventData(raw) {
|
|
447
|
+
if (!isRecord(raw)) {
|
|
448
|
+
throw new Error("Invalid TWAP order event: data must be an object");
|
|
449
|
+
}
|
|
450
|
+
const data = keysToCamelCase(raw);
|
|
376
451
|
for (const key of Object.keys(data)) {
|
|
377
452
|
if (data[key] === null) {
|
|
378
453
|
delete data[key];
|
|
@@ -393,10 +468,139 @@ function parseTwapOrderEvent(rawData) {
|
|
|
393
468
|
if (typeof state !== "string" || state.length === 0) {
|
|
394
469
|
throw new Error("Invalid TWAP order event: state must be a non-empty string");
|
|
395
470
|
}
|
|
471
|
+
return data;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Parse a user balance payload. Shared by the live event and the subscribe-time
|
|
475
|
+
* snapshot (see {@link parseConditionalOrderEventData}).
|
|
476
|
+
*
|
|
477
|
+
* `tokenSymbol` and `referenceId` are read as nullable rather than optional:
|
|
478
|
+
* the server sends both keys always, carrying `null` when unset, and
|
|
479
|
+
* `UserBalanceEventData` declares them `string | null`.
|
|
480
|
+
*/
|
|
481
|
+
function parseUserBalanceEventData(raw) {
|
|
482
|
+
if (!isRecord(raw)) {
|
|
483
|
+
throw new Error("Invalid balance event: data must be an object");
|
|
484
|
+
}
|
|
485
|
+
const data = keysToCamelCase(raw);
|
|
396
486
|
return {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
487
|
+
tokenAddress: readString(data, "tokenAddress"),
|
|
488
|
+
tokenSymbol: readNullableString(data, "tokenSymbol"),
|
|
489
|
+
available: readString(data, "available"),
|
|
490
|
+
availableRaw: readString(data, "availableRaw"),
|
|
491
|
+
locked: readString(data, "locked"),
|
|
492
|
+
lockedRaw: readString(data, "lockedRaw"),
|
|
493
|
+
total: readString(data, "total"),
|
|
494
|
+
totalRaw: readString(data, "totalRaw"),
|
|
495
|
+
// Free-form on the wire and read leniently: a snapshot row carries the
|
|
496
|
+
// `snapshot` literal, a live frame the mutation cause, and a reason added
|
|
497
|
+
// server-side after this SDK shipped must not drop the row (0XM-2627).
|
|
498
|
+
reason: readString(data, "reason"),
|
|
499
|
+
referenceId: readNullableString(data, "referenceId"),
|
|
500
|
+
updatedAt: readString(data, "updatedAt"),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Parse one resting order from an `orders` channel snapshot.
|
|
505
|
+
*
|
|
506
|
+
* This is the one snapshot row that is NOT the channel's live payload: an order
|
|
507
|
+
* event describes a change, a snapshot row describes resting state, so the row
|
|
508
|
+
* carries `status` and the cumulative fill totals and carries none of the
|
|
509
|
+
* change-only fill metadata.
|
|
510
|
+
*
|
|
511
|
+
* The optional fields are read as nullable, not optional: they are Rust
|
|
512
|
+
* `Option`s serialized without `skip_serializing_if`, so the key is always
|
|
513
|
+
* present carrying `null`. `clientOrderId` is the one field the server omits
|
|
514
|
+
* outright, so it is the one read as optional.
|
|
515
|
+
*/
|
|
516
|
+
function parseOrderSnapshotItem(raw) {
|
|
517
|
+
if (!isRecord(raw)) {
|
|
518
|
+
throw new Error("Invalid order snapshot: item must be an object");
|
|
519
|
+
}
|
|
520
|
+
const item = keysToCamelCase(raw);
|
|
521
|
+
// Read once: this is the only genuinely-omitted field, and it is spread in
|
|
522
|
+
// conditionally so an absent one stays absent rather than becoming
|
|
523
|
+
// `clientOrderId: undefined`, which a structural comparison would see.
|
|
524
|
+
const clientOrderId = readOptionalString(item, "clientOrderId");
|
|
525
|
+
const version = readOptionalNumber(item, "version");
|
|
526
|
+
return {
|
|
527
|
+
orderId: readString(item, "orderId"),
|
|
528
|
+
userId: readString(item, "userId"),
|
|
529
|
+
applicationId: readString(item, "applicationId"),
|
|
530
|
+
tradingPairId: readString(item, "tradingPairId"),
|
|
531
|
+
tradingPair: readString(item, "tradingPair"),
|
|
532
|
+
// Value fields are read as plain strings and cast, never matched against an
|
|
533
|
+
// allow-list: an unrecognized value must not drop the row, or a consumer
|
|
534
|
+
// seeding from the baseline would silently lose an order that is really
|
|
535
|
+
// resting on the book (0XM-2627).
|
|
536
|
+
tradingMode: readString(item, "tradingMode"),
|
|
537
|
+
side: readString(item, "side"),
|
|
538
|
+
orderType: readString(item, "orderType"),
|
|
539
|
+
price: readNullableString(item, "price"),
|
|
540
|
+
quantity: readString(item, "quantity"),
|
|
541
|
+
quantityRaw: readString(item, "quantityRaw"),
|
|
542
|
+
filledQuantity: readString(item, "filledQuantity"),
|
|
543
|
+
filledQuantityRaw: readString(item, "filledQuantityRaw"),
|
|
544
|
+
remainingQuantity: readNullableString(item, "remainingQuantity"),
|
|
545
|
+
remainingQuantityRaw: readNullableString(item, "remainingQuantityRaw"),
|
|
546
|
+
status: readString(item, "status"),
|
|
547
|
+
timeInForce: readNullableString(item, "timeInForce"),
|
|
548
|
+
averageFillPrice: readNullableString(item, "averageFillPrice"),
|
|
549
|
+
createdAt: readString(item, "createdAt"),
|
|
550
|
+
updatedAt: readNullableString(item, "updatedAt"),
|
|
551
|
+
marginAccountId: readNullableString(item, "marginAccountId"),
|
|
552
|
+
positionId: readNullableString(item, "positionId"),
|
|
553
|
+
leverage: readNullableString(item, "leverage"),
|
|
554
|
+
reduceOnly: readBoolean(item, "reduceOnly"),
|
|
555
|
+
...(clientOrderId === undefined ? {} : { clientOrderId }),
|
|
556
|
+
// Spread in conditionally for the same reason as `clientOrderId`: the
|
|
557
|
+
// server omits the key when unknown, and absent must stay absent rather
|
|
558
|
+
// than becoming `version: undefined` — a resync compares this value, and
|
|
559
|
+
// absent means unknown, never zero (0XM-2440).
|
|
560
|
+
...(version === undefined ? {} : { version }),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Wrap a caller's `onSnapshot` in the internal snapshot-frame handler.
|
|
565
|
+
*
|
|
566
|
+
* The frame's `data` is the channel's COMPLETE current state as a flat array,
|
|
567
|
+
* and that completeness is the whole contract: a consumer is expected to
|
|
568
|
+
* replace state from it, so absence from the array means the entity is gone.
|
|
569
|
+
*
|
|
570
|
+
* Delivery is therefore all-or-nothing. If the payload is not an array, or any
|
|
571
|
+
* single row fails to parse, NOTHING is delivered.
|
|
572
|
+
*
|
|
573
|
+
* Skipping just the unreadable row and delivering the rest would be worse than
|
|
574
|
+
* delivering nothing, because the array would still be presented as complete: a
|
|
575
|
+
* consumer would drop the entity that row described — zero its balance, remove
|
|
576
|
+
* its market, cancel its order in the UI — on the strength of an omission the
|
|
577
|
+
* SDK itself created. Nor is that self-correcting. A resting order or an
|
|
578
|
+
* unchanged market has no later diff to repair it, so the deletion would stand
|
|
579
|
+
* for the life of the connection.
|
|
580
|
+
*
|
|
581
|
+
* Withholding the frame instead leaves the consumer on whatever baseline it
|
|
582
|
+
* already had, and the channel keeps streaming live events over it. That is a
|
|
583
|
+
* strictly smaller loss, and it is the same all-or-nothing rule this channel's
|
|
584
|
+
* original `instrument` snapshot already applied.
|
|
585
|
+
*
|
|
586
|
+
* `[]` is a legitimate baseline, not a failure, and is always delivered:
|
|
587
|
+
* "nothing is open" is exactly the information a consumer needs to clear stale
|
|
588
|
+
* state after a reconnect.
|
|
589
|
+
*/
|
|
590
|
+
function snapshotFrameHandler(channel, parseItem, onSnapshot) {
|
|
591
|
+
return (rawData) => {
|
|
592
|
+
try {
|
|
593
|
+
if (!Array.isArray(rawData)) {
|
|
594
|
+
throw new Error(`Invalid ${channel} snapshot: data must be an array`);
|
|
595
|
+
}
|
|
596
|
+
// Parsed fully before anything is handed over, so a failure part-way
|
|
597
|
+
// through cannot deliver a truncated baseline.
|
|
598
|
+
const items = rawData.map(parseItem);
|
|
599
|
+
onSnapshot(items);
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
console.error(`WebSocket: Error processing ${channel} snapshot`, err);
|
|
603
|
+
}
|
|
400
604
|
};
|
|
401
605
|
}
|
|
402
606
|
/**
|
|
@@ -643,10 +847,9 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
643
847
|
sendAuthenticate();
|
|
644
848
|
resubscribeAll();
|
|
645
849
|
options.onStatusChange?.("connected");
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
// newer than the rows they duplicate.
|
|
850
|
+
// Send auth/subscribe requests before asking the application to
|
|
851
|
+
// reconcile. This callback does not wait for acknowledgements or
|
|
852
|
+
// snapshots, and the SDK does not perform REST recovery itself.
|
|
650
853
|
if (pendingResyncCode !== null) {
|
|
651
854
|
const code = pendingResyncCode;
|
|
652
855
|
pendingResyncCode = null;
|
|
@@ -759,7 +962,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
759
962
|
};
|
|
760
963
|
};
|
|
761
964
|
// --- Channel-specific subscription methods ---
|
|
762
|
-
const subscribeOrders = (tradingPairId, tradingMode, handler) => {
|
|
965
|
+
const subscribeOrders = (tradingPairId, tradingMode, handler, onSnapshot) => {
|
|
763
966
|
const channel = `orders:${tradingPairId}:${tradingMode}`;
|
|
764
967
|
return subscribe(channel, (rawData) => {
|
|
765
968
|
try {
|
|
@@ -775,7 +978,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
775
978
|
catch (err) {
|
|
776
979
|
console.error("WebSocket: Error processing order event", err);
|
|
777
980
|
}
|
|
778
|
-
});
|
|
981
|
+
}, onSnapshot && snapshotFrameHandler("orders", parseOrderSnapshotItem, onSnapshot));
|
|
779
982
|
};
|
|
780
983
|
const subscribeOrderbook = (tradingPairId, tradingMode, magnitude, quotationMode, handler) => {
|
|
781
984
|
if (!tradingPairId || !tradingMode || magnitude === undefined || !quotationMode) {
|
|
@@ -957,7 +1160,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
957
1160
|
* Subscribe to authenticated user's order events (all trading pairs)
|
|
958
1161
|
* Requires authentication - user is identified from the signed session-key handshake
|
|
959
1162
|
*/
|
|
960
|
-
const subscribeUserOrders = (handler) => {
|
|
1163
|
+
const subscribeUserOrders = (handler, onSnapshot) => {
|
|
961
1164
|
// User is identified from the authenticated session, so channel is just "orders"
|
|
962
1165
|
const channel = "orders";
|
|
963
1166
|
return subscribe(channel, (rawData) => {
|
|
@@ -974,44 +1177,33 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
974
1177
|
catch (err) {
|
|
975
1178
|
console.error("WebSocket: Error processing user order event", err);
|
|
976
1179
|
}
|
|
977
|
-
});
|
|
1180
|
+
}, onSnapshot && snapshotFrameHandler("orders", parseOrderSnapshotItem, onSnapshot));
|
|
978
1181
|
};
|
|
979
1182
|
/**
|
|
980
1183
|
* Subscribe to authenticated user's balance events
|
|
981
1184
|
* Requires authentication - user is identified from the signed session-key handshake
|
|
982
1185
|
*/
|
|
983
|
-
const subscribeBalances = (handler) => {
|
|
1186
|
+
const subscribeBalances = (handler, onSnapshot) => {
|
|
984
1187
|
// User is identified from the authenticated session, so channel is just "balances"
|
|
985
1188
|
const channel = "balances";
|
|
986
1189
|
return subscribe(channel, (rawData) => {
|
|
987
1190
|
try {
|
|
988
|
-
|
|
989
|
-
|
|
1191
|
+
if (!isRecord(rawData)) {
|
|
1192
|
+
throw new Error("Invalid balance event: payload must be an object");
|
|
1193
|
+
}
|
|
990
1194
|
const event = {
|
|
991
1195
|
eventType: "balance_update",
|
|
992
|
-
userId:
|
|
993
|
-
data:
|
|
994
|
-
tokenAddress: balanceData.token_address,
|
|
995
|
-
tokenSymbol: balanceData.token_symbol,
|
|
996
|
-
available: balanceData.available,
|
|
997
|
-
availableRaw: balanceData.available_raw,
|
|
998
|
-
locked: balanceData.locked,
|
|
999
|
-
lockedRaw: balanceData.locked_raw,
|
|
1000
|
-
total: balanceData.total,
|
|
1001
|
-
totalRaw: balanceData.total_raw,
|
|
1002
|
-
reason: balanceData.reason,
|
|
1003
|
-
referenceId: balanceData.reference_id,
|
|
1004
|
-
updatedAt: balanceData.updated_at,
|
|
1005
|
-
},
|
|
1196
|
+
userId: readString(rawData, "user_id"),
|
|
1197
|
+
data: parseUserBalanceEventData(rawData.data),
|
|
1006
1198
|
};
|
|
1007
1199
|
handler(event);
|
|
1008
1200
|
}
|
|
1009
1201
|
catch (err) {
|
|
1010
1202
|
console.error("WebSocket: Error processing balance event", err);
|
|
1011
1203
|
}
|
|
1012
|
-
});
|
|
1204
|
+
}, onSnapshot && snapshotFrameHandler("balances", parseUserBalanceEventData, onSnapshot));
|
|
1013
1205
|
};
|
|
1014
|
-
const subscribeConditionalOrders = (handler, tradingPairId) => {
|
|
1206
|
+
const subscribeConditionalOrders = (handler, tradingPairId, onSnapshot) => {
|
|
1015
1207
|
if (!handler) {
|
|
1016
1208
|
throw new Error("conditionalOrders subscription requires a handler");
|
|
1017
1209
|
}
|
|
@@ -1026,9 +1218,9 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1026
1218
|
catch (err) {
|
|
1027
1219
|
console.error("WebSocket: Error processing conditional order event", err);
|
|
1028
1220
|
}
|
|
1029
|
-
});
|
|
1221
|
+
}, onSnapshot && snapshotFrameHandler("conditional orders", parseConditionalOrderEventData, onSnapshot));
|
|
1030
1222
|
};
|
|
1031
|
-
const subscribeTwapOrders = (handler, tradingPairId) => {
|
|
1223
|
+
const subscribeTwapOrders = (handler, tradingPairId, onSnapshot) => {
|
|
1032
1224
|
if (!handler) {
|
|
1033
1225
|
throw new Error("twapOrders subscription requires a handler");
|
|
1034
1226
|
}
|
|
@@ -1043,16 +1235,16 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1043
1235
|
catch (err) {
|
|
1044
1236
|
console.error("WebSocket: Error processing TWAP order event", err);
|
|
1045
1237
|
}
|
|
1046
|
-
});
|
|
1238
|
+
}, onSnapshot && snapshotFrameHandler("TWAP orders", parseTwapOrderEventData, onSnapshot));
|
|
1047
1239
|
};
|
|
1048
1240
|
/**
|
|
1049
1241
|
* Subscribe to authenticated user's position updates.
|
|
1050
1242
|
* Requires authentication - user is identified from the signed session-key
|
|
1051
1243
|
* handshake. Omit `tradingPairId` for all pairs, or pass one to filter.
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1244
|
+
* Detected broadcast lag closes the connection with 1013; reconcile
|
|
1245
|
+
* missed keys and terminal updates on reconnect.
|
|
1054
1246
|
*/
|
|
1055
|
-
const subscribePositions = (handler, tradingPairId) => {
|
|
1247
|
+
const subscribePositions = (handler, tradingPairId, onSnapshot) => {
|
|
1056
1248
|
if (!handler) {
|
|
1057
1249
|
throw new Error("positions subscription requires a handler");
|
|
1058
1250
|
}
|
|
@@ -1067,7 +1259,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1067
1259
|
catch (err) {
|
|
1068
1260
|
console.error("WebSocket: Error processing position event", err);
|
|
1069
1261
|
}
|
|
1070
|
-
});
|
|
1262
|
+
}, onSnapshot && snapshotFrameHandler("positions", parsePositionEventData, onSnapshot));
|
|
1071
1263
|
};
|
|
1072
1264
|
/**
|
|
1073
1265
|
* Subscribe to authenticated user's liquidation alerts.
|
|
@@ -1076,7 +1268,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1076
1268
|
* Lag policy is Disconnect: a subscriber that falls behind is closed with
|
|
1077
1269
|
* `1013`; resync by refetching a snapshot (no wire sequence number).
|
|
1078
1270
|
*/
|
|
1079
|
-
const subscribeLiquidations = (handler, tradingPairId) => {
|
|
1271
|
+
const subscribeLiquidations = (handler, tradingPairId, onSnapshot) => {
|
|
1080
1272
|
if (!handler) {
|
|
1081
1273
|
throw new Error("liquidations subscription requires a handler");
|
|
1082
1274
|
}
|
|
@@ -1091,17 +1283,17 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1091
1283
|
catch (err) {
|
|
1092
1284
|
console.error("WebSocket: Error processing liquidation event", err);
|
|
1093
1285
|
}
|
|
1094
|
-
});
|
|
1286
|
+
}, onSnapshot && snapshotFrameHandler("liquidations", parseLiquidationEventData, onSnapshot));
|
|
1095
1287
|
};
|
|
1096
1288
|
/**
|
|
1097
1289
|
* Subscribe to the authenticated user's account-level margin health updates
|
|
1098
1290
|
* (equity, collateral, free collateral, margin ratio, maintenance
|
|
1099
1291
|
* requirement). Requires authentication - the user is identified from the
|
|
1100
|
-
* signed session-key handshake.
|
|
1101
|
-
*
|
|
1292
|
+
* signed session-key handshake. Detected broadcast lag closes the connection
|
|
1293
|
+
* with 1013; reconnect and reconcile missed state. Each frame is a terminal-state snapshot
|
|
1102
1294
|
* per margin account, not an additive delta.
|
|
1103
1295
|
*/
|
|
1104
|
-
const subscribeAccount = (handler) => {
|
|
1296
|
+
const subscribeAccount = (handler, onSnapshot) => {
|
|
1105
1297
|
if (!handler) {
|
|
1106
1298
|
throw new Error("account subscription requires a handler");
|
|
1107
1299
|
}
|
|
@@ -1112,7 +1304,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1112
1304
|
catch (err) {
|
|
1113
1305
|
console.error("WebSocket: Error processing account event", err);
|
|
1114
1306
|
}
|
|
1115
|
-
});
|
|
1307
|
+
}, onSnapshot && snapshotFrameHandler("account", parseAccountEventData, onSnapshot));
|
|
1116
1308
|
};
|
|
1117
1309
|
/**
|
|
1118
1310
|
* Subscribe to market lifecycle + configuration changes (public): listings,
|
|
@@ -1124,10 +1316,10 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1124
1316
|
* of the current active-pair list arrives before the live diffs; a
|
|
1125
1317
|
* subscriber joining a channel this client already holds is replayed the
|
|
1126
1318
|
* current baseline from a local cache instead (the server snapshots only
|
|
1127
|
-
* genuinely new wire subscriptions).
|
|
1128
|
-
*
|
|
1319
|
+
* genuinely new wire subscriptions). Detected broadcast lag closes the connection
|
|
1320
|
+
* with 1013; a new wire subscription provides a fresh baseline.
|
|
1129
1321
|
*/
|
|
1130
|
-
const subscribeInstruments = (handler, tradingPairId) => {
|
|
1322
|
+
const subscribeInstruments = (handler, tradingPairId, onSnapshot) => {
|
|
1131
1323
|
if (!handler) {
|
|
1132
1324
|
throw new Error("instruments subscription requires a handler");
|
|
1133
1325
|
}
|
|
@@ -1166,37 +1358,82 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1166
1358
|
},
|
|
1167
1359
|
// The subscribe-time baseline arrives as one Snapshot frame whose data is
|
|
1168
1360
|
// a flat InstrumentEventData[] (a `listing` per active market, no
|
|
1169
|
-
// per-item envelope).
|
|
1170
|
-
//
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1361
|
+
// per-item envelope).
|
|
1362
|
+
//
|
|
1363
|
+
// Where it goes depends on whether the caller opted in. With
|
|
1364
|
+
// `onSnapshot`, the rows are handed over as one array and the event
|
|
1365
|
+
// handler sees none of them — that array is the snapshot BOUNDARY a
|
|
1366
|
+
// consumer needs to replace its market set wholesale after a reconnect,
|
|
1367
|
+
// which per-market events cannot express. Without it, the rows are fanned
|
|
1368
|
+
// out as one synthetic event per market, which is how this channel has
|
|
1369
|
+
// always delivered its baseline; changing that for callers who never
|
|
1370
|
+
// asked would be a silent behaviour break.
|
|
1371
|
+
//
|
|
1372
|
+
// Either way the local cache is replaced wholesale rather than upserted,
|
|
1373
|
+
// so markets absent from the snapshot (delisted while disconnected) do
|
|
1374
|
+
// not linger.
|
|
1375
|
+
snapshotFrameHandler("instrument", parseInstrumentEventData, (items) => {
|
|
1376
|
+
const events = items.map((item) => ({ tradingPairId: item.tradingPairId, data: item }));
|
|
1377
|
+
instrumentBaseline.set(channel, new Map(events.map((event) => [event.tradingPairId, event])));
|
|
1378
|
+
if (onSnapshot) {
|
|
1379
|
+
onSnapshot(items);
|
|
1380
|
+
return;
|
|
1184
1381
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1382
|
+
for (const event of events) {
|
|
1383
|
+
handler(event);
|
|
1187
1384
|
}
|
|
1188
|
-
});
|
|
1385
|
+
}));
|
|
1189
1386
|
// Replay only while the socket is open: during abnormal-close backoff the
|
|
1190
1387
|
// handlers and cache survive but the state is pre-outage — the reconnect's
|
|
1191
1388
|
// resubscribe clears it and a fresh authoritative snapshot follows, so a
|
|
1192
1389
|
// backoff-time subscriber must wait for that instead.
|
|
1193
1390
|
if (isLateSubscriber && ws?.readyState === WebSocket.OPEN) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1391
|
+
// Presence, not size. An entry exists only once a snapshot has been
|
|
1392
|
+
// received for the channel, so an EMPTY one is a genuine "no active
|
|
1393
|
+
// markets" baseline rather than "nothing known yet" — and under this
|
|
1394
|
+
// API empty is authoritative information a late subscriber needs in
|
|
1395
|
+
// order to clear stale markets.
|
|
1396
|
+
if (instrumentBaseline.has(channel)) {
|
|
1197
1397
|
queueMicrotask(() => {
|
|
1198
1398
|
if (unsubscribed)
|
|
1199
1399
|
return;
|
|
1400
|
+
// Read INSIDE the microtask, and re-check the socket. This
|
|
1401
|
+
// subscriber is already registered by now, so a live frame landing
|
|
1402
|
+
// first would update the cache and reach `handler` directly — a
|
|
1403
|
+
// snapshot array captured before the microtask would then arrive
|
|
1404
|
+
// afterwards carrying older state, and a consumer that replaces its
|
|
1405
|
+
// market map from it would go backwards. A `disconnect()` in the
|
|
1406
|
+
// same window drops the cache outright, and delivering a baseline
|
|
1407
|
+
// after that would be inventing one.
|
|
1408
|
+
const cached = instrumentBaseline.get(channel);
|
|
1409
|
+
if (!cached || ws?.readyState !== WebSocket.OPEN)
|
|
1410
|
+
return;
|
|
1411
|
+
const replay = [...cached.values()];
|
|
1412
|
+
// The replay IS this subscriber's baseline — it stands in for the
|
|
1413
|
+
// wire snapshot the suppressed Subscribe never triggered — so it
|
|
1414
|
+
// follows the same delivery contract: one array to `onSnapshot`
|
|
1415
|
+
// when opted in, per-market events otherwise.
|
|
1416
|
+
//
|
|
1417
|
+
// The two forms carry deliberately different sets. The ARRAY is
|
|
1418
|
+
// filtered to active markets, because that is what a real snapshot
|
|
1419
|
+
// is — the server enumerates active pairs, while this cache also
|
|
1420
|
+
// holds the delistings it has seen live — and a consumer replaces
|
|
1421
|
+
// its market set from the array, so a delisted row there would
|
|
1422
|
+
// resurrect a market that is gone. The per-market EVENTS are not
|
|
1423
|
+
// filtered: a delisting is a legitimate event, it is what tells a
|
|
1424
|
+
// legacy subscriber the market went inactive, and dropping it would
|
|
1425
|
+
// change a caller that never opted into any of this.
|
|
1426
|
+
if (onSnapshot) {
|
|
1427
|
+
try {
|
|
1428
|
+
onSnapshot(replay.filter((event) => event.data.isActive).map((event) => event.data));
|
|
1429
|
+
}
|
|
1430
|
+
catch (err) {
|
|
1431
|
+
console.error("WebSocket: Error processing instrument baseline replay", err);
|
|
1432
|
+
}
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
// Contained per event, not per replay: a handler that throws on one
|
|
1436
|
+
// market must not cost the subscriber every market after it.
|
|
1200
1437
|
for (const event of replay) {
|
|
1201
1438
|
try {
|
|
1202
1439
|
handler(event);
|
|
@@ -1223,7 +1460,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1223
1460
|
* frame carries the market's full current stats as a terminal-state snapshot.
|
|
1224
1461
|
* Use {@link subscribeMarketStatsAll} for the all-markets summary instead.
|
|
1225
1462
|
*/
|
|
1226
|
-
const subscribeMarketStats = (tradingPairId, handler) => {
|
|
1463
|
+
const subscribeMarketStats = (tradingPairId, handler, onSnapshot) => {
|
|
1227
1464
|
if (!tradingPairId) {
|
|
1228
1465
|
throw new Error("marketStats subscription requires a tradingPairId");
|
|
1229
1466
|
}
|
|
@@ -1237,13 +1474,13 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1237
1474
|
catch (err) {
|
|
1238
1475
|
console.error("WebSocket: Error processing market stats event", err);
|
|
1239
1476
|
}
|
|
1240
|
-
});
|
|
1477
|
+
}, onSnapshot && snapshotFrameHandler("market stats", parseMarketStatsData, onSnapshot));
|
|
1241
1478
|
};
|
|
1242
1479
|
/**
|
|
1243
1480
|
* Subscribe to the all-markets statistics summary (public): one summary row
|
|
1244
1481
|
* per market in a single frame per push.
|
|
1245
1482
|
*/
|
|
1246
|
-
const subscribeMarketStatsAll = (handler) => {
|
|
1483
|
+
const subscribeMarketStatsAll = (handler, onSnapshot) => {
|
|
1247
1484
|
if (!handler) {
|
|
1248
1485
|
throw new Error("marketStatsAll subscription requires a handler");
|
|
1249
1486
|
}
|
|
@@ -1261,7 +1498,12 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1261
1498
|
catch (err) {
|
|
1262
1499
|
console.error("WebSocket: Error processing market stats all event", err);
|
|
1263
1500
|
}
|
|
1264
|
-
}
|
|
1501
|
+
},
|
|
1502
|
+
// The per-market channel needs no such guard: snapshot frames are routed
|
|
1503
|
+
// by exact channel, so `market_stats:<pair>` never reaches the bare
|
|
1504
|
+
// `market_stats` subscription's snapshot handler the way its live frames
|
|
1505
|
+
// reach the event handler.
|
|
1506
|
+
onSnapshot && snapshotFrameHandler("all-markets stats", parseMarketStatsSummary, onSnapshot));
|
|
1265
1507
|
};
|
|
1266
1508
|
return {
|
|
1267
1509
|
connect,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xmonaco/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.54",
|
|
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.54",
|
|
23
|
+
"@0xmonaco/types": "1.0.54",
|
|
24
24
|
"@noble/curves": "^1.9.1",
|
|
25
25
|
"@noble/hashes": "^1.8.0",
|
|
26
26
|
"http-status-codes": "^2.3.0"
|