@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.
- package/dist/index.cjs +505 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +500 -5
- package/dist/index.js.map +1 -1
- package/dist/leader-ws/index.cjs +527 -0
- package/dist/leader-ws/index.cjs.map +1 -0
- package/dist/leader-ws/index.d.cts +337 -0
- package/dist/leader-ws/index.d.ts +337 -0
- package/dist/leader-ws/index.js +520 -0
- package/dist/leader-ws/index.js.map +1 -0
- package/dist/ws/index.cjs +5 -4
- package/dist/ws/index.cjs.map +1 -1
- package/dist/ws/index.d.cts +1 -1
- package/dist/ws/index.d.ts +1 -1
- package/dist/ws/index.js +5 -4
- package/dist/ws/index.js.map +1 -1
- package/package.json +11 -1
- package/src/index.ts +32 -1
- package/src/leader-ws/client.ts +377 -0
- package/src/leader-ws/decoder.ts +314 -0
- package/src/leader-ws/index.ts +58 -0
- package/src/leader-ws/types.ts +212 -0
- package/src/ws/client.ts +2 -2
- package/src/ws/decoder.ts +7 -3
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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 {
|