@k256/sdk 0.3.2 → 0.5.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.
package/src/ws/client.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import { decodeMessage, decodePoolUpdateBatch } from './decoder';
27
- import { MessageType, type DecodedMessage, type PoolUpdateMessage, type BlockStatsMessage } from './types';
27
+ import { MessageType, type DecodedMessage, type PoolUpdateMessage, type BlockStatsMessage, type PriceUpdateMessage, type PriceBatchMessage, type PriceSnapshotMessage } from './types';
28
28
 
29
29
  /**
30
30
  * RFC 6455 WebSocket Close Codes
@@ -105,6 +105,16 @@ export interface SubscribeQuoteOptions {
105
105
  refreshIntervalMs?: number;
106
106
  }
107
107
 
108
+ /**
109
+ * Price subscription options
110
+ */
111
+ export interface SubscribePriceOptions {
112
+ /** Token mint addresses to track */
113
+ tokens: string[];
114
+ /** Minimum bps change to receive an update (default 10) */
115
+ thresholdBps?: number;
116
+ }
117
+
108
118
  /**
109
119
  * WebSocket client configuration
110
120
  */
@@ -163,6 +173,12 @@ export interface K256WebSocketClientConfig {
163
173
  onQuote?: (data: DecodedMessage & { type: 'quote' }) => void;
164
174
  /** Called on quote subscription confirmed */
165
175
  onQuoteSubscribed?: (data: DecodedMessage & { type: 'quote_subscribed' }) => void;
176
+ /** Called on single price update */
177
+ onPriceUpdate?: (message: PriceUpdateMessage) => void;
178
+ /** Called on batched price updates */
179
+ onPriceBatch?: (message: PriceBatchMessage) => void;
180
+ /** Called on initial price snapshot (after subscribe) */
181
+ onPriceSnapshot?: (message: PriceSnapshotMessage) => void;
166
182
  /** Called on heartbeat */
167
183
  onHeartbeat?: (data: DecodedMessage & { type: 'heartbeat' }) => void;
168
184
  /** Called on pong response (with round-trip latency) */
@@ -228,8 +244,8 @@ export class K256WebSocketClient {
228
244
  private config: Required<Omit<K256WebSocketClientConfig,
229
245
  'onStateChange' | 'onConnect' | 'onDisconnect' | 'onReconnecting' | 'onError' |
230
246
  'onSubscribed' | 'onPoolUpdate' | 'onPoolUpdateBatch' | 'onFeeMarket' | 'onBlockStats' |
231
- 'onBlockhash' | 'onQuote' | 'onQuoteSubscribed' | 'onHeartbeat' | 'onPong' |
232
- 'onMessage' | 'onRawMessage'
247
+ 'onBlockhash' | 'onQuote' | 'onQuoteSubscribed' | 'onPriceUpdate' | 'onPriceBatch' |
248
+ 'onPriceSnapshot' | 'onHeartbeat' | 'onPong' | 'onMessage' | 'onRawMessage'
233
249
  >> & K256WebSocketClientConfig;
234
250
 
235
251
  private _state: ConnectionState = 'disconnected';
@@ -242,6 +258,7 @@ export class K256WebSocketClient {
242
258
  private lastHeartbeatTime = 0;
243
259
  private pendingSubscription: SubscribeOptions | null = null;
244
260
  private pendingQuoteSubscription: SubscribeQuoteOptions | null = null;
261
+ private pendingPriceSubscription: SubscribePriceOptions | null = null;
245
262
  private isIntentionallyClosed = false;
246
263
 
247
264
  /** Current connection state */
@@ -351,12 +368,39 @@ export class K256WebSocketClient {
351
368
  this.ws?.send(msg);
352
369
  }
353
370
 
371
+ /**
372
+ * Subscribe to real-time price updates
373
+ * @param options - Token mints and threshold configuration
374
+ */
375
+ subscribePrices(options: SubscribePriceOptions): void {
376
+ this.pendingPriceSubscription = options;
377
+
378
+ if (!this.isConnected) {
379
+ return;
380
+ }
381
+
382
+ this.sendPriceSubscription(options);
383
+ }
384
+
385
+ /**
386
+ * Unsubscribe from price updates
387
+ */
388
+ unsubscribePrices(): void {
389
+ this.pendingPriceSubscription = null;
390
+
391
+ if (!this.isConnected) return;
392
+
393
+ const buf = new Uint8Array([MessageType.UnsubscribePrice]);
394
+ this.ws?.send(buf);
395
+ }
396
+
354
397
  /**
355
398
  * Unsubscribe from all channels
356
399
  */
357
400
  unsubscribe(): void {
358
401
  this.pendingSubscription = null;
359
402
  this.pendingQuoteSubscription = null;
403
+ this.pendingPriceSubscription = null;
360
404
 
361
405
  if (!this.isConnected) return;
362
406
 
@@ -427,6 +471,9 @@ export class K256WebSocketClient {
427
471
  if (this.pendingQuoteSubscription) {
428
472
  this.sendQuoteSubscription(this.pendingQuoteSubscription);
429
473
  }
474
+ if (this.pendingPriceSubscription) {
475
+ this.sendPriceSubscription(this.pendingPriceSubscription);
476
+ }
430
477
 
431
478
  this.config.onConnect?.();
432
479
  resolve();
@@ -553,6 +600,21 @@ export class K256WebSocketClient {
553
600
  case 'quote_subscribed':
554
601
  this.config.onQuoteSubscribed?.(decoded as DecodedMessage & { type: 'quote_subscribed' });
555
602
  break;
603
+ case 'price_update':
604
+ this.config.onPriceUpdate?.(decoded as PriceUpdateMessage);
605
+ break;
606
+ case 'price_batch':
607
+ this.config.onPriceBatch?.(decoded as PriceBatchMessage);
608
+ for (const entry of (decoded as PriceBatchMessage).data) {
609
+ this.config.onPriceUpdate?.({ type: 'price_update', data: entry });
610
+ }
611
+ break;
612
+ case 'price_snapshot':
613
+ this.config.onPriceSnapshot?.(decoded as PriceSnapshotMessage);
614
+ for (const entry of (decoded as PriceSnapshotMessage).data) {
615
+ this.config.onPriceUpdate?.({ type: 'price_update', data: entry });
616
+ }
617
+ break;
556
618
  case 'heartbeat':
557
619
  this.lastHeartbeatTime = Date.now();
558
620
  this.resetHeartbeatTimeout();
@@ -622,6 +684,19 @@ export class K256WebSocketClient {
622
684
  this.ws?.send(JSON.stringify(msg));
623
685
  }
624
686
 
687
+ private sendPriceSubscription(options: SubscribePriceOptions): void {
688
+ const json = JSON.stringify({
689
+ tokens: options.tokens,
690
+ thresholdBps: options.thresholdBps ?? 10,
691
+ });
692
+ const encoder = new TextEncoder();
693
+ const jsonBytes = encoder.encode(json);
694
+ const buf = new Uint8Array(1 + jsonBytes.length);
695
+ buf[0] = MessageType.SubscribePrice;
696
+ buf.set(jsonBytes, 1);
697
+ this.ws?.send(buf);
698
+ }
699
+
625
700
  private setState(state: ConnectionState): void {
626
701
  if (this._state !== state) {
627
702
  const prevState = this._state;
package/src/ws/decoder.ts CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { base58Encode } from '../utils/base58';
11
11
  import type { BlockMiniStats, TrendDirection } from '../types';
12
- import { MessageType, type DecodedMessage, type PoolUpdateMessage, type FeeMarketMessage } from './types';
12
+ import { MessageType, type DecodedMessage, type PoolUpdateMessage, type FeeMarketMessage, type PriceEntry, type PriceBatchMessage, type PriceSnapshotMessage } from './types';
13
13
 
14
14
  /**
15
15
  * Decode a binary WebSocket message from K2
@@ -232,11 +232,66 @@ export function decodeMessage(data: ArrayBuffer): DecodedMessage | null {
232
232
  };
233
233
  }
234
234
 
235
+ case MessageType.PriceUpdate: {
236
+ // Single price entry (56 bytes, no count prefix):
237
+ // [mint:32B][usd_price:u64 LE][slot:u64 LE][timestamp_ms:u64 LE]
238
+ // K2 currently doesn't send 0x11, but handle for forward compat.
239
+ if (payload.byteLength < 56) return null;
240
+ const mintBytes = new Uint8Array(payload, 0, 32);
241
+ return {
242
+ type: 'price_update',
243
+ data: {
244
+ mint: base58Encode(mintBytes),
245
+ usdPrice: Number(payloadView.getBigUint64(32, true)) / 1e12,
246
+ slot: Number(payloadView.getBigUint64(40, true)),
247
+ timestampMs: Number(payloadView.getBigUint64(48, true)),
248
+ },
249
+ };
250
+ }
251
+
252
+ case MessageType.PriceBatch:
253
+ case MessageType.PriceSnapshot: {
254
+ // Both use identical binary format from serialize_price_message():
255
+ // [count:u16 LE][entry₁:56B]...[entryₙ:56B]
256
+ // Each entry: [mint:32B][usd_price:u64 LE][slot:u64 LE][timestamp_ms:u64 LE]
257
+ if (payload.byteLength < 2) return null;
258
+ const entries = decodePriceEntries(payload);
259
+ const priceType = msgType === MessageType.PriceSnapshot ? 'price_snapshot' : 'price_batch';
260
+ return { type: priceType, data: entries } as PriceBatchMessage | PriceSnapshotMessage;
261
+ }
262
+
235
263
  default:
236
264
  return null;
237
265
  }
238
266
  }
239
267
 
268
+ /**
269
+ * Decode price entries from a PriceBatch or PriceSnapshot payload.
270
+ *
271
+ * Wire format: [count:u16 LE][entry₁:56B]...[entryₙ:56B]
272
+ */
273
+ export function decodePriceEntries(payload: ArrayBuffer): PriceEntry[] {
274
+ const view = new DataView(payload);
275
+ if (payload.byteLength < 2) return [];
276
+
277
+ const count = view.getUint16(0, true);
278
+ const entries: PriceEntry[] = [];
279
+ let offset = 2;
280
+
281
+ for (let i = 0; i < count && offset + 56 <= payload.byteLength; i++) {
282
+ const mintBytes = new Uint8Array(payload, offset, 32);
283
+ entries.push({
284
+ mint: base58Encode(mintBytes),
285
+ usdPrice: Number(view.getBigUint64(offset + 32, true)) / 1e12,
286
+ slot: Number(view.getBigUint64(offset + 40, true)),
287
+ timestampMs: Number(view.getBigUint64(offset + 48, true)),
288
+ });
289
+ offset += 56;
290
+ }
291
+
292
+ return entries;
293
+ }
294
+
240
295
  /**
241
296
  * Decode a batch of pool updates
242
297
  *
package/src/ws/index.ts CHANGED
@@ -33,10 +33,11 @@ export type {
33
33
  ConnectionState,
34
34
  SubscribeOptions,
35
35
  SubscribeQuoteOptions,
36
+ SubscribePriceOptions,
36
37
  } from './client';
37
38
 
38
39
  // Decoder (for advanced usage)
39
- export { decodeMessage, decodePoolUpdateBatch } from './decoder';
40
+ export { decodeMessage, decodePoolUpdateBatch, decodePriceEntries } from './decoder';
40
41
 
41
42
  // Types
42
43
  export { MessageType } from './types';
@@ -48,6 +49,10 @@ export type {
48
49
  BlockStatsMessage,
49
50
  BlockhashMessage,
50
51
  QuoteMessage,
52
+ PriceEntry,
53
+ PriceUpdateMessage,
54
+ PriceBatchMessage,
55
+ PriceSnapshotMessage,
51
56
  HeartbeatMessage,
52
57
  ErrorMessage,
53
58
  SubscribedMessage,
package/src/ws/types.ts CHANGED
@@ -44,6 +44,16 @@ export const MessageType = {
44
44
  PoolUpdateBatch: 0x0e,
45
45
  /** Block-level statistics (v3) */
46
46
  BlockStats: 0x0f,
47
+ /** Subscribe to price updates (JSON) - Client → Server */
48
+ SubscribePrice: 0x10,
49
+ /** Single price update (bincode 56B) - Server → Client */
50
+ PriceUpdate: 0x11,
51
+ /** Batched price updates (bincode [u16 count][entries...]) - Server → Client */
52
+ PriceBatch: 0x12,
53
+ /** Initial price snapshot (same binary format as PriceBatch) - Server → Client */
54
+ PriceSnapshot: 0x13,
55
+ /** Unsubscribe from prices (no payload) - Client → Server */
56
+ UnsubscribePrice: 0x14,
47
57
  /** Error message (UTF-8 string) */
48
58
  Error: 0xff,
49
59
  } as const;
@@ -214,6 +224,42 @@ export interface QuoteSubscribedMessage {
214
224
  };
215
225
  }
216
226
 
227
+ /**
228
+ * Single decoded price entry (shared by PriceUpdate, PriceBatch, PriceSnapshot)
229
+ */
230
+ export interface PriceEntry {
231
+ mint: string;
232
+ usdPrice: number;
233
+ slot: number;
234
+ timestampMs: number;
235
+ }
236
+
237
+ /**
238
+ * Single price update from binary message (0x11)
239
+ * Note: K2 currently sends all updates via PriceBatch (0x12)
240
+ */
241
+ export interface PriceUpdateMessage {
242
+ type: 'price_update';
243
+ data: PriceEntry;
244
+ }
245
+
246
+ /**
247
+ * Batched price updates from binary message (0x12)
248
+ */
249
+ export interface PriceBatchMessage {
250
+ type: 'price_batch';
251
+ data: PriceEntry[];
252
+ }
253
+
254
+ /**
255
+ * Initial price snapshot from binary message (0x13)
256
+ * Same binary format as PriceBatch — sent once after subscribe_price
257
+ */
258
+ export interface PriceSnapshotMessage {
259
+ type: 'price_snapshot';
260
+ data: PriceEntry[];
261
+ }
262
+
217
263
  /**
218
264
  * Error message from server
219
265
  */
@@ -243,6 +289,9 @@ export type DecodedMessage =
243
289
  | BlockStatsMessage
244
290
  | BlockhashMessage
245
291
  | QuoteMessage
292
+ | PriceUpdateMessage
293
+ | PriceBatchMessage
294
+ | PriceSnapshotMessage
246
295
  | HeartbeatMessage
247
296
  | SubscribedMessage
248
297
  | QuoteSubscribedMessage