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 +2 -2
- package/chain/authz.js +1 -9
- package/chain/fee-grants.js +3 -3
- package/chain/index.js +30 -167
- package/chain/queries.js +13 -4
- package/client/index.js +1 -3
- package/connection/discovery.js +11 -11
- package/cosmjs-setup.js +4 -151
- package/defaults.js +1 -1
- package/node-connect.js +17 -12
- package/package.json +1 -1
- package/pricing/index.js +3 -26
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 ||
|
|
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
|
package/chain/fee-grants.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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';
|
package/connection/discovery.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Node Discovery — query, fetch, enrich, index, and score nodes.
|
|
3
3
|
*
|
|
4
|
-
* Handles
|
|
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
|
|
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
|
|
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, '
|
|
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 (
|
|
197
|
+
// ─── Full Node Catalog (RPC-first, no per-node status checks) ───────────────
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
-
* Fetch ALL active nodes
|
|
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
|
|
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
|
-
'
|
|
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
|
|
278
|
+
* Enrich chain nodes with type/country/city by probing each node's status API.
|
|
279
279
|
*
|
|
280
|
-
* @param {Array} nodes - Raw
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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
|
|
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, '
|
|
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 (
|
|
526
|
+
// ─── Full Node Catalog (RPC-first, no per-node status checks) ───────────────
|
|
524
527
|
|
|
525
528
|
/**
|
|
526
|
-
* Fetch ALL active nodes
|
|
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
|
|
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
|
-
'
|
|
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
|
|
607
|
+
* Enrich chain nodes with type/country/city by probing each node's status API.
|
|
605
608
|
*
|
|
606
|
-
* @param {Array} nodes - Raw
|
|
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
|
+
"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 {
|
|
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
|
-
|
|
43
|
-
|
|
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 };
|