@k256/sdk 0.1.6 → 0.2.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/decoder.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import { base58Encode } from '../utils/base58';
11
- import { MessageType, type DecodedMessage, type PoolUpdateMessage } from './types';
11
+ import type { BlockMiniStats, TrendDirection } from '../types';
12
+ import { MessageType, type DecodedMessage, type PoolUpdateMessage, type FeeMarketMessage } from './types';
12
13
 
13
14
  /**
14
15
  * Decode a binary WebSocket message from K2
@@ -69,84 +70,78 @@ export function decodeMessage(data: ArrayBuffer): DecodedMessage | null {
69
70
  }
70
71
 
71
72
  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;
73
+ // FeeMarketWire bincode layout (per-writable-account model):
74
+ // Header (42 bytes):
75
+ // Offset 0: slot: u64 (8 bytes)
76
+ // Offset 8: timestamp_ms: u64 (8 bytes)
77
+ // Offset 16: recommended: u64 (8 bytes)
78
+ // Offset 24: state: u8 (1 byte)
79
+ // Offset 25: is_stale: bool (1 byte)
80
+ // Offset 26: block_utilization_pct: f32 (4 bytes)
81
+ // Offset 30: blocks_in_window: u32 (4 bytes)
82
+ // Offset 34: account_count: u64 (8 bytes) [bincode Vec length]
83
+ // Per account (92 bytes each):
84
+ // pubkey: [u8; 32], total_txs: u32, active_slots: u32,
85
+ // cu_consumed: u64, utilization_pct: f32,
86
+ // p25: u64, p50: u64, p75: u64, p90: u64, min_nonzero_price: u64
87
+ if (payload.byteLength < 42) return null;
93
88
 
94
89
  const slot = Number(payloadView.getBigUint64(0, true));
95
90
  const timestampMs = Number(payloadView.getBigUint64(8, true));
96
91
  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));
92
+ const state = payloadView.getUint8(24);
93
+ const isStale = payloadView.getUint8(25) !== 0;
94
+ const blockUtilizationPct = payloadView.getFloat32(26, true);
95
+ const blocksInWindow = payloadView.getUint32(30, true);
96
+ const accountCount = Number(payloadView.getBigUint64(34, true));
97
+
98
+ const accounts: FeeMarketMessage['data']['accounts'] = [];
99
+ let offset = 42;
100
+ for (let i = 0; i < accountCount && offset + 92 <= payload.byteLength; i++) {
101
+ const pubkeyBytes = new Uint8Array(payload, offset, 32);
102
+ const pubkey = base58Encode(pubkeyBytes);
103
+ const totalTxs = payloadView.getUint32(offset + 32, true);
104
+ const activeSlots = payloadView.getUint32(offset + 36, true);
105
+ const cuConsumed = Number(payloadView.getBigUint64(offset + 40, true));
106
+ const utilizationPct = payloadView.getFloat32(offset + 48, true);
107
+ const p25 = Number(payloadView.getBigUint64(offset + 52, true));
108
+ const p50 = Number(payloadView.getBigUint64(offset + 60, true));
109
+ const p75 = Number(payloadView.getBigUint64(offset + 68, true));
110
+ const p90 = Number(payloadView.getBigUint64(offset + 76, true));
111
+ const minNonzeroPrice = Number(payloadView.getBigUint64(offset + 84, true));
112
+
113
+ accounts.push({
114
+ pubkey, totalTxs, activeSlots, cuConsumed, utilizationPct,
115
+ p25, p50, p75, p90, minNonzeroPrice,
116
+ });
117
+ offset += 92;
107
118
  }
108
119
 
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));
120
+ // Decode recent_blocks (Vec<BlockMiniStats>) — v3
121
+ const recentBlocksCount = Number(payloadView.getBigUint64(offset, true));
122
+ offset += 8;
123
+ const recentBlocks: BlockMiniStats[] = [];
124
+ for (let i = 0; i < recentBlocksCount; i++) {
125
+ const rbSlot = Number(payloadView.getBigUint64(offset, true)); offset += 8;
126
+ const rbCuConsumed = Number(payloadView.getBigUint64(offset, true)); offset += 8;
127
+ const rbTxCount = payloadView.getUint32(offset, true); offset += 4;
128
+ const rbUtilizationPct = payloadView.getFloat32(offset, true); offset += 4;
129
+ const rbAvgCuPrice = Number(payloadView.getBigUint64(offset, true)); offset += 8;
130
+ recentBlocks.push({ slot: rbSlot, cuConsumed: rbCuConsumed, txCount: rbTxCount, utilizationPct: rbUtilizationPct, avgCuPrice: rbAvgCuPrice });
125
131
  }
126
132
 
133
+ // Decode trend (u8) — v3
134
+ const trendByte = payloadView.getUint8(offset); offset += 1;
135
+ const trend: TrendDirection = trendByte === 0 ? 'rising' : trendByte === 1 ? 'falling' : 'stable';
136
+
127
137
  return {
128
- type: 'priority_fees',
138
+ type: 'fee_market',
129
139
  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,
140
+ slot, timestampMs, recommended, state, isStale,
141
+ blockUtilizationPct, blocksInWindow, accounts,
142
+ recentBlocks, trend,
148
143
  },
149
- };
144
+ } as FeeMarketMessage;
150
145
  }
151
146
 
152
147
  case MessageType.Blockhash: {
@@ -204,6 +199,35 @@ export function decodeMessage(data: ArrayBuffer): DecodedMessage | null {
204
199
  };
205
200
  }
206
201
 
202
+ case MessageType.BlockStats: {
203
+ // BlockStats (0x0F) — v3
204
+ // Layout: slot(u64) + cu_consumed(u64) + execution_cu(u64) + cu_limit(u64) + cu_remaining(u64)
205
+ // + utilization_pct(f32) + tx_count(u32) + avg_cu_per_tx(u32) + avg_cu_price(u64)
206
+ // + min_cu_price(u64) + max_cu_price(u64) + timestamp_ms(u64)
207
+ let offset = 0; // payload already has type byte stripped
208
+ const slot = Number(payloadView.getBigUint64(offset, true)); offset += 8;
209
+ const cuConsumed = Number(payloadView.getBigUint64(offset, true)); offset += 8;
210
+ const executionCu = Number(payloadView.getBigUint64(offset, true)); offset += 8;
211
+ const cuLimit = Number(payloadView.getBigUint64(offset, true)); offset += 8;
212
+ const cuRemaining = Number(payloadView.getBigUint64(offset, true)); offset += 8;
213
+ const utilizationPct = payloadView.getFloat32(offset, true); offset += 4;
214
+ const txCount = payloadView.getUint32(offset, true); offset += 4;
215
+ const avgCuPerTx = payloadView.getUint32(offset, true); offset += 4;
216
+ const avgCuPrice = Number(payloadView.getBigUint64(offset, true)); offset += 8;
217
+ const minCuPrice = Number(payloadView.getBigUint64(offset, true)); offset += 8;
218
+ const maxCuPrice = Number(payloadView.getBigUint64(offset, true)); offset += 8;
219
+ const timestampMs = Number(payloadView.getBigUint64(offset, true)); offset += 8;
220
+
221
+ return {
222
+ type: 'block_stats',
223
+ data: {
224
+ slot, cuConsumed, executionCu, cuLimit, cuRemaining, utilizationPct,
225
+ txCount, avgCuPerTx, avgCuPrice, minCuPrice, maxCuPrice, timestampMs,
226
+ },
227
+ receivedAt: Date.now(),
228
+ };
229
+ }
230
+
207
231
  default:
208
232
  return null;
209
233
  }
@@ -341,19 +365,7 @@ function decodePoolUpdate(payload: ArrayBuffer, payloadView: DataView): PoolUpda
341
365
  },
342
366
  };
343
367
  } catch {
344
- return {
345
- type: 'pool_update',
346
- data: {
347
- sequence: 0,
348
- slot: 0,
349
- writeVersion: 0,
350
- protocol: 'unknown',
351
- poolAddress: '',
352
- tokenMints: [],
353
- tokenBalances: [],
354
- tokenDecimals: [],
355
- },
356
- };
368
+ return null;
357
369
  }
358
370
  }
359
371
 
@@ -461,24 +473,6 @@ function decodeQuote(payload: ArrayBuffer, payloadView: DataView): DecodedMessag
461
473
  },
462
474
  };
463
475
  } catch {
464
- return {
465
- type: 'quote',
466
- data: {
467
- topicId: '',
468
- timestampMs: 0,
469
- sequence: 0,
470
- inputMint: '',
471
- outputMint: '',
472
- inAmount: '0',
473
- outAmount: '0',
474
- priceImpactBps: 0,
475
- contextSlot: 0,
476
- algorithm: '',
477
- isImprovement: false,
478
- isCached: false,
479
- isStale: false,
480
- routePlan: null,
481
- },
482
- };
476
+ return null;
483
477
  }
484
478
  }
package/src/ws/index.ts CHANGED
@@ -44,7 +44,8 @@ export type {
44
44
  MessageTypeValue,
45
45
  DecodedMessage,
46
46
  PoolUpdateMessage,
47
- PriorityFeesMessage,
47
+ FeeMarketMessage,
48
+ BlockStatsMessage,
48
49
  BlockhashMessage,
49
50
  QuoteMessage,
50
51
  HeartbeatMessage,
package/src/ws/types.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  * See: https://github.com/k256-xyz for protocol documentation
6
6
  */
7
7
 
8
+ import type { BlockMiniStats, TrendDirection, BlockStats } from '../types';
9
+
8
10
  /**
9
11
  * WebSocket message type constants
10
12
  *
@@ -40,6 +42,8 @@ export const MessageType = {
40
42
  Heartbeat: 0x0d,
41
43
  /** Batched pool updates for high throughput */
42
44
  PoolUpdateBatch: 0x0e,
45
+ /** Block-level statistics (v3) */
46
+ BlockStats: 0x0f,
43
47
  /** Error message (UTF-8 string) */
44
48
  Error: 0xff,
45
49
  } as const;
@@ -66,42 +70,61 @@ export interface PoolUpdateMessage {
66
70
  }
67
71
 
68
72
  /**
69
- * Decoded priority fees from binary message
70
- *
71
- * All fields from K2 PriorityFeesWire
73
+ * Decoded fee market from binary message (per-writable-account model)
74
+ *
75
+ * All fields from K2 FeeMarketWire
72
76
  */
73
- export interface PriorityFeesMessage {
74
- type: 'priority_fees';
77
+ export interface FeeMarketMessage {
78
+ type: 'fee_market';
75
79
  data: {
76
80
  slot: number;
77
81
  timestampMs: number;
78
- /** Recommended priority fee in micro-lamports */
82
+ /** Recommended priority fee in microlamports/CU (max p75 across hottest accounts) */
79
83
  recommended: number;
80
84
  /** Fee state: 0=low, 1=normal, 2=high, 3=extreme */
81
85
  state: number;
82
86
  /** True if data is stale (no recent samples) */
83
87
  isStale: boolean;
84
- // Swap fee percentiles (for ≥50K CU transactions)
85
- swapP50: number;
86
- swapP75: number;
87
- swapP90: number;
88
- swapP99: number;
89
- /** Number of samples used for swap percentiles */
90
- swapSamples: number;
91
- // Landing probability fees (fee to land with X% probability)
92
- landingP50Fee: number;
93
- landingP75Fee: number;
94
- landingP90Fee: number;
95
- landingP99Fee: number;
96
- // Top fee tiers
97
- top10Fee: number;
98
- top25Fee: number;
99
- // Spike detection
100
- spikeDetected: boolean;
101
- spikeFee: number;
88
+ /** Block utilization percentage (0-100) */
89
+ blockUtilizationPct: number;
90
+ /** Number of blocks in the observation window */
91
+ blocksInWindow: number;
92
+ /** Per-writable-account fee data */
93
+ accounts: {
94
+ /** Account public key (Base58) */
95
+ pubkey: string;
96
+ /** Total transactions touching this account */
97
+ totalTxs: number;
98
+ /** Active slots for this account */
99
+ activeSlots: number;
100
+ /** Total CU consumed */
101
+ cuConsumed: number;
102
+ /** Utilization percentage (0-100) of 12M CU limit */
103
+ utilizationPct: number;
104
+ /** Fee percentiles in microlamports/CU */
105
+ p25: number;
106
+ p50: number;
107
+ p75: number;
108
+ p90: number;
109
+ /** Minimum non-zero fee observed */
110
+ minNonzeroPrice: number;
111
+ }[];
112
+ /** Recent block mini-stats (v3) */
113
+ recentBlocks: BlockMiniStats[];
114
+ /** Fee trend direction (v3) */
115
+ trend: TrendDirection;
102
116
  };
103
117
  }
104
118
 
119
+ /**
120
+ * Decoded block stats from binary message (v3)
121
+ */
122
+ export interface BlockStatsMessage {
123
+ type: 'block_stats';
124
+ data: BlockStats;
125
+ receivedAt: number;
126
+ }
127
+
105
128
  /**
106
129
  * Decoded blockhash from binary message
107
130
  */
@@ -216,7 +239,8 @@ export interface PongMessage {
216
239
  */
217
240
  export type DecodedMessage =
218
241
  | PoolUpdateMessage
219
- | PriorityFeesMessage
242
+ | FeeMarketMessage
243
+ | BlockStatsMessage
220
244
  | BlockhashMessage
221
245
  | QuoteMessage
222
246
  | HeartbeatMessage