@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,490 @@
1
+ /**
2
+ * Binary message decoder for K256 WebSocket protocol
3
+ *
4
+ * Decodes binary messages from K2 server into typed JavaScript objects.
5
+ * Supports both single messages and batched pool updates.
6
+ *
7
+ * Wire format: [1 byte MessageType][N bytes Payload]
8
+ */
9
+
10
+ import { base58Encode } from '../utils/base58';
11
+ import { MessageType, type DecodedMessage, type PoolUpdateMessage } from './types';
12
+
13
+ /**
14
+ * Decode a binary WebSocket message from K2
15
+ *
16
+ * @param data - Raw binary data from WebSocket
17
+ * @returns Decoded message or null if unrecognized type
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * ws.onmessage = (event) => {
22
+ * if (event.data instanceof ArrayBuffer) {
23
+ * const message = decodeMessage(event.data);
24
+ * if (message?.type === 'pool_update') {
25
+ * console.log(message.data.poolAddress);
26
+ * }
27
+ * }
28
+ * };
29
+ * ```
30
+ */
31
+ export function decodeMessage(data: ArrayBuffer): DecodedMessage | null {
32
+ const view = new DataView(data);
33
+ if (data.byteLength < 1) return null;
34
+
35
+ const msgType = view.getUint8(0);
36
+ const payload = data.slice(1);
37
+ const payloadView = new DataView(payload);
38
+
39
+ switch (msgType) {
40
+ case MessageType.Subscribed:
41
+ case MessageType.QuoteSubscribed:
42
+ case MessageType.Heartbeat: {
43
+ // JSON payload
44
+ const decoder = new TextDecoder();
45
+ const text = decoder.decode(payload);
46
+ try {
47
+ let type: string;
48
+ if (msgType === MessageType.QuoteSubscribed) {
49
+ type = 'quote_subscribed';
50
+ } else if (msgType === MessageType.Heartbeat) {
51
+ type = 'heartbeat';
52
+ } else {
53
+ type = 'subscribed';
54
+ }
55
+ return {
56
+ type,
57
+ data: JSON.parse(text),
58
+ } as DecodedMessage;
59
+ } catch {
60
+ return { type: 'error', data: { message: text } };
61
+ }
62
+ }
63
+
64
+ case MessageType.Error: {
65
+ // UTF-8 string payload
66
+ const decoder = new TextDecoder();
67
+ const text = decoder.decode(payload);
68
+ return { type: 'error', data: { message: text } };
69
+ }
70
+
71
+ case MessageType.PriorityFees: {
72
+ // PriorityFeesWire bincode layout:
73
+ // Offset 0: slot: u64
74
+ // Offset 8: timestamp_ms: u64
75
+ // Offset 16: recommended: u64
76
+ // Offset 24: state: u8 (0=low, 1=normal, 2=high, 3=extreme)
77
+ // Offset 25: is_stale: bool
78
+ // Offset 26: swap_p50: u64
79
+ // Offset 34: swap_p75: u64
80
+ // Offset 42: swap_p90: u64
81
+ // Offset 50: swap_p99: u64
82
+ // Offset 58: swap_samples: u32
83
+ // Offset 62: landing_p50_fee: u64
84
+ // Offset 70: landing_p75_fee: u64
85
+ // Offset 78: landing_p90_fee: u64
86
+ // Offset 86: landing_p99_fee: u64
87
+ // Offset 94: top_10_fee: u64
88
+ // Offset 102: top_25_fee: u64
89
+ // Offset 110: spike_detected: bool
90
+ // Offset 111: spike_fee: u64
91
+ // Total: 119 bytes
92
+ if (payload.byteLength < 24) return null;
93
+
94
+ const slot = Number(payloadView.getBigUint64(0, true));
95
+ const timestampMs = Number(payloadView.getBigUint64(8, true));
96
+ const recommended = Number(payloadView.getBigUint64(16, true));
97
+ const state = payload.byteLength > 24 ? payloadView.getUint8(24) : 1;
98
+ const isStale = payload.byteLength > 25 ? payloadView.getUint8(25) !== 0 : false;
99
+
100
+ // Swap percentiles (offset 26-57)
101
+ let swapP50 = 0, swapP75 = 0, swapP90 = 0, swapP99 = 0;
102
+ if (payload.byteLength >= 58) {
103
+ swapP50 = Number(payloadView.getBigUint64(26, true));
104
+ swapP75 = Number(payloadView.getBigUint64(34, true));
105
+ swapP90 = Number(payloadView.getBigUint64(42, true));
106
+ swapP99 = Number(payloadView.getBigUint64(50, true));
107
+ }
108
+
109
+ // Extended fields (offset 58+)
110
+ let swapSamples = 0;
111
+ let landingP50Fee = 0, landingP75Fee = 0, landingP90Fee = 0, landingP99Fee = 0;
112
+ let top10Fee = 0, top25Fee = 0;
113
+ let spikeDetected = false, spikeFee = 0;
114
+
115
+ if (payload.byteLength >= 119) {
116
+ swapSamples = payloadView.getUint32(58, true);
117
+ landingP50Fee = Number(payloadView.getBigUint64(62, true));
118
+ landingP75Fee = Number(payloadView.getBigUint64(70, true));
119
+ landingP90Fee = Number(payloadView.getBigUint64(78, true));
120
+ landingP99Fee = Number(payloadView.getBigUint64(86, true));
121
+ top10Fee = Number(payloadView.getBigUint64(94, true));
122
+ top25Fee = Number(payloadView.getBigUint64(102, true));
123
+ spikeDetected = payloadView.getUint8(110) !== 0;
124
+ spikeFee = Number(payloadView.getBigUint64(111, true));
125
+ }
126
+
127
+ return {
128
+ type: 'priority_fees',
129
+ data: {
130
+ slot,
131
+ timestampMs,
132
+ recommended,
133
+ state,
134
+ isStale,
135
+ swapP50,
136
+ swapP75,
137
+ swapP90,
138
+ swapP99,
139
+ swapSamples,
140
+ landingP50Fee,
141
+ landingP75Fee,
142
+ landingP90Fee,
143
+ landingP99Fee,
144
+ top10Fee,
145
+ top25Fee,
146
+ spikeDetected,
147
+ spikeFee,
148
+ },
149
+ };
150
+ }
151
+
152
+ case MessageType.Blockhash: {
153
+ // BlockhashWire bincode layout
154
+ if (payload.byteLength < 65) return null;
155
+
156
+ const slot = Number(payloadView.getBigUint64(0, true));
157
+ const timestampMs = Number(payloadView.getBigUint64(8, true));
158
+ const blockhashBytes = new Uint8Array(payload, 16, 32);
159
+ const blockHeight = Number(payloadView.getBigUint64(48, true));
160
+ const lastValidBlockHeight = Number(payloadView.getBigUint64(56, true));
161
+ const isStale = payloadView.getUint8(64) !== 0;
162
+
163
+ return {
164
+ type: 'blockhash',
165
+ data: {
166
+ slot,
167
+ timestampMs,
168
+ blockhash: base58Encode(blockhashBytes),
169
+ blockHeight,
170
+ lastValidBlockHeight,
171
+ isStale,
172
+ },
173
+ };
174
+ }
175
+
176
+ case MessageType.PoolUpdate: {
177
+ return decodePoolUpdate(payload, payloadView);
178
+ }
179
+
180
+ case MessageType.PoolUpdateBatch: {
181
+ // Batched pool updates: [u16 count][u32 len][payload]...
182
+ // Returns array of individual updates
183
+ if (payload.byteLength < 2) return null;
184
+
185
+ const updates = decodePoolUpdateBatch(payload);
186
+ if (updates.length === 0) return null;
187
+
188
+ // Return first update for single-message interface
189
+ // Use decodePoolUpdateBatch() directly for batch handling
190
+ return updates[0];
191
+ }
192
+
193
+ case MessageType.Quote: {
194
+ return decodeQuote(payload, payloadView);
195
+ }
196
+
197
+ case MessageType.Pong: {
198
+ if (payload.byteLength < 8) return null;
199
+ return {
200
+ type: 'pong',
201
+ data: {
202
+ timestampMs: Number(payloadView.getBigUint64(0, true)),
203
+ },
204
+ };
205
+ }
206
+
207
+ default:
208
+ return null;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Decode a batch of pool updates
214
+ *
215
+ * Use this when you need to process all updates in a batch.
216
+ * For high-throughput scenarios, batches can contain 50-200 updates.
217
+ *
218
+ * @param data - Raw payload (without the 0x0E type prefix)
219
+ * @returns Array of decoded pool updates
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * if (msgType === MessageType.PoolUpdateBatch) {
224
+ * const updates = decodePoolUpdateBatch(payload);
225
+ * for (const update of updates) {
226
+ * console.log(update.data.poolAddress);
227
+ * }
228
+ * }
229
+ * ```
230
+ */
231
+ export function decodePoolUpdateBatch(payload: ArrayBuffer): PoolUpdateMessage[] {
232
+ const view = new DataView(payload);
233
+ if (payload.byteLength < 2) return [];
234
+
235
+ const count = view.getUint16(0, true);
236
+ const updates: PoolUpdateMessage[] = [];
237
+ let offset = 2;
238
+
239
+ for (let i = 0; i < count && offset + 4 <= payload.byteLength; i++) {
240
+ const payloadLen = view.getUint32(offset, true);
241
+ offset += 4;
242
+
243
+ if (offset + payloadLen > payload.byteLength) break;
244
+
245
+ // Decode individual pool update (payload is WITHOUT the 0x01 type prefix)
246
+ const updatePayload = payload.slice(offset, offset + payloadLen);
247
+ const updateView = new DataView(updatePayload);
248
+ const decoded = decodePoolUpdate(updatePayload, updateView);
249
+
250
+ if (decoded) {
251
+ updates.push(decoded);
252
+ }
253
+
254
+ offset += payloadLen;
255
+ }
256
+
257
+ return updates;
258
+ }
259
+
260
+ /**
261
+ * Decode a single pool update payload
262
+ */
263
+ function decodePoolUpdate(payload: ArrayBuffer, payloadView: DataView): PoolUpdateMessage | null {
264
+ if (payload.byteLength < 50) return null;
265
+
266
+ try {
267
+ let offset = 0;
268
+ const decoder = new TextDecoder();
269
+
270
+ // Skip serialized_state (Bytes: u64 len + bytes)
271
+ const serializedStateLen = Number(payloadView.getBigUint64(offset, true));
272
+ offset += 8 + serializedStateLen;
273
+
274
+ if (payload.byteLength < offset + 24) return null;
275
+
276
+ // sequence (u64)
277
+ const sequence = Number(payloadView.getBigUint64(offset, true));
278
+ offset += 8;
279
+
280
+ // slot (u64)
281
+ const slot = Number(payloadView.getBigUint64(offset, true));
282
+ offset += 8;
283
+
284
+ // write_version (u64)
285
+ const writeVersion = Number(payloadView.getBigUint64(offset, true));
286
+ offset += 8;
287
+
288
+ // protocol_name (String: u64 len + utf8 bytes)
289
+ const protocolLen = Number(payloadView.getBigUint64(offset, true));
290
+ offset += 8;
291
+ const protocolBytes = new Uint8Array(payload, offset, protocolLen);
292
+ const protocol = decoder.decode(protocolBytes);
293
+ offset += protocolLen;
294
+
295
+ // pool_address ([u8; 32])
296
+ const poolAddr = new Uint8Array(payload, offset, 32);
297
+ offset += 32;
298
+
299
+ // all_token_mints (Vec<[u8; 32]>)
300
+ const tokenMintCount = Number(payloadView.getBigUint64(offset, true));
301
+ offset += 8;
302
+ const tokenMints: string[] = [];
303
+ for (let i = 0; i < tokenMintCount && offset + 32 <= payload.byteLength; i++) {
304
+ const mint = new Uint8Array(payload, offset, 32);
305
+ tokenMints.push(base58Encode(mint));
306
+ offset += 32;
307
+ }
308
+
309
+ // all_token_balances (Vec<u64>)
310
+ const balanceCount = Number(payloadView.getBigUint64(offset, true));
311
+ offset += 8;
312
+ const tokenBalances: string[] = [];
313
+ for (let i = 0; i < balanceCount && offset + 8 <= payload.byteLength; i++) {
314
+ tokenBalances.push(payloadView.getBigUint64(offset, true).toString());
315
+ offset += 8;
316
+ }
317
+
318
+ // all_token_decimals (Vec<i32>)
319
+ const decimalsCount = Number(payloadView.getBigUint64(offset, true));
320
+ offset += 8;
321
+ const tokenDecimals: number[] = [];
322
+ for (let i = 0; i < decimalsCount && offset + 4 <= payload.byteLength; i++) {
323
+ tokenDecimals.push(payloadView.getInt32(offset, true));
324
+ offset += 4;
325
+ }
326
+
327
+ // is_valid (bool)
328
+ const isValid = offset < payload.byteLength ? payloadView.getUint8(offset) !== 0 : true;
329
+ offset += 1;
330
+
331
+ // best_bid and best_ask (Option<OrderLevel>) - skip for now
332
+ // They use bincode Option encoding: 0 = None, 1 + data = Some
333
+
334
+ return {
335
+ type: 'pool_update',
336
+ data: {
337
+ sequence,
338
+ slot,
339
+ writeVersion,
340
+ protocol,
341
+ poolAddress: base58Encode(poolAddr),
342
+ tokenMints,
343
+ tokenBalances,
344
+ tokenDecimals,
345
+ isValid,
346
+ },
347
+ };
348
+ } catch {
349
+ return {
350
+ type: 'pool_update',
351
+ data: {
352
+ sequence: 0,
353
+ slot: 0,
354
+ writeVersion: 0,
355
+ protocol: 'unknown',
356
+ poolAddress: '',
357
+ tokenMints: [],
358
+ tokenBalances: [],
359
+ tokenDecimals: [],
360
+ isValid: false,
361
+ },
362
+ };
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Decode a quote message payload
368
+ */
369
+ function decodeQuote(payload: ArrayBuffer, payloadView: DataView): DecodedMessage | null {
370
+ if (payload.byteLength < 8) return null;
371
+
372
+ try {
373
+ let offset = 0;
374
+ const decoder = new TextDecoder();
375
+
376
+ // Helper to read bincode String (u64 len + UTF-8 bytes)
377
+ const readString = (): string => {
378
+ const len = Number(payloadView.getBigUint64(offset, true));
379
+ offset += 8;
380
+ const bytes = new Uint8Array(payload, offset, len);
381
+ offset += len;
382
+ return decoder.decode(bytes);
383
+ };
384
+
385
+ // topic_id (String)
386
+ const topicId = readString();
387
+
388
+ // timestamp_ms (u64)
389
+ const timestampMs = Number(payloadView.getBigUint64(offset, true));
390
+ offset += 8;
391
+
392
+ // sequence (u64)
393
+ const sequence = Number(payloadView.getBigUint64(offset, true));
394
+ offset += 8;
395
+
396
+ // input_mint ([u8; 32])
397
+ const inputMintBytes = new Uint8Array(payload, offset, 32);
398
+ offset += 32;
399
+
400
+ // output_mint ([u8; 32])
401
+ const outputMintBytes = new Uint8Array(payload, offset, 32);
402
+ offset += 32;
403
+
404
+ // in_amount (u64)
405
+ const inAmount = payloadView.getBigUint64(offset, true).toString();
406
+ offset += 8;
407
+
408
+ // out_amount (u64)
409
+ const outAmount = payloadView.getBigUint64(offset, true).toString();
410
+ offset += 8;
411
+
412
+ // price_impact_bps (i32)
413
+ const priceImpactBps = payloadView.getInt32(offset, true);
414
+ offset += 4;
415
+
416
+ // context_slot (u64)
417
+ const contextSlot = Number(payloadView.getBigUint64(offset, true));
418
+ offset += 8;
419
+
420
+ // algorithm (String)
421
+ const algorithm = readString();
422
+
423
+ // is_improvement (bool)
424
+ const isImprovement = payloadView.getUint8(offset) !== 0;
425
+ offset += 1;
426
+
427
+ // is_cached (bool)
428
+ const isCached = payloadView.getUint8(offset) !== 0;
429
+ offset += 1;
430
+
431
+ // is_stale (bool)
432
+ const isStale = payloadView.getUint8(offset) !== 0;
433
+ offset += 1;
434
+
435
+ // route_plan_json (Vec<u8> - bincode: u64 len + bytes)
436
+ let routePlan = null;
437
+ if (offset + 8 <= payload.byteLength) {
438
+ const routePlanLen = Number(payloadView.getBigUint64(offset, true));
439
+ offset += 8;
440
+ if (routePlanLen > 0 && offset + routePlanLen <= payload.byteLength) {
441
+ const routePlanBytes = new Uint8Array(payload, offset, routePlanLen);
442
+ try {
443
+ routePlan = JSON.parse(decoder.decode(routePlanBytes));
444
+ } catch {
445
+ // Route plan JSON parsing failed, leave as null
446
+ }
447
+ }
448
+ }
449
+
450
+ return {
451
+ type: 'quote',
452
+ data: {
453
+ topicId,
454
+ timestampMs,
455
+ sequence,
456
+ inputMint: base58Encode(inputMintBytes),
457
+ outputMint: base58Encode(outputMintBytes),
458
+ inAmount,
459
+ outAmount,
460
+ priceImpactBps,
461
+ contextSlot,
462
+ algorithm,
463
+ isImprovement,
464
+ isCached,
465
+ isStale,
466
+ routePlan,
467
+ },
468
+ };
469
+ } catch {
470
+ return {
471
+ type: 'quote',
472
+ data: {
473
+ topicId: '',
474
+ timestampMs: 0,
475
+ sequence: 0,
476
+ inputMint: '',
477
+ outputMint: '',
478
+ inAmount: '0',
479
+ outAmount: '0',
480
+ priceImpactBps: 0,
481
+ contextSlot: 0,
482
+ algorithm: '',
483
+ isImprovement: false,
484
+ isCached: false,
485
+ isStale: false,
486
+ routePlan: null,
487
+ },
488
+ };
489
+ }
490
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * WebSocket module for K256 SDK
3
+ *
4
+ * Provides production-grade WebSocket client with:
5
+ * - Binary and JSON mode support
6
+ * - Automatic reconnection with exponential backoff
7
+ * - Ping/pong keepalive
8
+ * - Full error handling with RFC 6455 close codes
9
+ *
10
+ * @module @k256/sdk/ws
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { K256WebSocketClient } from '@k256/sdk/ws';
15
+ *
16
+ * const client = new K256WebSocketClient({
17
+ * apiKey: 'your-api-key',
18
+ * mode: 'binary',
19
+ * onPoolUpdate: (update) => console.log(update),
20
+ * });
21
+ *
22
+ * await client.connect();
23
+ * client.subscribe({ channels: ['pools'] });
24
+ * ```
25
+ */
26
+
27
+ // Client
28
+ export { K256WebSocketClient, K256WebSocketError, CloseCode } from './client';
29
+ export type {
30
+ K256WebSocketClientConfig,
31
+ K256ErrorCode,
32
+ CloseCodeValue,
33
+ ConnectionState,
34
+ SubscribeOptions,
35
+ SubscribeQuoteOptions,
36
+ } from './client';
37
+
38
+ // Decoder (for advanced usage)
39
+ export { decodeMessage, decodePoolUpdateBatch } from './decoder';
40
+
41
+ // Types
42
+ export { MessageType } from './types';
43
+ export type {
44
+ MessageTypeValue,
45
+ DecodedMessage,
46
+ PoolUpdateMessage,
47
+ PriorityFeesMessage,
48
+ BlockhashMessage,
49
+ QuoteMessage,
50
+ HeartbeatMessage,
51
+ ErrorMessage,
52
+ SubscribedMessage,
53
+ QuoteSubscribedMessage,
54
+ PongMessage,
55
+ } from './types';