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.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/batch.js +2 -2
  3. package/chain/authz.js +1 -9
  4. package/chain/fee-grants.js +199 -3
  5. package/chain/index.js +36 -167
  6. package/chain/queries.js +118 -7
  7. package/chain/rpc.js +58 -2
  8. package/client/index.js +1 -3
  9. package/client.js +17 -1
  10. package/connection/connect.js +17 -5
  11. package/connection/disconnect.js +86 -10
  12. package/connection/discovery.js +11 -11
  13. package/connection/index.js +2 -0
  14. package/cosmjs-setup.js +30 -153
  15. package/defaults.js +1 -1
  16. package/index.js +5 -1
  17. package/node-connect.js +118 -25
  18. package/operator.js +2 -0
  19. package/package.json +2 -5
  20. package/pricing/index.js +3 -26
  21. package/types/index.d.ts +2 -2
  22. package/ai-path/ADMIN-ELEVATION.md +0 -116
  23. package/ai-path/AI-MANIFESTO.md +0 -185
  24. package/ai-path/BREAKING.md +0 -74
  25. package/ai-path/CHECKLIST.md +0 -619
  26. package/ai-path/CONNECTION-STEPS.md +0 -724
  27. package/ai-path/DECISION-TREE.md +0 -422
  28. package/ai-path/DEPENDENCIES.md +0 -459
  29. package/ai-path/E2E-FLOW.md +0 -1707
  30. package/ai-path/FAILURES.md +0 -410
  31. package/ai-path/GUIDE.md +0 -1315
  32. package/ai-path/README.md +0 -599
  33. package/ai-path/SPLIT-TUNNEL.md +0 -266
  34. package/ai-path/cli.js +0 -548
  35. package/ai-path/connect.js +0 -1028
  36. package/ai-path/discover.js +0 -178
  37. package/ai-path/environment.js +0 -266
  38. package/ai-path/errors.js +0 -86
  39. package/ai-path/examples/autonomous-agent.mjs +0 -220
  40. package/ai-path/examples/multi-region.mjs +0 -174
  41. package/ai-path/examples/one-shot.mjs +0 -31
  42. package/ai-path/index.js +0 -79
  43. package/ai-path/pricing.js +0 -137
  44. package/ai-path/recommend.js +0 -413
  45. package/ai-path/run-admin.vbs +0 -25
  46. package/ai-path/setup.js +0 -291
  47. 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
- // ─── 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
- }
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
- // ─── 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
- }
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.3.0';
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 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
@@ -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
- await disconnectState(state);
1461
- if (opts.log || defaultLog) (opts.log || defaultLog)(`[connect] Disconnected from ${prev?.nodeAddress || 'previous node'}`);
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
- * Clean up all active tunnels and system proxy.
2270
- * ALWAYS call this on exit — a stale WireGuard tunnel will kill your internet.
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
- /** Disconnect a specific state instance (internal). */
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
- // End session on chain (best-effort, fire-and-forget — never blocks disconnect)
2300
- if (prev?.sessionId && state._mnemonic) {
2301
- _endSessionOnChain(prev.sessionId, state._mnemonic, state._feeGranter).catch(e => {
2302
- console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
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 disconnectState(_defaultState);
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
@@ -77,6 +77,8 @@ export {
77
77
  grantPlanSubscribers,
78
78
  renewExpiringGrants,
79
79
  monitorFeeGrants,
80
+ streamGrantPlanSubscribers,
81
+ computeFeeGrantGasCosts,
80
82
  } from './cosmjs-setup.js';
81
83
 
82
84
  // ─── Authz ──────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "blue-js-sdk",
3
- "version": "2.3.0",
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 { 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 };
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[]>;