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/CHANGELOG.md +34 -0
- package/batch.js +6 -10
- package/chain/authz.js +1 -9
- package/chain/fee-grants.js +53 -2
- package/chain/index.js +30 -167
- package/chain/queries.js +98 -12
- package/chain/rpc.js +169 -0
- package/client/index.js +1 -3
- package/connection/discovery.js +11 -11
- package/cosmjs-setup.js +68 -521
- package/defaults.js +121 -1
- package/index.js +13 -0
- package/node-connect.js +23 -14
- package/package.json +1 -1
- package/pricing/index.js +3 -26
- package/session-manager.js +6 -4
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 = '
|
|
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,
|
|
37
|
-
|
|
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
|
|
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
|
|
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, '
|
|
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 (
|
|
526
|
+
// ─── Full Node Catalog (RPC-first, no per-node status checks) ───────────────
|
|
520
527
|
|
|
521
528
|
/**
|
|
522
|
-
* Fetch ALL active nodes
|
|
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
|
|
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
|
-
'
|
|
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
|
|
607
|
+
* Enrich chain nodes with type/country/city by probing each node's status API.
|
|
601
608
|
*
|
|
602
|
-
* @param {Array} nodes - Raw
|
|
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.
|
|
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 };
|
package/session-manager.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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');
|