@temple-digital-group/temple-canton-js 2.0.2 → 2.0.3-beta.0

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.
Files changed (38) hide show
  1. package/README.md +457 -457
  2. package/index.js +15 -15
  3. package/package.json +49 -49
  4. package/src/api/config.d.ts +20 -20
  5. package/src/api/index.ts +322 -322
  6. package/src/api/tokenStore.ts +30 -30
  7. package/src/api/types.ts +196 -196
  8. package/src/auth0/index.d.ts +1 -1
  9. package/src/auth0/index.js +50 -50
  10. package/src/canton/deposits.ts +563 -563
  11. package/src/canton/helpers.ts +266 -266
  12. package/src/canton/index.d.ts +41 -41
  13. package/src/canton/index.js +3472 -3472
  14. package/src/canton/instrumentCatalog.d.ts +7 -7
  15. package/src/canton/instrumentCatalog.js +283 -283
  16. package/src/canton/request_schemas/cancel_orders_amulet.json +77 -77
  17. package/src/canton/request_schemas/cancel_orders_utility.json +68 -68
  18. package/src/canton/request_schemas/create_order_proposal_amulet.json +94 -94
  19. package/src/canton/request_schemas/create_order_proposal_utility.json +121 -121
  20. package/src/canton/request_schemas/create_utility_credential.json +31 -31
  21. package/src/canton/request_schemas/execute_transfer_factory.json +43 -43
  22. package/src/canton/request_schemas/get_allocation_factory.json +21 -21
  23. package/src/canton/request_schemas/get_amulet_holdings.json +21 -21
  24. package/src/canton/request_schemas/get_instrument_configurations.json +21 -21
  25. package/src/canton/request_schemas/get_locked_amulet_holdings.json +21 -21
  26. package/src/canton/request_schemas/get_order_proposals.json +21 -21
  27. package/src/canton/request_schemas/get_orders.json +21 -21
  28. package/src/canton/request_schemas/get_sender_credentials.json +22 -22
  29. package/src/canton/request_schemas/get_transfer_factory.json +28 -28
  30. package/src/canton/request_schemas/get_utility_holdings.json +21 -21
  31. package/src/canton/request_schemas/unlock_amulet.json +38 -38
  32. package/src/canton/walletAdapter.d.ts +7 -7
  33. package/src/canton/walletAdapter.js +112 -112
  34. package/src/canton/withdrawals.ts +511 -511
  35. package/src/config/index.d.ts +63 -63
  36. package/src/config/index.js +188 -188
  37. package/src/websocket/index.ts +341 -341
  38. package/src/websocket/ws.d.ts +24 -24
@@ -1,341 +1,341 @@
1
- import config from "../../src/config/index.js";
2
- import NodeWebSocket from "ws";
3
-
4
- // Environment-aware WebSocket constructor
5
- const WS: typeof globalThis.WebSocket =
6
- typeof WebSocket !== "undefined" ? WebSocket : (NodeWebSocket as unknown as typeof WebSocket);
7
-
8
- // Whether we're in Node.js (can set headers on upgrade) vs browser (must auth via message)
9
- const isNode = typeof process !== "undefined" && typeof process.versions?.node === "string";
10
-
11
- type MessageCallback = (data: unknown) => void;
12
-
13
- export type Granularity = 60 | 300 | 900 | 3600 | 14400 | 86400;
14
-
15
- /** Normalize CC → Amulet in symbol strings */
16
- function normalizeSymbol(symbol: string): string {
17
- return symbol.replace(/\bCC\b/g, "Amulet");
18
- }
19
-
20
- export class TempleWebSocket {
21
- private ws: InstanceType<typeof WS> | null = null;
22
- /** Channel subscriptions — require a subscribe message to the server */
23
- private subscriptions: Map<string, Set<MessageCallback>> = new Map();
24
- /** User event subscriptions — auto-delivered after auth, no subscribe message needed */
25
- private userEventSubscribers: Map<string, Set<MessageCallback>> = new Map();
26
- private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
27
- private pingInterval: ReturnType<typeof setInterval> | null = null;
28
- private reconnectAttempts = 0;
29
- private maxReconnectDelay = 30_000;
30
- private authenticated = false;
31
-
32
- /** Whether to automatically reconnect on close/error */
33
- autoReconnect = true;
34
-
35
- /** Event callbacks */
36
- onConnect: (() => void) | null = null;
37
- onDisconnect: ((code: number, reason: string) => void) | null = null;
38
- onError: ((error: unknown) => void) | null = null;
39
- onAuth: ((success: boolean, userId?: number) => void) | null = null;
40
-
41
- get connected(): boolean {
42
- return this.ws?.readyState === WS.OPEN;
43
- }
44
-
45
- /**
46
- * Connect to the Temple WebSocket server.
47
- * Auth is handled automatically via header (Node.js) or message (browser).
48
- */
49
- connect(): void {
50
- if (this.ws && (this.ws.readyState === WS.OPEN || this.ws.readyState === WS.CONNECTING)) {
51
- return;
52
- }
53
-
54
- const url = config.WS_URL;
55
- const apiKey = config.API_KEY;
56
-
57
- if (isNode && apiKey) {
58
- // Node.js: auth via HTTP upgrade header
59
- this.ws = new WS(url, { headers: { "X-API-Key": apiKey } } as unknown as string[]);
60
- } else {
61
- this.ws = new WS(url);
62
- }
63
-
64
- this.ws.onopen = () => {
65
- this.reconnectAttempts = 0;
66
-
67
- // Browser: auth via message after connect
68
- if (!isNode && apiKey) {
69
- this.send({ type: "auth", api_key: apiKey });
70
- } else {
71
- this.authenticated = true;
72
- }
73
-
74
- // Re-subscribe to all active channels (market data only, not user events)
75
- const channels = [...this.subscriptions.keys()];
76
- if (channels.length > 0) {
77
- this.send({ type: "subscribe", channels });
78
- }
79
-
80
- // Keepalive ping every 30s
81
- this.pingInterval = setInterval(() => {
82
- if (this.connected) {
83
- this.send({ type: "ping" });
84
- }
85
- }, 30_000);
86
-
87
- this.onConnect?.();
88
- };
89
-
90
- this.ws.onmessage = (event: MessageEvent) => {
91
- this.handleMessage(typeof event.data === "string" ? event.data : String(event.data));
92
- };
93
-
94
- this.ws.onclose = (event: CloseEvent) => {
95
- this.cleanup();
96
- this.onDisconnect?.(event.code, event.reason);
97
- if (this.autoReconnect) {
98
- this.scheduleReconnect();
99
- }
100
- };
101
-
102
- this.ws.onerror = (event: Event) => {
103
- this.onError?.(event);
104
- };
105
- }
106
-
107
- /** Disconnect and stop auto-reconnect. */
108
- disconnect(): void {
109
- this.autoReconnect = false;
110
- this.cleanup();
111
- if (this.ws) {
112
- this.ws.close(1000, "client disconnect");
113
- this.ws = null;
114
- }
115
- }
116
-
117
- /**
118
- * Subscribe to a market data channel. Sends a subscribe message to the server.
119
- * Returns an unsubscribe function.
120
- *
121
- * @param channel - Channel pattern (e.g. "orderbook:Amulet/USDCx", "trades:Amulet/USDCx")
122
- * @param callback - Called with parsed message data for this channel
123
- * @returns Unsubscribe function
124
- */
125
- subscribe(channel: string, callback: MessageCallback): () => void {
126
- if (!this.subscriptions.has(channel)) {
127
- this.subscriptions.set(channel, new Set());
128
- // Send subscribe if already connected
129
- if (this.connected) {
130
- this.send({ type: "subscribe", channels: [channel] });
131
- }
132
- }
133
- this.subscriptions.get(channel)!.add(callback);
134
-
135
- // Return unsubscribe function
136
- return () => this.unsubscribe(channel, callback);
137
- }
138
-
139
- /**
140
- * Unsubscribe a callback (or all callbacks) from a market data channel.
141
- */
142
- unsubscribe(channel: string, callback?: MessageCallback): void {
143
- const callbacks = this.subscriptions.get(channel);
144
- if (!callbacks) return;
145
-
146
- if (callback) {
147
- callbacks.delete(callback);
148
- }
149
-
150
- if (!callback || callbacks.size === 0) {
151
- this.subscriptions.delete(channel);
152
- if (this.connected) {
153
- this.send({ type: "unsubscribe", channels: [channel] });
154
- }
155
- }
156
- }
157
-
158
- /**
159
- * Subscribe to user events (auto-delivered after auth, no subscribe message sent).
160
- * The server pushes these automatically once authenticated.
161
- * Returns an unsubscribe function.
162
- *
163
- * @param event - Event name (e.g. "trade", "order", "balance")
164
- * @param callback - Called with the event data
165
- * @returns Unsubscribe function
166
- */
167
- onUserEvent(event: string, callback: MessageCallback): () => void {
168
- if (!this.userEventSubscribers.has(event)) {
169
- this.userEventSubscribers.set(event, new Set());
170
- }
171
- this.userEventSubscribers.get(event)!.add(callback);
172
-
173
- return () => {
174
- const handlers = this.userEventSubscribers.get(event);
175
- if (handlers) {
176
- handlers.delete(callback);
177
- if (handlers.size === 0) {
178
- this.userEventSubscribers.delete(event);
179
- }
180
- }
181
- };
182
- }
183
-
184
- private send(msg: Record<string, unknown>): void {
185
- if (this.ws && this.ws.readyState === WS.OPEN) {
186
- this.ws.send(JSON.stringify(msg));
187
- }
188
- }
189
-
190
- private handleMessage(raw: string): void {
191
- let parsed: Record<string, unknown>;
192
- try {
193
- parsed = JSON.parse(raw);
194
- } catch {
195
- return;
196
- }
197
-
198
- const type = parsed.type as string | undefined;
199
-
200
- // Handle auth response
201
- if (type === "authenticated") {
202
- this.authenticated = true;
203
- this.onAuth?.(true, parsed.user_id as number | undefined);
204
- return;
205
- }
206
- if (type === "auth_expired") {
207
- this.authenticated = false;
208
- this.onAuth?.(false);
209
- return;
210
- }
211
-
212
- // Handle pong
213
- if (type === "pong") return;
214
-
215
- // Handle subscribed/unsubscribed confirmations
216
- if (type === "subscribed" || type === "unsubscribed") return;
217
-
218
- // Handle server errors
219
- if (type === "error") {
220
- this.onError?.(parsed);
221
- return;
222
- }
223
-
224
- // Route user data (auto-pushed after auth, keyed by event name)
225
- if (type === "user_data") {
226
- const event = parsed.event as string | undefined;
227
- if (event && this.userEventSubscribers.has(event)) {
228
- const handlers = this.userEventSubscribers.get(event)!;
229
- for (const cb of handlers) {
230
- try {
231
- cb(parsed.data ?? parsed);
232
- } catch {
233
- // Don't let one bad callback kill the others
234
- }
235
- }
236
- }
237
- return;
238
- }
239
-
240
- // Route channel data (market data from subscriptions)
241
- if (type === "data") {
242
- const channel = parsed.channel as string | undefined;
243
- if (channel && this.subscriptions.has(channel)) {
244
- const callbacks = this.subscriptions.get(channel)!;
245
- for (const cb of callbacks) {
246
- try {
247
- cb(parsed.data ?? parsed);
248
- } catch {
249
- // Don't let one bad callback kill the others
250
- }
251
- }
252
- }
253
- return;
254
- }
255
- }
256
-
257
- private scheduleReconnect(): void {
258
- if (this.reconnectTimer) return;
259
- const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay);
260
- this.reconnectAttempts++;
261
- this.reconnectTimer = setTimeout(() => {
262
- this.reconnectTimer = null;
263
- this.connect();
264
- }, delay);
265
- }
266
-
267
- private cleanup(): void {
268
- if (this.pingInterval) {
269
- clearInterval(this.pingInterval);
270
- this.pingInterval = null;
271
- }
272
- if (this.reconnectTimer) {
273
- clearTimeout(this.reconnectTimer);
274
- this.reconnectTimer = null;
275
- }
276
- this.authenticated = false;
277
- }
278
- }
279
-
280
- // ─── Singleton & Convenience API ─────────────────────────────────────────────
281
-
282
- let instance: TempleWebSocket | null = null;
283
-
284
- /** Get or create the shared WebSocket instance. Auto-connects if not already. */
285
- export function createWebSocket(): TempleWebSocket {
286
- if (!instance) {
287
- instance = new TempleWebSocket();
288
- }
289
- if (!instance.connected) {
290
- instance.connect();
291
- }
292
- return instance;
293
- }
294
-
295
- /** Disconnect and destroy the shared WebSocket instance. */
296
- export function disconnectWebSocket(): void {
297
- if (instance) {
298
- instance.disconnect();
299
- instance = null;
300
- }
301
- }
302
-
303
- // ─── Market Data Subscriptions ───────────────────────────────────────────────
304
-
305
- export function subscribeOrderbook(symbol: string, cb: MessageCallback): () => void {
306
- return createWebSocket().subscribe(`orderbook:${normalizeSymbol(symbol)}`, cb);
307
- }
308
-
309
- export function subscribeTrades(symbol: string, cb: MessageCallback): () => void {
310
- return createWebSocket().subscribe(`trades:${normalizeSymbol(symbol)}`, cb);
311
- }
312
-
313
- export function subscribeTicker(symbol: string, cb: MessageCallback): () => void {
314
- return createWebSocket().subscribe(`ticker:${normalizeSymbol(symbol)}`, cb);
315
- }
316
-
317
- export function subscribeCandles(symbol: string, granularity: Granularity, cb: MessageCallback): () => void {
318
- return createWebSocket().subscribe(`candles:${normalizeSymbol(symbol)}:${granularity}`, cb);
319
- }
320
-
321
- export function subscribeOracle(symbol: string, cb: MessageCallback): () => void {
322
- return createWebSocket().subscribe(`oracle:${symbol}`, cb);
323
- }
324
-
325
- export function subscribeOracleVolume(symbol: string, cb: MessageCallback): () => void {
326
- return createWebSocket().subscribe(`oracle_volume:${symbol}`, cb);
327
- }
328
-
329
- // ─── User Data Subscriptions (auto-delivered after auth) ─────────────────────
330
-
331
- export function subscribeUserOrders(cb: MessageCallback): () => void {
332
- return createWebSocket().onUserEvent("user_order", cb);
333
- }
334
-
335
- export function subscribeUserTrades(cb: MessageCallback): () => void {
336
- return createWebSocket().onUserEvent("user_trade", cb);
337
- }
338
-
339
- export function subscribeUserBalances(cb: MessageCallback): () => void {
340
- return createWebSocket().onUserEvent("user_balance", cb);
341
- }
1
+ import config from "../../src/config/index.js";
2
+ import NodeWebSocket from "ws";
3
+
4
+ // Environment-aware WebSocket constructor
5
+ const WS: typeof globalThis.WebSocket =
6
+ typeof WebSocket !== "undefined" ? WebSocket : (NodeWebSocket as unknown as typeof WebSocket);
7
+
8
+ // Whether we're in Node.js (can set headers on upgrade) vs browser (must auth via message)
9
+ const isNode = typeof process !== "undefined" && typeof process.versions?.node === "string";
10
+
11
+ type MessageCallback = (data: unknown) => void;
12
+
13
+ export type Granularity = 60 | 300 | 900 | 3600 | 14400 | 86400;
14
+
15
+ /** Normalize CC → Amulet in symbol strings */
16
+ function normalizeSymbol(symbol: string): string {
17
+ return symbol.replace(/\bCC\b/g, "Amulet");
18
+ }
19
+
20
+ export class TempleWebSocket {
21
+ private ws: InstanceType<typeof WS> | null = null;
22
+ /** Channel subscriptions — require a subscribe message to the server */
23
+ private subscriptions: Map<string, Set<MessageCallback>> = new Map();
24
+ /** User event subscriptions — auto-delivered after auth, no subscribe message needed */
25
+ private userEventSubscribers: Map<string, Set<MessageCallback>> = new Map();
26
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
27
+ private pingInterval: ReturnType<typeof setInterval> | null = null;
28
+ private reconnectAttempts = 0;
29
+ private maxReconnectDelay = 30_000;
30
+ private authenticated = false;
31
+
32
+ /** Whether to automatically reconnect on close/error */
33
+ autoReconnect = true;
34
+
35
+ /** Event callbacks */
36
+ onConnect: (() => void) | null = null;
37
+ onDisconnect: ((code: number, reason: string) => void) | null = null;
38
+ onError: ((error: unknown) => void) | null = null;
39
+ onAuth: ((success: boolean, userId?: number) => void) | null = null;
40
+
41
+ get connected(): boolean {
42
+ return this.ws?.readyState === WS.OPEN;
43
+ }
44
+
45
+ /**
46
+ * Connect to the Temple WebSocket server.
47
+ * Auth is handled automatically via header (Node.js) or message (browser).
48
+ */
49
+ connect(): void {
50
+ if (this.ws && (this.ws.readyState === WS.OPEN || this.ws.readyState === WS.CONNECTING)) {
51
+ return;
52
+ }
53
+
54
+ const url = config.WS_URL;
55
+ const apiKey = config.API_KEY;
56
+
57
+ if (isNode && apiKey) {
58
+ // Node.js: auth via HTTP upgrade header
59
+ this.ws = new WS(url, { headers: { "X-API-Key": apiKey } } as unknown as string[]);
60
+ } else {
61
+ this.ws = new WS(url);
62
+ }
63
+
64
+ this.ws.onopen = () => {
65
+ this.reconnectAttempts = 0;
66
+
67
+ // Browser: auth via message after connect
68
+ if (!isNode && apiKey) {
69
+ this.send({ type: "auth", api_key: apiKey });
70
+ } else {
71
+ this.authenticated = true;
72
+ }
73
+
74
+ // Re-subscribe to all active channels (market data only, not user events)
75
+ const channels = [...this.subscriptions.keys()];
76
+ if (channels.length > 0) {
77
+ this.send({ type: "subscribe", channels });
78
+ }
79
+
80
+ // Keepalive ping every 30s
81
+ this.pingInterval = setInterval(() => {
82
+ if (this.connected) {
83
+ this.send({ type: "ping" });
84
+ }
85
+ }, 30_000);
86
+
87
+ this.onConnect?.();
88
+ };
89
+
90
+ this.ws.onmessage = (event: MessageEvent) => {
91
+ this.handleMessage(typeof event.data === "string" ? event.data : String(event.data));
92
+ };
93
+
94
+ this.ws.onclose = (event: CloseEvent) => {
95
+ this.cleanup();
96
+ this.onDisconnect?.(event.code, event.reason);
97
+ if (this.autoReconnect) {
98
+ this.scheduleReconnect();
99
+ }
100
+ };
101
+
102
+ this.ws.onerror = (event: Event) => {
103
+ this.onError?.(event);
104
+ };
105
+ }
106
+
107
+ /** Disconnect and stop auto-reconnect. */
108
+ disconnect(): void {
109
+ this.autoReconnect = false;
110
+ this.cleanup();
111
+ if (this.ws) {
112
+ this.ws.close(1000, "client disconnect");
113
+ this.ws = null;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Subscribe to a market data channel. Sends a subscribe message to the server.
119
+ * Returns an unsubscribe function.
120
+ *
121
+ * @param channel - Channel pattern (e.g. "orderbook:Amulet/USDCx", "trades:Amulet/USDCx")
122
+ * @param callback - Called with parsed message data for this channel
123
+ * @returns Unsubscribe function
124
+ */
125
+ subscribe(channel: string, callback: MessageCallback): () => void {
126
+ if (!this.subscriptions.has(channel)) {
127
+ this.subscriptions.set(channel, new Set());
128
+ // Send subscribe if already connected
129
+ if (this.connected) {
130
+ this.send({ type: "subscribe", channels: [channel] });
131
+ }
132
+ }
133
+ this.subscriptions.get(channel)!.add(callback);
134
+
135
+ // Return unsubscribe function
136
+ return () => this.unsubscribe(channel, callback);
137
+ }
138
+
139
+ /**
140
+ * Unsubscribe a callback (or all callbacks) from a market data channel.
141
+ */
142
+ unsubscribe(channel: string, callback?: MessageCallback): void {
143
+ const callbacks = this.subscriptions.get(channel);
144
+ if (!callbacks) return;
145
+
146
+ if (callback) {
147
+ callbacks.delete(callback);
148
+ }
149
+
150
+ if (!callback || callbacks.size === 0) {
151
+ this.subscriptions.delete(channel);
152
+ if (this.connected) {
153
+ this.send({ type: "unsubscribe", channels: [channel] });
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Subscribe to user events (auto-delivered after auth, no subscribe message sent).
160
+ * The server pushes these automatically once authenticated.
161
+ * Returns an unsubscribe function.
162
+ *
163
+ * @param event - Event name (e.g. "trade", "order", "balance")
164
+ * @param callback - Called with the event data
165
+ * @returns Unsubscribe function
166
+ */
167
+ onUserEvent(event: string, callback: MessageCallback): () => void {
168
+ if (!this.userEventSubscribers.has(event)) {
169
+ this.userEventSubscribers.set(event, new Set());
170
+ }
171
+ this.userEventSubscribers.get(event)!.add(callback);
172
+
173
+ return () => {
174
+ const handlers = this.userEventSubscribers.get(event);
175
+ if (handlers) {
176
+ handlers.delete(callback);
177
+ if (handlers.size === 0) {
178
+ this.userEventSubscribers.delete(event);
179
+ }
180
+ }
181
+ };
182
+ }
183
+
184
+ private send(msg: Record<string, unknown>): void {
185
+ if (this.ws && this.ws.readyState === WS.OPEN) {
186
+ this.ws.send(JSON.stringify(msg));
187
+ }
188
+ }
189
+
190
+ private handleMessage(raw: string): void {
191
+ let parsed: Record<string, unknown>;
192
+ try {
193
+ parsed = JSON.parse(raw);
194
+ } catch {
195
+ return;
196
+ }
197
+
198
+ const type = parsed.type as string | undefined;
199
+
200
+ // Handle auth response
201
+ if (type === "authenticated") {
202
+ this.authenticated = true;
203
+ this.onAuth?.(true, parsed.user_id as number | undefined);
204
+ return;
205
+ }
206
+ if (type === "auth_expired") {
207
+ this.authenticated = false;
208
+ this.onAuth?.(false);
209
+ return;
210
+ }
211
+
212
+ // Handle pong
213
+ if (type === "pong") return;
214
+
215
+ // Handle subscribed/unsubscribed confirmations
216
+ if (type === "subscribed" || type === "unsubscribed") return;
217
+
218
+ // Handle server errors
219
+ if (type === "error") {
220
+ this.onError?.(parsed);
221
+ return;
222
+ }
223
+
224
+ // Route user data (auto-pushed after auth, keyed by event name)
225
+ if (type === "user_data") {
226
+ const event = parsed.event as string | undefined;
227
+ if (event && this.userEventSubscribers.has(event)) {
228
+ const handlers = this.userEventSubscribers.get(event)!;
229
+ for (const cb of handlers) {
230
+ try {
231
+ cb(parsed.data ?? parsed);
232
+ } catch {
233
+ // Don't let one bad callback kill the others
234
+ }
235
+ }
236
+ }
237
+ return;
238
+ }
239
+
240
+ // Route channel data (market data from subscriptions)
241
+ if (type === "data") {
242
+ const channel = parsed.channel as string | undefined;
243
+ if (channel && this.subscriptions.has(channel)) {
244
+ const callbacks = this.subscriptions.get(channel)!;
245
+ for (const cb of callbacks) {
246
+ try {
247
+ cb(parsed.data ?? parsed);
248
+ } catch {
249
+ // Don't let one bad callback kill the others
250
+ }
251
+ }
252
+ }
253
+ return;
254
+ }
255
+ }
256
+
257
+ private scheduleReconnect(): void {
258
+ if (this.reconnectTimer) return;
259
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay);
260
+ this.reconnectAttempts++;
261
+ this.reconnectTimer = setTimeout(() => {
262
+ this.reconnectTimer = null;
263
+ this.connect();
264
+ }, delay);
265
+ }
266
+
267
+ private cleanup(): void {
268
+ if (this.pingInterval) {
269
+ clearInterval(this.pingInterval);
270
+ this.pingInterval = null;
271
+ }
272
+ if (this.reconnectTimer) {
273
+ clearTimeout(this.reconnectTimer);
274
+ this.reconnectTimer = null;
275
+ }
276
+ this.authenticated = false;
277
+ }
278
+ }
279
+
280
+ // ─── Singleton & Convenience API ─────────────────────────────────────────────
281
+
282
+ let instance: TempleWebSocket | null = null;
283
+
284
+ /** Get or create the shared WebSocket instance. Auto-connects if not already. */
285
+ export function createWebSocket(): TempleWebSocket {
286
+ if (!instance) {
287
+ instance = new TempleWebSocket();
288
+ }
289
+ if (!instance.connected) {
290
+ instance.connect();
291
+ }
292
+ return instance;
293
+ }
294
+
295
+ /** Disconnect and destroy the shared WebSocket instance. */
296
+ export function disconnectWebSocket(): void {
297
+ if (instance) {
298
+ instance.disconnect();
299
+ instance = null;
300
+ }
301
+ }
302
+
303
+ // ─── Market Data Subscriptions ───────────────────────────────────────────────
304
+
305
+ export function subscribeOrderbook(symbol: string, cb: MessageCallback): () => void {
306
+ return createWebSocket().subscribe(`orderbook:${normalizeSymbol(symbol)}`, cb);
307
+ }
308
+
309
+ export function subscribeTrades(symbol: string, cb: MessageCallback): () => void {
310
+ return createWebSocket().subscribe(`trades:${normalizeSymbol(symbol)}`, cb);
311
+ }
312
+
313
+ export function subscribeTicker(symbol: string, cb: MessageCallback): () => void {
314
+ return createWebSocket().subscribe(`ticker:${normalizeSymbol(symbol)}`, cb);
315
+ }
316
+
317
+ export function subscribeCandles(symbol: string, granularity: Granularity, cb: MessageCallback): () => void {
318
+ return createWebSocket().subscribe(`candles:${normalizeSymbol(symbol)}:${granularity}`, cb);
319
+ }
320
+
321
+ export function subscribeOracle(symbol: string, cb: MessageCallback): () => void {
322
+ return createWebSocket().subscribe(`oracle:${symbol}`, cb);
323
+ }
324
+
325
+ export function subscribeOracleVolume(symbol: string, cb: MessageCallback): () => void {
326
+ return createWebSocket().subscribe(`oracle_volume:${symbol}`, cb);
327
+ }
328
+
329
+ // ─── User Data Subscriptions (auto-delivered after auth) ─────────────────────
330
+
331
+ export function subscribeUserOrders(cb: MessageCallback): () => void {
332
+ return createWebSocket().onUserEvent("user_order", cb);
333
+ }
334
+
335
+ export function subscribeUserTrades(cb: MessageCallback): () => void {
336
+ return createWebSocket().onUserEvent("user_trade", cb);
337
+ }
338
+
339
+ export function subscribeUserBalances(cb: MessageCallback): () => void {
340
+ return createWebSocket().onUserEvent("user_balance", cb);
341
+ }