@0xmonaco/core 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.
|
@@ -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(), {
|
|
@@ -7,8 +7,6 @@ const CONNECTION_TIMEOUT = 10000;
|
|
|
7
7
|
const HEARTBEAT_INTERVAL = 15000;
|
|
8
8
|
const MAX_RECONNECT_DELAY = 30000;
|
|
9
9
|
const RECONNECT_BASE_DELAY = 1000;
|
|
10
|
-
const TWAP_ORDER_STATES = ["PENDING", "ACTIVE", "COMPLETED", "CANCELLED"];
|
|
11
|
-
const INSTRUMENT_CHANGE_KINDS = ["listing", "delisting", "halt", "unhalt", "config_change"];
|
|
12
10
|
function isRecord(value) {
|
|
13
11
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
12
|
}
|
|
@@ -28,6 +26,33 @@ function readOptionalString(source, field) {
|
|
|
28
26
|
}
|
|
29
27
|
return value;
|
|
30
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
|
+
}
|
|
31
56
|
function readBoolean(source, field) {
|
|
32
57
|
const value = source[field];
|
|
33
58
|
if (typeof value !== "boolean") {
|
|
@@ -86,148 +111,183 @@ function readOptionalEnumValue(source, field) {
|
|
|
86
111
|
const value = readOptionalString(source, field);
|
|
87
112
|
return value === undefined || value.length === 0 ? undefined : value;
|
|
88
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
|
+
}
|
|
89
162
|
function parseConditionalOrderEvent(rawData) {
|
|
90
163
|
if (!isRecord(rawData)) {
|
|
91
164
|
throw new Error("Invalid conditional order event: payload must be an object");
|
|
92
165
|
}
|
|
93
|
-
if (!isRecord(rawData.data)) {
|
|
94
|
-
throw new Error("Invalid conditional order event: data must be an object");
|
|
95
|
-
}
|
|
96
|
-
const data = keysToCamelCase(rawData.data);
|
|
97
166
|
return {
|
|
98
167
|
// event_type is the event-class discriminator, validated strictly so a
|
|
99
168
|
// mis-routed frame is rejected rather than parsed as a conditional order.
|
|
100
|
-
// The value fields
|
|
101
|
-
// checked against an allow-list — so an unexpected *value* never
|
|
102
|
-
// 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
|
|
103
172
|
// message-handler catch, so a strict enum here means a silently lost frame
|
|
104
173
|
// with nothing surfaced to the consumer.
|
|
105
174
|
eventType: readEnum(rawData, "event_type", ["conditional_order_update"]),
|
|
106
175
|
userId: readString(rawData, "user_id"),
|
|
107
|
-
data:
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
updatedAt: readString(data, "updatedAt"),
|
|
137
|
-
},
|
|
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"),
|
|
138
205
|
};
|
|
139
206
|
}
|
|
140
207
|
function parsePositionEvent(rawData) {
|
|
141
208
|
if (!isRecord(rawData)) {
|
|
142
209
|
throw new Error("Invalid position event: payload must be an object");
|
|
143
210
|
}
|
|
144
|
-
if (!isRecord(rawData.data)) {
|
|
145
|
-
throw new Error("Invalid position event: data must be an object");
|
|
146
|
-
}
|
|
147
|
-
const data = keysToCamelCase(rawData.data);
|
|
148
211
|
return {
|
|
149
212
|
// event_type is the event-class discriminator, validated strictly so a
|
|
150
213
|
// mis-routed frame is rejected rather than parsed as a position. The value
|
|
151
|
-
// fields
|
|
152
|
-
// 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.
|
|
153
216
|
eventType: readEnum(rawData, "event_type", ["position_update"]),
|
|
154
217
|
userId: readString(rawData, "user_id"),
|
|
155
|
-
data:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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"),
|
|
173
242
|
};
|
|
174
243
|
}
|
|
175
244
|
function parseLiquidationEvent(rawData) {
|
|
176
245
|
if (!isRecord(rawData)) {
|
|
177
246
|
throw new Error("Invalid liquidation event: payload must be an object");
|
|
178
247
|
}
|
|
179
|
-
if (!isRecord(rawData.data)) {
|
|
180
|
-
throw new Error("Invalid liquidation event: data must be an object");
|
|
181
|
-
}
|
|
182
|
-
const data = keysToCamelCase(rawData.data);
|
|
183
248
|
return {
|
|
184
249
|
// event_type is the event-class discriminator, validated strictly so a
|
|
185
250
|
// mis-routed frame is rejected rather than parsed as a liquidation. The
|
|
186
|
-
// free-form value fields
|
|
187
|
-
// 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.
|
|
188
253
|
eventType: readEnum(rawData, "event_type", ["liquidation_alert"]),
|
|
189
254
|
userId: readString(rawData, "user_id"),
|
|
190
|
-
data:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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"),
|
|
203
279
|
};
|
|
204
280
|
}
|
|
205
281
|
function parseAccountEvent(rawData) {
|
|
206
282
|
if (!isRecord(rawData)) {
|
|
207
283
|
throw new Error("Invalid account event: payload must be an object");
|
|
208
284
|
}
|
|
209
|
-
if (!isRecord(rawData.data)) {
|
|
210
|
-
throw new Error("Invalid account event: data must be an object");
|
|
211
|
-
}
|
|
212
|
-
const data = keysToCamelCase(rawData.data);
|
|
213
285
|
return {
|
|
214
286
|
// event_type is the event-class discriminator, validated strictly so a
|
|
215
287
|
// mis-routed frame is rejected rather than parsed as an account update.
|
|
216
288
|
eventType: readEnum(rawData, "event_type", ["account_update"]),
|
|
217
289
|
userId: readString(rawData, "user_id"),
|
|
218
|
-
data:
|
|
219
|
-
marginAccountId: readString(data, "marginAccountId"),
|
|
220
|
-
// Monotonic per-account publish counter (0XM-2498): a plain JSON number on
|
|
221
|
-
// the wire, used to order/dedupe frames across reconnect races. Must be a
|
|
222
|
-
// non-negative safe integer — anything else is a malformed frame.
|
|
223
|
-
sequence: readSequence(data, "sequence"),
|
|
224
|
-
equity: readString(data, "equity"),
|
|
225
|
-
collateral: readString(data, "collateral"),
|
|
226
|
-
freeCollateral: readString(data, "freeCollateral"),
|
|
227
|
-
marginRatio: readString(data, "marginRatio"),
|
|
228
|
-
maintenanceRequirement: readString(data, "maintenanceRequirement"),
|
|
229
|
-
updatedAt: readString(data, "updatedAt"),
|
|
230
|
-
},
|
|
290
|
+
data: parseAccountEventData(rawData.data),
|
|
231
291
|
};
|
|
232
292
|
}
|
|
233
293
|
function parseMarketStatsData(raw) {
|
|
@@ -310,7 +370,14 @@ function parseInstrumentEventData(raw) {
|
|
|
310
370
|
}
|
|
311
371
|
const data = keysToCamelCase(raw);
|
|
312
372
|
return {
|
|
313
|
-
|
|
373
|
+
// Read as a plain non-empty string, not matched against a value list: an
|
|
374
|
+
// unrecognized change kind must not throw, because the throw is swallowed
|
|
375
|
+
// by the message-handler catch and takes the whole frame with it — and
|
|
376
|
+
// every instrument frame carries the market's FULL current config, so a
|
|
377
|
+
// dropped frame can hide a market going inactive behind an unfamiliar
|
|
378
|
+
// label. The declared `InstrumentChangeKind` union documents the values
|
|
379
|
+
// the server sends today, not a runtime guarantee.
|
|
380
|
+
change: readString(data, "change"),
|
|
314
381
|
tradingPairId: readString(data, "tradingPairId"),
|
|
315
382
|
symbol: readString(data, "symbol"),
|
|
316
383
|
isActive: readBoolean(data, "isActive"),
|
|
@@ -341,12 +408,9 @@ function parseInstrumentEvent(rawData) {
|
|
|
341
408
|
};
|
|
342
409
|
}
|
|
343
410
|
/**
|
|
344
|
-
* Parse a `twap_orders` channel frame into a typed event.
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* fields never break older clients. The server serializes absent optional
|
|
348
|
-
* fields as explicit `null`; those are dropped so the parsed snapshot matches
|
|
349
|
-
* 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}.
|
|
350
414
|
*/
|
|
351
415
|
function parseTwapOrderEvent(rawData) {
|
|
352
416
|
if (!isRecord(rawData)) {
|
|
@@ -363,7 +427,27 @@ function parseTwapOrderEvent(rawData) {
|
|
|
363
427
|
if (typeof userId !== "string" || userId.length === 0) {
|
|
364
428
|
throw new Error("Invalid TWAP order event: user_id must be a non-empty string");
|
|
365
429
|
}
|
|
366
|
-
|
|
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);
|
|
367
451
|
for (const key of Object.keys(data)) {
|
|
368
452
|
if (data[key] === null) {
|
|
369
453
|
delete data[key];
|
|
@@ -373,14 +457,150 @@ function parseTwapOrderEvent(rawData) {
|
|
|
373
457
|
if (typeof twapOrderId !== "string" || twapOrderId.length === 0) {
|
|
374
458
|
throw new Error("Invalid TWAP order event: twapOrderId must be a non-empty string");
|
|
375
459
|
}
|
|
460
|
+
// `state` is required, so an absent/non-string/empty one is a malformed
|
|
461
|
+
// frame — but an unrecognized VALUE is passed through untouched. Matching it
|
|
462
|
+
// against a value list would throw, the message-handler catch would swallow
|
|
463
|
+
// the throw, and the whole update would be lost: a TWAP order whose state
|
|
464
|
+
// this SDK version predates would stop reporting its fills entirely
|
|
465
|
+
// (0XM-2627's failure mode). The declared `TwapOrderState` union documents
|
|
466
|
+
// the values the server sends today, not a runtime guarantee.
|
|
376
467
|
const state = data.state;
|
|
377
|
-
if (typeof state !== "string" ||
|
|
378
|
-
throw new Error(
|
|
468
|
+
if (typeof state !== "string" || state.length === 0) {
|
|
469
|
+
throw new Error("Invalid TWAP order event: state must be a non-empty string");
|
|
379
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);
|
|
380
486
|
return {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
}
|
|
384
604
|
};
|
|
385
605
|
}
|
|
386
606
|
/**
|
|
@@ -553,6 +773,21 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
553
773
|
return frameChannel === subscriptionChannel || frameChannel.startsWith(`${subscriptionChannel}:`);
|
|
554
774
|
};
|
|
555
775
|
const routeMessage = (msg) => {
|
|
776
|
+
if (msg.type === "Error") {
|
|
777
|
+
// Connection-scoped, not channel-scoped: an Error frame matches no
|
|
778
|
+
// subscription, so without this it reached no consumer at all and a
|
|
779
|
+
// failed subscribe / invalidated session / unreadable subscribe-time
|
|
780
|
+
// snapshot was silent. `code` is passed through whatever its value —
|
|
781
|
+
// a code added server-side after this SDK shipped must still reach the
|
|
782
|
+
// app rather than be dropped for being unrecognized (0XM-2627's
|
|
783
|
+
// leniency contract) — but a frame missing either field is malformed,
|
|
784
|
+
// and synthesizing the missing half would send the app down a branch on
|
|
785
|
+
// data the server never sent.
|
|
786
|
+
if (typeof msg.code === "string" && typeof msg.message === "string") {
|
|
787
|
+
options.onError?.({ code: msg.code, message: msg.message });
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
556
791
|
if (!msg.channel)
|
|
557
792
|
return;
|
|
558
793
|
if (msg.type === "Snapshot") {
|
|
@@ -728,7 +963,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
728
963
|
};
|
|
729
964
|
};
|
|
730
965
|
// --- Channel-specific subscription methods ---
|
|
731
|
-
const subscribeOrders = (tradingPairId, tradingMode, handler) => {
|
|
966
|
+
const subscribeOrders = (tradingPairId, tradingMode, handler, onSnapshot) => {
|
|
732
967
|
const channel = `orders:${tradingPairId}:${tradingMode}`;
|
|
733
968
|
return subscribe(channel, (rawData) => {
|
|
734
969
|
try {
|
|
@@ -744,7 +979,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
744
979
|
catch (err) {
|
|
745
980
|
console.error("WebSocket: Error processing order event", err);
|
|
746
981
|
}
|
|
747
|
-
});
|
|
982
|
+
}, onSnapshot && snapshotFrameHandler("orders", parseOrderSnapshotItem, onSnapshot));
|
|
748
983
|
};
|
|
749
984
|
const subscribeOrderbook = (tradingPairId, tradingMode, magnitude, quotationMode, handler) => {
|
|
750
985
|
if (!tradingPairId || !tradingMode || magnitude === undefined || !quotationMode) {
|
|
@@ -816,7 +1051,15 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
816
1051
|
// Concrete price series of this candle (`trade`/`mark`). Present on
|
|
817
1052
|
// every current frame; a wildcard (`*`) subscriber reads it to tell the
|
|
818
1053
|
// two streams apart. Omitted when a legacy frame carries no `price_type`.
|
|
819
|
-
|
|
1054
|
+
//
|
|
1055
|
+
// Passed through rather than matched against the two known values: a
|
|
1056
|
+
// wildcard subscription accepts any suffix, so a series added
|
|
1057
|
+
// server-side after this version reaches this parser, and reporting it
|
|
1058
|
+
// as `undefined` would leave a wildcard subscriber unable to do the one
|
|
1059
|
+
// thing the field exists for. Only a missing or non-string value is
|
|
1060
|
+
// treated as absent.
|
|
1061
|
+
const rawPriceType = data.price_type;
|
|
1062
|
+
const priceType = typeof rawPriceType === "string" && rawPriceType !== "" ? rawPriceType : undefined;
|
|
820
1063
|
const event = {
|
|
821
1064
|
// `trading_pair_id` is the pair UUID; `symbol` is the display string.
|
|
822
1065
|
// `tradingPairId` must carry the UUID (the symbol lives in `candlestick.s`).
|
|
@@ -887,12 +1130,17 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
887
1130
|
token: (movementData.token ?? movementData.token_address),
|
|
888
1131
|
tokenAddress: (movementData.token ?? movementData.token_address),
|
|
889
1132
|
assetId: movementData.asset_id,
|
|
1133
|
+
symbol: movementData.symbol,
|
|
890
1134
|
amount: String(movementData.amount),
|
|
891
1135
|
amountRaw: movementData.amount_raw,
|
|
892
1136
|
balanceBefore: movementData.balance_before != null ? String(movementData.balance_before) : undefined,
|
|
1137
|
+
balanceBeforeRaw: movementData.balance_before_raw,
|
|
893
1138
|
balanceAfter: movementData.balance_after != null ? String(movementData.balance_after) : undefined,
|
|
1139
|
+
balanceAfterRaw: movementData.balance_after_raw,
|
|
894
1140
|
lockedBefore: movementData.locked_before != null ? String(movementData.locked_before) : undefined,
|
|
1141
|
+
lockedBeforeRaw: movementData.locked_before_raw,
|
|
895
1142
|
lockedAfter: movementData.locked_after != null ? String(movementData.locked_after) : undefined,
|
|
1143
|
+
lockedAfterRaw: movementData.locked_after_raw,
|
|
896
1144
|
referenceId: movementData.reference_id,
|
|
897
1145
|
referenceType: movementData.reference_type,
|
|
898
1146
|
description: movementData.description,
|
|
@@ -913,7 +1161,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
913
1161
|
* Subscribe to authenticated user's order events (all trading pairs)
|
|
914
1162
|
* Requires authentication - user is identified from the signed session-key handshake
|
|
915
1163
|
*/
|
|
916
|
-
const subscribeUserOrders = (handler) => {
|
|
1164
|
+
const subscribeUserOrders = (handler, onSnapshot) => {
|
|
917
1165
|
// User is identified from the authenticated session, so channel is just "orders"
|
|
918
1166
|
const channel = "orders";
|
|
919
1167
|
return subscribe(channel, (rawData) => {
|
|
@@ -930,44 +1178,33 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
930
1178
|
catch (err) {
|
|
931
1179
|
console.error("WebSocket: Error processing user order event", err);
|
|
932
1180
|
}
|
|
933
|
-
});
|
|
1181
|
+
}, onSnapshot && snapshotFrameHandler("orders", parseOrderSnapshotItem, onSnapshot));
|
|
934
1182
|
};
|
|
935
1183
|
/**
|
|
936
1184
|
* Subscribe to authenticated user's balance events
|
|
937
1185
|
* Requires authentication - user is identified from the signed session-key handshake
|
|
938
1186
|
*/
|
|
939
|
-
const subscribeBalances = (handler) => {
|
|
1187
|
+
const subscribeBalances = (handler, onSnapshot) => {
|
|
940
1188
|
// User is identified from the authenticated session, so channel is just "balances"
|
|
941
1189
|
const channel = "balances";
|
|
942
1190
|
return subscribe(channel, (rawData) => {
|
|
943
1191
|
try {
|
|
944
|
-
|
|
945
|
-
|
|
1192
|
+
if (!isRecord(rawData)) {
|
|
1193
|
+
throw new Error("Invalid balance event: payload must be an object");
|
|
1194
|
+
}
|
|
946
1195
|
const event = {
|
|
947
1196
|
eventType: "balance_update",
|
|
948
|
-
userId:
|
|
949
|
-
data:
|
|
950
|
-
tokenAddress: balanceData.token_address,
|
|
951
|
-
tokenSymbol: balanceData.token_symbol,
|
|
952
|
-
available: balanceData.available,
|
|
953
|
-
availableRaw: balanceData.available_raw,
|
|
954
|
-
locked: balanceData.locked,
|
|
955
|
-
lockedRaw: balanceData.locked_raw,
|
|
956
|
-
total: balanceData.total,
|
|
957
|
-
totalRaw: balanceData.total_raw,
|
|
958
|
-
reason: balanceData.reason,
|
|
959
|
-
referenceId: balanceData.reference_id,
|
|
960
|
-
updatedAt: balanceData.updated_at,
|
|
961
|
-
},
|
|
1197
|
+
userId: readString(rawData, "user_id"),
|
|
1198
|
+
data: parseUserBalanceEventData(rawData.data),
|
|
962
1199
|
};
|
|
963
1200
|
handler(event);
|
|
964
1201
|
}
|
|
965
1202
|
catch (err) {
|
|
966
1203
|
console.error("WebSocket: Error processing balance event", err);
|
|
967
1204
|
}
|
|
968
|
-
});
|
|
1205
|
+
}, onSnapshot && snapshotFrameHandler("balances", parseUserBalanceEventData, onSnapshot));
|
|
969
1206
|
};
|
|
970
|
-
const subscribeConditionalOrders = (handler, tradingPairId) => {
|
|
1207
|
+
const subscribeConditionalOrders = (handler, tradingPairId, onSnapshot) => {
|
|
971
1208
|
if (!handler) {
|
|
972
1209
|
throw new Error("conditionalOrders subscription requires a handler");
|
|
973
1210
|
}
|
|
@@ -982,9 +1219,9 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
982
1219
|
catch (err) {
|
|
983
1220
|
console.error("WebSocket: Error processing conditional order event", err);
|
|
984
1221
|
}
|
|
985
|
-
});
|
|
1222
|
+
}, onSnapshot && snapshotFrameHandler("conditional orders", parseConditionalOrderEventData, onSnapshot));
|
|
986
1223
|
};
|
|
987
|
-
const subscribeTwapOrders = (handler, tradingPairId) => {
|
|
1224
|
+
const subscribeTwapOrders = (handler, tradingPairId, onSnapshot) => {
|
|
988
1225
|
if (!handler) {
|
|
989
1226
|
throw new Error("twapOrders subscription requires a handler");
|
|
990
1227
|
}
|
|
@@ -999,7 +1236,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
999
1236
|
catch (err) {
|
|
1000
1237
|
console.error("WebSocket: Error processing TWAP order event", err);
|
|
1001
1238
|
}
|
|
1002
|
-
});
|
|
1239
|
+
}, onSnapshot && snapshotFrameHandler("TWAP orders", parseTwapOrderEventData, onSnapshot));
|
|
1003
1240
|
};
|
|
1004
1241
|
/**
|
|
1005
1242
|
* Subscribe to authenticated user's position updates.
|
|
@@ -1008,7 +1245,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1008
1245
|
* Lag policy is Conflate: a lagging subscriber is fast-forwarded, no gap
|
|
1009
1246
|
* signal.
|
|
1010
1247
|
*/
|
|
1011
|
-
const subscribePositions = (handler, tradingPairId) => {
|
|
1248
|
+
const subscribePositions = (handler, tradingPairId, onSnapshot) => {
|
|
1012
1249
|
if (!handler) {
|
|
1013
1250
|
throw new Error("positions subscription requires a handler");
|
|
1014
1251
|
}
|
|
@@ -1023,7 +1260,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1023
1260
|
catch (err) {
|
|
1024
1261
|
console.error("WebSocket: Error processing position event", err);
|
|
1025
1262
|
}
|
|
1026
|
-
});
|
|
1263
|
+
}, onSnapshot && snapshotFrameHandler("positions", parsePositionEventData, onSnapshot));
|
|
1027
1264
|
};
|
|
1028
1265
|
/**
|
|
1029
1266
|
* Subscribe to authenticated user's liquidation alerts.
|
|
@@ -1032,7 +1269,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1032
1269
|
* Lag policy is Disconnect: a subscriber that falls behind is closed with
|
|
1033
1270
|
* `1013`; resync by refetching a snapshot (no wire sequence number).
|
|
1034
1271
|
*/
|
|
1035
|
-
const subscribeLiquidations = (handler, tradingPairId) => {
|
|
1272
|
+
const subscribeLiquidations = (handler, tradingPairId, onSnapshot) => {
|
|
1036
1273
|
if (!handler) {
|
|
1037
1274
|
throw new Error("liquidations subscription requires a handler");
|
|
1038
1275
|
}
|
|
@@ -1047,7 +1284,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1047
1284
|
catch (err) {
|
|
1048
1285
|
console.error("WebSocket: Error processing liquidation event", err);
|
|
1049
1286
|
}
|
|
1050
|
-
});
|
|
1287
|
+
}, onSnapshot && snapshotFrameHandler("liquidations", parseLiquidationEventData, onSnapshot));
|
|
1051
1288
|
};
|
|
1052
1289
|
/**
|
|
1053
1290
|
* Subscribe to the authenticated user's account-level margin health updates
|
|
@@ -1057,7 +1294,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1057
1294
|
* is fast-forwarded, no gap signal. Each frame is a terminal-state snapshot
|
|
1058
1295
|
* per margin account, not an additive delta.
|
|
1059
1296
|
*/
|
|
1060
|
-
const subscribeAccount = (handler) => {
|
|
1297
|
+
const subscribeAccount = (handler, onSnapshot) => {
|
|
1061
1298
|
if (!handler) {
|
|
1062
1299
|
throw new Error("account subscription requires a handler");
|
|
1063
1300
|
}
|
|
@@ -1068,7 +1305,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1068
1305
|
catch (err) {
|
|
1069
1306
|
console.error("WebSocket: Error processing account event", err);
|
|
1070
1307
|
}
|
|
1071
|
-
});
|
|
1308
|
+
}, onSnapshot && snapshotFrameHandler("account", parseAccountEventData, onSnapshot));
|
|
1072
1309
|
};
|
|
1073
1310
|
/**
|
|
1074
1311
|
* Subscribe to market lifecycle + configuration changes (public): listings,
|
|
@@ -1083,7 +1320,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1083
1320
|
* genuinely new wire subscriptions). Lag policy is Conflate: a lagging
|
|
1084
1321
|
* subscriber is fast-forwarded, no gap signal.
|
|
1085
1322
|
*/
|
|
1086
|
-
const subscribeInstruments = (handler, tradingPairId) => {
|
|
1323
|
+
const subscribeInstruments = (handler, tradingPairId, onSnapshot) => {
|
|
1087
1324
|
if (!handler) {
|
|
1088
1325
|
throw new Error("instruments subscription requires a handler");
|
|
1089
1326
|
}
|
|
@@ -1122,37 +1359,82 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1122
1359
|
},
|
|
1123
1360
|
// The subscribe-time baseline arrives as one Snapshot frame whose data is
|
|
1124
1361
|
// a flat InstrumentEventData[] (a `listing` per active market, no
|
|
1125
|
-
// per-item envelope).
|
|
1126
|
-
//
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1362
|
+
// per-item envelope).
|
|
1363
|
+
//
|
|
1364
|
+
// Where it goes depends on whether the caller opted in. With
|
|
1365
|
+
// `onSnapshot`, the rows are handed over as one array and the event
|
|
1366
|
+
// handler sees none of them — that array is the snapshot BOUNDARY a
|
|
1367
|
+
// consumer needs to replace its market set wholesale after a reconnect,
|
|
1368
|
+
// which per-market events cannot express. Without it, the rows are fanned
|
|
1369
|
+
// out as one synthetic event per market, which is how this channel has
|
|
1370
|
+
// always delivered its baseline; changing that for callers who never
|
|
1371
|
+
// asked would be a silent behaviour break.
|
|
1372
|
+
//
|
|
1373
|
+
// Either way the local cache is replaced wholesale rather than upserted,
|
|
1374
|
+
// so markets absent from the snapshot (delisted while disconnected) do
|
|
1375
|
+
// not linger.
|
|
1376
|
+
snapshotFrameHandler("instrument", parseInstrumentEventData, (items) => {
|
|
1377
|
+
const events = items.map((item) => ({ tradingPairId: item.tradingPairId, data: item }));
|
|
1378
|
+
instrumentBaseline.set(channel, new Map(events.map((event) => [event.tradingPairId, event])));
|
|
1379
|
+
if (onSnapshot) {
|
|
1380
|
+
onSnapshot(items);
|
|
1381
|
+
return;
|
|
1140
1382
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1383
|
+
for (const event of events) {
|
|
1384
|
+
handler(event);
|
|
1143
1385
|
}
|
|
1144
|
-
});
|
|
1386
|
+
}));
|
|
1145
1387
|
// Replay only while the socket is open: during abnormal-close backoff the
|
|
1146
1388
|
// handlers and cache survive but the state is pre-outage — the reconnect's
|
|
1147
1389
|
// resubscribe clears it and a fresh authoritative snapshot follows, so a
|
|
1148
1390
|
// backoff-time subscriber must wait for that instead.
|
|
1149
1391
|
if (isLateSubscriber && ws?.readyState === WebSocket.OPEN) {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1392
|
+
// Presence, not size. An entry exists only once a snapshot has been
|
|
1393
|
+
// received for the channel, so an EMPTY one is a genuine "no active
|
|
1394
|
+
// markets" baseline rather than "nothing known yet" — and under this
|
|
1395
|
+
// API empty is authoritative information a late subscriber needs in
|
|
1396
|
+
// order to clear stale markets.
|
|
1397
|
+
if (instrumentBaseline.has(channel)) {
|
|
1153
1398
|
queueMicrotask(() => {
|
|
1154
1399
|
if (unsubscribed)
|
|
1155
1400
|
return;
|
|
1401
|
+
// Read INSIDE the microtask, and re-check the socket. This
|
|
1402
|
+
// subscriber is already registered by now, so a live frame landing
|
|
1403
|
+
// first would update the cache and reach `handler` directly — a
|
|
1404
|
+
// snapshot array captured before the microtask would then arrive
|
|
1405
|
+
// afterwards carrying older state, and a consumer that replaces its
|
|
1406
|
+
// market map from it would go backwards. A `disconnect()` in the
|
|
1407
|
+
// same window drops the cache outright, and delivering a baseline
|
|
1408
|
+
// after that would be inventing one.
|
|
1409
|
+
const cached = instrumentBaseline.get(channel);
|
|
1410
|
+
if (!cached || ws?.readyState !== WebSocket.OPEN)
|
|
1411
|
+
return;
|
|
1412
|
+
const replay = [...cached.values()];
|
|
1413
|
+
// The replay IS this subscriber's baseline — it stands in for the
|
|
1414
|
+
// wire snapshot the suppressed Subscribe never triggered — so it
|
|
1415
|
+
// follows the same delivery contract: one array to `onSnapshot`
|
|
1416
|
+
// when opted in, per-market events otherwise.
|
|
1417
|
+
//
|
|
1418
|
+
// The two forms carry deliberately different sets. The ARRAY is
|
|
1419
|
+
// filtered to active markets, because that is what a real snapshot
|
|
1420
|
+
// is — the server enumerates active pairs, while this cache also
|
|
1421
|
+
// holds the delistings it has seen live — and a consumer replaces
|
|
1422
|
+
// its market set from the array, so a delisted row there would
|
|
1423
|
+
// resurrect a market that is gone. The per-market EVENTS are not
|
|
1424
|
+
// filtered: a delisting is a legitimate event, it is what tells a
|
|
1425
|
+
// legacy subscriber the market went inactive, and dropping it would
|
|
1426
|
+
// change a caller that never opted into any of this.
|
|
1427
|
+
if (onSnapshot) {
|
|
1428
|
+
try {
|
|
1429
|
+
onSnapshot(replay.filter((event) => event.data.isActive).map((event) => event.data));
|
|
1430
|
+
}
|
|
1431
|
+
catch (err) {
|
|
1432
|
+
console.error("WebSocket: Error processing instrument baseline replay", err);
|
|
1433
|
+
}
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
// Contained per event, not per replay: a handler that throws on one
|
|
1437
|
+
// market must not cost the subscriber every market after it.
|
|
1156
1438
|
for (const event of replay) {
|
|
1157
1439
|
try {
|
|
1158
1440
|
handler(event);
|
|
@@ -1179,7 +1461,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1179
1461
|
* frame carries the market's full current stats as a terminal-state snapshot.
|
|
1180
1462
|
* Use {@link subscribeMarketStatsAll} for the all-markets summary instead.
|
|
1181
1463
|
*/
|
|
1182
|
-
const subscribeMarketStats = (tradingPairId, handler) => {
|
|
1464
|
+
const subscribeMarketStats = (tradingPairId, handler, onSnapshot) => {
|
|
1183
1465
|
if (!tradingPairId) {
|
|
1184
1466
|
throw new Error("marketStats subscription requires a tradingPairId");
|
|
1185
1467
|
}
|
|
@@ -1193,13 +1475,13 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1193
1475
|
catch (err) {
|
|
1194
1476
|
console.error("WebSocket: Error processing market stats event", err);
|
|
1195
1477
|
}
|
|
1196
|
-
});
|
|
1478
|
+
}, onSnapshot && snapshotFrameHandler("market stats", parseMarketStatsData, onSnapshot));
|
|
1197
1479
|
};
|
|
1198
1480
|
/**
|
|
1199
1481
|
* Subscribe to the all-markets statistics summary (public): one summary row
|
|
1200
1482
|
* per market in a single frame per push.
|
|
1201
1483
|
*/
|
|
1202
|
-
const subscribeMarketStatsAll = (handler) => {
|
|
1484
|
+
const subscribeMarketStatsAll = (handler, onSnapshot) => {
|
|
1203
1485
|
if (!handler) {
|
|
1204
1486
|
throw new Error("marketStatsAll subscription requires a handler");
|
|
1205
1487
|
}
|
|
@@ -1217,7 +1499,12 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1217
1499
|
catch (err) {
|
|
1218
1500
|
console.error("WebSocket: Error processing market stats all event", err);
|
|
1219
1501
|
}
|
|
1220
|
-
}
|
|
1502
|
+
},
|
|
1503
|
+
// The per-market channel needs no such guard: snapshot frames are routed
|
|
1504
|
+
// by exact channel, so `market_stats:<pair>` never reaches the bare
|
|
1505
|
+
// `market_stats` subscription's snapshot handler the way its live frames
|
|
1506
|
+
// reach the event handler.
|
|
1507
|
+
onSnapshot && snapshotFrameHandler("all-markets stats", parseMarketStatsSummary, onSnapshot));
|
|
1221
1508
|
};
|
|
1222
1509
|
return {
|
|
1223
1510
|
connect,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xmonaco/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.53",
|
|
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.53",
|
|
23
|
+
"@0xmonaco/types": "1.0.53",
|
|
24
24
|
"@noble/curves": "^1.9.1",
|
|
25
25
|
"@noble/hashes": "^1.8.0",
|
|
26
26
|
"http-status-codes": "^2.3.0"
|