blue-js-sdk 2.3.0 → 2.6.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/README.md +3 -3
- package/batch.js +2 -2
- package/chain/authz.js +1 -9
- package/chain/fee-grants.js +199 -3
- package/chain/index.js +36 -167
- package/chain/queries.js +118 -7
- package/chain/rpc.js +58 -2
- package/client/index.js +1 -3
- package/client.js +17 -1
- package/connection/connect.js +17 -5
- package/connection/disconnect.js +86 -10
- package/connection/discovery.js +11 -11
- package/connection/index.js +2 -0
- package/cosmjs-setup.js +30 -153
- package/defaults.js +1 -1
- package/index.js +5 -1
- package/node-connect.js +118 -25
- package/operator.js +2 -0
- package/package.json +2 -5
- package/pricing/index.js +3 -26
- package/types/index.d.ts +2 -2
- package/ai-path/ADMIN-ELEVATION.md +0 -116
- package/ai-path/AI-MANIFESTO.md +0 -185
- package/ai-path/BREAKING.md +0 -74
- package/ai-path/CHECKLIST.md +0 -619
- package/ai-path/CONNECTION-STEPS.md +0 -724
- package/ai-path/DECISION-TREE.md +0 -422
- package/ai-path/DEPENDENCIES.md +0 -459
- package/ai-path/E2E-FLOW.md +0 -1707
- package/ai-path/FAILURES.md +0 -410
- package/ai-path/GUIDE.md +0 -1315
- package/ai-path/README.md +0 -599
- package/ai-path/SPLIT-TUNNEL.md +0 -266
- package/ai-path/cli.js +0 -548
- package/ai-path/connect.js +0 -1028
- package/ai-path/discover.js +0 -178
- package/ai-path/environment.js +0 -266
- package/ai-path/errors.js +0 -86
- package/ai-path/examples/autonomous-agent.mjs +0 -220
- package/ai-path/examples/multi-region.mjs +0 -174
- package/ai-path/examples/one-shot.mjs +0 -31
- package/ai-path/index.js +0 -79
- package/ai-path/pricing.js +0 -137
- package/ai-path/recommend.js +0 -413
- package/ai-path/run-admin.vbs +0 -25
- package/ai-path/setup.js +0 -291
- package/ai-path/wallet.js +0 -137
package/cosmjs-setup.js
CHANGED
|
@@ -87,6 +87,8 @@ import {
|
|
|
87
87
|
getExpiringGrants as _getExpiringGrants,
|
|
88
88
|
renewExpiringGrants as _renewExpiringGrants,
|
|
89
89
|
monitorFeeGrants as _monitorFeeGrants,
|
|
90
|
+
streamGrantPlanSubscribers as _streamGrantPlanSubscribers,
|
|
91
|
+
computeFeeGrantGasCosts as _computeFeeGrantGasCosts,
|
|
90
92
|
} from './chain/fee-grants.js';
|
|
91
93
|
|
|
92
94
|
// ─── Input Validation Helpers ────────────────────────────────────────────────
|
|
@@ -462,29 +464,11 @@ export function extractId(txResult, eventPattern, keyNames) {
|
|
|
462
464
|
}
|
|
463
465
|
|
|
464
466
|
// ─── LCD Query Helper ────────────────────────────────────────────────────────
|
|
467
|
+
// Canonical LCD functions live in chain/lcd.js. Re-export for backward compatibility.
|
|
465
468
|
|
|
466
469
|
import axios from 'axios';
|
|
467
|
-
import { publicEndpointAgent } from './tls-trust.js';
|
|
468
470
|
|
|
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
|
-
}
|
|
471
|
+
export { lcd, lcdQuery, lcdQueryAll, lcdPaginatedSafe } from './chain/lcd.js';
|
|
488
472
|
|
|
489
473
|
// ─── Query Helpers ───────────────────────────────────────────────────────────
|
|
490
474
|
|
|
@@ -503,9 +487,15 @@ export async function getBalance(client, address) {
|
|
|
503
487
|
* Returns session ID (BigInt) or null. Use this to avoid double-paying.
|
|
504
488
|
*
|
|
505
489
|
* Note: Sessions have a nested base_session object containing the actual data.
|
|
490
|
+
*
|
|
491
|
+
* @param {string} lcdUrl - LCD endpoint URL
|
|
492
|
+
* @param {string} walletAddr - sent1... wallet address
|
|
493
|
+
* @param {string} nodeAddr - sentnode1... node address
|
|
494
|
+
* @param {object} [opts] - Optional. Pass `{ onStaleDuplicate: (BigInt) => void }` to
|
|
495
|
+
* receive fire-and-forget cancellation callbacks for stale duplicate sessions.
|
|
506
496
|
*/
|
|
507
|
-
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
|
|
508
|
-
return _findExistingSession(lcdUrl, walletAddr, nodeAddr);
|
|
497
|
+
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr, opts) {
|
|
498
|
+
return _findExistingSession(lcdUrl, walletAddr, nodeAddr, opts);
|
|
509
499
|
}
|
|
510
500
|
|
|
511
501
|
/**
|
|
@@ -933,90 +923,7 @@ export async function queryAuthzGrants(lcdUrl, granter, grantee) {
|
|
|
933
923
|
return _queryAuthzGrants(lcdUrl, granter, grantee);
|
|
934
924
|
}
|
|
935
925
|
|
|
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
|
-
}
|
|
926
|
+
// LCD Query Helpers — canonical implementations in chain/lcd.js, re-exported above.
|
|
1020
927
|
|
|
1021
928
|
// ─── Plan Subscriber Helpers (v25b) ──────────────────────────────────────────
|
|
1022
929
|
|
|
@@ -1107,6 +1014,22 @@ export function monitorFeeGrants(opts = {}) {
|
|
|
1107
1014
|
return _monitorFeeGrants(opts);
|
|
1108
1015
|
}
|
|
1109
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* Stream progress as we grant fee allowances to all plan subscribers in batches.
|
|
1019
|
+
* Async generator. See chain/fee-grants.js for event shapes.
|
|
1020
|
+
*/
|
|
1021
|
+
export function streamGrantPlanSubscribers(planId, opts = {}) {
|
|
1022
|
+
return _streamGrantPlanSubscribers(planId, opts);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Sum udvpn fees the granter has paid on behalf of a plan's subscribers.
|
|
1027
|
+
* Iterates subscribers, pulls TX history, filters on fee.granter.
|
|
1028
|
+
*/
|
|
1029
|
+
export async function computeFeeGrantGasCosts(planId, opts = {}) {
|
|
1030
|
+
return _computeFeeGrantGasCosts(planId, opts);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1110
1033
|
// ─── Query Helpers (v25c) ────────────────────────────────────────────────────
|
|
1111
1034
|
|
|
1112
1035
|
/**
|
|
@@ -1176,53 +1099,7 @@ export function buildEndSessionMsg(from, sessionId) {
|
|
|
1176
1099
|
};
|
|
1177
1100
|
}
|
|
1178
1101
|
|
|
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
|
-
}
|
|
1102
|
+
// lcdPaginatedSafe — canonical implementation in chain/lcd.js, re-exported above.
|
|
1226
1103
|
|
|
1227
1104
|
// ─── v26c: Session & Subscription Queries ────────────────────────────────────
|
|
1228
1105
|
|
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/index.js
CHANGED
|
@@ -27,6 +27,9 @@ export {
|
|
|
27
27
|
enrichNodes,
|
|
28
28
|
buildNodeIndex,
|
|
29
29
|
disconnect,
|
|
30
|
+
disconnectAndEndSession,
|
|
31
|
+
disconnectState,
|
|
32
|
+
disconnectStateAndEndSession,
|
|
30
33
|
isConnected,
|
|
31
34
|
getStatus,
|
|
32
35
|
registerCleanupHandlers,
|
|
@@ -52,7 +55,6 @@ export {
|
|
|
52
55
|
disableDnsLeakPrevention,
|
|
53
56
|
events,
|
|
54
57
|
ConnectionState,
|
|
55
|
-
disconnectState,
|
|
56
58
|
tryFastReconnect,
|
|
57
59
|
} from './node-connect.js';
|
|
58
60
|
|
|
@@ -311,6 +313,7 @@ export {
|
|
|
311
313
|
rpcQueryFeeGrantsIssued,
|
|
312
314
|
rpcQueryAuthzGrants,
|
|
313
315
|
rpcQueryProvider,
|
|
316
|
+
rpcGetTxByHash,
|
|
314
317
|
} from './chain/rpc.js';
|
|
315
318
|
|
|
316
319
|
// ─── Subscription Sharing (plan operator → user onboarding) ────────────────
|
|
@@ -323,6 +326,7 @@ export {
|
|
|
323
326
|
|
|
324
327
|
export {
|
|
325
328
|
querySubscriptionAllocations,
|
|
329
|
+
getTxByHash,
|
|
326
330
|
} from './chain/queries.js';
|
|
327
331
|
|
|
328
332
|
// ─── TypeScript Client (extends CosmJS SigningStargateClient) ───────────────
|
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
|
|
@@ -1008,7 +1011,17 @@ export async function connectDirect(opts) {
|
|
|
1008
1011
|
if (!forceNewSession) {
|
|
1009
1012
|
progress(onProgress, logFn, 'session', 'Checking for existing session...');
|
|
1010
1013
|
checkAborted(signal);
|
|
1011
|
-
sessionId = await findExistingSession(lcd, account.address, opts.nodeAddress
|
|
1014
|
+
sessionId = await findExistingSession(lcd, account.address, opts.nodeAddress, {
|
|
1015
|
+
// Dedup: if multiple active sessions exist for this node (stale duplicates from
|
|
1016
|
+
// crashes or multi-client wallets), keep the highest-ID one. Silently cancel stale
|
|
1017
|
+
// lower-ID sessions before MsgStartSession — mirrors C# SessionManager dedup pattern.
|
|
1018
|
+
onStaleDuplicate: (staleId) => {
|
|
1019
|
+
logFn?.(`[connect] Cancelling stale duplicate session ${staleId} on ${opts.nodeAddress}...`);
|
|
1020
|
+
_endSessionOnChain(staleId, opts.mnemonic, opts.feeGranter || null).catch(e => {
|
|
1021
|
+
logFn?.(`[connect] Failed to cancel stale session ${staleId}: ${e.message}`);
|
|
1022
|
+
});
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1012
1025
|
if (sessionId && isSessionPoisoned(String(sessionId))) {
|
|
1013
1026
|
progress(onProgress, logFn, 'session', `Session ${sessionId} previously failed — skipping`);
|
|
1014
1027
|
sessionId = null;
|
|
@@ -1457,8 +1470,10 @@ async function connectInternal(opts, paymentStrategy, retryStrategy, state = _de
|
|
|
1457
1470
|
{ nodeAddress: state.connection?.nodeAddress });
|
|
1458
1471
|
}
|
|
1459
1472
|
const prev = state.connection;
|
|
1460
|
-
|
|
1461
|
-
|
|
1473
|
+
// Hard disconnect: user is actively connecting to a different node,
|
|
1474
|
+
// so the old session should be settled and the deposit refunded.
|
|
1475
|
+
await disconnectStateAndEndSession(state);
|
|
1476
|
+
if (opts.log || defaultLog) (opts.log || defaultLog)(`[connect] Ended session on ${prev?.nodeAddress || 'previous node'} — connecting to new node`);
|
|
1462
1477
|
}
|
|
1463
1478
|
|
|
1464
1479
|
const onProgress = opts.onProgress || null;
|
|
@@ -2264,13 +2279,35 @@ function formatUptime(ms) {
|
|
|
2264
2279
|
}
|
|
2265
2280
|
|
|
2266
2281
|
// ─── Disconnect ──────────────────────────────────────────────────────────────
|
|
2282
|
+
//
|
|
2283
|
+
// TWO DISCONNECT PATHS — CHOOSE INTENT EXPLICITLY.
|
|
2284
|
+
//
|
|
2285
|
+
// Soft: disconnect() / disconnectState(state)
|
|
2286
|
+
// - Tears down the local tunnel (WireGuard/V2Ray) and cleans up system state.
|
|
2287
|
+
// - Leaves the on-chain session in status=1 (active).
|
|
2288
|
+
// - Next connectDirect() to the SAME node reuses the session via
|
|
2289
|
+
// findExistingSession — no new MsgStartSession TX, no new payment, bandwidth preserved.
|
|
2290
|
+
// - Use when: user is pausing, network changed, closing the app temporarily.
|
|
2291
|
+
//
|
|
2292
|
+
// Hard: disconnectAndEndSession() / disconnectStateAndEndSession(state)
|
|
2293
|
+
// - Tears down the tunnel AND broadcasts MsgCancelSession on chain.
|
|
2294
|
+
// - Session moves status=1 → settling → refund after ~2h settlement window.
|
|
2295
|
+
// - Use when: user is done with this node, switching nodes permanently,
|
|
2296
|
+
// or wants the bandwidth deposit back.
|
|
2297
|
+
//
|
|
2298
|
+
// Internal: _disconnectInternal(state, { endSession })
|
|
2299
|
+
// - Caller MUST pass endSession explicitly as true or false.
|
|
2300
|
+
// - No default — forces intentional choice at every callsite.
|
|
2267
2301
|
|
|
2268
2302
|
/**
|
|
2269
|
-
*
|
|
2270
|
-
*
|
|
2303
|
+
* Internal disconnect implementation. Caller must explicitly pass endSession.
|
|
2304
|
+
* @param {object} state - ConnectionState instance
|
|
2305
|
+
* @param {{ endSession: boolean }} opts
|
|
2306
|
+
* endSession: true → broadcast MsgCancelSession (hard disconnect)
|
|
2307
|
+
* endSession: false → preserve on-chain session for reuse (soft disconnect)
|
|
2308
|
+
* @private
|
|
2271
2309
|
*/
|
|
2272
|
-
|
|
2273
|
-
export async function disconnectState(state) {
|
|
2310
|
+
async function _disconnectInternal(state, { endSession }) {
|
|
2274
2311
|
// v30: Signal any running connectAuto() retry loop to abort, and release the
|
|
2275
2312
|
// connection lock so the user can reconnect after disconnect completes.
|
|
2276
2313
|
_abortConnect = true;
|
|
@@ -2296,11 +2333,18 @@ export async function disconnectState(state) {
|
|
|
2296
2333
|
state.wgTunnel = null;
|
|
2297
2334
|
}
|
|
2298
2335
|
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2336
|
+
if (endSession) {
|
|
2337
|
+
// Hard disconnect: broadcast MsgCancelSession — session settles and refunds deposit.
|
|
2338
|
+
if (prev?.sessionId && state._mnemonic) {
|
|
2339
|
+
_endSessionOnChain(prev.sessionId, state._mnemonic, state._feeGranter).catch(e => {
|
|
2340
|
+
console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
} else {
|
|
2344
|
+
// Soft disconnect: leave session on chain in status=1 for reuse.
|
|
2345
|
+
if (prev?.sessionId) {
|
|
2346
|
+
console.log(`[sentinel-sdk] Session ${prev.sessionId} preserved on chain (status=1) for future reuse — no MsgCancelSession broadcast.`);
|
|
2347
|
+
}
|
|
2304
2348
|
}
|
|
2305
2349
|
} finally {
|
|
2306
2350
|
// ALWAYS clear connection state — even if teardown threw
|
|
@@ -2314,8 +2358,55 @@ export async function disconnectState(state) {
|
|
|
2314
2358
|
}
|
|
2315
2359
|
}
|
|
2316
2360
|
|
|
2361
|
+
/**
|
|
2362
|
+
* Soft disconnect — tear down the local tunnel, leave the on-chain session active.
|
|
2363
|
+
*
|
|
2364
|
+
* A subsequent connectDirect() to the SAME node will reuse the session via
|
|
2365
|
+
* findExistingSession — no new MsgStartSession TX, no new payment, remaining
|
|
2366
|
+
* bandwidth is preserved.
|
|
2367
|
+
*
|
|
2368
|
+
* Use when: user is pausing, network changed, or closing the app temporarily.
|
|
2369
|
+
* To settle the session on-chain and reclaim the unused deposit, use
|
|
2370
|
+
* disconnectAndEndSession() instead.
|
|
2371
|
+
*
|
|
2372
|
+
* @param {object} state - ConnectionState instance
|
|
2373
|
+
*/
|
|
2374
|
+
export async function disconnectState(state) {
|
|
2375
|
+
return _disconnectInternal(state, { endSession: false });
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/**
|
|
2379
|
+
* Soft disconnect (default state) — tear down the tunnel, leave on-chain session active.
|
|
2380
|
+
*
|
|
2381
|
+
* @see disconnectState
|
|
2382
|
+
*/
|
|
2317
2383
|
export async function disconnect() {
|
|
2318
|
-
return
|
|
2384
|
+
return _disconnectInternal(_defaultState, { endSession: false });
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/**
|
|
2388
|
+
* Hard disconnect — tear down the tunnel AND broadcast MsgCancelSession on chain.
|
|
2389
|
+
*
|
|
2390
|
+
* The session settles after the ~2h inactive_pending window. The node refunds
|
|
2391
|
+
* the unused portion of the bandwidth deposit (for peer-to-peer sessions).
|
|
2392
|
+
* For plan-based sessions, this stops metering against the plan allocation.
|
|
2393
|
+
*
|
|
2394
|
+
* Use when: user is done with this node (switching nodes permanently, ending
|
|
2395
|
+
* their session, or wants the deposit back).
|
|
2396
|
+
*
|
|
2397
|
+
* @param {object} state - ConnectionState instance
|
|
2398
|
+
*/
|
|
2399
|
+
export async function disconnectStateAndEndSession(state) {
|
|
2400
|
+
return _disconnectInternal(state, { endSession: true });
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/**
|
|
2404
|
+
* Hard disconnect (default state) — tear down the tunnel AND broadcast MsgCancelSession.
|
|
2405
|
+
*
|
|
2406
|
+
* @see disconnectStateAndEndSession
|
|
2407
|
+
*/
|
|
2408
|
+
export async function disconnectAndEndSession() {
|
|
2409
|
+
return _disconnectInternal(_defaultState, { endSession: true });
|
|
2319
2410
|
}
|
|
2320
2411
|
|
|
2321
2412
|
// ─── Session End (on-chain cleanup) ──────────────────────────────────────────
|
|
@@ -2436,15 +2527,17 @@ export function registerCleanupHandlers() {
|
|
|
2436
2527
|
if (orphans?.cleaned?.length) console.log('[sentinel-sdk] Recovered orphans:', orphans.cleaned.join(', '));
|
|
2437
2528
|
emergencyCleanupSync(); // kill stale tunnels from previous crash
|
|
2438
2529
|
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); });
|
|
2530
|
+
process.on('exit', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); resetQueryRpcCache(); disconnectRpc(); });
|
|
2531
|
+
process.on('SIGINT', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); resetQueryRpcCache(); disconnectRpc(); process.exit(130); });
|
|
2532
|
+
process.on('SIGTERM', () => { if (_killSwitchEnabled) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); resetQueryRpcCache(); disconnectRpc(); process.exit(143); });
|
|
2442
2533
|
process.on('uncaughtException', (err) => {
|
|
2443
2534
|
console.error('Uncaught exception:', err);
|
|
2444
2535
|
if (_killSwitchEnabled) disableKillSwitch();
|
|
2445
2536
|
clearSystemProxy();
|
|
2446
2537
|
killOrphanV2Ray();
|
|
2447
2538
|
emergencyCleanupSync();
|
|
2539
|
+
resetQueryRpcCache();
|
|
2540
|
+
disconnectRpc();
|
|
2448
2541
|
process.exit(1);
|
|
2449
2542
|
});
|
|
2450
2543
|
}
|
package/operator.js
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blue-js-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"sentinel": "./cli/index.js"
|
|
10
|
-
"sentinel-ai": "./ai-path/cli.js"
|
|
9
|
+
"sentinel": "./cli/index.js"
|
|
11
10
|
},
|
|
12
11
|
"exports": {
|
|
13
12
|
".": {
|
|
@@ -16,7 +15,6 @@
|
|
|
16
15
|
},
|
|
17
16
|
"./consumer": "./consumer.js",
|
|
18
17
|
"./operator": "./operator.js",
|
|
19
|
-
"./ai-path": "./ai-path/index.js",
|
|
20
18
|
"./blue": {
|
|
21
19
|
"types": "./dist/index.d.ts",
|
|
22
20
|
"default": "./dist/index.js"
|
|
@@ -49,7 +47,6 @@
|
|
|
49
47
|
"errors/",
|
|
50
48
|
"examples/",
|
|
51
49
|
"docs/",
|
|
52
|
-
"ai-path/",
|
|
53
50
|
"bin/setup.js",
|
|
54
51
|
"dist/",
|
|
55
52
|
"src/",
|
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/types/index.d.ts
CHANGED
|
@@ -469,8 +469,8 @@ export function isMnemonicValid(mnemonic: string): boolean;
|
|
|
469
469
|
/** Get current P2P price in USD */
|
|
470
470
|
export function getDvpnPrice(): Promise<number>;
|
|
471
471
|
|
|
472
|
-
/** Find existing active session for wallet+node pair */
|
|
473
|
-
export function findExistingSession(lcdUrl: string, walletAddr: string, nodeAddr: string): Promise<bigint | null>;
|
|
472
|
+
/** Find existing active session for wallet+node pair. Deduplicates stale sessions via onStaleDuplicate callback. */
|
|
473
|
+
export function findExistingSession(lcdUrl: string, walletAddr: string, nodeAddr: string, opts?: { onStaleDuplicate?: (sessionId: bigint) => void }): Promise<bigint | null>;
|
|
474
474
|
|
|
475
475
|
/** Fetch active nodes from LCD with pagination */
|
|
476
476
|
export function fetchActiveNodes(lcdUrl: string, limit?: number, maxPages?: number): Promise<unknown[]>;
|