blue-js-sdk 2.3.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/batch.js CHANGED
@@ -25,7 +25,7 @@
25
25
  */
26
26
 
27
27
  import { ChainError, ErrorCodes } from './errors.js';
28
- import { sleep } from './defaults.js';
28
+ import { sleep, DEFAULT_LCD } from './defaults.js';
29
29
  import {
30
30
  broadcast,
31
31
  extractAllSessionIds,
@@ -246,7 +246,7 @@ async function _processBatch(client, account, batch, gigabytes, denom, ctx) {
246
246
  export async function waitForBatchSessions(nodeAddrs, walletAddr, lcdUrl, options = {}) {
247
247
  const maxWaitMs = options.maxWaitMs ?? DEFAULT_POLL_TIMEOUT;
248
248
  const pollIntervalMs = options.pollIntervalMs ?? 2000;
249
- const baseLcd = lcdUrl || 'https://lcd.sentinel.co';
249
+ const baseLcd = lcdUrl || DEFAULT_LCD;
250
250
 
251
251
  if (nodeAddrs.length === 0) return { confirmed: [], pending: [] };
252
252
 
package/chain/authz.js CHANGED
@@ -11,7 +11,6 @@
11
11
 
12
12
  import { protoString, protoEmbedded, protoInt64 } from '../v3protocol.js';
13
13
  import { ChainError, ErrorCodes } from '../errors.js';
14
- import { lcd, lcdPaginatedSafe } from './lcd.js';
15
14
  import { buildRegistry } from './client.js';
16
15
 
17
16
  // ─── Protobuf Helpers ───────────────────────────────────────────────────────
@@ -99,11 +98,4 @@ export function encodeForExec(msgs) {
99
98
  });
100
99
  }
101
100
 
102
- /**
103
- * Query authz grants between granter and grantee.
104
- * @returns {Promise<Array>} Array of grant objects
105
- */
106
- export async function queryAuthzGrants(lcdUrl, granter, grantee) {
107
- const { items } = await lcdPaginatedSafe(lcdUrl, `/cosmos/authz/v1beta1/grants?granter=${granter}&grantee=${grantee}`, 'grants');
108
- return items;
109
- }
101
+ // queryAuthzGrants removed — use RPC-first version from chain/queries.js
@@ -13,7 +13,7 @@ import { EventEmitter } from 'events';
13
13
  import { protoString, protoInt64, protoEmbedded } from '../v3protocol.js';
14
14
  import { LCD_ENDPOINTS } from '../defaults.js';
15
15
  import { ValidationError, ErrorCodes } from '../errors.js';
16
- import { lcd, lcdPaginatedSafe, lcdQueryAll } from './lcd.js';
16
+ import { lcdQuery, lcdPaginatedSafe } from './lcd.js';
17
17
  import { isSameKey } from './wallet.js';
18
18
  import { queryPlanSubscribers } from './queries.js';
19
19
  import {
@@ -178,9 +178,9 @@ export async function queryFeeGrant(lcdUrl, granter, grantee) {
178
178
  }
179
179
  } catch { /* fall through to LCD */ }
180
180
 
181
- // LCD fallback
181
+ // LCD fallback (with endpoint failover via lcdQuery)
182
182
  try {
183
- const data = await lcd(lcdUrl, `/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`);
183
+ const data = await lcdQuery(`/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`, { lcdUrl });
184
184
  return data.allowance || null;
185
185
  } catch { return null; } // 404 = no grant
186
186
  }
package/chain/index.js CHANGED
@@ -50,6 +50,19 @@ import { publicEndpointAgent } from '../security/index.js';
50
50
  // Wallet — validation helpers (used by chain functions that validate addresses)
51
51
  import { validateMnemonic, validateAddress } from '../wallet/index.js';
52
52
 
53
+ // RPC-first query modules — delegates LCD-only functions to these
54
+ import {
55
+ findExistingSession as _rpcFindExistingSession,
56
+ fetchActiveNodes as _rpcFetchActiveNodes,
57
+ getNetworkOverview as _rpcGetNetworkOverview,
58
+ getNodePrices as _rpcGetNodePrices,
59
+ discoverPlanIds as _rpcDiscoverPlanIds,
60
+ } from './queries.js';
61
+ import {
62
+ queryFeeGrants as _rpcQueryFeeGrants,
63
+ queryFeeGrant as _rpcQueryFeeGrant,
64
+ } from './fee-grants.js';
65
+
53
66
  // ─── All Type URL Constants ──────────────────────────────────────────────────
54
67
 
55
68
  export const MSG_TYPES = {
@@ -345,19 +358,10 @@ export function txResponse(result) {
345
358
  /**
346
359
  * Find an existing active session for a wallet+node pair.
347
360
  * Returns session ID (BigInt) or null. Use this to avoid double-paying.
348
- *
349
- * Note: Sessions have a nested base_session object containing the actual data.
361
+ * Delegates to chain/queries.js RPC-first implementation.
350
362
  */
351
363
  export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
352
- const data = await lcd(lcdUrl, `/sentinel/session/v3/sessions?address=${walletAddr}&status=1&pagination.limit=100`);
353
- for (const s of (data.sessions || [])) {
354
- const bs = s.base_session || s; // session data is nested in base_session
355
- if (bs.node_address !== nodeAddr) continue;
356
- const maxBytes = parseInt(bs.max_bytes || '0');
357
- const used = parseInt(bs.download_bytes || '0') + parseInt(bs.upload_bytes || '0');
358
- if (maxBytes === 0 || used < maxBytes) return BigInt(bs.id);
359
- }
360
- return null;
364
+ return _rpcFindExistingSession(lcdUrl, walletAddr, nodeAddr);
361
365
  }
362
366
 
363
367
  /**
@@ -377,176 +381,44 @@ export function resolveNodeUrl(node) {
377
381
  }
378
382
 
379
383
  /**
380
- * Fetch all active nodes from LCD with pagination.
384
+ * Fetch all active nodes with RPC-first + LCD fallback.
381
385
  * Returns array of node objects. Each node has `remote_url` resolved from `remote_addrs`.
386
+ * Delegates to chain/queries.js RPC-first implementation.
382
387
  */
383
388
  export async function fetchActiveNodes(lcdUrl, limit = 500, maxPages = 20) {
384
- const nodes = [];
385
- let nextKey = null;
386
- let page = 0;
387
- do {
388
- const keyParam = nextKey ? `&pagination.key=${encodeURIComponent(nextKey)}` : '';
389
- const data = await lcd(lcdUrl, `/sentinel/node/v3/nodes?status=1&pagination.limit=${limit}${keyParam}`);
390
- nodes.push(...(data.nodes || []));
391
- nextKey = data.pagination?.next_key || null;
392
- page++;
393
- } while (nextKey && page < maxPages);
394
- // Add computed remote_url for backward compatibility
395
- for (const n of nodes) {
396
- try { n.remote_url = resolveNodeUrl(n); } catch { /* skip nodes with no address */ }
397
- }
398
- return nodes;
389
+ return _rpcFetchActiveNodes(lcdUrl, limit, maxPages);
399
390
  }
400
391
 
401
392
  /**
402
393
  * Get a quick network overview — total nodes, counts by country and service type, average prices.
403
- * Perfect for dashboard UIs, onboarding screens, and network health displays.
394
+ * Delegates to chain/queries.js RPC-first implementation.
404
395
  *
405
396
  * @param {string} [lcdUrl] - LCD endpoint (default: cascading fallback)
406
397
  * @returns {Promise<{ totalNodes: number, byCountry: Array<{country: string, count: number}>, byType: {wireguard: number, v2ray: number, unknown: number}, averagePrice: {gigabyteDvpn: number, hourlyDvpn: number}, nodes: Array }>}
407
- *
408
- * @example
409
- * const overview = await getNetworkOverview();
410
- * console.log(`${overview.totalNodes} nodes across ${overview.byCountry.length} countries`);
411
- * console.log(`Average: ${overview.averagePrice.gigabyteDvpn.toFixed(3)} P2P/GB`);
412
398
  */
413
399
  export async function getNetworkOverview(lcdUrl) {
414
- const fetchFn = async (url) => fetchActiveNodes(url);
415
- let nodes;
416
- if (lcdUrl) {
417
- nodes = await fetchFn(lcdUrl);
418
- } else {
419
- const result = await tryWithFallback(LCD_ENDPOINTS, fetchFn, 'getNetworkOverview');
420
- nodes = result.result;
421
- }
422
-
423
- // Filter to nodes that accept udvpn
424
- const active = nodes.filter(n => n.remote_url && (n.gigabyte_prices || []).some(p => p.denom === 'udvpn'));
425
-
426
- // Count by country (from LCD metadata, limited — enrichNodes gives better data)
427
- const countryMap = {};
428
- for (const n of active) {
429
- const c = n.location?.country || n.country || 'Unknown';
430
- countryMap[c] = (countryMap[c] || 0) + 1;
431
- }
432
- const byCountry = Object.entries(countryMap)
433
- .map(([country, count]) => ({ country, count }))
434
- .sort((a, b) => b.count - a.count);
435
-
436
- // Count by type (type not in LCD — estimate from service_type field if present)
437
- const byType = { wireguard: 0, v2ray: 0, unknown: 0 };
438
- for (const n of active) {
439
- const t = n.service_type || n.type;
440
- if (t === 'wireguard' || t === 1) byType.wireguard++;
441
- else if (t === 'v2ray' || t === 2) byType.v2ray++;
442
- else byType.unknown++;
443
- }
444
-
445
- // Average prices
446
- let gbTotal = 0, gbCount = 0, hrTotal = 0, hrCount = 0;
447
- for (const n of active) {
448
- const gb = (n.gigabyte_prices || []).find(p => p.denom === 'udvpn');
449
- if (gb?.quote_value) { gbTotal += parseInt(gb.quote_value, 10); gbCount++; }
450
- const hr = (n.hourly_prices || []).find(p => p.denom === 'udvpn');
451
- if (hr?.quote_value) { hrTotal += parseInt(hr.quote_value, 10); hrCount++; }
452
- }
453
-
454
- return {
455
- totalNodes: active.length,
456
- byCountry,
457
- byType,
458
- averagePrice: {
459
- gigabyteDvpn: gbCount > 0 ? (gbTotal / gbCount) / 1_000_000 : 0,
460
- hourlyDvpn: hrCount > 0 ? (hrTotal / hrCount) / 1_000_000 : 0,
461
- },
462
- nodes: active,
463
- };
400
+ return _rpcGetNetworkOverview(lcdUrl);
464
401
  }
465
402
 
466
403
  /**
467
404
  * Discover plan IDs by probing subscription endpoints.
468
- * Workaround for /sentinel/plan/v3/plans returning 501 Not Implemented.
405
+ * Delegates to chain/queries.js RPC-first implementation.
469
406
  * Returns sorted array of plan IDs that have at least 1 subscription.
470
407
  */
471
408
  export async function discoverPlanIds(lcdUrl, maxId = 100) {
472
- const ids = [];
473
- const batchSize = 10;
474
- for (let batch = 0; batch < maxId / batchSize; batch++) {
475
- const checks = [];
476
- for (let i = batch * batchSize + 1; i <= (batch + 1) * batchSize; i++) {
477
- checks.push(
478
- lcd(lcdUrl, `/sentinel/subscription/v3/plans/${i}/subscriptions?pagination.limit=1&pagination.count_total=true`)
479
- .then(d => { if (parseInt(d.pagination?.total || '0') > 0) ids.push(i); })
480
- .catch(() => {})
481
- );
482
- }
483
- await Promise.all(checks);
484
- }
485
- return ids.sort((a, b) => a - b);
409
+ return _rpcDiscoverPlanIds(lcdUrl, maxId);
486
410
  }
487
411
 
488
412
  /**
489
- * Get standardized prices for a node — abstracts V3 LCD price parsing entirely.
490
- *
491
- * Solves the common "NaN / GB" problem by defensively extracting quote_value,
492
- * base_value, or amount from the nested LCD response structure.
413
+ * Get standardized prices for a node — abstracts V3 price parsing entirely.
414
+ * Delegates to chain/queries.js RPC-first implementation (direct node lookup, not full-list scan).
493
415
  *
494
416
  * @param {string} nodeAddress - sentnode1... address
495
417
  * @param {string} [lcdUrl] - LCD endpoint URL (default: cascading fallback across all endpoints)
496
418
  * @returns {Promise<{ gigabyte: { dvpn: number, udvpn: number, raw: object|null }, hourly: { dvpn: number, udvpn: number, raw: object|null }, denom: string, nodeAddress: string }>}
497
- *
498
- * @example
499
- * const prices = await getNodePrices('sentnode1abc...');
500
- * console.log(`${prices.gigabyte.dvpn} P2P/GB, ${prices.hourly.dvpn} P2P/hr`);
501
- * // Use prices.gigabyte.raw for the full { denom, base_value, quote_value } object
502
- * // needed by encodeMsgStartSession's max_price field.
503
419
  */
504
420
  export async function getNodePrices(nodeAddress, lcdUrl) {
505
- if (typeof nodeAddress !== 'string' || !/^sentnode1[a-z0-9]{38}$/.test(nodeAddress)) {
506
- throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be a valid sentnode1... bech32 address (46 characters)', { value: nodeAddress });
507
- }
508
-
509
- const fetchNode = async (baseUrl) => {
510
- let nextKey = null;
511
- let pages = 0;
512
- do {
513
- const keyParam = nextKey ? `&pagination.key=${encodeURIComponent(nextKey)}` : '';
514
- const data = await lcd(baseUrl, `/sentinel/node/v3/nodes?status=1&pagination.limit=500${keyParam}`);
515
- const nodes = data.nodes || [];
516
- const found = nodes.find(n => n.address === nodeAddress);
517
- if (found) return found;
518
- nextKey = data.pagination?.next_key || null;
519
- pages++;
520
- } while (nextKey && pages < 20);
521
- return null;
522
- };
523
-
524
- let node;
525
- if (lcdUrl) {
526
- node = await fetchNode(lcdUrl);
527
- } else {
528
- const result = await tryWithFallback(LCD_ENDPOINTS, fetchNode, 'getNodePrices');
529
- node = result.result;
530
- }
531
-
532
- if (!node) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found on LCD (may be inactive or deregistered)`, { nodeAddress });
533
-
534
- function extractPrice(priceArray) {
535
- if (!Array.isArray(priceArray)) return { dvpn: 0, udvpn: 0, raw: null };
536
- const entry = priceArray.find(p => p.denom === 'udvpn');
537
- if (!entry) return { dvpn: 0, udvpn: 0, raw: null };
538
- // Defensive fallback chain: quote_value (V3 current) -> base_value -> amount (legacy)
539
- const rawVal = entry.quote_value || entry.base_value || entry.amount || '0';
540
- const udvpn = parseInt(rawVal, 10) || 0;
541
- return { dvpn: parseFloat((udvpn / 1_000_000).toFixed(6)), udvpn, raw: entry };
542
- }
543
-
544
- return {
545
- gigabyte: extractPrice(node.gigabyte_prices),
546
- hourly: extractPrice(node.hourly_prices),
547
- denom: 'P2P',
548
- nodeAddress,
549
- };
421
+ return _rpcGetNodePrices(nodeAddress, lcdUrl);
550
422
  }
551
423
 
552
424
  // ─── Display & Serialization Helpers ────────────────────────────────────────
@@ -761,22 +633,20 @@ export function buildRevokeFeeGrantMsg(granter, grantee) {
761
633
 
762
634
  /**
763
635
  * Query fee grants given to a grantee.
636
+ * Delegates to chain/fee-grants.js RPC-first implementation.
764
637
  * @returns {Promise<Array>} Array of allowance objects
765
638
  */
766
639
  export async function queryFeeGrants(lcdUrl, grantee) {
767
- const data = await lcd(lcdUrl, `/cosmos/feegrant/v1beta1/allowances/${grantee}`);
768
- return data.allowances || [];
640
+ return _rpcQueryFeeGrants(lcdUrl, grantee);
769
641
  }
770
642
 
771
643
  /**
772
644
  * Query a specific fee grant between granter and grantee.
645
+ * Delegates to chain/fee-grants.js RPC-first implementation.
773
646
  * @returns {Promise<object|null>} Allowance object or null
774
647
  */
775
648
  export async function queryFeeGrant(lcdUrl, granter, grantee) {
776
- try {
777
- const data = await lcd(lcdUrl, `/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`);
778
- return data.allowance || null;
779
- } catch { return null; } // 404 = no grant
649
+ return _rpcQueryFeeGrant(lcdUrl, granter, grantee);
780
650
  }
781
651
 
782
652
  /**
@@ -878,14 +748,7 @@ export function encodeForExec(msgs) {
878
748
  });
879
749
  }
880
750
 
881
- /**
882
- * Query authz grants between granter and grantee.
883
- * @returns {Promise<Array>} Array of grant objects
884
- */
885
- export async function queryAuthzGrants(lcdUrl, granter, grantee) {
886
- const data = await lcd(lcdUrl, `/cosmos/authz/v1beta1/grants?granter=${granter}&grantee=${grantee}`);
887
- return data.grants || [];
888
- }
751
+ // queryAuthzGrants removed — use RPC-first version from chain/queries.js
889
752
 
890
753
  // Re-export extractSessionId for convenience (from protocol module)
891
754
  export { extractSessionId };
package/chain/queries.js CHANGED
@@ -56,6 +56,15 @@ async function getRpcClient() {
56
56
  return _rpcClientPromise;
57
57
  }
58
58
 
59
+ /**
60
+ * Clear the cached RPC query client. Called during process cleanup
61
+ * to ensure WebSocket connections are properly closed.
62
+ */
63
+ export function resetQueryRpcCache() {
64
+ _rpcClient = null;
65
+ _rpcClientPromise = null;
66
+ }
67
+
59
68
  // ─── Query Helpers ───────────────────────────────────────────────────────────
60
69
 
61
70
  /**
@@ -344,9 +353,9 @@ export async function querySessionById(lcdUrl, sessionId) {
344
353
  }
345
354
  } catch { /* fall through to LCD */ }
346
355
 
347
- // LCD fallback
356
+ // LCD fallback (with endpoint failover via lcdQuery)
348
357
  try {
349
- const data = await lcd(lcdUrl, `/sentinel/session/v3/sessions/${sessionId}`);
358
+ const data = await lcdQuery(`/sentinel/session/v3/sessions/${sessionId}`, { lcdUrl });
350
359
  const raw = data?.session;
351
360
  if (!raw) return null;
352
361
  return flattenSession(raw);
@@ -372,9 +381,9 @@ export async function querySessionAllocation(lcdUrl, sessionId) {
372
381
  } catch { /* fall through */ }
373
382
 
374
383
  if (!s) {
375
- // LCD fallback
384
+ // LCD fallback (with endpoint failover via lcdQuery)
376
385
  try {
377
- const data = await lcd(lcdUrl, `/sentinel/session/v3/sessions/${sessionId}`);
386
+ const data = await lcdQuery(`/sentinel/session/v3/sessions/${sessionId}`, { lcdUrl });
378
387
  s = data.session?.base_session || data.session || null;
379
388
  } catch { return null; }
380
389
  }
package/client/index.js CHANGED
@@ -33,9 +33,7 @@ import {
33
33
  events as sdkEvents, ConnectionState,
34
34
  } from '../connection/index.js';
35
35
  import {
36
- createWallet, privKeyFromMnemonic, createClient, broadcast,
37
- createSafeBroadcaster, getBalance, findExistingSession, fetchActiveNodes,
38
- discoverPlanIds, resolveNodeUrl, lcd, MSG_TYPES,
36
+ createWallet, createClient, getBalance,
39
37
  } from '../chain/index.js';
40
38
  import { nodeStatusV3 } from '../protocol/index.js';
41
39
  import { createNodeHttpsAgent, clearKnownNode, clearAllKnownNodes, getKnownNode } from '../security/index.js';
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Node Discovery — query, fetch, enrich, index, and score nodes.
3
3
  *
4
- * Handles LCD queries for online nodes, caching, quality scoring,
5
- * and geographic indexing.
4
+ * Handles RPC-first queries (LCD fallback) for online nodes, caching,
5
+ * quality scoring, and geographic indexing.
6
6
  */
7
7
 
8
8
  import {
@@ -63,7 +63,7 @@ export function scoreNode(status) {
63
63
  // ─── Query Nodes ─────────────────────────────────────────────────────────────
64
64
 
65
65
  /**
66
- * Fetch active nodes from LCD and check which are actually online.
66
+ * Fetch active nodes via RPC-first (LCD fallback) and check which are actually online.
67
67
  * Returns array sorted by quality score (best first).
68
68
  *
69
69
  * Built-in quality scoring (from 400+ node tests):
@@ -119,12 +119,12 @@ async function _queryOnlineNodesImpl(options = {}) {
119
119
  const logFn = options.log || null;
120
120
  const brokenAddrs = new Set(BROKEN_NODES.map(n => n.address));
121
121
 
122
- // 1. Fetch ALL active nodes from LCD uses lcdPaginatedSafe (handles broken pagination)
122
+ // 1. Fetch ALL active nodes via RPC-first (falls back to LCD if RPC fails)
123
123
  let nodes = [];
124
124
  if (options.lcdUrl) {
125
125
  nodes = await fetchActiveNodes(options.lcdUrl);
126
126
  } else {
127
- const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'LCD node list');
127
+ const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'RPC-first node list');
128
128
  nodes = result;
129
129
  }
130
130
 
@@ -194,12 +194,12 @@ async function _queryOnlineNodesImpl(options = {}) {
194
194
  return online;
195
195
  }
196
196
 
197
- // ─── Full Node Catalog (LCD only, no per-node status checks) ────────────────
197
+ // ─── Full Node Catalog (RPC-first, no per-node status checks) ───────────────
198
198
 
199
199
  /**
200
- * Fetch ALL active nodes from the LCD. No per-node HTTP checks — instant.
200
+ * Fetch ALL active nodes via RPC-first (LCD fallback). No per-node HTTP checks — instant.
201
201
  *
202
- * Returns every node that accepts udvpn, with LCD data only:
202
+ * Returns every node that accepts udvpn, with chain data:
203
203
  * address, remote_url, gigabyte_prices, hourly_prices.
204
204
  *
205
205
  * Use this for: building node lists/maps, country pickers, price comparisons.
@@ -217,7 +217,7 @@ export async function fetchAllNodes(options = {}) {
217
217
  const { result } = await tryWithFallback(
218
218
  LCD_ENDPOINTS,
219
219
  async (url) => fetchActiveNodes(url),
220
- 'LCD full node list',
220
+ 'RPC-first full node list',
221
221
  );
222
222
  nodes = result;
223
223
  }
@@ -275,9 +275,9 @@ export function buildNodeIndex(nodes) {
275
275
  }
276
276
 
277
277
  /**
278
- * Enrich LCD nodes with type/country/city by probing each node's status API.
278
+ * Enrich chain nodes with type/country/city by probing each node's status API.
279
279
  *
280
- * @param {Array} nodes - Raw LCD nodes from fetchAllNodes()
280
+ * @param {Array} nodes - Raw chain nodes from fetchAllNodes()
281
281
  * @param {object} [options]
282
282
  * @param {number} [options.concurrency=30] - Parallel probes
283
283
  * @param {function} [options.onProgress] - Callback: ({ total, done, enriched }) => void
package/cosmjs-setup.js CHANGED
@@ -462,29 +462,11 @@ export function extractId(txResult, eventPattern, keyNames) {
462
462
  }
463
463
 
464
464
  // ─── LCD Query Helper ────────────────────────────────────────────────────────
465
+ // Canonical LCD functions live in chain/lcd.js. Re-export for backward compatibility.
465
466
 
466
467
  import axios from 'axios';
467
- import { publicEndpointAgent } from './tls-trust.js';
468
468
 
469
- /**
470
- * Query a Sentinel LCD REST endpoint.
471
- * Checks both HTTP status AND gRPC error codes in response body.
472
- * Uses CA-validated HTTPS for LCD public infrastructure (valid CA certs).
473
- *
474
- * Usage:
475
- * const data = await lcd('https://lcd.sentinel.co', '/sentinel/node/v3/nodes?status=1');
476
- */
477
- export async function lcd(baseUrl, path) {
478
- // Accept Endpoint objects ({ url, name }) or bare strings
479
- const base = typeof baseUrl === 'object' ? baseUrl.url : baseUrl;
480
- const url = `${base}${path}`;
481
- const res = await axios.get(url, { httpsAgent: publicEndpointAgent, timeout: 15000 });
482
- const data = res.data;
483
- if (data?.code && data.code !== 0) {
484
- throw new ChainError(ErrorCodes.LCD_ERROR, `LCD ${path}: code=${data.code} ${data.message || ''}`, { path, code: data.code, message: data.message });
485
- }
486
- return data;
487
- }
469
+ export { lcd, lcdQuery, lcdQueryAll, lcdPaginatedSafe } from './chain/lcd.js';
488
470
 
489
471
  // ─── Query Helpers ───────────────────────────────────────────────────────────
490
472
 
@@ -933,90 +915,7 @@ export async function queryAuthzGrants(lcdUrl, granter, grantee) {
933
915
  return _queryAuthzGrants(lcdUrl, granter, grantee);
934
916
  }
935
917
 
936
- // ─── LCD Query Helpers (v25b) ────────────────────────────────────────────────
937
- // General-purpose LCD query with timeout, retry, error wrapping, and pagination.
938
-
939
- /**
940
- * Single LCD query with timeout, single retry on network error, and ChainError wrapping.
941
- * Uses the fallback endpoint list if no lcdUrl is provided.
942
- *
943
- * @param {string} path - LCD path (e.g. '/sentinel/node/v3/nodes?status=1')
944
- * @param {object} [opts]
945
- * @param {string} [opts.lcdUrl] - Specific LCD endpoint (or uses fallback chain)
946
- * @param {number} [opts.timeout] - Request timeout in ms (default: 15000)
947
- * @returns {Promise<any>} Parsed JSON response
948
- */
949
- export async function lcdQuery(path, opts = {}) {
950
- const timeout = opts.timeout || 15000;
951
- const doQuery = async (baseUrl) => {
952
- try {
953
- return await lcd(baseUrl, path);
954
- } catch (err) {
955
- // Single retry on network error
956
- if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.message?.includes('timeout')) {
957
- await new Promise(r => setTimeout(r, 1000));
958
- return await lcd(baseUrl, path);
959
- }
960
- throw err;
961
- }
962
- };
963
-
964
- if (opts.lcdUrl) {
965
- return doQuery(opts.lcdUrl);
966
- }
967
- const { result } = await tryWithFallback(LCD_ENDPOINTS, doQuery, `LCD ${path}`);
968
- return result;
969
- }
970
-
971
- /**
972
- * Auto-paginating LCD query. Fetches all pages via next_key, returns all results + chain total.
973
- *
974
- * @param {string} basePath - LCD path without pagination params (e.g. '/sentinel/node/v3/nodes?status=1')
975
- * @param {object} [opts]
976
- * @param {string} [opts.lcdUrl] - Specific LCD endpoint (or uses fallback chain)
977
- * @param {number} [opts.limit] - Page size (default: 200)
978
- * @param {number} [opts.timeout] - Per-page timeout (default: 15000)
979
- * @param {string} [opts.dataKey] - Key for the results array in response (default: auto-detect first array)
980
- * @returns {Promise<{ items: any[], total: number|null }>}
981
- */
982
- export async function lcdQueryAll(basePath, opts = {}) {
983
- const limit = opts.limit || 200;
984
- const dataKey = opts.dataKey || null;
985
-
986
- const fetchAll = async (baseUrl) => {
987
- let allItems = [];
988
- let nextKey = null;
989
- let chainTotal = null;
990
- let isFirst = true;
991
- do {
992
- const sep = basePath.includes('?') ? '&' : '?';
993
- let url = `${basePath}${sep}pagination.limit=${limit}`;
994
- if (isFirst) url += '&pagination.count_total=true';
995
- if (nextKey) url += `&pagination.key=${encodeURIComponent(nextKey)}`;
996
- const data = await lcd(baseUrl, url);
997
- if (isFirst && data.pagination?.total) {
998
- chainTotal = parseInt(data.pagination.total, 10);
999
- }
1000
- // Auto-detect data key: first array property that isn't 'pagination'
1001
- const key = dataKey || Object.keys(data).find(k => k !== 'pagination' && Array.isArray(data[k]));
1002
- const pageItems = key ? (data[key] || []) : [];
1003
- allItems = allItems.concat(pageItems);
1004
- nextKey = data.pagination?.next_key || null;
1005
- isFirst = false;
1006
- } while (nextKey);
1007
-
1008
- if (chainTotal && allItems.length !== chainTotal) {
1009
- console.warn(`[lcdQueryAll] Pagination mismatch: got ${allItems.length}, chain reports ${chainTotal}`);
1010
- }
1011
- return { items: allItems, total: chainTotal };
1012
- };
1013
-
1014
- if (opts.lcdUrl) {
1015
- return fetchAll(opts.lcdUrl);
1016
- }
1017
- const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchAll, `LCD paginated ${basePath}`);
1018
- return result;
1019
- }
918
+ // LCD Query Helpers canonical implementations in chain/lcd.js, re-exported above.
1020
919
 
1021
920
  // ─── Plan Subscriber Helpers (v25b) ──────────────────────────────────────────
1022
921
 
@@ -1176,53 +1075,7 @@ export function buildEndSessionMsg(from, sessionId) {
1176
1075
  };
1177
1076
  }
1178
1077
 
1179
- // ─── v26c: Defensive Pagination ──────────────────────────────────────────────
1180
-
1181
- /**
1182
- * Paginated LCD query that handles Sentinel's broken pagination.
1183
- * Tries next_key first. If next_key is null but we got exactly `limit` results
1184
- * (suggesting truncation), falls back to a single large request.
1185
- *
1186
- * @param {string} lcdUrl - LCD base URL
1187
- * @param {string} path - Endpoint path (e.g. '/sentinel/node/v3/plans/36/nodes')
1188
- * @param {string} itemsKey - Response array key ('nodes', 'subscriptions', 'sessions')
1189
- * @param {object} [opts]
1190
- * @param {number} [opts.limit=500] - Page size for paginated requests
1191
- * @param {number} [opts.fallbackLimit=5000] - Single-request limit if pagination broken
1192
- * @returns {Promise<{ items: any[], total: number }>}
1193
- */
1194
- export async function lcdPaginatedSafe(lcdUrl, path, itemsKey, opts = {}) {
1195
- const limit = opts.limit || 500;
1196
- const fallbackLimit = opts.fallbackLimit || 5000;
1197
- const baseLcd = lcdUrl || LCD_ENDPOINTS[0].url;
1198
- const sep = path.includes('?') ? '&' : '?';
1199
-
1200
- const firstPage = await lcd(baseLcd, `${path}${sep}pagination.limit=${limit}`);
1201
- const firstItems = firstPage[itemsKey] || [];
1202
- const nextKey = firstPage.pagination?.next_key;
1203
-
1204
- // Fewer than limit = that's everything
1205
- if (firstItems.length < limit) {
1206
- return { items: firstItems, total: firstItems.length };
1207
- }
1208
-
1209
- // next_key exists = pagination works, follow it
1210
- if (nextKey) {
1211
- let allItems = [...firstItems];
1212
- let key = nextKey;
1213
- while (key) {
1214
- const page = await lcd(baseLcd, `${path}${sep}pagination.limit=${limit}&pagination.key=${encodeURIComponent(key)}`);
1215
- allItems.push(...(page[itemsKey] || []));
1216
- key = page.pagination?.next_key || null;
1217
- }
1218
- return { items: allItems, total: allItems.length };
1219
- }
1220
-
1221
- // next_key null but hit limit = broken pagination. Single large request.
1222
- const fullData = await lcd(baseLcd, `${path}${sep}pagination.limit=${fallbackLimit}`);
1223
- const allItems = fullData[itemsKey] || [];
1224
- return { items: allItems, total: allItems.length };
1225
- }
1078
+ // lcdPaginatedSafe canonical implementation in chain/lcd.js, re-exported above.
1226
1079
 
1227
1080
  // ─── v26c: Session & Subscription Queries ────────────────────────────────────
1228
1081
 
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 = '2.3.0';
28
+ export const SDK_VERSION = '2.4.0';
29
29
 
30
30
  // ─── Timestamps ──────────────────────────────────────────────────────────────
31
31
 
package/node-connect.js CHANGED
@@ -39,8 +39,11 @@ import {
39
39
 
40
40
  import {
41
41
  findExistingSession, fetchActiveNodes, queryNode, resolveNodeUrl,
42
+ resetQueryRpcCache,
42
43
  } from './chain/queries.js';
43
44
 
45
+ import { disconnectRpc } from './chain/rpc.js';
46
+
44
47
  import {
45
48
  nodeStatusV3, generateWgKeyPair, initHandshakeV3,
46
49
  writeWgConfig, generateV2RayUUID, initHandshakeV3V2Ray,
@@ -389,7 +392,7 @@ export function clearSystemProxy(state) {
389
392
  // ─── Query Nodes ─────────────────────────────────────────────────────────────
390
393
 
391
394
  /**
392
- * 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.
393
396
  * Returns array sorted by quality score (best first).
394
397
  *
395
398
  * Built-in quality scoring (from 400+ node tests):
@@ -445,12 +448,12 @@ async function _queryOnlineNodesImpl(options = {}) {
445
448
  const logFn = options.log || null;
446
449
  const brokenAddrs = new Set(BROKEN_NODES.map(n => n.address));
447
450
 
448
- // 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)
449
452
  let nodes = [];
450
453
  if (options.lcdUrl) {
451
454
  nodes = await fetchActiveNodes(options.lcdUrl);
452
455
  } else {
453
- const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'LCD node list');
456
+ const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'RPC-first node list');
454
457
  nodes = result;
455
458
  }
456
459
 
@@ -520,12 +523,12 @@ async function _queryOnlineNodesImpl(options = {}) {
520
523
  return online;
521
524
  }
522
525
 
523
- // ─── Full Node Catalog (LCD only, no per-node status checks) ────────────────
526
+ // ─── Full Node Catalog (RPC-first, no per-node status checks) ───────────────
524
527
 
525
528
  /**
526
- * 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.
527
530
  *
528
- * Returns every node that accepts udvpn, with LCD data only:
531
+ * Returns every node that accepts udvpn, with chain data:
529
532
  * address, remote_url, gigabyte_prices, hourly_prices.
530
533
  *
531
534
  * Use this for: building node lists/maps, country pickers, price comparisons.
@@ -543,7 +546,7 @@ export async function fetchAllNodes(options = {}) {
543
546
  const { result } = await tryWithFallback(
544
547
  LCD_ENDPOINTS,
545
548
  async (url) => fetchActiveNodes(url),
546
- 'LCD full node list',
549
+ 'RPC-first full node list',
547
550
  );
548
551
  nodes = result;
549
552
  }
@@ -601,9 +604,9 @@ export function buildNodeIndex(nodes) {
601
604
  }
602
605
 
603
606
  /**
604
- * 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.
605
608
  *
606
- * @param {Array} nodes - Raw LCD nodes from fetchAllNodes()
609
+ * @param {Array} nodes - Raw chain nodes from fetchAllNodes()
607
610
  * @param {object} [options]
608
611
  * @param {number} [options.concurrency=30] - Parallel probes
609
612
  * @param {function} [options.onProgress] - Callback: ({ total, done, enriched }) => void
@@ -2436,15 +2439,17 @@ export function registerCleanupHandlers() {
2436
2439
  if (orphans?.cleaned?.length) console.log('[sentinel-sdk] Recovered orphans:', orphans.cleaned.join(', '));
2437
2440
  emergencyCleanupSync(); // kill stale tunnels from previous crash
2438
2441
  killOrphanV2Ray(); // kill orphaned v2ray from previous crash
2439
- process.on('exit', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); });
2440
- process.on('SIGINT', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); process.exit(130); });
2441
- 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); });
2442
2445
  process.on('uncaughtException', (err) => {
2443
2446
  console.error('Uncaught exception:', err);
2444
2447
  if (_killSwitchEnabled) disableKillSwitch();
2445
2448
  clearSystemProxy();
2446
2449
  killOrphanV2Ray();
2447
2450
  emergencyCleanupSync();
2451
+ resetQueryRpcCache();
2452
+ disconnectRpc();
2448
2453
  process.exit(1);
2449
2454
  });
2450
2455
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blue-js-sdk",
3
- "version": "2.3.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 };