@k256/sdk 0.1.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.
@@ -0,0 +1,889 @@
1
+ // src/utils/base58.ts
2
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
3
+ function base58Encode(bytes) {
4
+ const digits = [0];
5
+ for (let i = 0; i < bytes.length; i++) {
6
+ let carry = bytes[i];
7
+ for (let j = 0; j < digits.length; j++) {
8
+ carry += digits[j] << 8;
9
+ digits[j] = carry % 58;
10
+ carry = carry / 58 | 0;
11
+ }
12
+ while (carry > 0) {
13
+ digits.push(carry % 58);
14
+ carry = carry / 58 | 0;
15
+ }
16
+ }
17
+ let leadingZeros = "";
18
+ for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
19
+ leadingZeros += "1";
20
+ }
21
+ return leadingZeros + digits.reverse().map((d) => BASE58_ALPHABET[d]).join("");
22
+ }
23
+
24
+ // src/ws/types.ts
25
+ var MessageType = {
26
+ /** Single pool state update (bincode) */
27
+ PoolUpdate: 1,
28
+ /** Subscribe request (JSON) - Client → Server */
29
+ Subscribe: 2,
30
+ /** Subscription confirmed (JSON) - Server → Client */
31
+ Subscribed: 3,
32
+ /** Unsubscribe all - Client → Server */
33
+ Unsubscribe: 4,
34
+ /** Priority fee update (bincode) */
35
+ PriorityFees: 5,
36
+ /** Recent blockhash (bincode) */
37
+ Blockhash: 6,
38
+ /** Streaming quote update (bincode) */
39
+ Quote: 7,
40
+ /** Quote subscription confirmed (JSON) */
41
+ QuoteSubscribed: 8,
42
+ /** Subscribe to quote stream (JSON) - Client → Server */
43
+ SubscribeQuote: 9,
44
+ /** Unsubscribe from quote (JSON) - Client → Server */
45
+ UnsubscribeQuote: 10,
46
+ /** Ping keepalive - Client → Server */
47
+ Ping: 11,
48
+ /** Pong response (bincode u64 timestamp) */
49
+ Pong: 12,
50
+ /** Connection heartbeat with stats (JSON) */
51
+ Heartbeat: 13,
52
+ /** Batched pool updates for high throughput */
53
+ PoolUpdateBatch: 14,
54
+ /** Error message (UTF-8 string) */
55
+ Error: 255
56
+ };
57
+
58
+ // src/ws/decoder.ts
59
+ function decodeMessage(data) {
60
+ const view = new DataView(data);
61
+ if (data.byteLength < 1) return null;
62
+ const msgType = view.getUint8(0);
63
+ const payload = data.slice(1);
64
+ const payloadView = new DataView(payload);
65
+ switch (msgType) {
66
+ case MessageType.Subscribed:
67
+ case MessageType.QuoteSubscribed:
68
+ case MessageType.Heartbeat: {
69
+ const decoder = new TextDecoder();
70
+ const text = decoder.decode(payload);
71
+ try {
72
+ let type;
73
+ if (msgType === MessageType.QuoteSubscribed) {
74
+ type = "quote_subscribed";
75
+ } else if (msgType === MessageType.Heartbeat) {
76
+ type = "heartbeat";
77
+ } else {
78
+ type = "subscribed";
79
+ }
80
+ return {
81
+ type,
82
+ data: JSON.parse(text)
83
+ };
84
+ } catch {
85
+ return { type: "error", data: { message: text } };
86
+ }
87
+ }
88
+ case MessageType.Error: {
89
+ const decoder = new TextDecoder();
90
+ const text = decoder.decode(payload);
91
+ return { type: "error", data: { message: text } };
92
+ }
93
+ case MessageType.PriorityFees: {
94
+ if (payload.byteLength < 24) return null;
95
+ const slot = Number(payloadView.getBigUint64(0, true));
96
+ const timestampMs = Number(payloadView.getBigUint64(8, true));
97
+ const recommended = Number(payloadView.getBigUint64(16, true));
98
+ const state = payload.byteLength > 24 ? payloadView.getUint8(24) : 1;
99
+ const isStale = payload.byteLength > 25 ? payloadView.getUint8(25) !== 0 : false;
100
+ let swapP50 = 0, swapP75 = 0, swapP90 = 0, swapP99 = 0;
101
+ if (payload.byteLength >= 58) {
102
+ swapP50 = Number(payloadView.getBigUint64(26, true));
103
+ swapP75 = Number(payloadView.getBigUint64(34, true));
104
+ swapP90 = Number(payloadView.getBigUint64(42, true));
105
+ swapP99 = Number(payloadView.getBigUint64(50, true));
106
+ }
107
+ let swapSamples = 0;
108
+ let landingP50Fee = 0, landingP75Fee = 0, landingP90Fee = 0, landingP99Fee = 0;
109
+ let top10Fee = 0, top25Fee = 0;
110
+ let spikeDetected = false, spikeFee = 0;
111
+ if (payload.byteLength >= 119) {
112
+ swapSamples = payloadView.getUint32(58, true);
113
+ landingP50Fee = Number(payloadView.getBigUint64(62, true));
114
+ landingP75Fee = Number(payloadView.getBigUint64(70, true));
115
+ landingP90Fee = Number(payloadView.getBigUint64(78, true));
116
+ landingP99Fee = Number(payloadView.getBigUint64(86, true));
117
+ top10Fee = Number(payloadView.getBigUint64(94, true));
118
+ top25Fee = Number(payloadView.getBigUint64(102, true));
119
+ spikeDetected = payloadView.getUint8(110) !== 0;
120
+ spikeFee = Number(payloadView.getBigUint64(111, true));
121
+ }
122
+ return {
123
+ type: "priority_fees",
124
+ data: {
125
+ slot,
126
+ timestampMs,
127
+ recommended,
128
+ state,
129
+ isStale,
130
+ swapP50,
131
+ swapP75,
132
+ swapP90,
133
+ swapP99,
134
+ swapSamples,
135
+ landingP50Fee,
136
+ landingP75Fee,
137
+ landingP90Fee,
138
+ landingP99Fee,
139
+ top10Fee,
140
+ top25Fee,
141
+ spikeDetected,
142
+ spikeFee
143
+ }
144
+ };
145
+ }
146
+ case MessageType.Blockhash: {
147
+ if (payload.byteLength < 65) return null;
148
+ const slot = Number(payloadView.getBigUint64(0, true));
149
+ const timestampMs = Number(payloadView.getBigUint64(8, true));
150
+ const blockhashBytes = new Uint8Array(payload, 16, 32);
151
+ const blockHeight = Number(payloadView.getBigUint64(48, true));
152
+ const lastValidBlockHeight = Number(payloadView.getBigUint64(56, true));
153
+ const isStale = payloadView.getUint8(64) !== 0;
154
+ return {
155
+ type: "blockhash",
156
+ data: {
157
+ slot,
158
+ timestampMs,
159
+ blockhash: base58Encode(blockhashBytes),
160
+ blockHeight,
161
+ lastValidBlockHeight,
162
+ isStale
163
+ }
164
+ };
165
+ }
166
+ case MessageType.PoolUpdate: {
167
+ return decodePoolUpdate(payload, payloadView);
168
+ }
169
+ case MessageType.PoolUpdateBatch: {
170
+ if (payload.byteLength < 2) return null;
171
+ const updates = decodePoolUpdateBatch(payload);
172
+ if (updates.length === 0) return null;
173
+ return updates[0];
174
+ }
175
+ case MessageType.Quote: {
176
+ return decodeQuote(payload, payloadView);
177
+ }
178
+ case MessageType.Pong: {
179
+ if (payload.byteLength < 8) return null;
180
+ return {
181
+ type: "pong",
182
+ data: {
183
+ timestampMs: Number(payloadView.getBigUint64(0, true))
184
+ }
185
+ };
186
+ }
187
+ default:
188
+ return null;
189
+ }
190
+ }
191
+ function decodePoolUpdateBatch(payload) {
192
+ const view = new DataView(payload);
193
+ if (payload.byteLength < 2) return [];
194
+ const count = view.getUint16(0, true);
195
+ const updates = [];
196
+ let offset = 2;
197
+ for (let i = 0; i < count && offset + 4 <= payload.byteLength; i++) {
198
+ const payloadLen = view.getUint32(offset, true);
199
+ offset += 4;
200
+ if (offset + payloadLen > payload.byteLength) break;
201
+ const updatePayload = payload.slice(offset, offset + payloadLen);
202
+ const updateView = new DataView(updatePayload);
203
+ const decoded = decodePoolUpdate(updatePayload, updateView);
204
+ if (decoded) {
205
+ updates.push(decoded);
206
+ }
207
+ offset += payloadLen;
208
+ }
209
+ return updates;
210
+ }
211
+ function decodePoolUpdate(payload, payloadView) {
212
+ if (payload.byteLength < 50) return null;
213
+ try {
214
+ let offset = 0;
215
+ const decoder = new TextDecoder();
216
+ const serializedStateLen = Number(payloadView.getBigUint64(offset, true));
217
+ offset += 8 + serializedStateLen;
218
+ if (payload.byteLength < offset + 24) return null;
219
+ const sequence = Number(payloadView.getBigUint64(offset, true));
220
+ offset += 8;
221
+ const slot = Number(payloadView.getBigUint64(offset, true));
222
+ offset += 8;
223
+ const writeVersion = Number(payloadView.getBigUint64(offset, true));
224
+ offset += 8;
225
+ const protocolLen = Number(payloadView.getBigUint64(offset, true));
226
+ offset += 8;
227
+ const protocolBytes = new Uint8Array(payload, offset, protocolLen);
228
+ const protocol = decoder.decode(protocolBytes);
229
+ offset += protocolLen;
230
+ const poolAddr = new Uint8Array(payload, offset, 32);
231
+ offset += 32;
232
+ const tokenMintCount = Number(payloadView.getBigUint64(offset, true));
233
+ offset += 8;
234
+ const tokenMints = [];
235
+ for (let i = 0; i < tokenMintCount && offset + 32 <= payload.byteLength; i++) {
236
+ const mint = new Uint8Array(payload, offset, 32);
237
+ tokenMints.push(base58Encode(mint));
238
+ offset += 32;
239
+ }
240
+ const balanceCount = Number(payloadView.getBigUint64(offset, true));
241
+ offset += 8;
242
+ const tokenBalances = [];
243
+ for (let i = 0; i < balanceCount && offset + 8 <= payload.byteLength; i++) {
244
+ tokenBalances.push(payloadView.getBigUint64(offset, true).toString());
245
+ offset += 8;
246
+ }
247
+ const decimalsCount = Number(payloadView.getBigUint64(offset, true));
248
+ offset += 8;
249
+ const tokenDecimals = [];
250
+ for (let i = 0; i < decimalsCount && offset + 4 <= payload.byteLength; i++) {
251
+ tokenDecimals.push(payloadView.getInt32(offset, true));
252
+ offset += 4;
253
+ }
254
+ const isValid = offset < payload.byteLength ? payloadView.getUint8(offset) !== 0 : true;
255
+ offset += 1;
256
+ return {
257
+ type: "pool_update",
258
+ data: {
259
+ sequence,
260
+ slot,
261
+ writeVersion,
262
+ protocol,
263
+ poolAddress: base58Encode(poolAddr),
264
+ tokenMints,
265
+ tokenBalances,
266
+ tokenDecimals,
267
+ isValid
268
+ }
269
+ };
270
+ } catch {
271
+ return {
272
+ type: "pool_update",
273
+ data: {
274
+ sequence: 0,
275
+ slot: 0,
276
+ writeVersion: 0,
277
+ protocol: "unknown",
278
+ poolAddress: "",
279
+ tokenMints: [],
280
+ tokenBalances: [],
281
+ tokenDecimals: [],
282
+ isValid: false
283
+ }
284
+ };
285
+ }
286
+ }
287
+ function decodeQuote(payload, payloadView) {
288
+ if (payload.byteLength < 8) return null;
289
+ try {
290
+ let offset = 0;
291
+ const decoder = new TextDecoder();
292
+ const readString = () => {
293
+ const len = Number(payloadView.getBigUint64(offset, true));
294
+ offset += 8;
295
+ const bytes = new Uint8Array(payload, offset, len);
296
+ offset += len;
297
+ return decoder.decode(bytes);
298
+ };
299
+ const topicId = readString();
300
+ const timestampMs = Number(payloadView.getBigUint64(offset, true));
301
+ offset += 8;
302
+ const sequence = Number(payloadView.getBigUint64(offset, true));
303
+ offset += 8;
304
+ const inputMintBytes = new Uint8Array(payload, offset, 32);
305
+ offset += 32;
306
+ const outputMintBytes = new Uint8Array(payload, offset, 32);
307
+ offset += 32;
308
+ const inAmount = payloadView.getBigUint64(offset, true).toString();
309
+ offset += 8;
310
+ const outAmount = payloadView.getBigUint64(offset, true).toString();
311
+ offset += 8;
312
+ const priceImpactBps = payloadView.getInt32(offset, true);
313
+ offset += 4;
314
+ const contextSlot = Number(payloadView.getBigUint64(offset, true));
315
+ offset += 8;
316
+ const algorithm = readString();
317
+ const isImprovement = payloadView.getUint8(offset) !== 0;
318
+ offset += 1;
319
+ const isCached = payloadView.getUint8(offset) !== 0;
320
+ offset += 1;
321
+ const isStale = payloadView.getUint8(offset) !== 0;
322
+ offset += 1;
323
+ let routePlan = null;
324
+ if (offset + 8 <= payload.byteLength) {
325
+ const routePlanLen = Number(payloadView.getBigUint64(offset, true));
326
+ offset += 8;
327
+ if (routePlanLen > 0 && offset + routePlanLen <= payload.byteLength) {
328
+ const routePlanBytes = new Uint8Array(payload, offset, routePlanLen);
329
+ try {
330
+ routePlan = JSON.parse(decoder.decode(routePlanBytes));
331
+ } catch {
332
+ }
333
+ }
334
+ }
335
+ return {
336
+ type: "quote",
337
+ data: {
338
+ topicId,
339
+ timestampMs,
340
+ sequence,
341
+ inputMint: base58Encode(inputMintBytes),
342
+ outputMint: base58Encode(outputMintBytes),
343
+ inAmount,
344
+ outAmount,
345
+ priceImpactBps,
346
+ contextSlot,
347
+ algorithm,
348
+ isImprovement,
349
+ isCached,
350
+ isStale,
351
+ routePlan
352
+ }
353
+ };
354
+ } catch {
355
+ return {
356
+ type: "quote",
357
+ data: {
358
+ topicId: "",
359
+ timestampMs: 0,
360
+ sequence: 0,
361
+ inputMint: "",
362
+ outputMint: "",
363
+ inAmount: "0",
364
+ outAmount: "0",
365
+ priceImpactBps: 0,
366
+ contextSlot: 0,
367
+ algorithm: "",
368
+ isImprovement: false,
369
+ isCached: false,
370
+ isStale: false,
371
+ routePlan: null
372
+ }
373
+ };
374
+ }
375
+ }
376
+
377
+ // src/ws/client.ts
378
+ var CloseCode = {
379
+ /** 1000: Normal closure - connection completed successfully */
380
+ NORMAL: 1e3,
381
+ /** 1001: Going away - server/client shutting down. Client: reconnect immediately */
382
+ GOING_AWAY: 1001,
383
+ /** 1002: Protocol error - invalid frame format. Client: fix client code */
384
+ PROTOCOL_ERROR: 1002,
385
+ /** 1003: Unsupported data - message type not supported */
386
+ UNSUPPORTED_DATA: 1003,
387
+ /** 1005: No status received (reserved, not sent over wire) */
388
+ NO_STATUS: 1005,
389
+ /** 1006: Abnormal closure - connection dropped without close frame */
390
+ ABNORMAL: 1006,
391
+ /** 1007: Invalid payload - malformed UTF-8 or data. Client: fix message format */
392
+ INVALID_PAYLOAD: 1007,
393
+ /** 1008: Policy violation - rate limit exceeded, auth failed. Client: check credentials/limits */
394
+ POLICY_VIOLATION: 1008,
395
+ /** 1009: Message too big - message exceeds size limits */
396
+ MESSAGE_TOO_BIG: 1009,
397
+ /** 1010: Missing extension - required extension not negotiated */
398
+ MISSING_EXTENSION: 1010,
399
+ /** 1011: Internal error - unexpected server error. Client: retry with backoff */
400
+ INTERNAL_ERROR: 1011,
401
+ /** 1012: Service restart - server is restarting. Client: reconnect after brief delay */
402
+ SERVICE_RESTART: 1012,
403
+ /** 1013: Try again later - server overloaded. Client: retry with backoff */
404
+ TRY_AGAIN_LATER: 1013,
405
+ /** 1014: Bad gateway - upstream connection failed */
406
+ BAD_GATEWAY: 1014,
407
+ /** 1015: TLS handshake failed (reserved, not sent over wire) */
408
+ TLS_HANDSHAKE: 1015
409
+ };
410
+ var K256WebSocketError = class extends Error {
411
+ constructor(code, message, closeCode, closeReason, cause) {
412
+ super(message);
413
+ this.code = code;
414
+ this.closeCode = closeCode;
415
+ this.closeReason = closeReason;
416
+ this.cause = cause;
417
+ this.name = "K256WebSocketError";
418
+ }
419
+ /** Check if error is recoverable (should trigger reconnect) */
420
+ get isRecoverable() {
421
+ switch (this.code) {
422
+ case "AUTH_FAILED":
423
+ case "RATE_LIMITED":
424
+ return false;
425
+ default:
426
+ return true;
427
+ }
428
+ }
429
+ /** Check if error is an auth failure */
430
+ get isAuthError() {
431
+ return this.code === "AUTH_FAILED" || this.closeCode === CloseCode.POLICY_VIOLATION;
432
+ }
433
+ };
434
+ var K256WebSocketClient = class {
435
+ ws = null;
436
+ config;
437
+ _state = "disconnected";
438
+ reconnectAttempts = 0;
439
+ reconnectTimer = null;
440
+ pingTimer = null;
441
+ pongTimer = null;
442
+ heartbeatTimer = null;
443
+ lastPingTime = 0;
444
+ lastHeartbeatTime = 0;
445
+ pendingSubscription = null;
446
+ pendingQuoteSubscription = null;
447
+ isIntentionallyClosed = false;
448
+ /** Current connection state */
449
+ get state() {
450
+ return this._state;
451
+ }
452
+ /** Whether currently connected */
453
+ get isConnected() {
454
+ return this._state === "connected" && this.ws?.readyState === WebSocket.OPEN;
455
+ }
456
+ /** Time since last heartbeat (ms) or null if no heartbeat received */
457
+ get timeSinceHeartbeat() {
458
+ return this.lastHeartbeatTime ? Date.now() - this.lastHeartbeatTime : null;
459
+ }
460
+ /** Current reconnect attempt number */
461
+ get currentReconnectAttempt() {
462
+ return this.reconnectAttempts;
463
+ }
464
+ constructor(config) {
465
+ this.config = {
466
+ url: "wss://gateway.k256.xyz/v1/ws",
467
+ mode: "binary",
468
+ autoReconnect: true,
469
+ reconnectDelayMs: 1e3,
470
+ maxReconnectDelayMs: 3e4,
471
+ maxReconnectAttempts: Infinity,
472
+ pingIntervalMs: 3e4,
473
+ pongTimeoutMs: 1e4,
474
+ heartbeatTimeoutMs: 15e3,
475
+ ...config
476
+ };
477
+ }
478
+ /**
479
+ * Connect to the WebSocket server
480
+ * @returns Promise that resolves when connected
481
+ */
482
+ async connect() {
483
+ if (this._state === "connected" || this._state === "connecting") {
484
+ return;
485
+ }
486
+ this.isIntentionallyClosed = false;
487
+ return this.doConnect();
488
+ }
489
+ /**
490
+ * Disconnect from the WebSocket server
491
+ * @param code - Close code (default: 1000 NORMAL)
492
+ * @param reason - Close reason
493
+ */
494
+ disconnect(code = CloseCode.NORMAL, reason = "Client disconnect") {
495
+ this.isIntentionallyClosed = true;
496
+ this.cleanup();
497
+ if (this.ws) {
498
+ try {
499
+ this.ws.close(code, reason);
500
+ } catch {
501
+ }
502
+ this.ws = null;
503
+ }
504
+ this.setState("closed");
505
+ }
506
+ /**
507
+ * Subscribe to channels
508
+ */
509
+ subscribe(options) {
510
+ this.pendingSubscription = options;
511
+ if (!this.isConnected) {
512
+ return;
513
+ }
514
+ this.sendSubscription(options);
515
+ }
516
+ /**
517
+ * Subscribe to a quote stream
518
+ */
519
+ subscribeQuote(options) {
520
+ this.pendingQuoteSubscription = options;
521
+ if (!this.isConnected) {
522
+ return;
523
+ }
524
+ this.sendQuoteSubscription(options);
525
+ }
526
+ /**
527
+ * Unsubscribe from a quote stream
528
+ * @param topicId - Topic ID from quote_subscribed response
529
+ */
530
+ unsubscribeQuote(topicId) {
531
+ if (!this.isConnected) return;
532
+ const msg = JSON.stringify({ type: "unsubscribe_quote", topicId });
533
+ this.ws?.send(msg);
534
+ }
535
+ /**
536
+ * Unsubscribe from all channels
537
+ */
538
+ unsubscribe() {
539
+ this.pendingSubscription = null;
540
+ this.pendingQuoteSubscription = null;
541
+ if (!this.isConnected) return;
542
+ this.ws?.send(JSON.stringify({ type: "unsubscribe" }));
543
+ }
544
+ /**
545
+ * Send a ping to measure latency
546
+ */
547
+ ping() {
548
+ if (!this.isConnected) return;
549
+ const pingData = new Uint8Array([MessageType.Ping]);
550
+ this.lastPingTime = Date.now();
551
+ this.ws?.send(pingData);
552
+ this.startPongTimeout();
553
+ }
554
+ // ─────────────────────────────────────────────────────────────────────────────
555
+ // Private methods
556
+ // ─────────────────────────────────────────────────────────────────────────────
557
+ async doConnect() {
558
+ return new Promise((resolve, reject) => {
559
+ this.setState("connecting");
560
+ const url = new URL(this.config.url);
561
+ url.searchParams.set("apiKey", this.config.apiKey);
562
+ try {
563
+ this.ws = new WebSocket(url.toString());
564
+ if (this.config.mode === "binary") {
565
+ this.ws.binaryType = "arraybuffer";
566
+ }
567
+ const connectTimeout = setTimeout(() => {
568
+ if (this.ws?.readyState !== WebSocket.OPEN) {
569
+ this.ws?.close();
570
+ const error = new K256WebSocketError(
571
+ "CONNECTION_FAILED",
572
+ "Connection timeout"
573
+ );
574
+ this.handleError(error);
575
+ reject(error);
576
+ }
577
+ }, 1e4);
578
+ this.ws.onopen = () => {
579
+ clearTimeout(connectTimeout);
580
+ this.setState("connected");
581
+ this.reconnectAttempts = 0;
582
+ this.lastHeartbeatTime = Date.now();
583
+ this.startPingInterval();
584
+ this.startHeartbeatTimeout();
585
+ if (this.pendingSubscription) {
586
+ this.sendSubscription(this.pendingSubscription);
587
+ }
588
+ if (this.pendingQuoteSubscription) {
589
+ this.sendQuoteSubscription(this.pendingQuoteSubscription);
590
+ }
591
+ this.config.onConnect?.();
592
+ resolve();
593
+ };
594
+ this.ws.onclose = (event) => {
595
+ clearTimeout(connectTimeout);
596
+ this.cleanup();
597
+ const wasClean = event.wasClean;
598
+ const code = event.code;
599
+ const reason = event.reason || this.getCloseReason(code);
600
+ this.config.onDisconnect?.(code, reason, wasClean);
601
+ if (!this.isIntentionallyClosed && this.config.autoReconnect) {
602
+ if (this.shouldReconnect(code)) {
603
+ this.scheduleReconnect();
604
+ } else {
605
+ const error = new K256WebSocketError(
606
+ this.getErrorCodeFromClose(code),
607
+ reason,
608
+ code,
609
+ reason
610
+ );
611
+ this.handleError(error);
612
+ this.setState("closed");
613
+ }
614
+ } else {
615
+ this.setState("disconnected");
616
+ }
617
+ };
618
+ this.ws.onerror = () => {
619
+ };
620
+ this.ws.onmessage = (event) => {
621
+ this.handleMessage(event.data);
622
+ };
623
+ } catch (error) {
624
+ const wsError = new K256WebSocketError(
625
+ "CONNECTION_FAILED",
626
+ "Failed to create WebSocket",
627
+ void 0,
628
+ void 0,
629
+ error
630
+ );
631
+ this.handleError(wsError);
632
+ reject(wsError);
633
+ }
634
+ });
635
+ }
636
+ handleMessage(data) {
637
+ this.config.onRawMessage?.(data);
638
+ try {
639
+ let decoded = null;
640
+ if (data instanceof ArrayBuffer) {
641
+ decoded = decodeMessage(data);
642
+ if (decoded === null) {
643
+ const view = new DataView(data);
644
+ if (view.byteLength > 0 && view.getUint8(0) === MessageType.PoolUpdateBatch) {
645
+ const payload = data.slice(1);
646
+ const updates = decodePoolUpdateBatch(payload);
647
+ if (this.config.onPoolUpdateBatch) {
648
+ this.config.onPoolUpdateBatch(updates);
649
+ }
650
+ for (const update of updates) {
651
+ this.config.onPoolUpdate?.(update);
652
+ this.config.onMessage?.(update);
653
+ }
654
+ return;
655
+ }
656
+ }
657
+ } else {
658
+ const parsed = JSON.parse(data);
659
+ decoded = {
660
+ type: parsed.type,
661
+ data: parsed.data || parsed
662
+ };
663
+ }
664
+ if (!decoded) {
665
+ return;
666
+ }
667
+ this.config.onMessage?.(decoded);
668
+ switch (decoded.type) {
669
+ case "subscribed":
670
+ this.config.onSubscribed?.(decoded);
671
+ break;
672
+ case "pool_update":
673
+ this.config.onPoolUpdate?.(decoded);
674
+ break;
675
+ case "priority_fees":
676
+ this.config.onPriorityFees?.(decoded);
677
+ break;
678
+ case "blockhash":
679
+ this.config.onBlockhash?.(decoded);
680
+ break;
681
+ case "quote":
682
+ this.config.onQuote?.(decoded);
683
+ break;
684
+ case "quote_subscribed":
685
+ this.config.onQuoteSubscribed?.(decoded);
686
+ break;
687
+ case "heartbeat":
688
+ this.lastHeartbeatTime = Date.now();
689
+ this.resetHeartbeatTimeout();
690
+ this.config.onHeartbeat?.(decoded);
691
+ break;
692
+ case "pong":
693
+ this.clearPongTimeout();
694
+ const latencyMs = this.lastPingTime ? Date.now() - this.lastPingTime : 0;
695
+ this.config.onPong?.(latencyMs);
696
+ break;
697
+ case "error":
698
+ const errorData = decoded.data;
699
+ const error = new K256WebSocketError(
700
+ "SERVER_ERROR",
701
+ errorData.message
702
+ );
703
+ this.handleError(error);
704
+ break;
705
+ }
706
+ } catch (error) {
707
+ const wsError = new K256WebSocketError(
708
+ "INVALID_MESSAGE",
709
+ "Failed to decode message",
710
+ void 0,
711
+ void 0,
712
+ error
713
+ );
714
+ this.handleError(wsError);
715
+ }
716
+ }
717
+ sendSubscription(options) {
718
+ const msg = {
719
+ type: "subscribe",
720
+ channels: options.channels
721
+ };
722
+ if (this.config.mode === "json") {
723
+ msg.format = "json";
724
+ }
725
+ if (options.pools?.length) {
726
+ msg.pools = options.pools;
727
+ }
728
+ if (options.protocols?.length) {
729
+ msg.protocols = options.protocols;
730
+ }
731
+ if (options.tokenPairs?.length) {
732
+ msg.token_pairs = options.tokenPairs;
733
+ }
734
+ this.ws?.send(JSON.stringify(msg));
735
+ }
736
+ sendQuoteSubscription(options) {
737
+ const msg = {
738
+ type: "subscribe_quote",
739
+ inputMint: options.inputMint,
740
+ outputMint: options.outputMint,
741
+ amount: typeof options.amount === "string" ? parseInt(options.amount, 10) : options.amount,
742
+ slippageBps: options.slippageBps,
743
+ refreshIntervalMs: options.refreshIntervalMs ?? 1e3
744
+ };
745
+ this.ws?.send(JSON.stringify(msg));
746
+ }
747
+ setState(state) {
748
+ if (this._state !== state) {
749
+ const prevState = this._state;
750
+ this._state = state;
751
+ this.config.onStateChange?.(state, prevState);
752
+ }
753
+ }
754
+ handleError(error) {
755
+ this.config.onError?.(error);
756
+ }
757
+ cleanup() {
758
+ if (this.pingTimer) {
759
+ clearInterval(this.pingTimer);
760
+ this.pingTimer = null;
761
+ }
762
+ if (this.pongTimer) {
763
+ clearTimeout(this.pongTimer);
764
+ this.pongTimer = null;
765
+ }
766
+ if (this.heartbeatTimer) {
767
+ clearTimeout(this.heartbeatTimer);
768
+ this.heartbeatTimer = null;
769
+ }
770
+ if (this.reconnectTimer) {
771
+ clearTimeout(this.reconnectTimer);
772
+ this.reconnectTimer = null;
773
+ }
774
+ }
775
+ startPingInterval() {
776
+ if (this.pingTimer) {
777
+ clearInterval(this.pingTimer);
778
+ }
779
+ this.pingTimer = setInterval(() => {
780
+ this.ping();
781
+ }, this.config.pingIntervalMs);
782
+ }
783
+ startPongTimeout() {
784
+ this.clearPongTimeout();
785
+ this.pongTimer = setTimeout(() => {
786
+ const error = new K256WebSocketError(
787
+ "PING_TIMEOUT",
788
+ "Server did not respond to ping"
789
+ );
790
+ this.handleError(error);
791
+ this.ws?.close(CloseCode.GOING_AWAY, "Ping timeout");
792
+ }, this.config.pongTimeoutMs);
793
+ }
794
+ clearPongTimeout() {
795
+ if (this.pongTimer) {
796
+ clearTimeout(this.pongTimer);
797
+ this.pongTimer = null;
798
+ }
799
+ }
800
+ startHeartbeatTimeout() {
801
+ this.resetHeartbeatTimeout();
802
+ }
803
+ resetHeartbeatTimeout() {
804
+ if (this.heartbeatTimer) {
805
+ clearTimeout(this.heartbeatTimer);
806
+ }
807
+ this.heartbeatTimer = setTimeout(() => {
808
+ const error = new K256WebSocketError(
809
+ "HEARTBEAT_TIMEOUT",
810
+ "No heartbeat received from server"
811
+ );
812
+ this.handleError(error);
813
+ }, this.config.heartbeatTimeoutMs);
814
+ }
815
+ shouldReconnect(closeCode) {
816
+ if (closeCode === CloseCode.POLICY_VIOLATION) {
817
+ return false;
818
+ }
819
+ if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
820
+ return false;
821
+ }
822
+ return true;
823
+ }
824
+ scheduleReconnect() {
825
+ this.setState("reconnecting");
826
+ this.reconnectAttempts++;
827
+ const baseDelay = Math.min(
828
+ this.config.reconnectDelayMs * Math.pow(2, this.reconnectAttempts - 1),
829
+ this.config.maxReconnectDelayMs
830
+ );
831
+ const jitter = Math.random() * 0.3 * baseDelay;
832
+ const delay = Math.floor(baseDelay + jitter);
833
+ this.config.onReconnecting?.(this.reconnectAttempts, delay);
834
+ this.reconnectTimer = setTimeout(async () => {
835
+ try {
836
+ await this.doConnect();
837
+ } catch {
838
+ }
839
+ }, delay);
840
+ }
841
+ getCloseReason(code) {
842
+ switch (code) {
843
+ case CloseCode.NORMAL:
844
+ return "Normal closure";
845
+ case CloseCode.GOING_AWAY:
846
+ return "Server shutting down";
847
+ case CloseCode.PROTOCOL_ERROR:
848
+ return "Protocol error";
849
+ case CloseCode.UNSUPPORTED_DATA:
850
+ return "Unsupported message type";
851
+ case CloseCode.ABNORMAL:
852
+ return "Connection lost unexpectedly";
853
+ case CloseCode.INVALID_PAYLOAD:
854
+ return "Invalid message data";
855
+ case CloseCode.POLICY_VIOLATION:
856
+ return "Authentication failed or rate limited";
857
+ case CloseCode.MESSAGE_TOO_BIG:
858
+ return "Message too large";
859
+ case CloseCode.INTERNAL_ERROR:
860
+ return "Server error";
861
+ case CloseCode.SERVICE_RESTART:
862
+ return "Server is restarting";
863
+ case CloseCode.TRY_AGAIN_LATER:
864
+ return "Server overloaded";
865
+ default:
866
+ return `Unknown close code: ${code}`;
867
+ }
868
+ }
869
+ getErrorCodeFromClose(code) {
870
+ switch (code) {
871
+ case CloseCode.POLICY_VIOLATION:
872
+ return "AUTH_FAILED";
873
+ case CloseCode.INTERNAL_ERROR:
874
+ case CloseCode.SERVICE_RESTART:
875
+ case CloseCode.TRY_AGAIN_LATER:
876
+ return "SERVER_ERROR";
877
+ case CloseCode.PROTOCOL_ERROR:
878
+ case CloseCode.UNSUPPORTED_DATA:
879
+ case CloseCode.INVALID_PAYLOAD:
880
+ return "PROTOCOL_ERROR";
881
+ default:
882
+ return "CONNECTION_LOST";
883
+ }
884
+ }
885
+ };
886
+
887
+ export { CloseCode, K256WebSocketClient, K256WebSocketError, MessageType, decodeMessage, decodePoolUpdateBatch };
888
+ //# sourceMappingURL=index.js.map
889
+ //# sourceMappingURL=index.js.map