blue-js-sdk 2.6.0 → 2.7.1
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/app-helpers.js +55 -0
- package/chain/broadcast.js +27 -0
- package/chain/fee-grants.js +77 -7
- package/chain/queries.js +72 -0
- package/chain/rpc.js +59 -2
- package/cli.js +26 -5
- package/client.js +62 -6
- package/connection/connect.js +103 -17
- package/connection/disconnect.js +9 -4
- 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 +42 -0
- package/defaults.js +38 -16
- package/docs/PRIVY-INTEGRATION.md +177 -0
- package/errors.js +167 -0
- package/index.js +70 -1
- package/node-connect.js +92 -40
- package/operator.js +24 -0
- package/package.json +11 -8
- 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/defaults.js
CHANGED
|
@@ -25,12 +25,14 @@ axios.defaults.adapter = 'http';
|
|
|
25
25
|
// This is the npm/semver version for consumers. Internal development iterations
|
|
26
26
|
// (v20, v21, v22, etc.) track feature milestones and are not exposed as exports.
|
|
27
27
|
|
|
28
|
-
export const SDK_VERSION = '2.
|
|
28
|
+
export const SDK_VERSION = '2.7.1';
|
|
29
29
|
|
|
30
30
|
// ─── Timestamps ──────────────────────────────────────────────────────────────
|
|
31
31
|
|
|
32
|
-
/** When these defaults were last verified against the live chain
|
|
33
|
-
|
|
32
|
+
/** When these defaults were last verified against the live chain.
|
|
33
|
+
* RPC + LCD endpoint lists refreshed 2026-05-02 (audit-rpc-endpoints.mjs).
|
|
34
|
+
* Other defaults (gas, transport rates, etc.) still 2026-03-08. */
|
|
35
|
+
export const LAST_VERIFIED = '2026-05-02T00:00:00Z';
|
|
34
36
|
|
|
35
37
|
/** Human-readable note for builders */
|
|
36
38
|
export const HARDCODED_NOTE = 'Static defaults — no RPC query server yet. Verify endpoints are live before production use. See README.md "Hardcoded Defaults" section.';
|
|
@@ -44,28 +46,48 @@ export const CHAIN_VERSION = 'v12.0.0'; // sentinelhub version
|
|
|
44
46
|
export const COSMOS_SDK_VERSION = '0.47.17';
|
|
45
47
|
|
|
46
48
|
// ─── RPC Endpoints (TX broadcast) ────────────────────────────────────────────
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
+
// Latency-sorted list of endpoints that passed consensus health check on
|
|
50
|
+
// 2026-05-02. "Healthy" means: connects, /status reports catching_up=false,
|
|
51
|
+
// and the bank balance for a known address matches the modal answer across
|
|
52
|
+
// all responding candidates within 50 blocks of tip. Run
|
|
53
|
+
// `node tools/audit-rpc-endpoints.mjs` to refresh.
|
|
54
|
+
//
|
|
55
|
+
// rpc.sentinel.co / lcd.sentinel.co are intentionally excluded: on 2026-05-02
|
|
56
|
+
// they were ~22k blocks behind tip and returning 0 for funded addresses while
|
|
57
|
+
// reporting catching_up=false on /status. Consumers that need them can add
|
|
58
|
+
// them at runtime via addRpcEndpoint() / addLcdEndpoint().
|
|
49
59
|
|
|
50
60
|
export const RPC_ENDPOINTS = [
|
|
51
|
-
{ url: 'https://rpc
|
|
52
|
-
{ url: 'https://
|
|
53
|
-
{ url: 'https://rpc.
|
|
54
|
-
{ url: 'https://sentinel-rpc.
|
|
55
|
-
{ url: 'https://rpc.
|
|
61
|
+
{ url: 'https://rpc-sentinel.busurnode.com', name: 'Busurnode', verified: '2026-05-02' },
|
|
62
|
+
{ url: 'https://rpc.trinitystake.io', name: 'Trinity Stake', verified: '2026-05-02' },
|
|
63
|
+
{ url: 'https://sentinel-rpc.publicnode.com', name: 'PublicNode (Allnodes)', verified: '2026-05-02' },
|
|
64
|
+
{ url: 'https://sentinel-rpc.polkachu.com', name: 'Polkachu', verified: '2026-05-02' },
|
|
65
|
+
{ url: 'https://rpc.mathnodes.com', name: 'MathNodes', verified: '2026-05-02' },
|
|
66
|
+
{ url: 'https://rpc.dvpn.roomit.xyz', name: 'Roomit', verified: '2026-05-02' },
|
|
67
|
+
{ url: 'https://rpc.sentinel.suchnode.net', name: 'SuchNode', verified: '2026-05-02' },
|
|
68
|
+
{ url: 'https://rpc.sentinel.chaintools.tech', name: 'ChainTools', verified: '2026-05-02' },
|
|
69
|
+
{ url: 'https://rpc.sentinel.validatus.com', name: 'Validatus', verified: '2026-05-02' },
|
|
70
|
+
{ url: 'https://rpc.sentinel.quokkastake.io', name: 'QuokkaStake', verified: '2026-05-02' },
|
|
71
|
+
{ url: 'https://rpc.sentineldao.com', name: 'Sentinel Growth DAO', verified: '2026-05-02' },
|
|
72
|
+
{ url: 'https://rpc-sentinel.chainvibes.com', name: 'ChainVibes', verified: '2026-05-02' },
|
|
56
73
|
];
|
|
57
74
|
|
|
58
75
|
export const DEFAULT_RPC = RPC_ENDPOINTS[0].url;
|
|
59
76
|
|
|
60
77
|
// ─── LCD Endpoints (REST queries) ────────────────────────────────────────────
|
|
61
|
-
//
|
|
62
|
-
//
|
|
78
|
+
// Same consensus methodology as RPC. lcd.sentinel.co excluded for the same
|
|
79
|
+
// stale-state reason.
|
|
63
80
|
|
|
64
81
|
export const LCD_ENDPOINTS = [
|
|
65
|
-
{ url: 'https://
|
|
66
|
-
{ url: 'https://sentinel-
|
|
67
|
-
{ url: 'https://api.sentinel.
|
|
68
|
-
{ url: 'https://sentinel-
|
|
82
|
+
{ url: 'https://api-sentinel.busurnode.com', name: 'Busurnode', verified: '2026-05-02' },
|
|
83
|
+
{ url: 'https://sentinel-rest.publicnode.com', name: 'PublicNode (Allnodes)', verified: '2026-05-02' },
|
|
84
|
+
{ url: 'https://api.sentinel.suchnode.net', name: 'SuchNode', verified: '2026-05-02' },
|
|
85
|
+
{ url: 'https://sentinel-api.polkachu.com', name: 'Polkachu', verified: '2026-05-02' },
|
|
86
|
+
{ url: 'https://api.dvpn.roomit.xyz', name: 'Roomit', verified: '2026-05-02' },
|
|
87
|
+
{ url: 'https://api.sentinel.quokkastake.io', name: 'QuokkaStake', verified: '2026-05-02' },
|
|
88
|
+
{ url: 'https://api.sentinel.chaintools.tech', name: 'ChainTools', verified: '2026-05-02' },
|
|
89
|
+
{ url: 'https://api-sentinel.chainvibes.com', name: 'ChainVibes', verified: '2026-05-02' },
|
|
90
|
+
{ url: 'https://api.sentinel.validatus.com', name: 'Validatus', verified: '2026-05-02' },
|
|
69
91
|
];
|
|
70
92
|
|
|
71
93
|
export const DEFAULT_LCD = LCD_ENDPOINTS[0].url;
|
|
@@ -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';
|