@0xmonaco/core 0.7.7 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/applications/api.d.ts +43 -0
- package/dist/api/applications/api.js +54 -0
- package/dist/api/applications/index.d.ts +4 -0
- package/dist/api/applications/index.js +4 -0
- package/dist/api/auth/api.d.ts +206 -0
- package/dist/api/auth/api.js +305 -0
- package/dist/api/auth/index.d.ts +4 -0
- package/dist/api/auth/index.js +4 -0
- package/dist/api/base.d.ts +123 -0
- package/dist/api/base.js +286 -0
- package/dist/api/fees/api.d.ts +72 -0
- package/dist/api/fees/api.js +90 -0
- package/dist/api/fees/index.d.ts +6 -0
- package/dist/api/fees/index.js +6 -0
- package/dist/api/index.d.ts +18 -0
- package/dist/api/index.js +18 -0
- package/dist/api/margin-accounts/api.d.ts +12 -0
- package/dist/api/margin-accounts/api.js +69 -0
- package/dist/api/margin-accounts/index.d.ts +1 -0
- package/dist/api/margin-accounts/index.js +1 -0
- package/dist/api/market/api.d.ts +20 -0
- package/dist/api/market/api.js +97 -0
- package/dist/api/market/index.d.ts +1 -0
- package/dist/api/market/index.js +1 -0
- package/dist/api/orderbook/api.d.ts +15 -0
- package/dist/api/orderbook/api.js +37 -0
- package/dist/api/orderbook/index.d.ts +1 -0
- package/dist/api/orderbook/index.js +1 -0
- package/dist/api/perp/index.d.ts +1 -0
- package/dist/api/perp/index.js +1 -0
- package/dist/api/perp/routes.d.ts +133 -0
- package/dist/api/perp/routes.js +85 -0
- package/dist/api/positions/api.d.ts +12 -0
- package/dist/api/positions/api.js +86 -0
- package/dist/api/positions/index.d.ts +1 -0
- package/dist/api/positions/index.js +1 -0
- package/dist/api/profile/api.d.ts +191 -0
- package/dist/api/profile/api.js +259 -0
- package/dist/api/profile/index.d.ts +6 -0
- package/dist/api/profile/index.js +6 -0
- package/dist/api/trades/api.d.ts +44 -0
- package/dist/api/trades/api.js +42 -0
- package/dist/api/trades/index.d.ts +1 -0
- package/dist/api/trades/index.js +1 -0
- package/dist/api/trading/api.d.ts +297 -0
- package/dist/api/trading/api.js +481 -0
- package/dist/api/trading/index.d.ts +4 -0
- package/dist/api/trading/index.js +4 -0
- package/dist/api/vault/api.d.ts +261 -0
- package/dist/api/vault/api.js +506 -0
- package/dist/api/vault/index.d.ts +4 -0
- package/dist/api/vault/index.js +4 -0
- package/dist/api/websocket/index.d.ts +3 -0
- package/dist/api/websocket/index.js +3 -0
- package/dist/api/websocket/types.d.ts +41 -0
- package/dist/api/websocket/types.js +0 -0
- package/dist/api/websocket/utils.d.ts +8 -0
- package/dist/api/websocket/utils.js +22 -0
- package/dist/api/websocket/websocket.d.ts +5 -0
- package/dist/api/websocket/websocket.js +556 -0
- package/dist/errors/errors.d.ts +381 -0
- package/dist/errors/errors.js +815 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/networks/index.d.ts +1 -0
- package/dist/networks/index.js +1 -0
- package/dist/networks/networks.d.ts +21 -0
- package/dist/networks/networks.js +46 -0
- package/dist/sdk.d.ts +134 -0
- package/dist/sdk.js +294 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/magnitude.d.ts +26 -0
- package/dist/utils/magnitude.js +31 -0
- package/package.json +3 -3
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { ALL_MAGNITUDES } from "../../utils";
|
|
2
|
+
import { keysToCamelCase } from "./utils";
|
|
3
|
+
// Connection constants
|
|
4
|
+
const CONNECTION_TIMEOUT = 10000;
|
|
5
|
+
const HEARTBEAT_INTERVAL = 15000;
|
|
6
|
+
const MAX_RECONNECT_DELAY = 30000;
|
|
7
|
+
const CONDITIONAL_ORDER_REASONS = ["created", "cancelled", "triggered", "failed", "oco_cancelled"];
|
|
8
|
+
const CONDITIONAL_ORDER_CONDITION_TYPES = ["STOP_LOSS", "TAKE_PROFIT"];
|
|
9
|
+
const CONDITIONAL_ORDER_TRIGGER_SOURCES = ["MARK_PRICE"];
|
|
10
|
+
const CONDITIONAL_ORDER_STATES = ["ACTIVE", "TRIGGERING", "TRIGGERED", "CANCELLED", "EXPIRED", "FAILED"];
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function readString(source, field) {
|
|
15
|
+
const value = source[field];
|
|
16
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
17
|
+
throw new Error(`Invalid conditional order event: ${field} must be a non-empty string`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function readOptionalString(source, field) {
|
|
22
|
+
const value = source[field];
|
|
23
|
+
if (value === undefined || value === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
throw new Error(`Invalid conditional order event: ${field} must be a string`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function readBoolean(source, field) {
|
|
31
|
+
const value = source[field];
|
|
32
|
+
if (typeof value !== "boolean") {
|
|
33
|
+
throw new Error(`Invalid conditional order event: ${field} must be a boolean`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function readOptionalNumber(source, field) {
|
|
38
|
+
const value = source[field];
|
|
39
|
+
if (value === undefined || value === null)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
42
|
+
throw new Error(`Invalid conditional order event: ${field} must be a finite number`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function readEnum(source, field, allowed) {
|
|
47
|
+
const value = readString(source, field);
|
|
48
|
+
if (!allowed.includes(value)) {
|
|
49
|
+
throw new Error(`Invalid conditional order event: ${field} must be one of ${allowed.join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function readOptionalEnum(source, field, allowed) {
|
|
54
|
+
const value = source[field];
|
|
55
|
+
if (value === undefined || value === null)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
58
|
+
throw new Error(`Invalid conditional order event: ${field} must be one of ${allowed.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function parseConditionalOrderEvent(rawData) {
|
|
63
|
+
if (!isRecord(rawData)) {
|
|
64
|
+
throw new Error("Invalid conditional order event: payload must be an object");
|
|
65
|
+
}
|
|
66
|
+
if (!isRecord(rawData.data)) {
|
|
67
|
+
throw new Error("Invalid conditional order event: data must be an object");
|
|
68
|
+
}
|
|
69
|
+
const data = keysToCamelCase(rawData.data);
|
|
70
|
+
return {
|
|
71
|
+
eventType: readEnum(rawData, "event_type", ["conditional_order_update"]),
|
|
72
|
+
userId: readString(rawData, "user_id"),
|
|
73
|
+
data: {
|
|
74
|
+
conditionalOrderId: readString(data, "conditionalOrderId"),
|
|
75
|
+
tradingPairId: readString(data, "tradingPairId"),
|
|
76
|
+
marginAccountId: readString(data, "marginAccountId"),
|
|
77
|
+
positionId: readOptionalString(data, "positionId"),
|
|
78
|
+
linkedGroupId: readOptionalString(data, "linkedGroupId"),
|
|
79
|
+
conditionType: readEnum(data, "conditionType", CONDITIONAL_ORDER_CONDITION_TYPES),
|
|
80
|
+
triggerSource: readEnum(data, "triggerSource", CONDITIONAL_ORDER_TRIGGER_SOURCES),
|
|
81
|
+
triggerPrice: readString(data, "triggerPrice"),
|
|
82
|
+
side: readEnum(data, "side", ["BUY", "SELL"]),
|
|
83
|
+
positionSide: readEnum(data, "positionSide", ["LONG", "SHORT", "NONE"]),
|
|
84
|
+
orderType: readEnum(data, "orderType", ["LIMIT", "MARKET"]),
|
|
85
|
+
limitPrice: readOptionalString(data, "limitPrice"),
|
|
86
|
+
quantity: readOptionalString(data, "quantity"),
|
|
87
|
+
slippageToleranceBps: readOptionalNumber(data, "slippageToleranceBps"),
|
|
88
|
+
reduceOnly: readBoolean(data, "reduceOnly"),
|
|
89
|
+
timeInForce: readOptionalEnum(data, "timeInForce", ["GTC", "IOC"]),
|
|
90
|
+
state: readEnum(data, "state", CONDITIONAL_ORDER_STATES),
|
|
91
|
+
triggeredOrderId: readOptionalString(data, "triggeredOrderId"),
|
|
92
|
+
triggeredAt: readOptionalString(data, "triggeredAt"),
|
|
93
|
+
cancelledAt: readOptionalString(data, "cancelledAt"),
|
|
94
|
+
expiresAt: readOptionalString(data, "expiresAt"),
|
|
95
|
+
failureReason: readOptionalString(data, "failureReason"),
|
|
96
|
+
reason: readEnum(data, "reason", CONDITIONAL_ORDER_REASONS),
|
|
97
|
+
updatedAt: readString(data, "updatedAt"),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create a Monaco WebSocket client
|
|
103
|
+
*/
|
|
104
|
+
export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
105
|
+
let ws = null;
|
|
106
|
+
let token = options.token;
|
|
107
|
+
let reconnectAttempts = 0;
|
|
108
|
+
let reconnectTimer = null;
|
|
109
|
+
let heartbeatTimer = null;
|
|
110
|
+
let autoReconnect = options.autoReconnect ?? true;
|
|
111
|
+
const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
112
|
+
// Handler storage: channel -> set of handlers (supports multiple subscribers per channel)
|
|
113
|
+
const handlers = new Map();
|
|
114
|
+
const getStatus = () => {
|
|
115
|
+
if (!ws)
|
|
116
|
+
return "disconnected";
|
|
117
|
+
switch (ws.readyState) {
|
|
118
|
+
case WebSocket.CONNECTING:
|
|
119
|
+
return "connecting";
|
|
120
|
+
case WebSocket.OPEN:
|
|
121
|
+
return "connected";
|
|
122
|
+
default:
|
|
123
|
+
return "disconnected";
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const getUrl = () => {
|
|
127
|
+
const url = new URL(baseUrl);
|
|
128
|
+
if (token)
|
|
129
|
+
url.searchParams.set("token", token);
|
|
130
|
+
return url.toString();
|
|
131
|
+
};
|
|
132
|
+
const stopHeartbeat = () => {
|
|
133
|
+
if (heartbeatTimer) {
|
|
134
|
+
clearInterval(heartbeatTimer);
|
|
135
|
+
heartbeatTimer = null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const startHeartbeat = () => {
|
|
139
|
+
stopHeartbeat();
|
|
140
|
+
heartbeatTimer = setInterval(() => {
|
|
141
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
142
|
+
ws.send(JSON.stringify({ type: "Ping" }));
|
|
143
|
+
}
|
|
144
|
+
}, HEARTBEAT_INTERVAL);
|
|
145
|
+
};
|
|
146
|
+
const stopReconnect = () => {
|
|
147
|
+
if (reconnectTimer) {
|
|
148
|
+
clearTimeout(reconnectTimer);
|
|
149
|
+
reconnectTimer = null;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const scheduleReconnect = () => {
|
|
153
|
+
if (!autoReconnect || reconnectAttempts >= maxReconnectAttempts) {
|
|
154
|
+
console.error("WebSocket: Max reconnect attempts reached");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
reconnectAttempts++;
|
|
158
|
+
const delay = Math.min(1000 * 2 ** reconnectAttempts, MAX_RECONNECT_DELAY);
|
|
159
|
+
reconnectTimer = setTimeout(() => {
|
|
160
|
+
connect().catch((err) => {
|
|
161
|
+
console.warn("WebSocket: Failed to reconnect:", err);
|
|
162
|
+
});
|
|
163
|
+
}, delay);
|
|
164
|
+
};
|
|
165
|
+
const send = (data) => {
|
|
166
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
167
|
+
ws.send(JSON.stringify(data));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const resubscribeAll = () => {
|
|
171
|
+
const channels = Array.from(handlers.keys());
|
|
172
|
+
if (channels.length > 0) {
|
|
173
|
+
send({ type: "Subscribe", channels });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const routeMessage = (msg) => {
|
|
177
|
+
if (msg.type !== "Event" || !msg.channel)
|
|
178
|
+
return;
|
|
179
|
+
// Find matching handlers and call all of them
|
|
180
|
+
for (const [subscriptionChannel, channelHandlers] of handlers) {
|
|
181
|
+
if (msg.channel === subscriptionChannel || msg.channel.startsWith(`${subscriptionChannel}:`)) {
|
|
182
|
+
for (const handler of channelHandlers) {
|
|
183
|
+
handler(msg.data);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const connect = () => {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
// Use WebSocket's built-in readyState instead of manual tracking
|
|
191
|
+
if (ws?.readyState === WebSocket.CONNECTING || ws?.readyState === WebSocket.OPEN) {
|
|
192
|
+
resolve();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const socket = new WebSocket(getUrl());
|
|
197
|
+
ws = socket;
|
|
198
|
+
const timeout = setTimeout(() => {
|
|
199
|
+
if (ws !== socket) {
|
|
200
|
+
resolve();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (socket.readyState === WebSocket.CONNECTING) {
|
|
204
|
+
socket.close(1000, "Connection timeout");
|
|
205
|
+
reject(new Error("WebSocket connection timeout"));
|
|
206
|
+
}
|
|
207
|
+
}, CONNECTION_TIMEOUT);
|
|
208
|
+
socket.onopen = () => {
|
|
209
|
+
clearTimeout(timeout);
|
|
210
|
+
if (ws !== socket) {
|
|
211
|
+
resolve();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
reconnectAttempts = 0;
|
|
215
|
+
startHeartbeat();
|
|
216
|
+
resubscribeAll();
|
|
217
|
+
options.onStatusChange?.("connected");
|
|
218
|
+
resolve();
|
|
219
|
+
};
|
|
220
|
+
socket.onmessage = (event) => {
|
|
221
|
+
if (ws !== socket)
|
|
222
|
+
return;
|
|
223
|
+
try {
|
|
224
|
+
const msg = JSON.parse(event.data);
|
|
225
|
+
// Handle pong silently
|
|
226
|
+
if (msg.type === "Pong")
|
|
227
|
+
return;
|
|
228
|
+
routeMessage(msg);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
console.warn("WebSocket: Failed to parse message", err);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
socket.onclose = (event) => {
|
|
235
|
+
clearTimeout(timeout);
|
|
236
|
+
if (ws !== socket) {
|
|
237
|
+
resolve();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
ws = null;
|
|
241
|
+
stopHeartbeat();
|
|
242
|
+
options.onStatusChange?.("disconnected");
|
|
243
|
+
// Reconnect on abnormal close
|
|
244
|
+
if (autoReconnect && event.code !== 1000) {
|
|
245
|
+
scheduleReconnect();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
socket.onerror = () => clearTimeout(timeout);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
options.onStatusChange?.("disconnected");
|
|
252
|
+
reject(err);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const disconnect = () => {
|
|
257
|
+
autoReconnect = false;
|
|
258
|
+
stopReconnect();
|
|
259
|
+
stopHeartbeat();
|
|
260
|
+
handlers.clear();
|
|
261
|
+
ws?.close(1000, "Client disconnect");
|
|
262
|
+
ws = null;
|
|
263
|
+
options.onStatusChange?.("disconnected");
|
|
264
|
+
};
|
|
265
|
+
const subscribe = (channel, handler) => {
|
|
266
|
+
const isFirstSubscriber = !handlers.has(channel);
|
|
267
|
+
// Add handler to the set for this channel
|
|
268
|
+
if (!handlers.has(channel)) {
|
|
269
|
+
handlers.set(channel, new Set());
|
|
270
|
+
}
|
|
271
|
+
handlers.get(channel)?.add(handler);
|
|
272
|
+
// Only send Subscribe message on first subscriber for this channel
|
|
273
|
+
if (isFirstSubscriber && ws?.readyState === WebSocket.OPEN) {
|
|
274
|
+
send({ type: "Subscribe", channels: [channel] });
|
|
275
|
+
}
|
|
276
|
+
// Return unsubscribe function
|
|
277
|
+
return () => {
|
|
278
|
+
const channelHandlers = handlers.get(channel);
|
|
279
|
+
if (channelHandlers) {
|
|
280
|
+
channelHandlers.delete(handler);
|
|
281
|
+
// Only send Unsubscribe on last subscriber for this channel
|
|
282
|
+
if (channelHandlers.size === 0) {
|
|
283
|
+
handlers.delete(channel);
|
|
284
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
285
|
+
send({ type: "Unsubscribe", channels: [channel] });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
// --- Channel-specific subscription methods ---
|
|
292
|
+
const subscribeOrders = (tradingPairId, tradingMode, handler) => {
|
|
293
|
+
const channel = `orders:${tradingPairId}:${tradingMode}`;
|
|
294
|
+
return subscribe(channel, (rawData) => {
|
|
295
|
+
try {
|
|
296
|
+
const data = rawData;
|
|
297
|
+
const event = {
|
|
298
|
+
orderId: data.order_id,
|
|
299
|
+
eventType: data.event_type,
|
|
300
|
+
timestamp: data.timestamp,
|
|
301
|
+
data: keysToCamelCase(data.data),
|
|
302
|
+
};
|
|
303
|
+
handler(event);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
console.error("WebSocket: Error processing order event", err);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const subscribeOrderbook = (tradingPairId, tradingMode, magnitude, quotationMode, handler) => {
|
|
311
|
+
if (!tradingPairId || !tradingMode || magnitude === undefined || !quotationMode) {
|
|
312
|
+
throw new Error(`orderbook subscription requires all params: tradingPairId="${tradingPairId}", tradingMode="${tradingMode}", magnitude=${magnitude} (valid: ${ALL_MAGNITUDES.join(", ")}), quotationMode="${quotationMode}" (valid: "BASE", "QUOTE")`);
|
|
313
|
+
}
|
|
314
|
+
if (!ALL_MAGNITUDES.includes(magnitude)) {
|
|
315
|
+
throw new Error(`Invalid magnitude: ${magnitude}. Must be one of: ${ALL_MAGNITUDES.join(", ")}`);
|
|
316
|
+
}
|
|
317
|
+
const channel = `orderbook:${tradingPairId}:${tradingMode}:${magnitude}:${quotationMode.toLowerCase()}`;
|
|
318
|
+
return subscribe(channel, (rawData) => {
|
|
319
|
+
try {
|
|
320
|
+
const data = rawData;
|
|
321
|
+
const orderbookData = data.data;
|
|
322
|
+
const event = {
|
|
323
|
+
tradingPairId: data.symbol,
|
|
324
|
+
tradingMode: data.trading_mode,
|
|
325
|
+
bids: (orderbookData?.bids || []).map((level) => ({
|
|
326
|
+
price: level.price,
|
|
327
|
+
quantity: level.quantity,
|
|
328
|
+
orderCount: level.order_count || 0,
|
|
329
|
+
})),
|
|
330
|
+
asks: (orderbookData?.asks || []).map((level) => ({
|
|
331
|
+
price: level.price,
|
|
332
|
+
quantity: level.quantity,
|
|
333
|
+
orderCount: level.order_count || 0,
|
|
334
|
+
})),
|
|
335
|
+
bestBid: orderbookData?.best_bid,
|
|
336
|
+
bestAsk: orderbookData?.best_ask,
|
|
337
|
+
bidVolume: orderbookData?.bid_volume || undefined,
|
|
338
|
+
askVolume: orderbookData?.ask_volume || undefined,
|
|
339
|
+
priceChange: orderbookData?.price_change
|
|
340
|
+
? {
|
|
341
|
+
side: orderbookData.price_change.side,
|
|
342
|
+
oldPrice: orderbookData.price_change.old_price,
|
|
343
|
+
newPrice: orderbookData.price_change.new_price,
|
|
344
|
+
levelRemoved: orderbookData.price_change.level_removed || false,
|
|
345
|
+
levelAdded: orderbookData.price_change.level_added || false,
|
|
346
|
+
}
|
|
347
|
+
: undefined,
|
|
348
|
+
baseDecimals: data.base_decimals || 0,
|
|
349
|
+
quoteDecimals: data.quote_decimals || 0,
|
|
350
|
+
timestamp: data.timestamp || new Date().toISOString(),
|
|
351
|
+
sequence: data.sequence_number || 0,
|
|
352
|
+
};
|
|
353
|
+
handler(event);
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
console.error("WebSocket: Error processing orderbook event", err);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
const subscribeOHLCV = (tradingPairId, tradingMode, interval, handler) => {
|
|
361
|
+
const channel = `ohlcv:${tradingPairId}:${tradingMode}:${interval}`;
|
|
362
|
+
return subscribe(channel, (rawData) => {
|
|
363
|
+
try {
|
|
364
|
+
const data = rawData;
|
|
365
|
+
const ohlcvData = data.data;
|
|
366
|
+
const event = {
|
|
367
|
+
tradingPairId: data.symbol,
|
|
368
|
+
tradingMode: data.trading_mode,
|
|
369
|
+
interval: data.interval,
|
|
370
|
+
candlestick: {
|
|
371
|
+
T: ohlcvData.period_start || 0,
|
|
372
|
+
t: ohlcvData.period_end || 0,
|
|
373
|
+
o: ohlcvData.open || "0",
|
|
374
|
+
h: ohlcvData.high || "0",
|
|
375
|
+
l: ohlcvData.low || "0",
|
|
376
|
+
c: ohlcvData.close || "0",
|
|
377
|
+
v: ohlcvData.volume || "0",
|
|
378
|
+
s: data.symbol,
|
|
379
|
+
i: data.interval,
|
|
380
|
+
n: ohlcvData.trades_count || 0,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
handler(event);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
console.error("WebSocket: Error processing OHLCV event", err);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
const subscribeTrades = (tradingPairId, handler) => {
|
|
391
|
+
const channel = `trades:${tradingPairId}`;
|
|
392
|
+
return subscribe(channel, (rawData) => {
|
|
393
|
+
try {
|
|
394
|
+
const data = rawData;
|
|
395
|
+
const tradeData = data.data;
|
|
396
|
+
const event = {
|
|
397
|
+
eventType: "trade",
|
|
398
|
+
tradingPairId: data.trading_pair_id,
|
|
399
|
+
tradingMode: data.trading_mode.toUpperCase(),
|
|
400
|
+
data: {
|
|
401
|
+
tradeId: tradeData.trade_id,
|
|
402
|
+
price: tradeData.price,
|
|
403
|
+
quantity: tradeData.quantity,
|
|
404
|
+
makerSide: tradeData.maker_side.toUpperCase(),
|
|
405
|
+
executedAt: tradeData.executed_at,
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
handler(event);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
console.error("WebSocket: Error processing trade event", err);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
const subscribeMovements = (handler) => {
|
|
416
|
+
// User is identified from the JWT token, so channel is just "movements"
|
|
417
|
+
const channel = "movements";
|
|
418
|
+
return subscribe(channel, (rawData) => {
|
|
419
|
+
try {
|
|
420
|
+
const data = rawData;
|
|
421
|
+
const movementData = data.data;
|
|
422
|
+
const event = {
|
|
423
|
+
eventType: "movement",
|
|
424
|
+
userId: data.user_id,
|
|
425
|
+
data: {
|
|
426
|
+
id: movementData.id,
|
|
427
|
+
entryType: movementData.entry_type,
|
|
428
|
+
transactionType: movementData.transaction_type,
|
|
429
|
+
tokenAddress: movementData.token_address,
|
|
430
|
+
amount: String(movementData.amount),
|
|
431
|
+
amountRaw: movementData.amount_raw,
|
|
432
|
+
balanceBefore: movementData.balance_before != null ? String(movementData.balance_before) : undefined,
|
|
433
|
+
balanceAfter: movementData.balance_after != null ? String(movementData.balance_after) : undefined,
|
|
434
|
+
lockedBefore: movementData.locked_before != null ? String(movementData.locked_before) : undefined,
|
|
435
|
+
lockedAfter: movementData.locked_after != null ? String(movementData.locked_after) : undefined,
|
|
436
|
+
referenceId: movementData.reference_id,
|
|
437
|
+
referenceType: movementData.reference_type,
|
|
438
|
+
description: movementData.description,
|
|
439
|
+
txHash: movementData.tx_hash,
|
|
440
|
+
createdAt: movementData.created_at,
|
|
441
|
+
decimals: movementData.decimals,
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
handler(event);
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
console.error("WebSocket: Error processing movement event", err);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
/**
|
|
452
|
+
* Subscribe to authenticated user's order events (all trading pairs)
|
|
453
|
+
* Requires authentication - user is identified from the JWT token
|
|
454
|
+
*/
|
|
455
|
+
const subscribeUserOrders = (handler) => {
|
|
456
|
+
// User is identified from the JWT token, so channel is just "orders"
|
|
457
|
+
const channel = "orders";
|
|
458
|
+
return subscribe(channel, (rawData) => {
|
|
459
|
+
try {
|
|
460
|
+
const data = rawData;
|
|
461
|
+
const event = {
|
|
462
|
+
orderId: data.order_id,
|
|
463
|
+
eventType: data.event_type,
|
|
464
|
+
timestamp: data.timestamp,
|
|
465
|
+
data: keysToCamelCase(data.data),
|
|
466
|
+
};
|
|
467
|
+
handler(event);
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
console.error("WebSocket: Error processing user order event", err);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
/**
|
|
475
|
+
* Subscribe to authenticated user's balance events
|
|
476
|
+
* Requires authentication - user is identified from the JWT token
|
|
477
|
+
*/
|
|
478
|
+
const subscribeBalances = (handler) => {
|
|
479
|
+
// User is identified from the JWT token, so channel is just "balances"
|
|
480
|
+
const channel = "balances";
|
|
481
|
+
return subscribe(channel, (rawData) => {
|
|
482
|
+
try {
|
|
483
|
+
const data = rawData;
|
|
484
|
+
const balanceData = data.data;
|
|
485
|
+
const event = {
|
|
486
|
+
eventType: "balance_update",
|
|
487
|
+
userId: data.user_id,
|
|
488
|
+
data: {
|
|
489
|
+
tokenAddress: balanceData.token_address,
|
|
490
|
+
tokenSymbol: balanceData.token_symbol,
|
|
491
|
+
available: balanceData.available,
|
|
492
|
+
availableRaw: balanceData.available_raw,
|
|
493
|
+
locked: balanceData.locked,
|
|
494
|
+
lockedRaw: balanceData.locked_raw,
|
|
495
|
+
total: balanceData.total,
|
|
496
|
+
totalRaw: balanceData.total_raw,
|
|
497
|
+
reason: balanceData.reason,
|
|
498
|
+
referenceId: balanceData.reference_id,
|
|
499
|
+
updatedAt: balanceData.updated_at,
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
handler(event);
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
console.error("WebSocket: Error processing balance event", err);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
const subscribeConditionalOrders = (handler, tradingPairId) => {
|
|
510
|
+
if (!handler) {
|
|
511
|
+
throw new Error("conditionalOrders subscription requires a handler");
|
|
512
|
+
}
|
|
513
|
+
if (tradingPairId !== undefined && tradingPairId.length === 0) {
|
|
514
|
+
throw new Error("conditionalOrders tradingPairId cannot be empty");
|
|
515
|
+
}
|
|
516
|
+
const channel = tradingPairId ? `conditional_orders:${tradingPairId}` : "conditional_orders";
|
|
517
|
+
return subscribe(channel, (rawData) => {
|
|
518
|
+
try {
|
|
519
|
+
handler(parseConditionalOrderEvent(rawData));
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
console.error("WebSocket: Error processing conditional order event", err);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
};
|
|
526
|
+
return {
|
|
527
|
+
connect,
|
|
528
|
+
disconnect,
|
|
529
|
+
isConnected: () => ws?.readyState === WebSocket.OPEN,
|
|
530
|
+
getStatus,
|
|
531
|
+
setToken: (newToken) => {
|
|
532
|
+
token = newToken || undefined;
|
|
533
|
+
stopReconnect();
|
|
534
|
+
const currentSocket = ws;
|
|
535
|
+
if (currentSocket?.readyState === WebSocket.OPEN ||
|
|
536
|
+
currentSocket?.readyState === WebSocket.CONNECTING ||
|
|
537
|
+
currentSocket?.readyState === WebSocket.CLOSING) {
|
|
538
|
+
currentSocket.close(1000, newToken ? "Token updated, reconnecting." : "Token cleared.");
|
|
539
|
+
ws = null;
|
|
540
|
+
}
|
|
541
|
+
if (!newToken)
|
|
542
|
+
return;
|
|
543
|
+
connect().catch((err) => {
|
|
544
|
+
console.warn("WebSocket: Failed to reconnect after token update:", err);
|
|
545
|
+
});
|
|
546
|
+
},
|
|
547
|
+
orders: subscribeOrders,
|
|
548
|
+
orderbook: subscribeOrderbook,
|
|
549
|
+
ohlcv: subscribeOHLCV,
|
|
550
|
+
trades: subscribeTrades,
|
|
551
|
+
movements: subscribeMovements,
|
|
552
|
+
userOrders: subscribeUserOrders,
|
|
553
|
+
balances: subscribeBalances,
|
|
554
|
+
conditionalOrders: subscribeConditionalOrders,
|
|
555
|
+
};
|
|
556
|
+
}
|