blue-js-sdk 2.4.0 → 2.7.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/app-helpers.js +55 -0
- package/chain/broadcast.js +27 -0
- package/chain/fee-grants.js +271 -5
- package/chain/index.js +8 -2
- package/chain/queries.js +177 -3
- package/chain/rpc.js +117 -4
- package/cli.js +26 -5
- package/client.js +79 -7
- package/connection/connect.js +119 -21
- package/connection/disconnect.js +93 -12
- package/connection/index.js +2 -0
- package/connection/logger.js +66 -0
- package/connection/resilience.js +12 -7
- package/connection/state.js +21 -12
- package/connection/tunnel.js +24 -8
- package/cosmjs-setup.js +68 -2
- package/docs/PRIVY-INTEGRATION.md +177 -0
- package/errors.js +167 -0
- package/index.js +75 -2
- package/node-connect.js +190 -50
- package/operator.js +26 -0
- package/package.json +11 -11
- package/session-manager.js +68 -0
- package/speedtest.js +139 -0
- package/test-all-logic.js +8 -6
- package/test-e2e.js +138 -0
- package/test-mainnet.js +2 -2
- package/test-plan-connect-e2e.js +235 -0
- package/test-subscription-flows.js +14 -4
- package/types/connection.d.ts +6 -2
- 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/node-connect.js
CHANGED
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
SentinelError, ValidationError, NodeError, ChainError, TunnelError, ErrorCodes,
|
|
63
63
|
} from './errors.js';
|
|
64
64
|
import { createNodeHttpsAgent, publicEndpointAgent } from './tls-trust.js';
|
|
65
|
+
import { withMnemonicRedaction } from './connection/logger.js';
|
|
65
66
|
|
|
66
67
|
// CA-validated agent for LCD/RPC public endpoints (valid CA certs)
|
|
67
68
|
const httpsAgent = publicEndpointAgent;
|
|
@@ -116,7 +117,9 @@ export class ConnectionState {
|
|
|
116
117
|
this.systemProxy = false;
|
|
117
118
|
this.connection = null; // { nodeAddress, serviceType, sessionId, connectedAt, socksPort? }
|
|
118
119
|
this.savedProxyState = null;
|
|
119
|
-
|
|
120
|
+
// v37 (security): store the derived OfflineSigner — NOT the BIP-39 mnemonic.
|
|
121
|
+
// See connection/state.js for the rationale (heap-dump attack surface).
|
|
122
|
+
this._wallet = null; // OfflineSigner — used by _endSessionOnChain on disconnect
|
|
120
123
|
this._feeGranter = null; // Stored for fee-granted session end on disconnect (0-P2P agents)
|
|
121
124
|
_activeStates.add(this);
|
|
122
125
|
}
|
|
@@ -128,8 +131,13 @@ export class ConnectionState {
|
|
|
128
131
|
const _activeStates = new Set();
|
|
129
132
|
const _defaultState = new ConnectionState();
|
|
130
133
|
|
|
131
|
-
// Default logger — can be overridden per-call via opts.log
|
|
132
|
-
|
|
134
|
+
// Default logger — can be overridden per-call via opts.log.
|
|
135
|
+
// Wrapped with mnemonic redaction so any 12–24 word BIP-39 phrase that ends up
|
|
136
|
+
// in a log argument is replaced with `[REDACTED MNEMONIC]` before reaching
|
|
137
|
+
// stdout. Defense-in-depth — the SDK does not currently log the mnemonic, but
|
|
138
|
+
// a future template-string bug or careless `JSON.stringify(opts)` won't leak
|
|
139
|
+
// it through the default logger.
|
|
140
|
+
let defaultLog = withMnemonicRedaction(console.log);
|
|
133
141
|
|
|
134
142
|
// ─── Wallet Cache ────────────────────────────────────────────────────────────
|
|
135
143
|
// v21: Cache wallet derivation (BIP39 → SLIP-10 is CPU-bound, ~300ms).
|
|
@@ -798,12 +806,12 @@ export async function tryFastReconnect(opts, state = _defaultState) {
|
|
|
798
806
|
if (_killSwitchEnabled) disableKillSwitch();
|
|
799
807
|
try { await disconnectWireGuard(); } catch {}
|
|
800
808
|
// End session on chain (fire-and-forget)
|
|
801
|
-
if (saved.sessionId && state.
|
|
802
|
-
_endSessionOnChain(saved.sessionId, state.
|
|
809
|
+
if (saved.sessionId && state._wallet) {
|
|
810
|
+
_endSessionOnChain(saved.sessionId, state._wallet, state._feeGranter).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
803
811
|
}
|
|
804
812
|
state.wgTunnel = null;
|
|
805
813
|
state.connection = null;
|
|
806
|
-
state.
|
|
814
|
+
state._wallet = null;
|
|
807
815
|
clearState();
|
|
808
816
|
},
|
|
809
817
|
};
|
|
@@ -923,11 +931,11 @@ export async function tryFastReconnect(opts, state = _defaultState) {
|
|
|
923
931
|
if (state.v2rayProc) { state.v2rayProc.kill(); state.v2rayProc = null; await sleep(500); }
|
|
924
932
|
if (state.systemProxy) clearSystemProxy(state);
|
|
925
933
|
// End session on chain (fire-and-forget)
|
|
926
|
-
if (sessionIdStr && state.
|
|
927
|
-
_endSessionOnChain(sessionIdStr, state.
|
|
934
|
+
if (sessionIdStr && state._wallet) {
|
|
935
|
+
_endSessionOnChain(sessionIdStr, state._wallet, state._feeGranter).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
928
936
|
}
|
|
929
937
|
state.connection = null;
|
|
930
|
-
state.
|
|
938
|
+
state._wallet = null;
|
|
931
939
|
clearState();
|
|
932
940
|
},
|
|
933
941
|
};
|
|
@@ -993,9 +1001,11 @@ export async function connectDirect(opts) {
|
|
|
993
1001
|
|
|
994
1002
|
// ── Fast Reconnect: check for saved credentials ──
|
|
995
1003
|
if (!forceNewSession) {
|
|
996
|
-
//
|
|
997
|
-
|
|
998
|
-
const
|
|
1004
|
+
// v37: Derive wallet up front so _endSessionOnChain() has a signer if fast reconnect succeeds.
|
|
1005
|
+
// cachedCreateWallet is keyed on mnemonic SHA256 — connectInternal() below reuses the same object.
|
|
1006
|
+
const fastState = opts._state || _defaultState;
|
|
1007
|
+
fastState._wallet = await cachedCreateWallet(opts.mnemonic);
|
|
1008
|
+
const fast = await tryFastReconnect(opts, fastState);
|
|
999
1009
|
if (fast) {
|
|
1000
1010
|
_circuitBreaker.delete(opts.nodeAddress);
|
|
1001
1011
|
return fast;
|
|
@@ -1011,7 +1021,20 @@ export async function connectDirect(opts) {
|
|
|
1011
1021
|
if (!forceNewSession) {
|
|
1012
1022
|
progress(onProgress, logFn, 'session', 'Checking for existing session...');
|
|
1013
1023
|
checkAborted(signal);
|
|
1014
|
-
sessionId = await findExistingSession(lcd, account.address, opts.nodeAddress
|
|
1024
|
+
sessionId = await findExistingSession(lcd, account.address, opts.nodeAddress, {
|
|
1025
|
+
// Dedup: if multiple active sessions exist for this node (stale duplicates from
|
|
1026
|
+
// crashes or multi-client wallets), keep the highest-ID one. Silently cancel stale
|
|
1027
|
+
// lower-ID sessions before MsgStartSession — mirrors C# SessionManager dedup pattern.
|
|
1028
|
+
onStaleDuplicate: (staleId) => {
|
|
1029
|
+
logFn?.(`[connect] Cancelling stale duplicate session ${staleId} on ${opts.nodeAddress}...`);
|
|
1030
|
+
// v37: pass the derived wallet (set on state above) instead of the mnemonic
|
|
1031
|
+
const w = (opts._state || _defaultState)._wallet;
|
|
1032
|
+
if (!w) { logFn?.(`[connect] No wallet on state — skipping stale session ${staleId} cleanup`); return; }
|
|
1033
|
+
_endSessionOnChain(staleId, w, opts.feeGranter || null).catch(e => {
|
|
1034
|
+
logFn?.(`[connect] Failed to cancel stale session ${staleId}: ${e.message}`);
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1015
1038
|
if (sessionId && isSessionPoisoned(String(sessionId))) {
|
|
1016
1039
|
progress(onProgress, logFn, 'session', `Session ${sessionId} previously failed — skipping`);
|
|
1017
1040
|
sessionId = null;
|
|
@@ -1142,7 +1165,7 @@ export async function connectAuto(opts) {
|
|
|
1142
1165
|
if (opts.circuitBreaker) configureCircuitBreaker(opts.circuitBreaker);
|
|
1143
1166
|
|
|
1144
1167
|
const maxAttempts = opts.maxAttempts || 3;
|
|
1145
|
-
const logFn = opts.log ||
|
|
1168
|
+
const logFn = opts.log || defaultLog;
|
|
1146
1169
|
const errors = [];
|
|
1147
1170
|
|
|
1148
1171
|
// If nodeAddress specified, try it first (skip circuit breaker check for explicit choice)
|
|
@@ -1433,6 +1456,38 @@ export async function connectViaSubscription(opts) {
|
|
|
1433
1456
|
|
|
1434
1457
|
// ─── Shared Validation ───────────────────────────────────────────────────────
|
|
1435
1458
|
|
|
1459
|
+
/**
|
|
1460
|
+
* Validate a chain endpoint URL. Enforces https:// for non-localhost endpoints
|
|
1461
|
+
* to prevent attackers from coercing the SDK into broadcasting signed TXs over
|
|
1462
|
+
* cleartext HTTP. Localhost (loopback) is exempted for local-node development.
|
|
1463
|
+
*
|
|
1464
|
+
* @param {*} url - Value to validate (allowed: undefined/null, or string)
|
|
1465
|
+
* @param {string} fieldName - 'rpcUrl' or 'lcdUrl', used in error messages
|
|
1466
|
+
*/
|
|
1467
|
+
function validateChainUrl(url, fieldName) {
|
|
1468
|
+
if (url == null) return;
|
|
1469
|
+
if (typeof url !== 'string') {
|
|
1470
|
+
throw new ValidationError(ErrorCodes.INVALID_URL, `${fieldName} must be a string URL`, { value: url });
|
|
1471
|
+
}
|
|
1472
|
+
let parsed;
|
|
1473
|
+
try { parsed = new URL(url); }
|
|
1474
|
+
catch { throw new ValidationError(ErrorCodes.INVALID_URL, `${fieldName} is not a valid URL`, { value: url }); }
|
|
1475
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
1476
|
+
throw new ValidationError(ErrorCodes.INVALID_URL, `${fieldName} must use http:// or https:// (got ${parsed.protocol})`, { value: url });
|
|
1477
|
+
}
|
|
1478
|
+
const isLocalhost = parsed.hostname === 'localhost'
|
|
1479
|
+
|| parsed.hostname === '127.0.0.1'
|
|
1480
|
+
|| parsed.hostname === '::1'
|
|
1481
|
+
|| parsed.hostname === '[::1]';
|
|
1482
|
+
if (parsed.protocol === 'http:' && !isLocalhost) {
|
|
1483
|
+
throw new ValidationError(
|
|
1484
|
+
ErrorCodes.INVALID_URL,
|
|
1485
|
+
`${fieldName} must use https:// for non-localhost endpoints. Cleartext HTTP leaks signed TXs and queries to network observers (got ${url}).`,
|
|
1486
|
+
{ value: url },
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1436
1491
|
function validateConnectOpts(opts, fnName) {
|
|
1437
1492
|
if (!opts || typeof opts !== 'object') throw new ValidationError(ErrorCodes.INVALID_OPTIONS, `${fnName}() requires an options object`);
|
|
1438
1493
|
if (typeof opts.mnemonic !== 'string' || opts.mnemonic.trim().split(/\s+/).length < 12) {
|
|
@@ -1441,8 +1496,8 @@ function validateConnectOpts(opts, fnName) {
|
|
|
1441
1496
|
if (typeof opts.nodeAddress !== 'string' || !/^sentnode1[a-z0-9]{38}$/.test(opts.nodeAddress)) {
|
|
1442
1497
|
throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be a valid sentnode1... bech32 address (47 characters)', { value: opts.nodeAddress });
|
|
1443
1498
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1499
|
+
validateChainUrl(opts.rpcUrl, 'rpcUrl');
|
|
1500
|
+
validateChainUrl(opts.lcdUrl, 'lcdUrl');
|
|
1446
1501
|
}
|
|
1447
1502
|
|
|
1448
1503
|
// ─── Shared Connect Flow (eliminates connectDirect/connectViaPlan duplication) ─
|
|
@@ -1460,8 +1515,10 @@ async function connectInternal(opts, paymentStrategy, retryStrategy, state = _de
|
|
|
1460
1515
|
{ nodeAddress: state.connection?.nodeAddress });
|
|
1461
1516
|
}
|
|
1462
1517
|
const prev = state.connection;
|
|
1463
|
-
|
|
1464
|
-
|
|
1518
|
+
// Hard disconnect: user is actively connecting to a different node,
|
|
1519
|
+
// so the old session should be settled and the deposit refunded.
|
|
1520
|
+
await disconnectStateAndEndSession(state);
|
|
1521
|
+
if (opts.log || defaultLog) (opts.log || defaultLog)(`[connect] Ended session on ${prev?.nodeAddress || 'previous node'} — connecting to new node`);
|
|
1465
1522
|
}
|
|
1466
1523
|
|
|
1467
1524
|
const onProgress = opts.onProgress || null;
|
|
@@ -1478,13 +1535,17 @@ async function connectInternal(opts, paymentStrategy, retryStrategy, state = _de
|
|
|
1478
1535
|
// v21: parallelized — saves ~300ms (was sequential)
|
|
1479
1536
|
progress(onProgress, logFn, 'wallet', 'Setting up wallet...');
|
|
1480
1537
|
checkAborted(signal);
|
|
1481
|
-
const [
|
|
1538
|
+
const [walletResult, privKey] = await Promise.all([
|
|
1482
1539
|
cachedCreateWallet(opts.mnemonic),
|
|
1483
1540
|
privKeyFromMnemonic(opts.mnemonic),
|
|
1484
1541
|
]);
|
|
1542
|
+
const { wallet, account } = walletResult;
|
|
1485
1543
|
|
|
1486
|
-
//
|
|
1487
|
-
|
|
1544
|
+
// v37 (security): store the derived OfflineSigner — NOT the raw BIP-39 phrase.
|
|
1545
|
+
// _endSessionOnChain only signs one TX on disconnect; it doesn't need the recovery
|
|
1546
|
+
// phrase, and keeping the phrase in heap for the full session expanded the
|
|
1547
|
+
// heap-dump attack surface unnecessarily.
|
|
1548
|
+
state._wallet = walletResult;
|
|
1488
1549
|
state._feeGranter = opts.feeGranter || null;
|
|
1489
1550
|
|
|
1490
1551
|
// 2. RPC connect + LCD lookup in parallel (independent network calls)
|
|
@@ -1872,12 +1933,12 @@ async function setupWireGuard({ remoteUrl, sessionId, privKey, fullTunnel, split
|
|
|
1872
1933
|
if (_killSwitchEnabled) disableKillSwitch();
|
|
1873
1934
|
try { await disconnectWireGuard(); } catch {} // tunnel may already be down
|
|
1874
1935
|
// End session on chain (fire-and-forget)
|
|
1875
|
-
if (sessionIdStr && state.
|
|
1876
|
-
_endSessionOnChain(sessionIdStr, state.
|
|
1936
|
+
if (sessionIdStr && state._wallet) {
|
|
1937
|
+
_endSessionOnChain(sessionIdStr, state._wallet, state._feeGranter).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
1877
1938
|
}
|
|
1878
1939
|
state.wgTunnel = null;
|
|
1879
1940
|
state.connection = null;
|
|
1880
|
-
state.
|
|
1941
|
+
state._wallet = null;
|
|
1881
1942
|
clearState();
|
|
1882
1943
|
},
|
|
1883
1944
|
};
|
|
@@ -2136,11 +2197,11 @@ async function setupV2Ray({ remoteUrl, serverHost, sessionId, privKey, v2rayExeP
|
|
|
2136
2197
|
if (state.v2rayProc) { state.v2rayProc.kill(); state.v2rayProc = null; await sleep(500); }
|
|
2137
2198
|
if (state.systemProxy) clearSystemProxy();
|
|
2138
2199
|
// End session on chain (fire-and-forget)
|
|
2139
|
-
if (sessionIdStr && state.
|
|
2140
|
-
_endSessionOnChain(sessionIdStr, state.
|
|
2200
|
+
if (sessionIdStr && state._wallet) {
|
|
2201
|
+
_endSessionOnChain(sessionIdStr, state._wallet, state._feeGranter).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
2141
2202
|
}
|
|
2142
2203
|
state.connection = null;
|
|
2143
|
-
state.
|
|
2204
|
+
state._wallet = null;
|
|
2144
2205
|
clearState();
|
|
2145
2206
|
},
|
|
2146
2207
|
};
|
|
@@ -2171,8 +2232,8 @@ export function getStatus() {
|
|
|
2171
2232
|
const stale = _defaultState.connection;
|
|
2172
2233
|
_defaultState.connection = null;
|
|
2173
2234
|
// End session on chain (fire-and-forget) to prevent stale session leaks
|
|
2174
|
-
if (stale?.sessionId && _defaultState.
|
|
2175
|
-
_endSessionOnChain(stale.sessionId, _defaultState.
|
|
2235
|
+
if (stale?.sessionId && _defaultState._wallet) {
|
|
2236
|
+
_endSessionOnChain(stale.sessionId, _defaultState._wallet, _defaultState._feeGranter).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
2176
2237
|
}
|
|
2177
2238
|
clearState();
|
|
2178
2239
|
events.emit('disconnected', { nodeAddress: stale.nodeAddress, serviceType: stale.serviceType, reason: 'phantom_state' });
|
|
@@ -2218,8 +2279,8 @@ export function getStatus() {
|
|
|
2218
2279
|
// clean up stale state. Prevents ghost "connected" status after tunnel dies.
|
|
2219
2280
|
if (!healthChecks.tunnelActive && !_defaultState.v2rayProc && !_defaultState.wgTunnel) {
|
|
2220
2281
|
// Both tunnel handles are null — connection state is stale
|
|
2221
|
-
if (conn?.sessionId && _defaultState.
|
|
2222
|
-
_endSessionOnChain(conn.sessionId, _defaultState.
|
|
2282
|
+
if (conn?.sessionId && _defaultState._wallet) {
|
|
2283
|
+
_endSessionOnChain(conn.sessionId, _defaultState._wallet, _defaultState._feeGranter).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
2223
2284
|
}
|
|
2224
2285
|
_defaultState.connection = null;
|
|
2225
2286
|
clearState();
|
|
@@ -2227,8 +2288,8 @@ export function getStatus() {
|
|
|
2227
2288
|
}
|
|
2228
2289
|
if (_defaultState.wgTunnel && !healthChecks.tunnelActive) {
|
|
2229
2290
|
// WireGuard state says connected but tunnel is dead — auto-cleanup
|
|
2230
|
-
if (conn?.sessionId && _defaultState.
|
|
2231
|
-
_endSessionOnChain(conn.sessionId, _defaultState.
|
|
2291
|
+
if (conn?.sessionId && _defaultState._wallet) {
|
|
2292
|
+
_endSessionOnChain(conn.sessionId, _defaultState._wallet, _defaultState._feeGranter).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
2232
2293
|
}
|
|
2233
2294
|
_defaultState.wgTunnel = null;
|
|
2234
2295
|
_defaultState.connection = null;
|
|
@@ -2238,8 +2299,8 @@ export function getStatus() {
|
|
|
2238
2299
|
}
|
|
2239
2300
|
if (_defaultState.v2rayProc && !healthChecks.tunnelActive) {
|
|
2240
2301
|
// V2Ray process died — auto-cleanup
|
|
2241
|
-
if (conn?.sessionId && _defaultState.
|
|
2242
|
-
_endSessionOnChain(conn.sessionId, _defaultState.
|
|
2302
|
+
if (conn?.sessionId && _defaultState._wallet) {
|
|
2303
|
+
_endSessionOnChain(conn.sessionId, _defaultState._wallet, _defaultState._feeGranter).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
2243
2304
|
}
|
|
2244
2305
|
_defaultState.v2rayProc = null;
|
|
2245
2306
|
_defaultState.connection = null;
|
|
@@ -2267,13 +2328,35 @@ function formatUptime(ms) {
|
|
|
2267
2328
|
}
|
|
2268
2329
|
|
|
2269
2330
|
// ─── Disconnect ──────────────────────────────────────────────────────────────
|
|
2331
|
+
//
|
|
2332
|
+
// TWO DISCONNECT PATHS — CHOOSE INTENT EXPLICITLY.
|
|
2333
|
+
//
|
|
2334
|
+
// Soft: disconnect() / disconnectState(state)
|
|
2335
|
+
// - Tears down the local tunnel (WireGuard/V2Ray) and cleans up system state.
|
|
2336
|
+
// - Leaves the on-chain session in status=1 (active).
|
|
2337
|
+
// - Next connectDirect() to the SAME node reuses the session via
|
|
2338
|
+
// findExistingSession — no new MsgStartSession TX, no new payment, bandwidth preserved.
|
|
2339
|
+
// - Use when: user is pausing, network changed, closing the app temporarily.
|
|
2340
|
+
//
|
|
2341
|
+
// Hard: disconnectAndEndSession() / disconnectStateAndEndSession(state)
|
|
2342
|
+
// - Tears down the tunnel AND broadcasts MsgCancelSession on chain.
|
|
2343
|
+
// - Session moves status=1 → settling → refund after ~2h settlement window.
|
|
2344
|
+
// - Use when: user is done with this node, switching nodes permanently,
|
|
2345
|
+
// or wants the bandwidth deposit back.
|
|
2346
|
+
//
|
|
2347
|
+
// Internal: _disconnectInternal(state, { endSession })
|
|
2348
|
+
// - Caller MUST pass endSession explicitly as true or false.
|
|
2349
|
+
// - No default — forces intentional choice at every callsite.
|
|
2270
2350
|
|
|
2271
2351
|
/**
|
|
2272
|
-
*
|
|
2273
|
-
*
|
|
2352
|
+
* Internal disconnect implementation. Caller must explicitly pass endSession.
|
|
2353
|
+
* @param {object} state - ConnectionState instance
|
|
2354
|
+
* @param {{ endSession: boolean }} opts
|
|
2355
|
+
* endSession: true → broadcast MsgCancelSession (hard disconnect)
|
|
2356
|
+
* endSession: false → preserve on-chain session for reuse (soft disconnect)
|
|
2357
|
+
* @private
|
|
2274
2358
|
*/
|
|
2275
|
-
|
|
2276
|
-
export async function disconnectState(state) {
|
|
2359
|
+
async function _disconnectInternal(state, { endSession }) {
|
|
2277
2360
|
// v30: Signal any running connectAuto() retry loop to abort, and release the
|
|
2278
2361
|
// connection lock so the user can reconnect after disconnect completes.
|
|
2279
2362
|
_abortConnect = true;
|
|
@@ -2299,15 +2382,22 @@ export async function disconnectState(state) {
|
|
|
2299
2382
|
state.wgTunnel = null;
|
|
2300
2383
|
}
|
|
2301
2384
|
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2385
|
+
if (endSession) {
|
|
2386
|
+
// Hard disconnect: broadcast MsgCancelSession — session settles and refunds deposit.
|
|
2387
|
+
if (prev?.sessionId && state._wallet) {
|
|
2388
|
+
_endSessionOnChain(prev.sessionId, state._wallet, state._feeGranter).catch(e => {
|
|
2389
|
+
console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
} else {
|
|
2393
|
+
// Soft disconnect: leave session on chain in status=1 for reuse.
|
|
2394
|
+
if (prev?.sessionId) {
|
|
2395
|
+
console.log(`[sentinel-sdk] Session ${prev.sessionId} preserved on chain (status=1) for future reuse — no MsgCancelSession broadcast.`);
|
|
2396
|
+
}
|
|
2307
2397
|
}
|
|
2308
2398
|
} finally {
|
|
2309
2399
|
// ALWAYS clear connection state — even if teardown threw
|
|
2310
|
-
state.
|
|
2400
|
+
state._wallet = null;
|
|
2311
2401
|
state._feeGranter = null;
|
|
2312
2402
|
state.connection = null;
|
|
2313
2403
|
clearState();
|
|
@@ -2317,8 +2407,55 @@ export async function disconnectState(state) {
|
|
|
2317
2407
|
}
|
|
2318
2408
|
}
|
|
2319
2409
|
|
|
2410
|
+
/**
|
|
2411
|
+
* Soft disconnect — tear down the local tunnel, leave the on-chain session active.
|
|
2412
|
+
*
|
|
2413
|
+
* A subsequent connectDirect() to the SAME node will reuse the session via
|
|
2414
|
+
* findExistingSession — no new MsgStartSession TX, no new payment, remaining
|
|
2415
|
+
* bandwidth is preserved.
|
|
2416
|
+
*
|
|
2417
|
+
* Use when: user is pausing, network changed, or closing the app temporarily.
|
|
2418
|
+
* To settle the session on-chain and reclaim the unused deposit, use
|
|
2419
|
+
* disconnectAndEndSession() instead.
|
|
2420
|
+
*
|
|
2421
|
+
* @param {object} state - ConnectionState instance
|
|
2422
|
+
*/
|
|
2423
|
+
export async function disconnectState(state) {
|
|
2424
|
+
return _disconnectInternal(state, { endSession: false });
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
/**
|
|
2428
|
+
* Soft disconnect (default state) — tear down the tunnel, leave on-chain session active.
|
|
2429
|
+
*
|
|
2430
|
+
* @see disconnectState
|
|
2431
|
+
*/
|
|
2320
2432
|
export async function disconnect() {
|
|
2321
|
-
return
|
|
2433
|
+
return _disconnectInternal(_defaultState, { endSession: false });
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
/**
|
|
2437
|
+
* Hard disconnect — tear down the tunnel AND broadcast MsgCancelSession on chain.
|
|
2438
|
+
*
|
|
2439
|
+
* The session settles after the ~2h inactive_pending window. The node refunds
|
|
2440
|
+
* the unused portion of the bandwidth deposit (for peer-to-peer sessions).
|
|
2441
|
+
* For plan-based sessions, this stops metering against the plan allocation.
|
|
2442
|
+
*
|
|
2443
|
+
* Use when: user is done with this node (switching nodes permanently, ending
|
|
2444
|
+
* their session, or wants the deposit back).
|
|
2445
|
+
*
|
|
2446
|
+
* @param {object} state - ConnectionState instance
|
|
2447
|
+
*/
|
|
2448
|
+
export async function disconnectStateAndEndSession(state) {
|
|
2449
|
+
return _disconnectInternal(state, { endSession: true });
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/**
|
|
2453
|
+
* Hard disconnect (default state) — tear down the tunnel AND broadcast MsgCancelSession.
|
|
2454
|
+
*
|
|
2455
|
+
* @see disconnectStateAndEndSession
|
|
2456
|
+
*/
|
|
2457
|
+
export async function disconnectAndEndSession() {
|
|
2458
|
+
return _disconnectInternal(_defaultState, { endSession: true });
|
|
2322
2459
|
}
|
|
2323
2460
|
|
|
2324
2461
|
// ─── Session End (on-chain cleanup) ──────────────────────────────────────────
|
|
@@ -2327,11 +2464,14 @@ export async function disconnect() {
|
|
|
2327
2464
|
* End a session on-chain. Best-effort, fire-and-forget.
|
|
2328
2465
|
* Prevents stale session accumulation on nodes.
|
|
2329
2466
|
* @param {string|bigint} sessionId - Session ID to end
|
|
2330
|
-
* @param {
|
|
2467
|
+
* @param {object} walletObj - { wallet, account } from cachedCreateWallet (NOT the mnemonic).
|
|
2468
|
+
* v37 (security): the BIP-39 phrase is no longer kept on ConnectionState.
|
|
2469
|
+
* @param {string} [feeGranter] - Optional fee grant payer for 0-P2P agents
|
|
2331
2470
|
* @private
|
|
2332
2471
|
*/
|
|
2333
|
-
async function _endSessionOnChain(sessionId,
|
|
2334
|
-
|
|
2472
|
+
async function _endSessionOnChain(sessionId, walletObj, feeGranter = null) {
|
|
2473
|
+
if (!walletObj) throw new SentinelError(ErrorCodes.INVALID_OPTIONS, '_endSessionOnChain requires a wallet object');
|
|
2474
|
+
const { wallet, account } = walletObj;
|
|
2335
2475
|
const client = await tryWithFallback(
|
|
2336
2476
|
RPC_ENDPOINTS,
|
|
2337
2477
|
async (url) => createClient(url, wallet),
|
|
@@ -2377,7 +2517,7 @@ export async function recoverSession(opts) {
|
|
|
2377
2517
|
if (!opts?.nodeAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'recoverSession requires opts.nodeAddress');
|
|
2378
2518
|
if (!opts?.mnemonic) throw new ValidationError(ErrorCodes.INVALID_MNEMONIC, 'recoverSession requires opts.mnemonic');
|
|
2379
2519
|
|
|
2380
|
-
const logFn = opts.log ||
|
|
2520
|
+
const logFn = opts.log || defaultLog;
|
|
2381
2521
|
const onProgress = opts.onProgress || null;
|
|
2382
2522
|
const sessionId = BigInt(opts.sessionId);
|
|
2383
2523
|
const timeouts = { ...DEFAULT_TIMEOUTS, ...opts.timeouts };
|
package/operator.js
CHANGED
|
@@ -77,8 +77,20 @@ export {
|
|
|
77
77
|
grantPlanSubscribers,
|
|
78
78
|
renewExpiringGrants,
|
|
79
79
|
monitorFeeGrants,
|
|
80
|
+
streamGrantPlanSubscribers,
|
|
81
|
+
computeFeeGrantGasCosts,
|
|
80
82
|
} from './cosmjs-setup.js';
|
|
81
83
|
|
|
84
|
+
export {
|
|
85
|
+
batchRevokeFeeGrants,
|
|
86
|
+
} from './operator/batch-revoke.js';
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
queryFeeGrantHistory,
|
|
90
|
+
decodeFeeGrantEvent,
|
|
91
|
+
attr,
|
|
92
|
+
} from './operator/feegrant-history.js';
|
|
93
|
+
|
|
82
94
|
// ─── Authz ──────────────────────────────────────────────────────────────────
|
|
83
95
|
|
|
84
96
|
export {
|
|
@@ -132,3 +144,17 @@ export {
|
|
|
132
144
|
encodeMsgUpdateSubscription,
|
|
133
145
|
encodeMsgUpdateSession,
|
|
134
146
|
} from './v3protocol.js';
|
|
147
|
+
|
|
148
|
+
// ─── Lease Batch Utilities ───────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
export {
|
|
151
|
+
autoLeaseNode,
|
|
152
|
+
batchLeaseNodes,
|
|
153
|
+
} from './operator/auto-lease.js';
|
|
154
|
+
// ─── Plan Ownership Pre-Flight ──────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
assertPlanOwnership,
|
|
158
|
+
PlanOwnershipError,
|
|
159
|
+
walletToProviderAddr,
|
|
160
|
+
} from './operator/plan-ownership.js';
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blue-js-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.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": "
|
|
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/",
|
|
@@ -66,6 +63,9 @@
|
|
|
66
63
|
"proto:generate": "npx protoc --ts_proto_out=generated --ts_proto_opt=esModuleInterop=true --ts_proto_opt=env=node --ts_proto_opt=outputTypeRegistry=true --proto_path=proto proto/sentinel/types/v1/*.proto proto/sentinel/node/v3/*.proto proto/sentinel/session/v3/*.proto proto/sentinel/plan/v3/*.proto proto/sentinel/subscription/v3/*.proto proto/sentinel/lease/v1/*.proto proto/sentinel/provider/v2/*.proto",
|
|
67
64
|
"test": "node test/smoke.js",
|
|
68
65
|
"test:exports": "node -e \"import('./index.js').then(m => console.log(Object.keys(m).length + ' exports OK'))\"",
|
|
66
|
+
"test:privy": "node test/privy-cosmos-signer.test.mjs && node test/privy-client-integration.test.mjs",
|
|
67
|
+
"test:privy:live": "node test/privy-live-chain.test.mjs",
|
|
68
|
+
"test:privy:server": "node test/privy-real-server.test.mjs",
|
|
69
69
|
"postinstall": "node bin/setup.js || true"
|
|
70
70
|
},
|
|
71
71
|
"keywords": [
|
|
@@ -89,18 +89,18 @@
|
|
|
89
89
|
"license": "MIT",
|
|
90
90
|
"repository": {
|
|
91
91
|
"type": "git",
|
|
92
|
-
"url": "https://github.com/Sentinel-Autonomybuilder/sentinel-dvpn-sdk.git"
|
|
92
|
+
"url": "git+https://github.com/Sentinel-Autonomybuilder/sentinel-dvpn-sdk.git"
|
|
93
93
|
},
|
|
94
94
|
"bugs": {
|
|
95
95
|
"url": "https://github.com/Sentinel-Autonomybuilder/sentinel-dvpn-sdk/issues"
|
|
96
96
|
},
|
|
97
97
|
"homepage": "https://github.com/Sentinel-Autonomybuilder/sentinel-dvpn-sdk#readme",
|
|
98
98
|
"dependencies": {
|
|
99
|
-
"@cosmjs/amino": "0.32.
|
|
100
|
-
"@cosmjs/crypto": "0.32.
|
|
101
|
-
"@cosmjs/encoding": "0.32.
|
|
102
|
-
"@cosmjs/proto-signing": "0.32.
|
|
103
|
-
"@cosmjs/stargate": "0.32.
|
|
99
|
+
"@cosmjs/amino": "0.32.4",
|
|
100
|
+
"@cosmjs/crypto": "0.32.4",
|
|
101
|
+
"@cosmjs/encoding": "0.32.4",
|
|
102
|
+
"@cosmjs/proto-signing": "0.32.4",
|
|
103
|
+
"@cosmjs/stargate": "0.32.4",
|
|
104
104
|
"@noble/curves": "^1.4.0",
|
|
105
105
|
"axios": "^1.7.0",
|
|
106
106
|
"dotenv": "^16.4.0",
|
package/session-manager.js
CHANGED
|
@@ -329,3 +329,71 @@ export class SessionManager {
|
|
|
329
329
|
if (this._logger) this._logger(msg);
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
|
+
|
|
333
|
+
// ─── Multi-message tx session extraction ─────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Extract session IDs keyed by node_address from a multi-message transaction.
|
|
337
|
+
*
|
|
338
|
+
* Background: a single tx can carry up to N MsgStartSession messages. The chain
|
|
339
|
+
* emits a `session_id` per session, but on most chain heights the events do NOT
|
|
340
|
+
* include `node_address` alongside the `session_id`. Confirmed empirically
|
|
341
|
+
* 2026-03-23. Naively pairing events to messages by index causes address
|
|
342
|
+
* mismatch, so any session_id without a colocated node_address is reported as
|
|
343
|
+
* an "orphan" — the caller MUST resolve it to a node by querying the chain.
|
|
344
|
+
*
|
|
345
|
+
* Returns a Map with two non-enumerable hint fields attached:
|
|
346
|
+
* - `_orphanIds` Array<bigint> session IDs needing chain lookup
|
|
347
|
+
* - `_needsChainLookup` boolean true if any expected node is unmapped
|
|
348
|
+
*
|
|
349
|
+
* @param {{ events?: Array }} txResult - Tx broadcast result (RPC or LCD shape)
|
|
350
|
+
* @param {string[]} [nodeAddrs] - Expected node addresses (used to compute
|
|
351
|
+
* `_needsChainLookup`). If omitted, the flag is true whenever orphans exist.
|
|
352
|
+
* @returns {Map<string, bigint> & { _orphanIds: bigint[], _needsChainLookup: boolean }}
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* const map = extractSessionMap(txResult, batch.map(b => b.node.address));
|
|
356
|
+
* if (map._needsChainLookup) {
|
|
357
|
+
* for (const sid of map._orphanIds) {
|
|
358
|
+
* const session = await querySessionById(client, sid);
|
|
359
|
+
* map.set(session.nodeAddress, sid);
|
|
360
|
+
* }
|
|
361
|
+
* }
|
|
362
|
+
*/
|
|
363
|
+
export function extractSessionMap(txResult, nodeAddrs) {
|
|
364
|
+
const map = new Map();
|
|
365
|
+
const orphanIds = [];
|
|
366
|
+
|
|
367
|
+
for (const event of (txResult?.events || [])) {
|
|
368
|
+
if (!/session/i.test(event.type)) continue;
|
|
369
|
+
let sessionId = null;
|
|
370
|
+
let nodeAddr = null;
|
|
371
|
+
for (const attr of (event.attributes || [])) {
|
|
372
|
+
const k = typeof attr.key === 'string'
|
|
373
|
+
? attr.key
|
|
374
|
+
: Buffer.from(attr.key, 'base64').toString('utf8');
|
|
375
|
+
const rawV = typeof attr.value === 'string'
|
|
376
|
+
? attr.value
|
|
377
|
+
: Buffer.from(attr.value, 'base64').toString('utf8');
|
|
378
|
+
const clean = rawV.replace(/"/g, '');
|
|
379
|
+
if (k === 'session_id' || k === 'id') {
|
|
380
|
+
try {
|
|
381
|
+
const id = BigInt(clean);
|
|
382
|
+
if (id > 0n) sessionId = id;
|
|
383
|
+
} catch { /* not a numeric id; skip */ }
|
|
384
|
+
}
|
|
385
|
+
if (k === 'node_address') nodeAddr = clean;
|
|
386
|
+
}
|
|
387
|
+
if (sessionId && nodeAddr) {
|
|
388
|
+
map.set(nodeAddr, sessionId);
|
|
389
|
+
} else if (sessionId) {
|
|
390
|
+
orphanIds.push(sessionId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Chain events NEVER include node_address on most heights. Caller resolves.
|
|
395
|
+
map._orphanIds = orphanIds;
|
|
396
|
+
map._needsChainLookup = orphanIds.length > 0
|
|
397
|
+
&& map.size < (nodeAddrs?.length || Number.POSITIVE_INFINITY);
|
|
398
|
+
return map;
|
|
399
|
+
}
|