@k256/sdk 0.2.0 → 0.3.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,58 @@
1
+ /**
2
+ * Leader Schedule WebSocket module for K256 SDK
3
+ *
4
+ * Real-time Solana leader schedule, gossip network, and routing data.
5
+ * Binary mode by default (wincode protocol). JSON mode opt-in via gateway.
6
+ *
7
+ * @module @k256/sdk/leader-ws
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { LeaderWebSocketClient } from '@k256/sdk/leader-ws';
12
+ *
13
+ * const client = new LeaderWebSocketClient({
14
+ * apiKey: 'your-api-key',
15
+ * onSlotUpdate: (msg) => console.log('Slot:', msg.data.slot),
16
+ * onRoutingHealth: (msg) => console.log('Coverage:', msg.data.coverage),
17
+ * onGossipDiff: (msg) => {
18
+ * // Merge into your local peer map using identity as key
19
+ * for (const peer of msg.data.added) peerMap.set(peer.identity, peer);
20
+ * for (const peer of msg.data.updated) peerMap.set(peer.identity, peer);
21
+ * for (const id of msg.data.removed) peerMap.delete(id);
22
+ * },
23
+ * });
24
+ *
25
+ * await client.connect();
26
+ * ```
27
+ */
28
+
29
+ // Client
30
+ export { LeaderWebSocketClient, LeaderWebSocketError } from './client';
31
+ export type {
32
+ LeaderWebSocketClientConfig,
33
+ LeaderErrorCode,
34
+ ConnectionState,
35
+ } from './client';
36
+
37
+ // Decoder (for advanced usage)
38
+ export { decodeLeaderMessage, LeaderMessageTag } from './decoder';
39
+
40
+ // Types
41
+ export { LeaderChannel, ALL_LEADER_CHANNELS } from './types';
42
+ export type {
43
+ LeaderChannelValue,
44
+ MessageKind,
45
+ MessageSchemaEntry,
46
+ LeaderDecodedMessage,
47
+ LeaderSubscribedMessage,
48
+ LeaderScheduleMessage,
49
+ GossipSnapshotMessage,
50
+ GossipDiffMessage,
51
+ GossipPeer,
52
+ SlotUpdateMessage,
53
+ RoutingHealthMessage,
54
+ SkipEventMessage,
55
+ IpChangeMessage,
56
+ LeaderHeartbeatMessage,
57
+ LeaderErrorMessage,
58
+ } from './types';
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Leader Schedule WebSocket message types and interfaces
3
+ *
4
+ * The leader-schedule WS uses wincode binary protocol (matching K2 pattern).
5
+ * Wire format: [1-byte tag][wincode payload]
6
+ * Every decoded message has:
7
+ * - type: message type name
8
+ * - kind: "snapshot" (full state) | "diff" (merge into snapshot) | "event" (append-only)
9
+ * - key: primary key field for merging (on diff/event types)
10
+ * - data: typed payload
11
+ *
12
+ * @see https://k256.xyz/docs/leader-schedule
13
+ */
14
+
15
+ /**
16
+ * Leader Schedule WebSocket channels
17
+ */
18
+ export const LeaderChannel = {
19
+ /** Full epoch leader schedule (on connect + epoch change) */
20
+ LeaderSchedule: 'leader_schedule',
21
+ /** Gossip peers (snapshot on connect, then diffs) */
22
+ Gossip: 'gossip',
23
+ /** Real-time slot updates with current leader */
24
+ Slots: 'slots',
25
+ /** Skip events, IP changes, routing health */
26
+ Alerts: 'alerts',
27
+ } as const;
28
+
29
+ export type LeaderChannelValue = typeof LeaderChannel[keyof typeof LeaderChannel];
30
+
31
+ /** All available channels */
32
+ export const ALL_LEADER_CHANNELS: LeaderChannelValue[] = [
33
+ LeaderChannel.LeaderSchedule,
34
+ LeaderChannel.Gossip,
35
+ LeaderChannel.Slots,
36
+ LeaderChannel.Alerts,
37
+ ];
38
+
39
+ /**
40
+ * Message kind — tells you how to consume the message
41
+ */
42
+ export type MessageKind = 'snapshot' | 'diff' | 'event';
43
+
44
+ // ═══════════════════════════════════════════════════════════════════════
45
+ // Message Interfaces
46
+ // ═══════════════════════════════════════════════════════════════════════
47
+
48
+ /** Protocol schema entry (included in subscribed handshake) */
49
+ export interface MessageSchemaEntry {
50
+ type: string;
51
+ tag: string;
52
+ kind: MessageKind;
53
+ key?: string;
54
+ description: string;
55
+ }
56
+
57
+ /** Subscribed response — connection handshake */
58
+ export interface LeaderSubscribedMessage {
59
+ type: 'subscribed';
60
+ data: {
61
+ channels: string[];
62
+ currentSlot: number;
63
+ epoch: number;
64
+ schema: MessageSchemaEntry[];
65
+ };
66
+ }
67
+
68
+ /** Full epoch leader schedule (snapshot — replaces previous) */
69
+ export interface LeaderScheduleMessage {
70
+ type: 'leader_schedule';
71
+ kind: 'snapshot';
72
+ data: {
73
+ epoch: number;
74
+ slotsInEpoch: number;
75
+ validators: number;
76
+ schedule: Array<{
77
+ identity: string;
78
+ slots: number;
79
+ slotIndices: number[];
80
+ }>;
81
+ };
82
+ }
83
+
84
+ /** Gossip peer data */
85
+ export interface GossipPeer {
86
+ identity: string;
87
+ tpuQuic: string | null;
88
+ tpuUdp: string | null;
89
+ tpuForwardsQuic: string | null;
90
+ tpuForwardsUdp: string | null;
91
+ tpuVote: string | null;
92
+ tpuVoteQuic: string | null;
93
+ gossipAddr: string | null;
94
+ shredVersion: number;
95
+ version: string;
96
+ activatedStake: number;
97
+ commission: number;
98
+ isDelinquent: boolean;
99
+ votePubkey: string;
100
+ lastVote: number;
101
+ rootSlot: number;
102
+ wallclock: number;
103
+ }
104
+
105
+ /** Full gossip peer list (snapshot — apply gossip_diff to keep current) */
106
+ export interface GossipSnapshotMessage {
107
+ type: 'gossip_snapshot';
108
+ kind: 'snapshot';
109
+ key: 'identity';
110
+ data: {
111
+ timestampMs: number;
112
+ count: number;
113
+ peers: GossipPeer[];
114
+ };
115
+ }
116
+
117
+ /** Incremental gossip changes (diff — merge into snapshot using identity) */
118
+ export interface GossipDiffMessage {
119
+ type: 'gossip_diff';
120
+ kind: 'diff';
121
+ key: 'identity';
122
+ data: {
123
+ timestampMs: number;
124
+ added: GossipPeer[];
125
+ removed: string[];
126
+ updated: GossipPeer[];
127
+ };
128
+ }
129
+
130
+ /** Current slot with leader identity (snapshot — each replaces previous) */
131
+ export interface SlotUpdateMessage {
132
+ type: 'slot_update';
133
+ kind: 'snapshot';
134
+ data: {
135
+ slot: number;
136
+ leader: string;
137
+ blockHeight: number;
138
+ };
139
+ }
140
+
141
+ /** Routing health summary (snapshot — each replaces previous) */
142
+ export interface RoutingHealthMessage {
143
+ type: 'routing_health';
144
+ kind: 'snapshot';
145
+ data: {
146
+ leadersTotal: number;
147
+ leadersInGossip: number;
148
+ leadersMissingGossip: string[];
149
+ leadersWithoutTpuQuic: string[];
150
+ leadersDelinquent: string[];
151
+ coverage: string;
152
+ };
153
+ }
154
+
155
+ /** Block production stats per validator (event — cumulative) */
156
+ export interface SkipEventMessage {
157
+ type: 'skip_event';
158
+ kind: 'event';
159
+ key: 'leader';
160
+ data: {
161
+ slot: number;
162
+ leader: string;
163
+ assigned: number;
164
+ produced: number;
165
+ };
166
+ }
167
+
168
+ /** Validator IP address change (event) */
169
+ export interface IpChangeMessage {
170
+ type: 'ip_change';
171
+ kind: 'event';
172
+ key: 'identity';
173
+ data: {
174
+ identity: string;
175
+ oldIp: string;
176
+ newIp: string;
177
+ timestampMs: number;
178
+ };
179
+ }
180
+
181
+ /** Server heartbeat (snapshot — each replaces previous) */
182
+ export interface LeaderHeartbeatMessage {
183
+ type: 'heartbeat';
184
+ kind: 'snapshot';
185
+ data: {
186
+ timestampMs: number;
187
+ currentSlot: number;
188
+ connectedClients: number;
189
+ gossipPeers: number;
190
+ };
191
+ }
192
+
193
+ /** Error from server */
194
+ export interface LeaderErrorMessage {
195
+ type: 'error';
196
+ data: {
197
+ message: string;
198
+ };
199
+ }
200
+
201
+ /** Union of all leader-schedule message types */
202
+ export type LeaderDecodedMessage =
203
+ | LeaderSubscribedMessage
204
+ | LeaderScheduleMessage
205
+ | GossipSnapshotMessage
206
+ | GossipDiffMessage
207
+ | SlotUpdateMessage
208
+ | RoutingHealthMessage
209
+ | SkipEventMessage
210
+ | IpChangeMessage
211
+ | LeaderHeartbeatMessage
212
+ | LeaderErrorMessage;
package/src/ws/client.ts CHANGED
@@ -131,7 +131,7 @@ export interface K256WebSocketClientConfig {
131
131
  pingIntervalMs?: number;
132
132
  /** Pong timeout in ms - disconnect if no pong (default: 10000) */
133
133
  pongTimeoutMs?: number;
134
- /** Heartbeat timeout in ms - warn if no heartbeat (default: 15000) */
134
+ /** Heartbeat timeout in ms - warn if no heartbeat (default: 45000, K2 sends every 30s) */
135
135
  heartbeatTimeoutMs?: number;
136
136
 
137
137
  // Event callbacks
@@ -274,7 +274,7 @@ export class K256WebSocketClient {
274
274
  maxReconnectAttempts: Infinity,
275
275
  pingIntervalMs: 30000,
276
276
  pongTimeoutMs: 10000,
277
- heartbeatTimeoutMs: 15000,
277
+ heartbeatTimeoutMs: 45000, // K2 sends heartbeats every 30s; allow 45s before warning
278
278
  ...config,
279
279
  };
280
280
  }
package/src/ws/decoder.ts CHANGED
@@ -118,10 +118,13 @@ export function decodeMessage(data: ArrayBuffer): DecodedMessage | null {
118
118
  }
119
119
 
120
120
  // Decode recent_blocks (Vec<BlockMiniStats>) — v3
121
- const recentBlocksCount = Number(payloadView.getBigUint64(offset, true));
121
+ // Guard: need at least 8 bytes for count + 1 byte for trend after
122
+ const recentBlocksCount = offset + 8 <= payload.byteLength
123
+ ? Number(payloadView.getBigUint64(offset, true))
124
+ : 0;
122
125
  offset += 8;
123
126
  const recentBlocks: BlockMiniStats[] = [];
124
- for (let i = 0; i < recentBlocksCount; i++) {
127
+ for (let i = 0; i < recentBlocksCount && offset + 32 <= payload.byteLength; i++) {
125
128
  const rbSlot = Number(payloadView.getBigUint64(offset, true)); offset += 8;
126
129
  const rbCuConsumed = Number(payloadView.getBigUint64(offset, true)); offset += 8;
127
130
  const rbTxCount = payloadView.getUint32(offset, true); offset += 4;
@@ -131,7 +134,8 @@ export function decodeMessage(data: ArrayBuffer): DecodedMessage | null {
131
134
  }
132
135
 
133
136
  // Decode trend (u8) — v3
134
- const trendByte = payloadView.getUint8(offset); offset += 1;
137
+ const trendByte = offset < payload.byteLength ? payloadView.getUint8(offset) : 2;
138
+ offset += 1;
135
139
  const trend: TrendDirection = trendByte === 0 ? 'rising' : trendByte === 1 ? 'falling' : 'stable';
136
140
 
137
141
  return {