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
|
@@ -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';
|