@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 CHANGED
@@ -188,10 +188,10 @@ function decodeMessage(data) {
188
188
  });
189
189
  offset += 92;
190
190
  }
191
- const recentBlocksCount = Number(payloadView.getBigUint64(offset, true));
191
+ const recentBlocksCount = offset + 8 <= payload.byteLength ? Number(payloadView.getBigUint64(offset, true)) : 0;
192
192
  offset += 8;
193
193
  const recentBlocks = [];
194
- for (let i = 0; i < recentBlocksCount; i++) {
194
+ for (let i = 0; i < recentBlocksCount && offset + 32 <= payload.byteLength; i++) {
195
195
  const rbSlot = Number(payloadView.getBigUint64(offset, true));
196
196
  offset += 8;
197
197
  const rbCuConsumed = Number(payloadView.getBigUint64(offset, true));
@@ -204,7 +204,7 @@ function decodeMessage(data) {
204
204
  offset += 8;
205
205
  recentBlocks.push({ slot: rbSlot, cuConsumed: rbCuConsumed, txCount: rbTxCount, utilizationPct: rbUtilizationPct, avgCuPrice: rbAvgCuPrice });
206
206
  }
207
- const trendByte = payloadView.getUint8(offset);
207
+ const trendByte = offset < payload.byteLength ? payloadView.getUint8(offset) : 2;
208
208
  offset += 1;
209
209
  const trend = trendByte === 0 ? "rising" : trendByte === 1 ? "falling" : "stable";
210
210
  return {
@@ -562,7 +562,8 @@ var K256WebSocketClient = class {
562
562
  maxReconnectAttempts: Infinity,
563
563
  pingIntervalMs: 3e4,
564
564
  pongTimeoutMs: 1e4,
565
- heartbeatTimeoutMs: 15e3,
565
+ heartbeatTimeoutMs: 45e3,
566
+ // K2 sends heartbeats every 30s; allow 45s before warning
566
567
  ...config
567
568
  };
568
569
  }
@@ -978,6 +979,500 @@ var K256WebSocketClient = class {
978
979
  }
979
980
  };
980
981
 
982
+ // src/leader-ws/decoder.ts
983
+ var LeaderMessageTag = {
984
+ Subscribe: 1,
985
+ Subscribed: 2,
986
+ LeaderSchedule: 16,
987
+ GossipSnapshot: 17,
988
+ GossipDiff: 18,
989
+ SlotUpdate: 19,
990
+ RoutingHealth: 20,
991
+ SkipEvent: 21,
992
+ IpChange: 22,
993
+ Heartbeat: 253,
994
+ Ping: 254,
995
+ Error: 255
996
+ };
997
+ function readU64(view, o) {
998
+ const val = Number(view.getBigUint64(o.v, true));
999
+ o.v += 8;
1000
+ return val;
1001
+ }
1002
+ function readU32(view, o) {
1003
+ const val = view.getUint32(o.v, true);
1004
+ o.v += 4;
1005
+ return val;
1006
+ }
1007
+ function readU16(view, o) {
1008
+ const val = view.getUint16(o.v, true);
1009
+ o.v += 2;
1010
+ return val;
1011
+ }
1012
+ function readU8(view, o) {
1013
+ const val = view.getUint8(o.v);
1014
+ o.v += 1;
1015
+ return val;
1016
+ }
1017
+ function readBool(view, o) {
1018
+ return readU8(view, o) !== 0;
1019
+ }
1020
+ function readPubkey(data, o) {
1021
+ const bytes = new Uint8Array(data, o.v, 32);
1022
+ o.v += 32;
1023
+ return base58Encode(bytes);
1024
+ }
1025
+ function readVecU8AsString(view, data, o) {
1026
+ const len = readU64(view, o);
1027
+ const bytes = new Uint8Array(data, o.v, len);
1028
+ o.v += len;
1029
+ return new TextDecoder().decode(bytes);
1030
+ }
1031
+ function readOptSocketAddr(view, o) {
1032
+ const tag = readU8(view, o);
1033
+ if (tag === 0) return null;
1034
+ const ipBytes = new Uint8Array(view.buffer, view.byteOffset + o.v, 16);
1035
+ o.v += 16;
1036
+ const port = readU16(view, o);
1037
+ const isIpv4 = readBool(view, o);
1038
+ if (isIpv4) {
1039
+ return `${ipBytes[12]}.${ipBytes[13]}.${ipBytes[14]}.${ipBytes[15]}:${port}`;
1040
+ }
1041
+ return `[ipv6]:${port}`;
1042
+ }
1043
+ function readPubkeyVec(view, data, o) {
1044
+ const count = readU64(view, o);
1045
+ const keys = [];
1046
+ for (let i = 0; i < count; i++) {
1047
+ keys.push(readPubkey(data, o));
1048
+ }
1049
+ return keys;
1050
+ }
1051
+ function readGossipPeer(view, data, o) {
1052
+ return {
1053
+ identity: readPubkey(data, o),
1054
+ tpuQuic: readOptSocketAddr(view, o),
1055
+ tpuUdp: readOptSocketAddr(view, o),
1056
+ tpuForwardsQuic: readOptSocketAddr(view, o),
1057
+ tpuForwardsUdp: readOptSocketAddr(view, o),
1058
+ tpuVote: readOptSocketAddr(view, o),
1059
+ tpuVoteQuic: readOptSocketAddr(view, o),
1060
+ gossipAddr: readOptSocketAddr(view, o),
1061
+ shredVersion: readU16(view, o),
1062
+ version: readVecU8AsString(view, data, o),
1063
+ activatedStake: readU64(view, o),
1064
+ commission: readU8(view, o),
1065
+ isDelinquent: readBool(view, o),
1066
+ votePubkey: readPubkey(data, o),
1067
+ lastVote: readU64(view, o),
1068
+ rootSlot: readU64(view, o),
1069
+ wallclock: readU64(view, o)
1070
+ };
1071
+ }
1072
+ function readGossipPeerVec(view, data, o) {
1073
+ const count = readU64(view, o);
1074
+ const peers = [];
1075
+ for (let i = 0; i < count; i++) {
1076
+ peers.push(readGossipPeer(view, data, o));
1077
+ }
1078
+ return peers;
1079
+ }
1080
+ function decodeLeaderMessage(data) {
1081
+ const view = new DataView(data);
1082
+ if (data.byteLength < 1) return null;
1083
+ const msgType = view.getUint8(0);
1084
+ const payload = data.slice(1);
1085
+ const pv = new DataView(payload);
1086
+ switch (msgType) {
1087
+ case LeaderMessageTag.Subscribed: {
1088
+ const text = new TextDecoder().decode(payload);
1089
+ try {
1090
+ return { type: "subscribed", data: JSON.parse(text) };
1091
+ } catch {
1092
+ return null;
1093
+ }
1094
+ }
1095
+ case LeaderMessageTag.Error: {
1096
+ return { type: "error", data: { message: new TextDecoder().decode(payload) } };
1097
+ }
1098
+ case LeaderMessageTag.SlotUpdate: {
1099
+ if (payload.byteLength < 48) return null;
1100
+ const o = { v: 0 };
1101
+ return {
1102
+ type: "slot_update",
1103
+ kind: "snapshot",
1104
+ data: {
1105
+ slot: readU64(pv, o),
1106
+ leader: readPubkey(payload, o),
1107
+ blockHeight: readU64(pv, o)
1108
+ }
1109
+ };
1110
+ }
1111
+ case LeaderMessageTag.Heartbeat: {
1112
+ if (payload.byteLength < 24) return null;
1113
+ const o = { v: 0 };
1114
+ return {
1115
+ type: "heartbeat",
1116
+ kind: "snapshot",
1117
+ data: {
1118
+ timestampMs: readU64(pv, o),
1119
+ currentSlot: readU64(pv, o),
1120
+ connectedClients: readU32(pv, o),
1121
+ gossipPeers: readU32(pv, o)
1122
+ }
1123
+ };
1124
+ }
1125
+ case LeaderMessageTag.SkipEvent: {
1126
+ if (payload.byteLength < 48) return null;
1127
+ const o = { v: 0 };
1128
+ return {
1129
+ type: "skip_event",
1130
+ kind: "event",
1131
+ key: "leader",
1132
+ data: {
1133
+ slot: readU64(pv, o),
1134
+ leader: readPubkey(payload, o),
1135
+ assigned: readU32(pv, o),
1136
+ produced: readU32(pv, o)
1137
+ }
1138
+ };
1139
+ }
1140
+ case LeaderMessageTag.RoutingHealth: {
1141
+ if (payload.byteLength < 8) return null;
1142
+ try {
1143
+ const o = { v: 0 };
1144
+ const leadersTotal = readU32(pv, o);
1145
+ const leadersInGossip = readU32(pv, o);
1146
+ const leadersMissingGossip = readPubkeyVec(pv, payload, o);
1147
+ const leadersWithoutTpuQuic = readPubkeyVec(pv, payload, o);
1148
+ const leadersDelinquent = readPubkeyVec(pv, payload, o);
1149
+ return {
1150
+ type: "routing_health",
1151
+ kind: "snapshot",
1152
+ data: {
1153
+ leadersTotal,
1154
+ leadersInGossip,
1155
+ leadersMissingGossip,
1156
+ leadersWithoutTpuQuic,
1157
+ leadersDelinquent,
1158
+ coverage: `${leadersTotal > 0 ? (leadersInGossip / leadersTotal * 100).toFixed(1) : 0}%`
1159
+ }
1160
+ };
1161
+ } catch {
1162
+ return null;
1163
+ }
1164
+ }
1165
+ case LeaderMessageTag.IpChange: {
1166
+ if (payload.byteLength < 32) return null;
1167
+ try {
1168
+ const o = { v: 0 };
1169
+ const identity = readPubkey(payload, o);
1170
+ const oldIp = readVecU8AsString(pv, payload, o);
1171
+ const newIp = readVecU8AsString(pv, payload, o);
1172
+ const timestampMs = readU64(pv, o);
1173
+ return {
1174
+ type: "ip_change",
1175
+ kind: "event",
1176
+ key: "identity",
1177
+ data: { identity, oldIp, newIp, timestampMs }
1178
+ };
1179
+ } catch {
1180
+ return null;
1181
+ }
1182
+ }
1183
+ case LeaderMessageTag.GossipSnapshot: {
1184
+ if (payload.byteLength < 8) return null;
1185
+ try {
1186
+ const o = { v: 0 };
1187
+ const timestampMs = readU64(pv, o);
1188
+ const peers = readGossipPeerVec(pv, payload, o);
1189
+ return {
1190
+ type: "gossip_snapshot",
1191
+ kind: "snapshot",
1192
+ key: "identity",
1193
+ data: { timestampMs, count: peers.length, peers }
1194
+ };
1195
+ } catch {
1196
+ return null;
1197
+ }
1198
+ }
1199
+ case LeaderMessageTag.GossipDiff: {
1200
+ if (payload.byteLength < 8) return null;
1201
+ try {
1202
+ const o = { v: 0 };
1203
+ const timestampMs = readU64(pv, o);
1204
+ const added = readGossipPeerVec(pv, payload, o);
1205
+ const removed = readPubkeyVec(pv, payload, o);
1206
+ const updated = readGossipPeerVec(pv, payload, o);
1207
+ return {
1208
+ type: "gossip_diff",
1209
+ kind: "diff",
1210
+ key: "identity",
1211
+ data: { timestampMs, added, removed, updated }
1212
+ };
1213
+ } catch {
1214
+ return null;
1215
+ }
1216
+ }
1217
+ case LeaderMessageTag.LeaderSchedule: {
1218
+ if (payload.byteLength < 16) return null;
1219
+ try {
1220
+ const o = { v: 0 };
1221
+ const epoch = readU64(pv, o);
1222
+ const slotsInEpoch = readU64(pv, o);
1223
+ const validatorCount = readU64(pv, o);
1224
+ const schedule = [];
1225
+ for (let i = 0; i < validatorCount; i++) {
1226
+ const identity = readPubkey(payload, o);
1227
+ const slotCount = readU64(pv, o);
1228
+ const slotIndices = [];
1229
+ for (let j = 0; j < slotCount; j++) {
1230
+ slotIndices.push(readU32(pv, o));
1231
+ }
1232
+ schedule.push({ identity, slots: slotIndices.length, slotIndices });
1233
+ }
1234
+ return {
1235
+ type: "leader_schedule",
1236
+ kind: "snapshot",
1237
+ data: { epoch, slotsInEpoch, validators: schedule.length, schedule }
1238
+ };
1239
+ } catch {
1240
+ return null;
1241
+ }
1242
+ }
1243
+ default:
1244
+ return null;
1245
+ }
1246
+ }
1247
+
1248
+ // src/leader-ws/types.ts
1249
+ var LeaderChannel = {
1250
+ /** Full epoch leader schedule (on connect + epoch change) */
1251
+ LeaderSchedule: "leader_schedule",
1252
+ /** Gossip peers (snapshot on connect, then diffs) */
1253
+ Gossip: "gossip",
1254
+ /** Real-time slot updates with current leader */
1255
+ Slots: "slots",
1256
+ /** Skip events, IP changes, routing health */
1257
+ Alerts: "alerts"
1258
+ };
1259
+ var ALL_LEADER_CHANNELS = [
1260
+ LeaderChannel.LeaderSchedule,
1261
+ LeaderChannel.Gossip,
1262
+ LeaderChannel.Slots,
1263
+ LeaderChannel.Alerts
1264
+ ];
1265
+
1266
+ // src/leader-ws/client.ts
1267
+ var LeaderWebSocketError = class extends Error {
1268
+ constructor(code, message, closeCode, closeReason) {
1269
+ super(message);
1270
+ this.code = code;
1271
+ this.closeCode = closeCode;
1272
+ this.closeReason = closeReason;
1273
+ this.name = "LeaderWebSocketError";
1274
+ }
1275
+ get isRecoverable() {
1276
+ return this.code !== "AUTH_FAILED";
1277
+ }
1278
+ };
1279
+ var LeaderWebSocketClient = class {
1280
+ ws = null;
1281
+ config;
1282
+ _state = "disconnected";
1283
+ reconnectAttempts = 0;
1284
+ reconnectTimer = null;
1285
+ isIntentionallyClosed = false;
1286
+ /** Current connection state */
1287
+ get state() {
1288
+ return this._state;
1289
+ }
1290
+ /** Whether currently connected */
1291
+ get isConnected() {
1292
+ return this._state === "connected" && this.ws?.readyState === WebSocket.OPEN;
1293
+ }
1294
+ constructor(config) {
1295
+ this.config = {
1296
+ url: "wss://gateway.k256.xyz/v1/leader-ws",
1297
+ mode: "binary",
1298
+ channels: ALL_LEADER_CHANNELS,
1299
+ autoReconnect: true,
1300
+ reconnectDelayMs: 1e3,
1301
+ maxReconnectDelayMs: 3e4,
1302
+ maxReconnectAttempts: Infinity,
1303
+ ...config
1304
+ };
1305
+ }
1306
+ /**
1307
+ * Connect to the leader-schedule WebSocket
1308
+ */
1309
+ async connect() {
1310
+ if (this._state === "connected" || this._state === "connecting") return;
1311
+ this.isIntentionallyClosed = false;
1312
+ this.setState("connecting");
1313
+ return new Promise((resolve, reject) => {
1314
+ try {
1315
+ const url = `${this.config.url}?apiKey=${encodeURIComponent(this.config.apiKey)}`;
1316
+ this.ws = new WebSocket(url);
1317
+ if (this.config.mode === "binary") {
1318
+ this.ws.binaryType = "arraybuffer";
1319
+ }
1320
+ this.ws.onopen = () => {
1321
+ this.setState("connected");
1322
+ this.reconnectAttempts = 0;
1323
+ if (this.config.mode === "binary") {
1324
+ const payload = JSON.stringify({ channels: this.config.channels });
1325
+ const bytes = new TextEncoder().encode(payload);
1326
+ const msg = new Uint8Array(1 + bytes.length);
1327
+ msg[0] = 1;
1328
+ msg.set(bytes, 1);
1329
+ this.ws.send(msg.buffer);
1330
+ } else {
1331
+ this.ws.send(JSON.stringify({
1332
+ type: "subscribe",
1333
+ channels: this.config.channels,
1334
+ format: "json"
1335
+ }));
1336
+ }
1337
+ this.config.onConnect?.();
1338
+ resolve();
1339
+ };
1340
+ this.ws.onmessage = (event) => {
1341
+ if (this.config.mode === "binary" && event.data instanceof ArrayBuffer) {
1342
+ const decoded = decodeLeaderMessage(event.data);
1343
+ if (decoded) {
1344
+ this.dispatchMessage(decoded);
1345
+ }
1346
+ } else if (typeof event.data === "string") {
1347
+ this.handleJsonMessage(event.data);
1348
+ }
1349
+ };
1350
+ this.ws.onclose = (event) => {
1351
+ const wasConnected = this._state === "connected";
1352
+ this.ws = null;
1353
+ if (this.isIntentionallyClosed) {
1354
+ this.setState("closed");
1355
+ this.config.onDisconnect?.(event.code, event.reason, event.wasClean);
1356
+ return;
1357
+ }
1358
+ this.config.onDisconnect?.(event.code, event.reason, event.wasClean);
1359
+ if (event.code === 1008 || event.code === 4001 || event.code === 4003) {
1360
+ this.setState("closed");
1361
+ this.config.onError?.(new LeaderWebSocketError(
1362
+ "AUTH_FAILED",
1363
+ `Authentication failed: ${event.reason}`,
1364
+ event.code,
1365
+ event.reason
1366
+ ));
1367
+ if (!wasConnected) reject(new LeaderWebSocketError("AUTH_FAILED", event.reason, event.code));
1368
+ return;
1369
+ }
1370
+ if (this.config.autoReconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) {
1371
+ this.scheduleReconnect();
1372
+ } else {
1373
+ this.setState("disconnected");
1374
+ }
1375
+ if (!wasConnected) reject(new LeaderWebSocketError("CONNECTION_FAILED", "WebSocket closed before connect"));
1376
+ };
1377
+ this.ws.onerror = () => {
1378
+ this.config.onError?.(new LeaderWebSocketError("CONNECTION_FAILED", "WebSocket connection error"));
1379
+ };
1380
+ } catch (err) {
1381
+ this.setState("disconnected");
1382
+ reject(err);
1383
+ }
1384
+ });
1385
+ }
1386
+ /**
1387
+ * Disconnect from the WebSocket
1388
+ */
1389
+ disconnect() {
1390
+ this.isIntentionallyClosed = true;
1391
+ this.clearTimers();
1392
+ if (this.ws) {
1393
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
1394
+ this.ws.close(1e3, "Client disconnect");
1395
+ }
1396
+ this.ws = null;
1397
+ }
1398
+ this.setState("closed");
1399
+ }
1400
+ // ── Private ──
1401
+ /** Handle JSON text frame (from gateway JSON mode) */
1402
+ handleJsonMessage(raw) {
1403
+ try {
1404
+ const msg = JSON.parse(raw);
1405
+ this.dispatchMessage(msg);
1406
+ } catch {
1407
+ this.config.onError?.(new LeaderWebSocketError("INVALID_MESSAGE", "Failed to parse message"));
1408
+ }
1409
+ }
1410
+ /** Dispatch a decoded message to typed callbacks */
1411
+ dispatchMessage(msg) {
1412
+ switch (msg.type) {
1413
+ case "subscribed":
1414
+ this.config.onSubscribed?.(msg);
1415
+ break;
1416
+ case "leader_schedule":
1417
+ this.config.onLeaderSchedule?.(msg);
1418
+ break;
1419
+ case "gossip_snapshot":
1420
+ this.config.onGossipSnapshot?.(msg);
1421
+ break;
1422
+ case "gossip_diff":
1423
+ this.config.onGossipDiff?.(msg);
1424
+ break;
1425
+ case "slot_update":
1426
+ this.config.onSlotUpdate?.(msg);
1427
+ break;
1428
+ case "routing_health":
1429
+ this.config.onRoutingHealth?.(msg);
1430
+ break;
1431
+ case "skip_event":
1432
+ this.config.onSkipEvent?.(msg);
1433
+ break;
1434
+ case "ip_change":
1435
+ this.config.onIpChange?.(msg);
1436
+ break;
1437
+ case "heartbeat":
1438
+ this.config.onHeartbeat?.(msg);
1439
+ break;
1440
+ case "error":
1441
+ this.config.onError?.(new LeaderWebSocketError(
1442
+ "SERVER_ERROR",
1443
+ msg.data.message
1444
+ ));
1445
+ break;
1446
+ }
1447
+ this.config.onMessage?.(msg);
1448
+ }
1449
+ setState(state) {
1450
+ const prev = this._state;
1451
+ if (prev === state) return;
1452
+ this._state = state;
1453
+ this.config.onStateChange?.(state, prev);
1454
+ }
1455
+ scheduleReconnect() {
1456
+ this.setState("reconnecting");
1457
+ this.reconnectAttempts++;
1458
+ const delay = Math.min(
1459
+ this.config.reconnectDelayMs * Math.pow(2, this.reconnectAttempts - 1),
1460
+ this.config.maxReconnectDelayMs
1461
+ );
1462
+ this.config.onReconnecting?.(this.reconnectAttempts, delay);
1463
+ this.reconnectTimer = setTimeout(() => {
1464
+ this.connect().catch(() => {
1465
+ });
1466
+ }, delay);
1467
+ }
1468
+ clearTimers() {
1469
+ if (this.reconnectTimer) {
1470
+ clearTimeout(this.reconnectTimer);
1471
+ this.reconnectTimer = null;
1472
+ }
1473
+ }
1474
+ };
1475
+
981
1476
  // src/types/index.ts
982
1477
  var NetworkState = /* @__PURE__ */ ((NetworkState2) => {
983
1478
  NetworkState2[NetworkState2["Low"] = 0] = "Low";
@@ -987,13 +1482,19 @@ var NetworkState = /* @__PURE__ */ ((NetworkState2) => {
987
1482
  return NetworkState2;
988
1483
  })(NetworkState || {});
989
1484
 
1485
+ exports.ALL_LEADER_CHANNELS = ALL_LEADER_CHANNELS;
990
1486
  exports.CloseCode = CloseCode;
991
1487
  exports.K256WebSocketClient = K256WebSocketClient;
992
1488
  exports.K256WebSocketError = K256WebSocketError;
1489
+ exports.LeaderChannel = LeaderChannel;
1490
+ exports.LeaderMessageTag = LeaderMessageTag;
1491
+ exports.LeaderWebSocketClient = LeaderWebSocketClient;
1492
+ exports.LeaderWebSocketError = LeaderWebSocketError;
993
1493
  exports.MessageType = MessageType;
994
1494
  exports.NetworkState = NetworkState;
995
1495
  exports.base58Decode = base58Decode;
996
1496
  exports.base58Encode = base58Encode;
1497
+ exports.decodeLeaderMessage = decodeLeaderMessage;
997
1498
  exports.decodeMessage = decodeMessage;
998
1499
  exports.decodePoolUpdateBatch = decodePoolUpdateBatch;
999
1500
  exports.isValidPubkey = isValidPubkey;