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.
Files changed (58) hide show
  1. package/README.md +3 -3
  2. package/app-helpers.js +55 -0
  3. package/chain/broadcast.js +27 -0
  4. package/chain/fee-grants.js +271 -5
  5. package/chain/index.js +8 -2
  6. package/chain/queries.js +177 -3
  7. package/chain/rpc.js +117 -4
  8. package/cli.js +26 -5
  9. package/client.js +79 -7
  10. package/connection/connect.js +119 -21
  11. package/connection/disconnect.js +93 -12
  12. package/connection/index.js +2 -0
  13. package/connection/logger.js +66 -0
  14. package/connection/resilience.js +12 -7
  15. package/connection/state.js +21 -12
  16. package/connection/tunnel.js +24 -8
  17. package/cosmjs-setup.js +68 -2
  18. package/docs/PRIVY-INTEGRATION.md +177 -0
  19. package/errors.js +167 -0
  20. package/index.js +75 -2
  21. package/node-connect.js +190 -50
  22. package/operator.js +26 -0
  23. package/package.json +11 -11
  24. package/session-manager.js +68 -0
  25. package/speedtest.js +139 -0
  26. package/test-all-logic.js +8 -6
  27. package/test-e2e.js +138 -0
  28. package/test-mainnet.js +2 -2
  29. package/test-plan-connect-e2e.js +235 -0
  30. package/test-subscription-flows.js +14 -4
  31. package/types/connection.d.ts +6 -2
  32. package/types/index.d.ts +2 -2
  33. package/ai-path/ADMIN-ELEVATION.md +0 -116
  34. package/ai-path/AI-MANIFESTO.md +0 -185
  35. package/ai-path/BREAKING.md +0 -74
  36. package/ai-path/CHECKLIST.md +0 -619
  37. package/ai-path/CONNECTION-STEPS.md +0 -724
  38. package/ai-path/DECISION-TREE.md +0 -422
  39. package/ai-path/DEPENDENCIES.md +0 -459
  40. package/ai-path/E2E-FLOW.md +0 -1707
  41. package/ai-path/FAILURES.md +0 -410
  42. package/ai-path/GUIDE.md +0 -1315
  43. package/ai-path/README.md +0 -599
  44. package/ai-path/SPLIT-TUNNEL.md +0 -266
  45. package/ai-path/cli.js +0 -548
  46. package/ai-path/connect.js +0 -1028
  47. package/ai-path/discover.js +0 -178
  48. package/ai-path/environment.js +0 -266
  49. package/ai-path/errors.js +0 -86
  50. package/ai-path/examples/autonomous-agent.mjs +0 -220
  51. package/ai-path/examples/multi-region.mjs +0 -174
  52. package/ai-path/examples/one-shot.mjs +0 -31
  53. package/ai-path/index.js +0 -79
  54. package/ai-path/pricing.js +0 -137
  55. package/ai-path/recommend.js +0 -413
  56. package/ai-path/run-admin.vbs +0 -25
  57. package/ai-path/setup.js +0 -291
  58. package/ai-path/wallet.js +0 -137
@@ -0,0 +1,177 @@
1
+ # Privy Integration
2
+
3
+ Privy provides embedded EVM/Solana wallets but has no native Cosmos signer. This SDK ships an adapter that bridges a Privy-held key to a cosmjs-compatible Cosmos signer, so a consumer can use Privy for auth/onboarding while still using every Sentinel SDK helper that takes a `wallet`.
4
+
5
+ The adapter lives in `auth/privy-cosmos-signer.js` and is re-exported from the SDK root.
6
+
7
+ ## Two strategies
8
+
9
+ The adapter supports two paths, selected by the `mode` field on `createPrivyCosmosSigner`. Pick the one that matches your custody requirements.
10
+
11
+ ### Mode A — `mnemonic` (seed-import)
12
+
13
+ The consumer triggers Privy's `exportWallet()`. The user reveals their seed once. The adapter re-derives a Cosmos secp256k1 key on the standard Cosmos HD path (`m/44'/118'/0'/0/0`) and wraps it in `DirectSecp256k1HdWallet`.
14
+
15
+ ```js
16
+ import { PrivyCosmosSigner } from 'blue-js-sdk';
17
+
18
+ const signer = await PrivyCosmosSigner.fromMnemonic({
19
+ mnemonic: privyExportedSeed,
20
+ prefix: 'sent',
21
+ });
22
+
23
+ const [account] = await signer.getAccounts();
24
+ // account.address === 'sent1...'
25
+ ```
26
+
27
+ Trust model: identical to a normal mnemonic wallet — the seed has left Privy's enclave. Use this when you need full broadcast capability and your UX can prompt the user to export once.
28
+
29
+ ### Mode B — `rawSign` (custody-preserving)
30
+
31
+ The seed never leaves Privy. The consumer supplies:
32
+
33
+ - `pubkey` — the compressed secp256k1 pubkey (33 bytes) Privy derived for this user on the Cosmos `m/44'/118'/0'/0/0` path.
34
+ - `signRawSecp256k1(digest32)` — async function that asks Privy to produce a 64-byte (`r||s`) signature over the supplied 32-byte digest using the same key.
35
+
36
+ The adapter computes the digest of the cosmjs `SignDoc` itself, so Privy only sees a hash.
37
+
38
+ ```js
39
+ import { PrivyCosmosSigner } from 'blue-js-sdk';
40
+
41
+ const signer = await PrivyCosmosSigner.fromRawSign({
42
+ pubkey: privyDerivedCompressedPubkey,
43
+ signRawSecp256k1: async (digest32) => {
44
+ const sig = await privy.signRawHash({ hash: digest32, curve: 'secp256k1' });
45
+ return sig; // Uint8Array(64), r||s
46
+ },
47
+ prefix: 'sent',
48
+ });
49
+ ```
50
+
51
+ Use this when you must keep custody inside Privy. Requirements on the callback:
52
+
53
+ - Returns a 64-byte (`r||s`) `Uint8Array`. The adapter rejects any other shape.
54
+ - The signature MUST be over the raw 32-byte digest the adapter passed in. Do not let Privy re-hash it (no `eth_sign`-style "Ethereum Signed Message" prefixing).
55
+ - Low-S form is preferred but not required — the adapter normalizes high-S signatures to low-S before encoding the result, since cosmos-sdk validators reject high-S since v0.42.
56
+
57
+ ## Address parity
58
+
59
+ Both modes derive the **same** `sent1...` address from the same seed. You can pre-compute the address in either direction with `deriveCosmosPubkeyFromMnemonic`:
60
+
61
+ ```js
62
+ import { deriveCosmosPubkeyFromMnemonic } from 'blue-js-sdk';
63
+
64
+ const { pubkey, address } = await deriveCosmosPubkeyFromMnemonic(mnemonic);
65
+ ```
66
+
67
+ This is useful when the consumer wants to display the user's `sent1...` address in Privy onboarding before a Mode B signer is wired up.
68
+
69
+ ## What the adapter is
70
+
71
+ The Mode A return value IS a `DirectSecp256k1HdWallet`. The Mode B return value is an `OfflineDirectSigner` — `getAccounts()` + `signDirect(signerAddress, signDoc)`. Either can be passed straight to `SigningStargateClient.connectWithSigner` and to every Sentinel SDK helper that accepts a `wallet`:
72
+
73
+ - `broadcast()`, `broadcastWithFeeGrant()`, `createSafeBroadcaster()` — TX broadcast
74
+ - Operator helpers: `autoLeaseNode()`, `batchLeaseNodes()`, `batchRevokeFeeGrants()`, etc.
75
+ - `SentinelClient` query surface — `getBalance()`, `getClient()`, `listNodes()`, etc.
76
+
77
+ ### Tunnel connect/disconnect — Mode A only (today)
78
+
79
+ VPN session start (`connect()`, `autoConnect()`, `connectPlan()`) and matching teardown perform a WireGuard/V2Ray handshake with the node. The handshake protocol requires the SDK to sign a small payload with the **raw** secp256k1 privkey **locally**, before any chain TX. That privkey is not available in Mode B — Privy's raw-sign endpoint signs digests but does not export the key.
80
+
81
+ In short:
82
+
83
+ | Operation | Mode A (mnemonic) | Mode B (rawSign) |
84
+ |---|---|---|
85
+ | `getBalance()`, `listNodes()`, queries | works | works |
86
+ | `broadcast()`, `broadcastWithFeeGrant()` | works | works |
87
+ | Operator helpers (`autoLeaseNode`, batch*) | works | works |
88
+ | `connect()`, `autoConnect()`, `connectPlan()` | works | **throws** with "VPN connect/disconnect requires a mnemonic" |
89
+
90
+ A signer-only `SentinelClient` will throw a helpful error from the connect methods rather than failing deep inside the handshake. Lifting this restriction requires either (a) refactoring the handshake to call out to `signRawSecp256k1`, or (b) Privy exposing a "raw secp256k1 sign" endpoint shaped like the cosmjs `Secp256k1.createSignature` signature already accepted in Mode B — both viable, neither in this PR.
91
+
92
+ ## Using `SentinelClient` with Privy
93
+
94
+ ```js
95
+ import { SentinelClient, PrivyCosmosSigner } from 'blue-js-sdk';
96
+
97
+ // Mode A — full feature set
98
+ const signer = await PrivyCosmosSigner.fromMnemonic({ mnemonic: privyExportedSeed });
99
+ const client = new SentinelClient({
100
+ signer,
101
+ rpcUrl: 'https://rpc.sentinel.co',
102
+ // mnemonic still required for VPN connect — see table above
103
+ mnemonic: privyExportedSeed,
104
+ });
105
+ const balance = await client.getBalance(); // works
106
+ const conn = await client.autoConnect(); // works (uses mnemonic)
107
+
108
+ // Mode B — custody-preserving (queries + broadcasts only)
109
+ const custodySigner = await PrivyCosmosSigner.fromRawSign({
110
+ pubkey: privyDerivedCompressedPubkey,
111
+ signRawSecp256k1: async (digest32) => privy.signRawHash({ hash: digest32, curve: 'secp256k1' }),
112
+ });
113
+ const queryClient = new SentinelClient({ signer: custodySigner, rpcUrl: 'https://rpc.sentinel.co' });
114
+ await queryClient.getBalance(); // works — queries Privy for the address
115
+ // await queryClient.connect(...); // throws: requires a mnemonic
116
+ ```
117
+
118
+ ## Unified factory
119
+
120
+ ```js
121
+ import { createPrivyCosmosSigner } from 'blue-js-sdk';
122
+
123
+ // Routes to fromMnemonic / fromRawSign by `mode`.
124
+ const signer = await createPrivyCosmosSigner({ mode: 'mnemonic', mnemonic });
125
+ // or
126
+ const signer = await createPrivyCosmosSigner({
127
+ mode: 'rawSign', pubkey, signRawSecp256k1,
128
+ });
129
+ ```
130
+
131
+ ## Failure modes
132
+
133
+ | Symptom | Cause | Fix |
134
+ |---------|-------|-----|
135
+ | `signerAddress mismatch (got X, signer holds Y)` | Caller passed a different `signerAddress` to `signDirect` than the one the signer derived from the pubkey. | Use the address from `getAccounts()[0]`. |
136
+ | `signRawSecp256k1 must return a 64-byte (r\|\|s) Uint8Array` | Privy callback returned DER, hex string, or included a recovery byte. | Strip to fixed 64-byte `r\|\|s` before returning. |
137
+ | Chain rejects TX with `signature verification failed` | Privy hashed the input again before signing (e.g. `eth_sign` prefixing). | Use Privy's "raw hash" sign endpoint, not `signMessage`. |
138
+ | Address differs between modes | Privy derived the pubkey on a non-Cosmos path. | Use Cosmos path `m/44'/118'/0'/0/0`; coinType MUST be 118. |
139
+
140
+ ## Tests
141
+
142
+ `test/privy-cosmos-signer.test.mjs` — 20 assertions covering:
143
+
144
+ - Mode A address parity with `createWallet()`
145
+ - `deriveCosmosPubkeyFromMnemonic` matches Mode A
146
+ - Mode B address parity with Mode A using the same seed
147
+ - `signDirect` produces a signature that verifies against the pubkey on `sha256(makeSignBytes(signDoc))`
148
+ - High-S signatures returned by the callback are normalized to low-S
149
+ - `signerAddress` mismatch is rejected
150
+ - Unified factory routes correctly and rejects unknown modes
151
+ - Static facade delegates to the underlying functions
152
+
153
+ ### Run the offline suite (CI-safe)
154
+
155
+ ```sh
156
+ npm run test:privy # 32 assertions, no network
157
+ ```
158
+
159
+ ### Run the live suites (require credentials, NOT in CI)
160
+
161
+ ```sh
162
+ # Mainnet broadcast — proves Sentinel chain accepts adapter signatures.
163
+ # Sends a 1 udvpn self-MsgSend; needs ~20000 udvpn for fee.
164
+ MNEMONIC="..." npm run test:privy:live
165
+
166
+ # Real Privy API — creates a server-managed Cosmos wallet on Privy and proves
167
+ # the bytes Privy's /raw_sign returns verify against the Privy-derived pubkey.
168
+ PRIVY_APP_ID="..." PRIVY_APP_SECRET="..." npm run test:privy:server
169
+ ```
170
+
171
+ `test/privy-client-integration.test.mjs` — 12 assertions covering:
172
+
173
+ - `SentinelClient({ signer })` — `getWallet()` returns the supplied signer + first account, no mnemonic required
174
+ - `SentinelClient({ mnemonic })` — backwards-compatible path still works
175
+ - `SentinelClient({})` — `getWallet()` throws with a helpful "mnemonic or signer" message
176
+ - `SentinelClient({ signer })` — `connect()`, `autoConnect()`, `connectPlan()` all reject with "requires a mnemonic" pointing to this doc
177
+ - Address parity between `PrivyCosmosSigner(mnemonic)` and `SentinelClient(mnemonic)`
package/errors.js CHANGED
@@ -71,6 +71,82 @@ export class SecurityError extends SentinelError {
71
71
  }
72
72
  }
73
73
 
74
+ // ─── Audit Errors ────────────────────────────────────────────────────────────
75
+ //
76
+ // Errors raised by audit / network-test pipelines. They carry a `.diag`
77
+ // blob — the structured snapshot of what was happening when the failure
78
+ // fired (handshake transcript, timings, transport state, etc.). UI surfaces
79
+ // render that into the per-row failure log so an operator can copy the
80
+ // full context.
81
+ //
82
+ // Audit errors share a hardcoded code per subclass — the subclass IS the
83
+ // taxonomy. Callers don't have to remember code strings; they catch by
84
+ // type.
85
+
86
+ /**
87
+ * Base class for audit / network-test failures.
88
+ * Adds a `.diag` field on top of SentinelError's `.details`.
89
+ *
90
+ * @param {string} message
91
+ * @param {string} code
92
+ * @param {object} [diag] - Diagnostic snapshot (handshake bytes, timings, etc.)
93
+ */
94
+ export class AuditError extends SentinelError {
95
+ constructor(message, code, diag = {}) {
96
+ super(code, message, diag);
97
+ this.name = 'AuditError';
98
+ this.diag = diag;
99
+ }
100
+ }
101
+
102
+ /** V3 handshake to the node failed (bad transport, timeout, malformed reply). */
103
+ export class HandshakeError extends AuditError {
104
+ constructor(message, diag = {}) {
105
+ super(message, 'HANDSHAKE_FAILED', diag);
106
+ this.name = 'HandshakeError';
107
+ }
108
+ }
109
+
110
+ /** Payment-side failure during audit (subscription sub-allocation, fee-grant, etc.). */
111
+ export class PaymentError extends AuditError {
112
+ constructor(message, diag = {}) {
113
+ super(message, 'PAYMENT_FAILED', diag);
114
+ this.name = 'PaymentError';
115
+ }
116
+ }
117
+
118
+ /** A pre-existing VPN process / WireGuard interface is interfering with the test. */
119
+ export class VpnInterferenceError extends AuditError {
120
+ constructor(message, diag = {}) {
121
+ super(message, 'VPN_INTERFERENCE', diag);
122
+ this.name = 'VpnInterferenceError';
123
+ }
124
+ }
125
+
126
+ /** Node failed to respond at all to status / status-update probe. */
127
+ export class NodeUnreachableError extends AuditError {
128
+ constructor(message, diag = {}) {
129
+ super(message, 'NODE_UNREACHABLE', diag);
130
+ this.name = 'NodeUnreachableError';
131
+ }
132
+ }
133
+
134
+ /** Wallet doesn't have enough udvpn for the audit run (mid-pipeline detection). */
135
+ export class InsufficientBalanceError extends AuditError {
136
+ constructor(message, diag = {}) {
137
+ super(message, 'INSUFFICIENT_BALANCE', diag);
138
+ this.name = 'InsufficientBalanceError';
139
+ }
140
+ }
141
+
142
+ /** Speed test phase failed (Cloudflare unreachable, all fallback hosts dead, etc.). */
143
+ export class SpeedTestError extends AuditError {
144
+ constructor(message, diag = {}) {
145
+ super(message, 'SPEEDTEST_FAILED', diag);
146
+ this.name = 'SpeedTestError';
147
+ }
148
+ }
149
+
74
150
  /** Error code constants — use these for switch/if checks instead of string parsing */
75
151
  export const ErrorCodes = {
76
152
  // Validation
@@ -120,9 +196,50 @@ export const ErrorCodes = {
120
196
  ALL_NODES_FAILED: 'ALL_NODES_FAILED',
121
197
  ALREADY_CONNECTED: 'ALREADY_CONNECTED',
122
198
  PARTIAL_CONNECTION_FAILED: 'PARTIAL_CONNECTION_FAILED',
199
+ NOT_CONNECTED: 'NOT_CONNECTED',
200
+ CONNECTION_IN_PROGRESS: 'CONNECTION_IN_PROGRESS',
201
+ HANDSHAKE_FAILED: 'HANDSHAKE_FAILED',
123
202
 
124
203
  // Chain timing
125
204
  CHAIN_LAG: 'CHAIN_LAG',
205
+ SEQUENCE_MISMATCH: 'SEQUENCE_MISMATCH',
206
+
207
+ // Subscription / Plan
208
+ SUBSCRIBE_FAILED: 'SUBSCRIBE_FAILED',
209
+ SUBSCRIPTION_NOT_FOUND: 'SUBSCRIPTION_NOT_FOUND',
210
+ SHARE_FAILED: 'SHARE_FAILED',
211
+
212
+ // Fee grants
213
+ FEE_GRANT_MISSING_AT_START: 'FEE_GRANT_MISSING_AT_START',
214
+ FEE_GRANT_EXPIRED: 'FEE_GRANT_EXPIRED',
215
+
216
+ // Node (additional states)
217
+ NODE_MISCONFIGURED: 'NODE_MISCONFIGURED',
218
+ NODE_DB_CORRUPT: 'NODE_DB_CORRUPT',
219
+ NODE_RPC_BROKEN: 'NODE_RPC_BROKEN',
220
+
221
+ // Fee grant
222
+ FEE_GRANT_MISSING_AT_START: 'FEE_GRANT_MISSING_AT_START',
223
+ FEE_GRANT_EXPIRED: 'FEE_GRANT_EXPIRED',
224
+
225
+ // Node extended
226
+ NODE_MISCONFIGURED: 'NODE_MISCONFIGURED',
227
+ NODE_DB_CORRUPT: 'NODE_DB_CORRUPT',
228
+ NODE_RPC_BROKEN: 'NODE_RPC_BROKEN',
229
+
230
+ // Chain extended
231
+ SEQUENCE_MISMATCH: 'SEQUENCE_MISMATCH',
232
+
233
+ // Connection lifecycle
234
+ NOT_CONNECTED: 'NOT_CONNECTED',
235
+ CONNECTION_IN_PROGRESS: 'CONNECTION_IN_PROGRESS',
236
+
237
+ // Audit / network-test pipeline (used by AuditError subclasses)
238
+ HANDSHAKE_FAILED: 'HANDSHAKE_FAILED',
239
+ PAYMENT_FAILED: 'PAYMENT_FAILED',
240
+ VPN_INTERFERENCE: 'VPN_INTERFERENCE',
241
+ NODE_UNREACHABLE: 'NODE_UNREACHABLE',
242
+ SPEEDTEST_FAILED: 'SPEEDTEST_FAILED',
126
243
  };
127
244
 
128
245
  // ─── Error Severity Classification ───────────────────────────────────────────
@@ -141,6 +258,12 @@ export const ERROR_SEVERITY = {
141
258
  [ErrorCodes.SESSION_POISONED]: 'fatal',
142
259
  [ErrorCodes.WG_NOT_AVAILABLE]: 'fatal',
143
260
  [ErrorCodes.NODE_DATABASE_CORRUPT]: 'retryable',
261
+ [ErrorCodes.ALREADY_CONNECTED]: 'fatal',
262
+ [ErrorCodes.NOT_CONNECTED]: 'fatal',
263
+ [ErrorCodes.CONNECTION_IN_PROGRESS]: 'fatal',
264
+ [ErrorCodes.ABORTED]: 'fatal',
265
+ [ErrorCodes.FEE_GRANT_MISSING_AT_START]: 'fatal',
266
+ [ErrorCodes.FEE_GRANT_EXPIRED]: 'fatal',
144
267
 
145
268
  // Retryable — node-level
146
269
  [ErrorCodes.NODE_NOT_FOUND]: 'retryable',
@@ -150,6 +273,9 @@ export const ERROR_SEVERITY = {
150
273
  [ErrorCodes.NODE_NO_UDVPN]: 'retryable',
151
274
  [ErrorCodes.NODE_CLOCK_DRIFT]: 'retryable',
152
275
  [ErrorCodes.NODE_INACTIVE]: 'retryable',
276
+ [ErrorCodes.NODE_MISCONFIGURED]: 'retryable',
277
+ [ErrorCodes.NODE_DB_CORRUPT]: 'retryable',
278
+ [ErrorCodes.NODE_RPC_BROKEN]: 'retryable',
153
279
  [ErrorCodes.V2RAY_ALL_FAILED]: 'retryable',
154
280
  [ErrorCodes.BROADCAST_FAILED]: 'retryable',
155
281
  [ErrorCodes.TX_FAILED]: 'retryable',
@@ -159,15 +285,44 @@ export const ERROR_SEVERITY = {
159
285
  [ErrorCodes.WG_NO_CONNECTIVITY]: 'retryable',
160
286
  [ErrorCodes.TUNNEL_SETUP_FAILED]: 'retryable',
161
287
  [ErrorCodes.CHAIN_LAG]: 'retryable',
288
+ [ErrorCodes.SEQUENCE_MISMATCH]: 'retryable',
289
+ [ErrorCodes.SUBSCRIBE_FAILED]: 'retryable',
290
+ [ErrorCodes.SUBSCRIPTION_NOT_FOUND]: 'retryable',
291
+ [ErrorCodes.SHARE_FAILED]: 'retryable',
292
+ [ErrorCodes.INVALID_ASSIGNED_IP]: 'retryable',
162
293
 
163
294
  // Recoverable — can resume with recoverSession()
164
295
  [ErrorCodes.SESSION_EXTRACT_FAILED]: 'recoverable',
165
296
  [ErrorCodes.PARTIAL_CONNECTION_FAILED]: 'recoverable',
166
297
  [ErrorCodes.SESSION_EXISTS]: 'recoverable',
298
+ [ErrorCodes.HANDSHAKE_FAILED]: 'recoverable',
167
299
 
168
300
  // Infrastructure — check system state
169
301
  [ErrorCodes.TLS_CERT_CHANGED]: 'infrastructure',
170
302
  [ErrorCodes.V2RAY_NOT_FOUND]: 'infrastructure',
303
+
304
+ // Fee grant
305
+ [ErrorCodes.FEE_GRANT_MISSING_AT_START]: 'fatal',
306
+ [ErrorCodes.FEE_GRANT_EXPIRED]: 'fatal',
307
+
308
+ // Node extended
309
+ [ErrorCodes.NODE_MISCONFIGURED]: 'retryable',
310
+ [ErrorCodes.NODE_DB_CORRUPT]: 'retryable',
311
+ [ErrorCodes.NODE_RPC_BROKEN]: 'retryable',
312
+
313
+ // Chain extended
314
+ [ErrorCodes.SEQUENCE_MISMATCH]: 'retryable',
315
+
316
+ // Connection lifecycle
317
+ [ErrorCodes.NOT_CONNECTED]: 'fatal',
318
+ [ErrorCodes.CONNECTION_IN_PROGRESS]: 'recoverable',
319
+
320
+ // Audit pipeline
321
+ [ErrorCodes.HANDSHAKE_FAILED]: 'retryable',
322
+ [ErrorCodes.PAYMENT_FAILED]: 'retryable',
323
+ [ErrorCodes.NODE_UNREACHABLE]: 'retryable',
324
+ [ErrorCodes.SPEEDTEST_FAILED]: 'retryable',
325
+ [ErrorCodes.VPN_INTERFERENCE]: 'infrastructure',
171
326
  };
172
327
 
173
328
  /** Check if an error should be retried. */
@@ -213,6 +368,18 @@ export function userMessage(error) {
213
368
  [ErrorCodes.CHAIN_LAG]: 'Session not yet confirmed on node. Wait a moment and try again.',
214
369
  [ErrorCodes.NODE_DATABASE_CORRUPT]: 'Node has a corrupted database. Try a different server.',
215
370
  [ErrorCodes.INVALID_ASSIGNED_IP]: 'Node returned an invalid IP address during handshake. Try a different server.',
371
+ [ErrorCodes.NODE_MISCONFIGURED]: 'Node is misconfigured. Try a different server.',
372
+ [ErrorCodes.NODE_DB_CORRUPT]: 'Node database is corrupt. Try a different server.',
373
+ [ErrorCodes.NODE_RPC_BROKEN]: 'Node backend is temporarily unavailable. Try again later.',
374
+ [ErrorCodes.NOT_CONNECTED]: 'Not connected to any node.',
375
+ [ErrorCodes.CONNECTION_IN_PROGRESS]: 'A connection attempt is already in progress.',
376
+ [ErrorCodes.HANDSHAKE_FAILED]: 'Connection handshake failed. Try again.',
377
+ [ErrorCodes.SEQUENCE_MISMATCH]: 'Transaction sequence error. Retry automatically.',
378
+ [ErrorCodes.SUBSCRIBE_FAILED]: 'Failed to subscribe to the plan. Check your balance and try again.',
379
+ [ErrorCodes.SUBSCRIPTION_NOT_FOUND]: 'Subscription not found after payment. Check chain state.',
380
+ [ErrorCodes.SHARE_FAILED]: 'Failed to share subscription bandwidth. Try again.',
381
+ [ErrorCodes.FEE_GRANT_MISSING_AT_START]: 'Plan owner has not issued a fee grant to this wallet. Contact the plan provider.',
382
+ [ErrorCodes.FEE_GRANT_EXPIRED]: 'The plan owner\'s fee grant has expired. Contact the plan provider to renew.',
216
383
  };
217
384
  return map[code] || error?.message || 'An unexpected error occurred.';
218
385
  }
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
 
@@ -93,6 +95,7 @@ export {
93
95
  buildRevokeFeeGrantMsg,
94
96
  queryFeeGrants,
95
97
  queryFeeGrant,
98
+ checkFeeGrant,
96
99
  // Authz
97
100
  buildAuthzGrantMsg,
98
101
  buildAuthzRevokeMsg,
@@ -105,6 +108,8 @@ export {
105
108
  // Plan Subscriber Helpers (v25b)
106
109
  queryPlanSubscribers,
107
110
  getPlanStats,
111
+ queryPlanDetails,
112
+ isActiveStatus,
108
113
  // Fee Grant Workflow (v25b)
109
114
  grantPlanSubscribers,
110
115
  queryFeeGrantsIssued,
@@ -185,6 +190,9 @@ export {
185
190
  flushSpeedTestDnsCache,
186
191
  compareSpeedTests,
187
192
  SPEEDTEST_DEFAULTS,
193
+ checkGoogleDirect,
194
+ checkGoogleViaSocks5,
195
+ resolveGoogleIp,
188
196
  } from './speedtest.js';
189
197
 
190
198
  // ─── Plan & Provider Management ─────────────────────────────────────────────
@@ -294,6 +302,7 @@ export {
294
302
  export {
295
303
  createRpcQueryClient,
296
304
  createRpcQueryClientWithFallback,
305
+ connectFailoverWithTimeout,
297
306
  disconnectRpc,
298
307
  rpcQueryNodes,
299
308
  rpcQueryNode,
@@ -311,6 +320,7 @@ export {
311
320
  rpcQueryFeeGrantsIssued,
312
321
  rpcQueryAuthzGrants,
313
322
  rpcQueryProvider,
323
+ rpcGetTxByHash,
314
324
  } from './chain/rpc.js';
315
325
 
316
326
  // ─── Subscription Sharing (plan operator → user onboarding) ────────────────
@@ -319,10 +329,12 @@ export {
319
329
  shareSubscription,
320
330
  shareSubscriptionWithFeeGrant,
321
331
  onboardPlanUser,
332
+ withBroadcastQueue,
322
333
  } from './chain/broadcast.js';
323
334
 
324
335
  export {
325
336
  querySubscriptionAllocations,
337
+ getTxByHash,
326
338
  } from './chain/queries.js';
327
339
 
328
340
  // ─── TypeScript Client (extends CosmJS SigningStargateClient) ───────────────
@@ -408,6 +420,13 @@ export {
408
420
  ChainError,
409
421
  TunnelError,
410
422
  SecurityError,
423
+ AuditError,
424
+ HandshakeError,
425
+ PaymentError,
426
+ VpnInterferenceError,
427
+ NodeUnreachableError,
428
+ InsufficientBalanceError,
429
+ SpeedTestError,
411
430
  ErrorCodes,
412
431
  ERROR_SEVERITY,
413
432
  isRetryable,
@@ -426,7 +445,7 @@ export {
426
445
 
427
446
  // ─── Session Manager ─────────────────────────────────────────────────────────
428
447
 
429
- export { SessionManager } from './session-manager.js';
448
+ export { SessionManager, extractSessionMap } from './session-manager.js';
430
449
 
431
450
  // ─── Batch Session Operations ────────────────────────────────────────────────
432
451
 
@@ -492,12 +511,22 @@ export {
492
511
  estimateSessionPrice,
493
512
  buildNodeDisplay,
494
513
  groupNodesByCountry,
514
+ CONTINENT_BY_CODE,
515
+ CONTINENT_NAMES,
516
+ countryToContinent,
495
517
  HOUR_OPTIONS,
496
518
  GB_OPTIONS,
497
519
  formatUptime,
498
520
  computeSessionAllocation,
499
521
  } from './app-helpers.js';
500
522
 
523
+ // ─── Auth Utilities (ADR-36, Keplr) ─────────────────────────────────────────
524
+
525
+ export {
526
+ sortedJsonStringify,
527
+ verifyAdr36Signature,
528
+ } from './auth/adr36.js';
529
+
501
530
  // ─── Instantiable Client Class ───────────────────────────────────────────────
502
531
 
503
532
  export { SentinelClient } from './client.js';
@@ -514,3 +543,47 @@ export {
514
543
  reorderOutbounds,
515
544
  getCacheStats,
516
545
  } from './audit.js';
546
+
547
+ // ─── Operator: Lease Batch Utilities ────────────────────────────────────────
548
+
549
+ export {
550
+ autoLeaseNode,
551
+ batchLeaseNodes,
552
+ } from './operator/auto-lease.js';
553
+ // ─── Operator: Batch Fee Grant Revoke ───────────────────────────────────────
554
+
555
+ export {
556
+ batchRevokeFeeGrants,
557
+ } from './operator/batch-revoke.js';
558
+
559
+ // ─── Operator: Fee Grant History ────────────────────────────────────────────
560
+
561
+ export {
562
+ queryFeeGrantHistory,
563
+ decodeFeeGrantEvent,
564
+ attr,
565
+ } from './operator/feegrant-history.js';
566
+ // ─── Plan Ownership Pre-Flight ──────────────────────────────────────────────
567
+
568
+ export {
569
+ assertPlanOwnership,
570
+ PlanOwnershipError,
571
+ walletToProviderAddr,
572
+ } from './operator/plan-ownership.js';
573
+ // ─── Auth Utilities (Keplr) ─────────────────────────────────────────
574
+
575
+ export {
576
+ buildKeplrSignDoc,
577
+ broadcastSignedKeplrTx,
578
+ } from './auth/keplr-signdoc.js';
579
+
580
+ // --- Auth Utilities (Privy embedded wallets) ---
581
+
582
+ export {
583
+ PrivyCosmosSigner,
584
+ PrivyRawSignDirectSigner,
585
+ privyCosmosSignerFromMnemonic,
586
+ privyCosmosSignerFromRawSign,
587
+ createPrivyCosmosSigner,
588
+ deriveCosmosPubkeyFromMnemonic,
589
+ } from './auth/privy-cosmos-signer.js';