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/connection/disconnect.js
CHANGED
|
@@ -26,15 +26,42 @@ import { DEFAULT_LCD, DEFAULT_TIMEOUTS } from '../defaults.js';
|
|
|
26
26
|
import { nodeStatusV3 } from '../v3protocol.js';
|
|
27
27
|
import { queryNode, privKeyFromMnemonic } from '../cosmjs-setup.js';
|
|
28
28
|
import { createNodeHttpsAgent } from '../tls-trust.js';
|
|
29
|
+
import { withMnemonicRedaction } from './logger.js';
|
|
30
|
+
|
|
31
|
+
// Default logger wraps console.log so any 12–24 word BIP-39 phrase that ends
|
|
32
|
+
// up in a log argument is replaced before reaching stdout. See connection/logger.js.
|
|
33
|
+
const defaultLog = withMnemonicRedaction(console.log);
|
|
29
34
|
|
|
30
35
|
// ─── Disconnect ──────────────────────────────────────────────────────────────
|
|
36
|
+
//
|
|
37
|
+
// TWO DISCONNECT PATHS — CHOOSE INTENT EXPLICITLY.
|
|
38
|
+
//
|
|
39
|
+
// Soft: disconnect() / disconnectState(state)
|
|
40
|
+
// - Tears down the local tunnel (WireGuard/V2Ray) and cleans up system state.
|
|
41
|
+
// - Leaves the on-chain session in status=1 (active).
|
|
42
|
+
// - Next connectDirect() to the SAME node reuses the session via
|
|
43
|
+
// findExistingSession — no new MsgStartSession TX, no new payment, bandwidth preserved.
|
|
44
|
+
// - Use when: user is pausing, network changed, closing the app temporarily.
|
|
45
|
+
//
|
|
46
|
+
// Hard: disconnectAndEndSession() / disconnectStateAndEndSession(state)
|
|
47
|
+
// - Tears down the tunnel AND broadcasts MsgCancelSession on chain.
|
|
48
|
+
// - Session moves status=1 → settling → refund after ~2h settlement window.
|
|
49
|
+
// - Use when: user is done with this node, switching nodes permanently,
|
|
50
|
+
// or wants the bandwidth deposit back.
|
|
51
|
+
//
|
|
52
|
+
// Internal: _disconnectInternal(state, { endSession })
|
|
53
|
+
// - Caller MUST pass endSession explicitly as true or false.
|
|
54
|
+
// - No default — forces intentional choice at every callsite.
|
|
31
55
|
|
|
32
56
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
57
|
+
* Internal disconnect implementation. Caller must explicitly pass endSession.
|
|
58
|
+
* @param {object} state - ConnectionState instance
|
|
59
|
+
* @param {{ endSession: boolean }} opts
|
|
60
|
+
* endSession: true → broadcast MsgCancelSession (hard disconnect)
|
|
61
|
+
* endSession: false → preserve on-chain session for reuse (soft disconnect)
|
|
62
|
+
* @private
|
|
35
63
|
*/
|
|
36
|
-
|
|
37
|
-
export async function disconnectState(state) {
|
|
64
|
+
async function _disconnectInternal(state, { endSession }) {
|
|
38
65
|
// v30: Signal any running connectAuto() retry loop to abort, and release the
|
|
39
66
|
// connection lock so the user can reconnect after disconnect completes.
|
|
40
67
|
setAbortConnect(true);
|
|
@@ -65,15 +92,22 @@ export async function disconnectState(state) {
|
|
|
65
92
|
try { disableDnsLeakPrevention(); } catch (e) { console.warn('[sentinel-sdk] DNS restore warning:', e.message); }
|
|
66
93
|
}
|
|
67
94
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
if (endSession) {
|
|
96
|
+
// Hard disconnect: broadcast MsgCancelSession — session settles and refunds deposit.
|
|
97
|
+
if (prev?.sessionId && state._wallet) {
|
|
98
|
+
_endSessionOnChain(prev.sessionId, state._wallet).catch(e => {
|
|
99
|
+
console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// Soft disconnect: leave session on chain in status=1 for reuse.
|
|
104
|
+
if (prev?.sessionId) {
|
|
105
|
+
console.log(`[sentinel-sdk] Session ${prev.sessionId} preserved on chain (status=1) for future reuse — no MsgCancelSession broadcast.`);
|
|
106
|
+
}
|
|
73
107
|
}
|
|
74
108
|
} finally {
|
|
75
109
|
// ALWAYS clear connection state — even if teardown threw
|
|
76
|
-
state.
|
|
110
|
+
state._wallet = null;
|
|
77
111
|
state.connection = null;
|
|
78
112
|
clearState();
|
|
79
113
|
clearWalletCache(); // v34: Clear cached wallet objects (private keys) from memory
|
|
@@ -82,8 +116,55 @@ export async function disconnectState(state) {
|
|
|
82
116
|
}
|
|
83
117
|
}
|
|
84
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Soft disconnect — tear down the local tunnel, leave the on-chain session active.
|
|
121
|
+
*
|
|
122
|
+
* A subsequent connectDirect() to the SAME node will reuse the session via
|
|
123
|
+
* findExistingSession — no new MsgStartSession TX, no new payment, remaining
|
|
124
|
+
* bandwidth is preserved.
|
|
125
|
+
*
|
|
126
|
+
* Use when: user is pausing, network changed, or closing the app temporarily.
|
|
127
|
+
* To settle the session on-chain and reclaim the unused deposit, use
|
|
128
|
+
* disconnectAndEndSession() instead.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} [state] - ConnectionState instance (defaults to _defaultState)
|
|
131
|
+
*/
|
|
132
|
+
export async function disconnectState(state) {
|
|
133
|
+
return _disconnectInternal(state, { endSession: false });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Soft disconnect (default state) — tear down the tunnel, leave on-chain session active.
|
|
138
|
+
*
|
|
139
|
+
* @see disconnectState
|
|
140
|
+
*/
|
|
85
141
|
export async function disconnect() {
|
|
86
|
-
return
|
|
142
|
+
return _disconnectInternal(_defaultState, { endSession: false });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Hard disconnect — tear down the tunnel AND broadcast MsgCancelSession on chain.
|
|
147
|
+
*
|
|
148
|
+
* The session settles after the ~2h inactive_pending window. The node refunds
|
|
149
|
+
* the unused portion of the bandwidth deposit (for peer-to-peer sessions).
|
|
150
|
+
* For plan-based sessions, this stops metering against the plan allocation.
|
|
151
|
+
*
|
|
152
|
+
* Use when: user is done with this node (switching nodes permanently, ending
|
|
153
|
+
* their session, or wants the deposit back).
|
|
154
|
+
*
|
|
155
|
+
* @param {object} [state] - ConnectionState instance (defaults to _defaultState)
|
|
156
|
+
*/
|
|
157
|
+
export async function disconnectStateAndEndSession(state) {
|
|
158
|
+
return _disconnectInternal(state, { endSession: true });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Hard disconnect (default state) — tear down the tunnel AND broadcast MsgCancelSession.
|
|
163
|
+
*
|
|
164
|
+
* @see disconnectStateAndEndSession
|
|
165
|
+
*/
|
|
166
|
+
export async function disconnectAndEndSession() {
|
|
167
|
+
return _disconnectInternal(_defaultState, { endSession: true });
|
|
87
168
|
}
|
|
88
169
|
|
|
89
170
|
// ─── Cleanup Registration ───────────────────────────────────────────────────
|
|
@@ -128,7 +209,7 @@ export async function recoverSession(opts) {
|
|
|
128
209
|
if (!opts?.nodeAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'recoverSession requires opts.nodeAddress');
|
|
129
210
|
if (!opts?.mnemonic) throw new ValidationError(ErrorCodes.INVALID_MNEMONIC, 'recoverSession requires opts.mnemonic');
|
|
130
211
|
|
|
131
|
-
const logFn = opts.log ||
|
|
212
|
+
const logFn = opts.log || defaultLog;
|
|
132
213
|
const onProgress = opts.onProgress || null;
|
|
133
214
|
const sessionId = BigInt(opts.sessionId);
|
|
134
215
|
const timeouts = { ...DEFAULT_TIMEOUTS, ...opts.timeouts };
|
package/connection/index.js
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redacting logger wrapper — defense-in-depth against accidental mnemonic leaks
|
|
3
|
+
* in SDK log output.
|
|
4
|
+
*
|
|
5
|
+
* The SDK's default logger is `console.log`. Any future bug that interpolates
|
|
6
|
+
* a mnemonic into a log template string (e.g. `log(`opts: ${JSON.stringify(opts)}`)`)
|
|
7
|
+
* would leak the BIP-39 phrase to stdout/stderr — and from there to terminal
|
|
8
|
+
* scrollback, CI logs, log-aggregation tools (Datadog, Loki, Sentry breadcrumbs),
|
|
9
|
+
* and shell history.
|
|
10
|
+
*
|
|
11
|
+
* This module wraps any logger function with a regex-based redactor that matches
|
|
12
|
+
* BIP-39 word sequences and replaces them with `[REDACTED MNEMONIC]` before the
|
|
13
|
+
* underlying logger sees them. It is NOT a substitute for the rule "do not log
|
|
14
|
+
* the mnemonic" — it is a safety net so that violation does not produce a leak.
|
|
15
|
+
*
|
|
16
|
+
* Performance: the regex runs only on string arguments and short-circuits if the
|
|
17
|
+
* argument has fewer than ~60 characters (a 12-word mnemonic is ~80 characters).
|
|
18
|
+
* Negligible overhead on the SDK's hot path (a few connect-time progress logs).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Match 12 / 15 / 18 / 21 / 24 lowercase BIP-39-shaped words separated by single
|
|
23
|
+
* spaces. We deliberately don't try to validate against the full 2048-word list
|
|
24
|
+
* here — the goal is to catch anything that *looks* like a mnemonic and redact
|
|
25
|
+
* it. False positives (e.g. a long lowercase sentence) are acceptable since the
|
|
26
|
+
* SDK's own log strings never contain 12+ consecutive lowercase-only ASCII words.
|
|
27
|
+
*
|
|
28
|
+
* BIP-39 words are 3–8 lowercase ASCII letters (a–z), no digits, no diacritics.
|
|
29
|
+
*/
|
|
30
|
+
const MNEMONIC_REGEX = /\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/g;
|
|
31
|
+
|
|
32
|
+
const REDACTED = '[REDACTED MNEMONIC]';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Redact mnemonic-shaped substrings from a single value. Strings are scanned;
|
|
36
|
+
* everything else is returned unchanged. We do NOT recurse into objects — the
|
|
37
|
+
* SDK's loggers are called with scalar args, and walking arbitrary objects
|
|
38
|
+
* would risk triggering custom getters that have side effects.
|
|
39
|
+
*
|
|
40
|
+
* @param {*} value
|
|
41
|
+
* @returns {*}
|
|
42
|
+
*/
|
|
43
|
+
function redactValue(value) {
|
|
44
|
+
if (typeof value !== 'string') return value;
|
|
45
|
+
if (value.length < 60) return value; // shortest plausible 12-word phrase ~ 60 chars
|
|
46
|
+
return value.replace(MNEMONIC_REGEX, REDACTED);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wrap a logger function so that mnemonic-shaped strings in its arguments are
|
|
51
|
+
* replaced with `[REDACTED MNEMONIC]` before they reach the wrapped logger.
|
|
52
|
+
* Pass-through for non-function input (returns it unchanged) so callers can
|
|
53
|
+
* disable logging by passing `null`.
|
|
54
|
+
*
|
|
55
|
+
* @param {Function|null|undefined} logFn - underlying logger (typically console.log)
|
|
56
|
+
* @returns {Function|null|undefined}
|
|
57
|
+
*/
|
|
58
|
+
export function withMnemonicRedaction(logFn) {
|
|
59
|
+
if (typeof logFn !== 'function') return logFn;
|
|
60
|
+
return function redactedLog(...args) {
|
|
61
|
+
return logFn(...args.map(redactValue));
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Exported for tests.
|
|
66
|
+
export const _internal = { MNEMONIC_REGEX, redactValue };
|
package/connection/resilience.js
CHANGED
|
@@ -30,6 +30,11 @@ import { findV2RayExe } from './tunnel.js';
|
|
|
30
30
|
import { enableKillSwitch, isKillSwitchEnabled as _isKillSwitchEnabled } from './security.js';
|
|
31
31
|
import { setSystemProxy, clearSystemProxy, checkPortFree } from './proxy.js';
|
|
32
32
|
import { connectAuto, connectViaSubscription, connectViaPlan } from './connect.js';
|
|
33
|
+
import { withMnemonicRedaction } from './logger.js';
|
|
34
|
+
|
|
35
|
+
// Default logger wraps console.log so any 12–24 word BIP-39 phrase that ends
|
|
36
|
+
// up in a log argument is replaced before reaching stdout. See connection/logger.js.
|
|
37
|
+
const defaultLog = withMnemonicRedaction(console.log);
|
|
33
38
|
|
|
34
39
|
// ─── Circuit Breaker ─────────────────────────────────────────────────────────
|
|
35
40
|
// v22: Skip nodes that repeatedly fail. Resets after TTL expires.
|
|
@@ -108,7 +113,7 @@ export async function tryFastReconnect(opts, state = _defaultState) {
|
|
|
108
113
|
if (!saved) return null;
|
|
109
114
|
|
|
110
115
|
const onProgress = opts.onProgress || null;
|
|
111
|
-
const logFn = opts.log ||
|
|
116
|
+
const logFn = opts.log || defaultLog;
|
|
112
117
|
const fullTunnel = opts.fullTunnel !== false;
|
|
113
118
|
const killSwitch = opts.killSwitch === true;
|
|
114
119
|
const systemProxy = opts.systemProxy === true;
|
|
@@ -210,12 +215,12 @@ export async function tryFastReconnect(opts, state = _defaultState) {
|
|
|
210
215
|
}
|
|
211
216
|
try { await disconnectWireGuard(); } catch {}
|
|
212
217
|
// End session on chain (fire-and-forget)
|
|
213
|
-
if (saved.sessionId && state.
|
|
214
|
-
_endSessionOnChain(saved.sessionId, state.
|
|
218
|
+
if (saved.sessionId && state._wallet) {
|
|
219
|
+
_endSessionOnChain(saved.sessionId, state._wallet).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
215
220
|
}
|
|
216
221
|
state.wgTunnel = null;
|
|
217
222
|
state.connection = null;
|
|
218
|
-
state.
|
|
223
|
+
state._wallet = null;
|
|
219
224
|
clearState();
|
|
220
225
|
},
|
|
221
226
|
};
|
|
@@ -335,11 +340,11 @@ export async function tryFastReconnect(opts, state = _defaultState) {
|
|
|
335
340
|
if (state.v2rayProc) { state.v2rayProc.kill(); state.v2rayProc = null; await sleep(500); }
|
|
336
341
|
if (state.systemProxy) clearSystemProxy(state);
|
|
337
342
|
// End session on chain (fire-and-forget)
|
|
338
|
-
if (sessionIdStr && state.
|
|
339
|
-
_endSessionOnChain(sessionIdStr, state.
|
|
343
|
+
if (sessionIdStr && state._wallet) {
|
|
344
|
+
_endSessionOnChain(sessionIdStr, state._wallet).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
340
345
|
}
|
|
341
346
|
state.connection = null;
|
|
342
|
-
state.
|
|
347
|
+
state._wallet = null;
|
|
343
348
|
clearState();
|
|
344
349
|
},
|
|
345
350
|
};
|
package/connection/state.js
CHANGED
|
@@ -82,7 +82,13 @@ export class ConnectionState {
|
|
|
82
82
|
this.systemProxy = false;
|
|
83
83
|
this.connection = null; // { nodeAddress, serviceType, sessionId, connectedAt, socksPort? }
|
|
84
84
|
this.savedProxyState = null;
|
|
85
|
-
|
|
85
|
+
// v37 (security): store the derived OfflineSigner wallet, NOT the raw BIP-39 mnemonic.
|
|
86
|
+
// Previously _mnemonic held the recovery phrase for the full session lifetime so
|
|
87
|
+
// that disconnect could sign MsgEndSession. The mnemonic is the highest-value
|
|
88
|
+
// secret in the SDK — keeping it on a long-lived object enlarged the heap-dump
|
|
89
|
+
// attack surface unnecessarily. The wallet object holds only the derived signing
|
|
90
|
+
// key, which is no more sensitive than the privKey buffer we were already keeping.
|
|
91
|
+
this._wallet = null; // OfflineSigner — used by _endSessionOnChain on disconnect
|
|
86
92
|
_activeStates.add(this);
|
|
87
93
|
}
|
|
88
94
|
get isConnected() { return !!(this.v2rayProc || this.wgTunnel); }
|
|
@@ -216,11 +222,14 @@ export function formatUptime(ms) {
|
|
|
216
222
|
* End a session on-chain. Best-effort, fire-and-forget.
|
|
217
223
|
* Prevents stale session accumulation on nodes.
|
|
218
224
|
* @param {string|bigint} sessionId - Session ID to end
|
|
219
|
-
* @param {
|
|
225
|
+
* @param {object} walletObj - { wallet, account } from cachedCreateWallet (NOT the mnemonic).
|
|
226
|
+
* Accepting the derived signer instead of the raw phrase keeps the BIP-39 mnemonic
|
|
227
|
+
* off the long-lived ConnectionState — see ConnectionState._wallet.
|
|
220
228
|
* @private
|
|
221
229
|
*/
|
|
222
|
-
export async function _endSessionOnChain(sessionId,
|
|
223
|
-
|
|
230
|
+
export async function _endSessionOnChain(sessionId, walletObj) {
|
|
231
|
+
if (!walletObj) throw new SentinelError(ErrorCodes.INVALID_OPTIONS, '_endSessionOnChain requires a wallet object');
|
|
232
|
+
const { wallet, account } = walletObj;
|
|
224
233
|
const client = await tryWithFallback(
|
|
225
234
|
RPC_ENDPOINTS,
|
|
226
235
|
async (url) => createClient(url, wallet),
|
|
@@ -261,8 +270,8 @@ export function getStatus() {
|
|
|
261
270
|
const stale = _defaultState.connection;
|
|
262
271
|
_defaultState.connection = null;
|
|
263
272
|
// End session on chain (fire-and-forget) to prevent stale session leaks
|
|
264
|
-
if (stale?.sessionId && _defaultState.
|
|
265
|
-
_endSessionOnChain(stale.sessionId, _defaultState.
|
|
273
|
+
if (stale?.sessionId && _defaultState._wallet) {
|
|
274
|
+
_endSessionOnChain(stale.sessionId, _defaultState._wallet).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
266
275
|
}
|
|
267
276
|
clearState();
|
|
268
277
|
events.emit('disconnected', { nodeAddress: stale.nodeAddress, serviceType: stale.serviceType, reason: 'phantom_state' });
|
|
@@ -308,8 +317,8 @@ export function getStatus() {
|
|
|
308
317
|
// clean up stale state. Prevents ghost "connected" status after tunnel dies.
|
|
309
318
|
if (!healthChecks.tunnelActive && !_defaultState.v2rayProc && !_defaultState.wgTunnel) {
|
|
310
319
|
// Both tunnel handles are null — connection state is stale
|
|
311
|
-
if (conn?.sessionId && _defaultState.
|
|
312
|
-
_endSessionOnChain(conn.sessionId, _defaultState.
|
|
320
|
+
if (conn?.sessionId && _defaultState._wallet) {
|
|
321
|
+
_endSessionOnChain(conn.sessionId, _defaultState._wallet).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
313
322
|
}
|
|
314
323
|
_defaultState.connection = null;
|
|
315
324
|
clearState();
|
|
@@ -317,8 +326,8 @@ export function getStatus() {
|
|
|
317
326
|
}
|
|
318
327
|
if (_defaultState.wgTunnel && !healthChecks.tunnelActive) {
|
|
319
328
|
// WireGuard state says connected but tunnel is dead — auto-cleanup
|
|
320
|
-
if (conn?.sessionId && _defaultState.
|
|
321
|
-
_endSessionOnChain(conn.sessionId, _defaultState.
|
|
329
|
+
if (conn?.sessionId && _defaultState._wallet) {
|
|
330
|
+
_endSessionOnChain(conn.sessionId, _defaultState._wallet).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
322
331
|
}
|
|
323
332
|
_defaultState.wgTunnel = null;
|
|
324
333
|
_defaultState.connection = null;
|
|
@@ -328,8 +337,8 @@ export function getStatus() {
|
|
|
328
337
|
}
|
|
329
338
|
if (_defaultState.v2rayProc && !healthChecks.tunnelActive) {
|
|
330
339
|
// V2Ray process died — auto-cleanup
|
|
331
|
-
if (conn?.sessionId && _defaultState.
|
|
332
|
-
_endSessionOnChain(conn.sessionId, _defaultState.
|
|
340
|
+
if (conn?.sessionId && _defaultState._wallet) {
|
|
341
|
+
_endSessionOnChain(conn.sessionId, _defaultState._wallet).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
333
342
|
}
|
|
334
343
|
_defaultState.v2rayProc = null;
|
|
335
344
|
_defaultState.connection = null;
|
package/connection/tunnel.js
CHANGED
|
@@ -389,12 +389,12 @@ async function setupWireGuard({ remoteUrl, sessionId, privKey, fullTunnel, split
|
|
|
389
389
|
if (isKillSwitchEnabled()) disableKillSwitch();
|
|
390
390
|
try { await disconnectWireGuard(); } catch {} // tunnel may already be down
|
|
391
391
|
// End session on chain (fire-and-forget)
|
|
392
|
-
if (sessionIdStr && state.
|
|
393
|
-
_endSessionOnChain(sessionIdStr, state.
|
|
392
|
+
if (sessionIdStr && state._wallet) {
|
|
393
|
+
_endSessionOnChain(sessionIdStr, state._wallet).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
394
394
|
}
|
|
395
395
|
state.wgTunnel = null;
|
|
396
396
|
state.connection = null;
|
|
397
|
-
state.
|
|
397
|
+
state._wallet = null;
|
|
398
398
|
clearState();
|
|
399
399
|
},
|
|
400
400
|
};
|
|
@@ -560,12 +560,28 @@ async function setupV2Ray({ remoteUrl, serverHost, sessionId, privKey, v2rayExeP
|
|
|
560
560
|
}
|
|
561
561
|
});
|
|
562
562
|
}
|
|
563
|
-
// Delete config after V2Ray reads it (contains UUID credentials)
|
|
564
|
-
|
|
563
|
+
// Delete config after V2Ray reads it (contains UUID credentials).
|
|
564
|
+
// Race-safe approach: delete the moment we know V2Ray has loaded config
|
|
565
|
+
// (port is up OR process exited), and a deadline-bound unref'd safety net.
|
|
566
|
+
// The previous fixed 2s timer raced the loop iterating to the next outbound,
|
|
567
|
+
// which overwrites cfgPath — the stale timer would then delete the NEW config.
|
|
568
|
+
let cfgDeleted = false;
|
|
569
|
+
const deleteCfg = () => {
|
|
570
|
+
if (cfgDeleted) return;
|
|
571
|
+
cfgDeleted = true;
|
|
572
|
+
try { unlinkSync(cfgPath); } catch {} // best-effort: V2Ray may have file open on Windows
|
|
573
|
+
};
|
|
574
|
+
proc.once('exit', deleteCfg);
|
|
575
|
+
const cfgSafetyTimer = setTimeout(deleteCfg, 5000);
|
|
576
|
+
cfgSafetyTimer.unref();
|
|
565
577
|
|
|
566
578
|
// Wait for SOCKS5 port to accept connections instead of fixed sleep.
|
|
567
579
|
// V2Ray binding is async — fixed 6s sleep causes false failures on slow starts.
|
|
568
580
|
const ready = await waitForPort(socksPort, timeouts.v2rayReady);
|
|
581
|
+
// Once the SOCKS port accepts connections, V2Ray has fully parsed the config —
|
|
582
|
+
// safe to delete (Windows: file is still open by V2Ray, unlink will return EBUSY,
|
|
583
|
+
// try/catch swallows; falls back to exit-handler or process orphan-cleanup).
|
|
584
|
+
if (ready) deleteCfg();
|
|
569
585
|
if (!ready || proc.exitCode !== null) {
|
|
570
586
|
progress(onProgress, logFn, 'tunnel', ` ${ob.tag}: v2ray ${proc.exitCode !== null ? `exited (code ${proc.exitCode})` : 'SOCKS5 port not ready'}, skipping`);
|
|
571
587
|
proc.kill();
|
|
@@ -653,11 +669,11 @@ async function setupV2Ray({ remoteUrl, serverHost, sessionId, privKey, v2rayExeP
|
|
|
653
669
|
if (state.v2rayProc) { state.v2rayProc.kill(); state.v2rayProc = null; await sleep(500); }
|
|
654
670
|
if (state.systemProxy) clearSystemProxy();
|
|
655
671
|
// End session on chain (fire-and-forget)
|
|
656
|
-
if (sessionIdStr && state.
|
|
657
|
-
_endSessionOnChain(sessionIdStr, state.
|
|
672
|
+
if (sessionIdStr && state._wallet) {
|
|
673
|
+
_endSessionOnChain(sessionIdStr, state._wallet).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
658
674
|
}
|
|
659
675
|
state.connection = null;
|
|
660
|
-
state.
|
|
676
|
+
state._wallet = null;
|
|
661
677
|
clearState();
|
|
662
678
|
},
|
|
663
679
|
};
|
package/cosmjs-setup.js
CHANGED
|
@@ -74,6 +74,8 @@ import {
|
|
|
74
74
|
getProviderByAddress as _getProviderByAddress,
|
|
75
75
|
queryPlanSubscribers as _queryPlanSubscribers,
|
|
76
76
|
getPlanStats as _getPlanStats,
|
|
77
|
+
queryPlanDetails as _queryPlanDetails,
|
|
78
|
+
isActiveStatus as _isActiveStatus,
|
|
77
79
|
querySubscriptionAllocations as _querySubscriptionAllocations,
|
|
78
80
|
queryAuthzGrants as _queryAuthzGrants,
|
|
79
81
|
loadVpnSettings as _loadVpnSettings,
|
|
@@ -83,10 +85,13 @@ import {
|
|
|
83
85
|
queryFeeGrants as _queryFeeGrants,
|
|
84
86
|
queryFeeGrantsIssued as _queryFeeGrantsIssued,
|
|
85
87
|
queryFeeGrant as _queryFeeGrant,
|
|
88
|
+
checkFeeGrant as _checkFeeGrant,
|
|
86
89
|
grantPlanSubscribers as _grantPlanSubscribers,
|
|
87
90
|
getExpiringGrants as _getExpiringGrants,
|
|
88
91
|
renewExpiringGrants as _renewExpiringGrants,
|
|
89
92
|
monitorFeeGrants as _monitorFeeGrants,
|
|
93
|
+
streamGrantPlanSubscribers as _streamGrantPlanSubscribers,
|
|
94
|
+
computeFeeGrantGasCosts as _computeFeeGrantGasCosts,
|
|
90
95
|
} from './chain/fee-grants.js';
|
|
91
96
|
|
|
92
97
|
// ─── Input Validation Helpers ────────────────────────────────────────────────
|
|
@@ -485,9 +490,15 @@ export async function getBalance(client, address) {
|
|
|
485
490
|
* Returns session ID (BigInt) or null. Use this to avoid double-paying.
|
|
486
491
|
*
|
|
487
492
|
* Note: Sessions have a nested base_session object containing the actual data.
|
|
493
|
+
*
|
|
494
|
+
* @param {string} lcdUrl - LCD endpoint URL
|
|
495
|
+
* @param {string} walletAddr - sent1... wallet address
|
|
496
|
+
* @param {string} nodeAddr - sentnode1... node address
|
|
497
|
+
* @param {object} [opts] - Optional. Pass `{ onStaleDuplicate: (BigInt) => void }` to
|
|
498
|
+
* receive fire-and-forget cancellation callbacks for stale duplicate sessions.
|
|
488
499
|
*/
|
|
489
|
-
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
|
|
490
|
-
return _findExistingSession(lcdUrl, walletAddr, nodeAddr);
|
|
500
|
+
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr, opts) {
|
|
501
|
+
return _findExistingSession(lcdUrl, walletAddr, nodeAddr, opts);
|
|
491
502
|
}
|
|
492
503
|
|
|
493
504
|
/**
|
|
@@ -804,6 +815,20 @@ export async function queryFeeGrant(lcdUrl, granter, grantee) {
|
|
|
804
815
|
return _queryFeeGrant(lcdUrl, granter, grantee);
|
|
805
816
|
}
|
|
806
817
|
|
|
818
|
+
/**
|
|
819
|
+
* Builder-friendly parsed fee-grant status — exists, expired, expiresAt,
|
|
820
|
+
* spendLimit, allowedMessages. RPC-first with LCD fallback. Use before
|
|
821
|
+
* broadcasting plan/subscription session TXs that rely on a fee granter.
|
|
822
|
+
*
|
|
823
|
+
* @param {string} lcdUrl
|
|
824
|
+
* @param {string} granter
|
|
825
|
+
* @param {string} grantee
|
|
826
|
+
* @returns {Promise<{ exists: boolean, expired: boolean, expiresAt: Date|null, spendLimit: Array<{denom:string,amount:string}>, allowedMessages: string[]|null, typeUrl: string, raw: object|null }>}
|
|
827
|
+
*/
|
|
828
|
+
export async function checkFeeGrant(lcdUrl, granter, grantee) {
|
|
829
|
+
return _checkFeeGrant(lcdUrl, granter, grantee);
|
|
830
|
+
}
|
|
831
|
+
|
|
807
832
|
/**
|
|
808
833
|
* Broadcast a TX with fee paid by a granter (fee grant).
|
|
809
834
|
* The grantee signs; the granter pays gas via their fee allowance.
|
|
@@ -945,6 +970,29 @@ export async function getPlanStats(planId, ownerAddress, opts = {}) {
|
|
|
945
970
|
return _getPlanStats(planId, ownerAddress, opts);
|
|
946
971
|
}
|
|
947
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Query a plan's on-chain metadata (provider, prices, duration, status).
|
|
975
|
+
* RPC-first with LCD fallback. Returns null if the plan does not exist.
|
|
976
|
+
*
|
|
977
|
+
* @param {number|string} planId
|
|
978
|
+
* @param {object} [opts]
|
|
979
|
+
* @param {string} [opts.lcdUrl]
|
|
980
|
+
* @returns {Promise<{ planId: string, provider: string, prices: Array, bytes: string, duration: string, status: number, statusAt: string|null, private: boolean } | null>}
|
|
981
|
+
*/
|
|
982
|
+
export async function queryPlanDetails(planId, opts = {}) {
|
|
983
|
+
return _queryPlanDetails(planId, opts);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Normalize chain status values. Returns true only for active status
|
|
988
|
+
* (never for transient INACTIVE_PENDING / status=3).
|
|
989
|
+
* @param {number|string|undefined} v
|
|
990
|
+
* @returns {boolean}
|
|
991
|
+
*/
|
|
992
|
+
export function isActiveStatus(v) {
|
|
993
|
+
return _isActiveStatus(v);
|
|
994
|
+
}
|
|
995
|
+
|
|
948
996
|
// ─── Fee Grant Workflow Helpers (v25b) ────────────────────────────────────────
|
|
949
997
|
|
|
950
998
|
/**
|
|
@@ -1006,6 +1054,22 @@ export function monitorFeeGrants(opts = {}) {
|
|
|
1006
1054
|
return _monitorFeeGrants(opts);
|
|
1007
1055
|
}
|
|
1008
1056
|
|
|
1057
|
+
/**
|
|
1058
|
+
* Stream progress as we grant fee allowances to all plan subscribers in batches.
|
|
1059
|
+
* Async generator. See chain/fee-grants.js for event shapes.
|
|
1060
|
+
*/
|
|
1061
|
+
export function streamGrantPlanSubscribers(planId, opts = {}) {
|
|
1062
|
+
return _streamGrantPlanSubscribers(planId, opts);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Sum udvpn fees the granter has paid on behalf of a plan's subscribers.
|
|
1067
|
+
* Iterates subscribers, pulls TX history, filters on fee.granter.
|
|
1068
|
+
*/
|
|
1069
|
+
export async function computeFeeGrantGasCosts(planId, opts = {}) {
|
|
1070
|
+
return _computeFeeGrantGasCosts(planId, opts);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1009
1073
|
// ─── Query Helpers (v25c) ────────────────────────────────────────────────────
|
|
1010
1074
|
|
|
1011
1075
|
/**
|
|
@@ -1124,6 +1188,8 @@ export async function hasActiveSubscription(address, planId, lcdUrl) {
|
|
|
1124
1188
|
return _hasActiveSubscription(address, planId, lcdUrl);
|
|
1125
1189
|
}
|
|
1126
1190
|
|
|
1191
|
+
|
|
1192
|
+
|
|
1127
1193
|
// ─── v26c: Display Helpers ───────────────────────────────────────────────────
|
|
1128
1194
|
|
|
1129
1195
|
/**
|