blue-js-sdk 2.6.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.
@@ -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
@@ -95,6 +95,7 @@ export {
95
95
  buildRevokeFeeGrantMsg,
96
96
  queryFeeGrants,
97
97
  queryFeeGrant,
98
+ checkFeeGrant,
98
99
  // Authz
99
100
  buildAuthzGrantMsg,
100
101
  buildAuthzRevokeMsg,
@@ -107,6 +108,8 @@ export {
107
108
  // Plan Subscriber Helpers (v25b)
108
109
  queryPlanSubscribers,
109
110
  getPlanStats,
111
+ queryPlanDetails,
112
+ isActiveStatus,
110
113
  // Fee Grant Workflow (v25b)
111
114
  grantPlanSubscribers,
112
115
  queryFeeGrantsIssued,
@@ -187,6 +190,9 @@ export {
187
190
  flushSpeedTestDnsCache,
188
191
  compareSpeedTests,
189
192
  SPEEDTEST_DEFAULTS,
193
+ checkGoogleDirect,
194
+ checkGoogleViaSocks5,
195
+ resolveGoogleIp,
190
196
  } from './speedtest.js';
191
197
 
192
198
  // ─── Plan & Provider Management ─────────────────────────────────────────────
@@ -296,6 +302,7 @@ export {
296
302
  export {
297
303
  createRpcQueryClient,
298
304
  createRpcQueryClientWithFallback,
305
+ connectFailoverWithTimeout,
299
306
  disconnectRpc,
300
307
  rpcQueryNodes,
301
308
  rpcQueryNode,
@@ -322,6 +329,7 @@ export {
322
329
  shareSubscription,
323
330
  shareSubscriptionWithFeeGrant,
324
331
  onboardPlanUser,
332
+ withBroadcastQueue,
325
333
  } from './chain/broadcast.js';
326
334
 
327
335
  export {
@@ -412,6 +420,13 @@ export {
412
420
  ChainError,
413
421
  TunnelError,
414
422
  SecurityError,
423
+ AuditError,
424
+ HandshakeError,
425
+ PaymentError,
426
+ VpnInterferenceError,
427
+ NodeUnreachableError,
428
+ InsufficientBalanceError,
429
+ SpeedTestError,
415
430
  ErrorCodes,
416
431
  ERROR_SEVERITY,
417
432
  isRetryable,
@@ -430,7 +445,7 @@ export {
430
445
 
431
446
  // ─── Session Manager ─────────────────────────────────────────────────────────
432
447
 
433
- export { SessionManager } from './session-manager.js';
448
+ export { SessionManager, extractSessionMap } from './session-manager.js';
434
449
 
435
450
  // ─── Batch Session Operations ────────────────────────────────────────────────
436
451
 
@@ -496,12 +511,22 @@ export {
496
511
  estimateSessionPrice,
497
512
  buildNodeDisplay,
498
513
  groupNodesByCountry,
514
+ CONTINENT_BY_CODE,
515
+ CONTINENT_NAMES,
516
+ countryToContinent,
499
517
  HOUR_OPTIONS,
500
518
  GB_OPTIONS,
501
519
  formatUptime,
502
520
  computeSessionAllocation,
503
521
  } from './app-helpers.js';
504
522
 
523
+ // ─── Auth Utilities (ADR-36, Keplr) ─────────────────────────────────────────
524
+
525
+ export {
526
+ sortedJsonStringify,
527
+ verifyAdr36Signature,
528
+ } from './auth/adr36.js';
529
+
505
530
  // ─── Instantiable Client Class ───────────────────────────────────────────────
506
531
 
507
532
  export { SentinelClient } from './client.js';
@@ -518,3 +543,47 @@ export {
518
543
  reorderOutbounds,
519
544
  getCacheStats,
520
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';