blue-js-sdk 2.2.0 → 2.4.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/defaults.js CHANGED
@@ -25,7 +25,7 @@ axios.defaults.adapter = 'http';
25
25
  // This is the npm/semver version for consumers. Internal development iterations
26
26
  // (v20, v21, v22, etc.) track feature milestones and are not exposed as exports.
27
27
 
28
- export const SDK_VERSION = '1.0.0';
28
+ export const SDK_VERSION = '2.4.0';
29
29
 
30
30
  // ─── Timestamps ──────────────────────────────────────────────────────────────
31
31
 
@@ -364,3 +364,123 @@ export async function tryWithFallback(endpoints, operation, label = 'operation')
364
364
  const { ChainError } = await import('./errors.js');
365
365
  throw new ChainError('ALL_ENDPOINTS_FAILED', `${label} failed on all ${endpoints.length} endpoints (verified ${LAST_VERIFIED}):\n${tried}\n\nAll endpoints may be down, or your network may be blocking HTTPS. Try curl-ing the URLs manually.`, { endpoints: errors });
366
366
  }
367
+
368
+ // ─── Runtime Endpoint Management ────────────────────────────────────────────
369
+ // Add/remove/reorder RPC and LCD endpoints at runtime without code changes.
370
+ // The arrays above are `const` but are Objects (mutable contents).
371
+
372
+ /**
373
+ * Add an RPC endpoint at runtime. Skips if URL already exists.
374
+ * @param {string} url - RPC URL (e.g. 'https://rpc.newprovider.com')
375
+ * @param {string} [name='Custom'] - Provider name
376
+ * @param {boolean} [prepend=false] - If true, adds to front (highest priority)
377
+ */
378
+ export function addRpcEndpoint(url, name = 'Custom', prepend = false) {
379
+ if (RPC_ENDPOINTS.some(e => e.url === url)) return;
380
+ const entry = { url, name, verified: new Date().toISOString().slice(0, 10) };
381
+ prepend ? RPC_ENDPOINTS.unshift(entry) : RPC_ENDPOINTS.push(entry);
382
+ }
383
+
384
+ /**
385
+ * Add an LCD endpoint at runtime. Skips if URL already exists.
386
+ * @param {string} url - LCD URL (e.g. 'https://api.newprovider.com')
387
+ * @param {string} [name='Custom'] - Provider name
388
+ * @param {boolean} [prepend=false] - If true, adds to front (highest priority)
389
+ */
390
+ export function addLcdEndpoint(url, name = 'Custom', prepend = false) {
391
+ if (LCD_ENDPOINTS.some(e => e.url === url)) return;
392
+ const entry = { url, name, verified: new Date().toISOString().slice(0, 10) };
393
+ prepend ? LCD_ENDPOINTS.unshift(entry) : LCD_ENDPOINTS.push(entry);
394
+ }
395
+
396
+ /**
397
+ * Remove an RPC endpoint by URL.
398
+ * @param {string} url
399
+ */
400
+ export function removeRpcEndpoint(url) {
401
+ const idx = RPC_ENDPOINTS.findIndex(e => e.url === url);
402
+ if (idx !== -1) RPC_ENDPOINTS.splice(idx, 1);
403
+ }
404
+
405
+ /**
406
+ * Remove an LCD endpoint by URL.
407
+ * @param {string} url
408
+ */
409
+ export function removeLcdEndpoint(url) {
410
+ const idx = LCD_ENDPOINTS.findIndex(e => e.url === url);
411
+ if (idx !== -1) LCD_ENDPOINTS.splice(idx, 1);
412
+ }
413
+
414
+ /**
415
+ * Replace ALL endpoints at once (e.g. from a config file or remote registry).
416
+ * @param {'rpc'|'lcd'} type
417
+ * @param {Array<{url: string, name?: string}>} endpoints
418
+ */
419
+ export function setEndpoints(type, endpoints) {
420
+ const target = type === 'rpc' ? RPC_ENDPOINTS : LCD_ENDPOINTS;
421
+ target.length = 0;
422
+ for (const ep of endpoints) {
423
+ target.push({ url: ep.url, name: ep.name || 'Custom', verified: new Date().toISOString().slice(0, 10) });
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Get current endpoint lists (for inspection/debugging).
429
+ * @returns {{ rpc: Array, lcd: Array }}
430
+ */
431
+ export function getEndpoints() {
432
+ return { rpc: [...RPC_ENDPOINTS], lcd: [...LCD_ENDPOINTS] };
433
+ }
434
+
435
+ /**
436
+ * Health-check RPC endpoints and reorder by latency (fastest first).
437
+ * Uses Tendermint /status endpoint which returns node info + sync status.
438
+ * @param {number} [timeoutMs=5000] - Timeout per endpoint
439
+ * @returns {Promise<Array<{url: string, name: string, latencyMs: number|null, blockHeight?: number}>>}
440
+ */
441
+ export async function checkRpcEndpointHealth(timeoutMs = 5000) {
442
+ const results = await Promise.all(RPC_ENDPOINTS.map(async (ep) => {
443
+ const start = Date.now();
444
+ try {
445
+ const controller = new AbortController();
446
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
447
+ const resp = await axios.get(`${ep.url}/status`, { signal: controller.signal, timeout: timeoutMs });
448
+ clearTimeout(timer);
449
+ const height = parseInt(resp.data?.result?.sync_info?.latest_block_height || '0', 10);
450
+ return { ...ep, latencyMs: Date.now() - start, blockHeight: height };
451
+ } catch {
452
+ return { ...ep, latencyMs: null };
453
+ }
454
+ }));
455
+ return results.sort((a, b) => {
456
+ if (a.latencyMs != null && b.latencyMs != null) return a.latencyMs - b.latencyMs;
457
+ if (a.latencyMs != null) return -1;
458
+ if (b.latencyMs != null) return 1;
459
+ return 0;
460
+ });
461
+ }
462
+
463
+ /**
464
+ * Health-check both RPC and LCD endpoints, reorder by latency.
465
+ * Moves fastest-responding endpoints to the front of each array.
466
+ * @param {number} [timeoutMs=5000]
467
+ * @returns {Promise<{ rpc: Array, lcd: Array }>}
468
+ */
469
+ export async function optimizeEndpoints(timeoutMs = 5000) {
470
+ const [rpcResults, lcdResults] = await Promise.all([
471
+ checkRpcEndpointHealth(timeoutMs),
472
+ checkEndpointHealth(LCD_ENDPOINTS, timeoutMs),
473
+ ]);
474
+ // Reorder arrays in-place: healthy first, by latency
475
+ const reorder = (target, results) => {
476
+ const healthy = results.filter(r => r.latencyMs != null);
477
+ const dead = results.filter(r => r.latencyMs == null);
478
+ target.length = 0;
479
+ for (const r of [...healthy, ...dead]) {
480
+ target.push({ url: r.url, name: r.name, verified: r.verified || LAST_VERIFIED });
481
+ }
482
+ };
483
+ reorder(RPC_ENDPOINTS, rpcResults);
484
+ reorder(LCD_ENDPOINTS, lcdResults);
485
+ return { rpc: rpcResults, lcd: lcdResults };
486
+ }
package/index.js CHANGED
@@ -307,6 +307,10 @@ export {
307
307
  rpcQueryPlan,
308
308
  rpcQueryBalance,
309
309
  rpcQueryFeeGrant,
310
+ rpcQueryFeeGrants,
311
+ rpcQueryFeeGrantsIssued,
312
+ rpcQueryAuthzGrants,
313
+ rpcQueryProvider,
310
314
  } from './chain/rpc.js';
311
315
 
312
316
  // ─── Subscription Sharing (plan operator → user onboarding) ────────────────
@@ -384,6 +388,15 @@ export {
384
388
  DEFAULT_DNS_PRESET,
385
389
  DNS_FALLBACK_ORDER,
386
390
  resolveDnsServers,
391
+ // Runtime endpoint management
392
+ addRpcEndpoint,
393
+ addLcdEndpoint,
394
+ removeRpcEndpoint,
395
+ removeLcdEndpoint,
396
+ setEndpoints,
397
+ getEndpoints,
398
+ checkRpcEndpointHealth,
399
+ optimizeEndpoints,
387
400
  } from './defaults.js';
388
401
 
389
402
  // ─── Typed Errors ────────────────────────────────────────────────────────────
package/node-connect.js CHANGED
@@ -33,10 +33,17 @@ import os from 'os';
33
33
 
34
34
  import {
35
35
  createWallet, privKeyFromMnemonic, createClient, broadcast, broadcastWithFeeGrant,
36
- extractId, findExistingSession, getBalance, MSG_TYPES, resolveNodeUrl,
37
- fetchActiveNodes, filterNodes, queryNode, buildEndSessionMsg,
36
+ extractId, getBalance, MSG_TYPES,
37
+ filterNodes, buildEndSessionMsg,
38
38
  } from './cosmjs-setup.js';
39
39
 
40
+ import {
41
+ findExistingSession, fetchActiveNodes, queryNode, resolveNodeUrl,
42
+ resetQueryRpcCache,
43
+ } from './chain/queries.js';
44
+
45
+ import { disconnectRpc } from './chain/rpc.js';
46
+
40
47
  import {
41
48
  nodeStatusV3, generateWgKeyPair, initHandshakeV3,
42
49
  writeWgConfig, generateV2RayUUID, initHandshakeV3V2Ray,
@@ -385,7 +392,7 @@ export function clearSystemProxy(state) {
385
392
  // ─── Query Nodes ─────────────────────────────────────────────────────────────
386
393
 
387
394
  /**
388
- * Fetch active nodes from LCD and check which are actually online.
395
+ * Fetch active nodes via RPC-first (LCD fallback) and check which are actually online.
389
396
  * Returns array sorted by quality score (best first).
390
397
  *
391
398
  * Built-in quality scoring (from 400+ node tests):
@@ -441,12 +448,12 @@ async function _queryOnlineNodesImpl(options = {}) {
441
448
  const logFn = options.log || null;
442
449
  const brokenAddrs = new Set(BROKEN_NODES.map(n => n.address));
443
450
 
444
- // 1. Fetch ALL active nodes from LCD uses lcdPaginatedSafe (handles broken pagination)
451
+ // 1. Fetch ALL active nodes via RPC-first (falls back to LCD if RPC fails)
445
452
  let nodes = [];
446
453
  if (options.lcdUrl) {
447
454
  nodes = await fetchActiveNodes(options.lcdUrl);
448
455
  } else {
449
- const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'LCD node list');
456
+ const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'RPC-first node list');
450
457
  nodes = result;
451
458
  }
452
459
 
@@ -516,12 +523,12 @@ async function _queryOnlineNodesImpl(options = {}) {
516
523
  return online;
517
524
  }
518
525
 
519
- // ─── Full Node Catalog (LCD only, no per-node status checks) ────────────────
526
+ // ─── Full Node Catalog (RPC-first, no per-node status checks) ───────────────
520
527
 
521
528
  /**
522
- * Fetch ALL active nodes from the LCD. No per-node HTTP checks — instant.
529
+ * Fetch ALL active nodes via RPC-first (LCD fallback). No per-node HTTP checks — instant.
523
530
  *
524
- * Returns every node that accepts udvpn, with LCD data only:
531
+ * Returns every node that accepts udvpn, with chain data:
525
532
  * address, remote_url, gigabyte_prices, hourly_prices.
526
533
  *
527
534
  * Use this for: building node lists/maps, country pickers, price comparisons.
@@ -539,7 +546,7 @@ export async function fetchAllNodes(options = {}) {
539
546
  const { result } = await tryWithFallback(
540
547
  LCD_ENDPOINTS,
541
548
  async (url) => fetchActiveNodes(url),
542
- 'LCD full node list',
549
+ 'RPC-first full node list',
543
550
  );
544
551
  nodes = result;
545
552
  }
@@ -597,9 +604,9 @@ export function buildNodeIndex(nodes) {
597
604
  }
598
605
 
599
606
  /**
600
- * Enrich LCD nodes with type/country/city by probing each node's status API.
607
+ * Enrich chain nodes with type/country/city by probing each node's status API.
601
608
  *
602
- * @param {Array} nodes - Raw LCD nodes from fetchAllNodes()
609
+ * @param {Array} nodes - Raw chain nodes from fetchAllNodes()
603
610
  * @param {object} [options]
604
611
  * @param {number} [options.concurrency=30] - Parallel probes
605
612
  * @param {function} [options.onProgress] - Callback: ({ total, done, enriched }) => void
@@ -2432,15 +2439,17 @@ export function registerCleanupHandlers() {
2432
2439
  if (orphans?.cleaned?.length) console.log('[sentinel-sdk] Recovered orphans:', orphans.cleaned.join(', '));
2433
2440
  emergencyCleanupSync(); // kill stale tunnels from previous crash
2434
2441
  killOrphanV2Ray(); // kill orphaned v2ray from previous crash
2435
- process.on('exit', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); });
2436
- process.on('SIGINT', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); process.exit(130); });
2437
- process.on('SIGTERM', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); process.exit(143); });
2442
+ process.on('exit', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); resetQueryRpcCache(); disconnectRpc(); });
2443
+ process.on('SIGINT', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); resetQueryRpcCache(); disconnectRpc(); process.exit(130); });
2444
+ process.on('SIGTERM', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); resetQueryRpcCache(); disconnectRpc(); process.exit(143); });
2438
2445
  process.on('uncaughtException', (err) => {
2439
2446
  console.error('Uncaught exception:', err);
2440
2447
  if (_killSwitchEnabled) disableKillSwitch();
2441
2448
  clearSystemProxy();
2442
2449
  killOrphanV2Ray();
2443
2450
  emergencyCleanupSync();
2451
+ resetQueryRpcCache();
2452
+ disconnectRpc();
2444
2453
  process.exit(1);
2445
2454
  });
2446
2455
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blue-js-sdk",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Decentralized VPN SDK for the Sentinel P2P bandwidth network. WireGuard + V2Ray tunnels, Cosmos blockchain, 900+ nodes. Tested on Windows. macOS/Linux support included but untested.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/pricing/index.js CHANGED
@@ -11,8 +11,7 @@
11
11
  * console.log(formatDvpn(prices.gigabyte.udvpn)); // "0.04 P2P"
12
12
  */
13
13
 
14
- import { lcd } from '../chain/index.js';
15
- import { fetchActiveNodes } from '../chain/index.js';
14
+ import { queryNode, fetchActiveNodes } from '../chain/queries.js';
16
15
  import { LCD_ENDPOINTS, tryWithFallback } from '../config/index.js';
17
16
  import { ValidationError, NodeError, ErrorCodes } from '../errors/index.js';
18
17
 
@@ -39,30 +38,8 @@ export async function getNodePrices(nodeAddress, lcdUrl) {
39
38
  throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be a valid sentnode1... bech32 address (46 characters)', { value: nodeAddress });
40
39
  }
41
40
 
42
- const fetchNode = async (baseUrl) => {
43
- let nextKey = null;
44
- let pages = 0;
45
- do {
46
- const keyParam = nextKey ? `&pagination.key=${encodeURIComponent(nextKey)}` : '';
47
- const data = await lcd(baseUrl, `/sentinel/node/v3/nodes?status=1&pagination.limit=500${keyParam}`);
48
- const nodes = data.nodes || [];
49
- const found = nodes.find(n => n.address === nodeAddress);
50
- if (found) return found;
51
- nextKey = data.pagination?.next_key || null;
52
- pages++;
53
- } while (nextKey && pages < 20);
54
- return null;
55
- };
56
-
57
- let node;
58
- if (lcdUrl) {
59
- node = await fetchNode(lcdUrl);
60
- } else {
61
- const result = await tryWithFallback(LCD_ENDPOINTS, fetchNode, 'getNodePrices');
62
- node = result.result;
63
- }
64
-
65
- if (!node) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found on LCD (may be inactive or deregistered)`, { nodeAddress });
41
+ // RPC-first single node lookup (was: paginating ALL nodes via LCD)
42
+ const node = await queryNode(nodeAddress, { lcdUrl });
66
43
 
67
44
  function extractPrice(priceArray) {
68
45
  if (!Array.isArray(priceArray)) return { dvpn: 0, udvpn: 0, raw: null };
@@ -21,7 +21,7 @@ import path from 'path';
21
21
  import os from 'os';
22
22
  import { ChainError, ErrorCodes } from './errors.js';
23
23
  import { DEFAULT_LCD } from './defaults.js';
24
- import { lcdPaginatedSafe } from './cosmjs-setup.js';
24
+ import { querySessions } from './chain/queries.js';
25
25
  import { loadPoisonedKeys, savePoisonedKeys } from './state.js';
26
26
 
27
27
  // ─── Constants ───────────────────────────────────────────────────────────────
@@ -94,11 +94,11 @@ export class SessionManager {
94
94
  }
95
95
 
96
96
  const map = new Map();
97
- const queryPath = `/sentinel/session/v3/sessions?address=${addr}&status=1`;
98
97
 
99
98
  let items;
100
99
  try {
101
- const result = await lcdPaginatedSafe(this._lcdUrl, queryPath, 'sessions');
100
+ // RPC-first via chain/queries.js returns flattened sessions
101
+ const result = await querySessions(addr, this._lcdUrl, { status: '1' });
102
102
  items = result.items || [];
103
103
  } catch (err) {
104
104
  throw new ChainError(
@@ -109,13 +109,15 @@ export class SessionManager {
109
109
  }
110
110
 
111
111
  for (const s of items) {
112
+ // querySessions returns flat sessions (base_session unwrapped)
112
113
  const bs = s.base_session || s;
113
114
  const nodeAddr = bs.node_address || bs.node;
114
115
  if (!nodeAddr) continue;
115
116
 
116
117
  const acct = bs.acc_address || bs.address;
117
118
  if (acct && acct !== addr) continue;
118
- if (bs.status && bs.status !== 'active') continue;
119
+ // RPC returns status as number (1=active), LCD as string
120
+ if (bs.status && bs.status !== 'active' && bs.status !== 1) continue;
119
121
 
120
122
  const maxBytes = parseInt(bs.max_bytes || '0');
121
123
  const used = parseInt(bs.download_bytes || '0') + parseInt(bs.upload_bytes || '0');