blue-js-sdk 2.0.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 (215) hide show
  1. package/CHANGELOG.md +446 -0
  2. package/LICENSE +21 -0
  3. package/README.md +75 -0
  4. package/ai-path/ADMIN-ELEVATION.md +116 -0
  5. package/ai-path/AI-MANIFESTO.md +185 -0
  6. package/ai-path/BREAKING.md +74 -0
  7. package/ai-path/CHECKLIST.md +619 -0
  8. package/ai-path/CONNECTION-STEPS.md +724 -0
  9. package/ai-path/DECISION-TREE.md +378 -0
  10. package/ai-path/DEPENDENCIES.md +459 -0
  11. package/ai-path/E2E-FLOW.md +1555 -0
  12. package/ai-path/FAILURES.md +403 -0
  13. package/ai-path/GUIDE.md +1217 -0
  14. package/ai-path/README.md +558 -0
  15. package/ai-path/SPLIT-TUNNEL.md +266 -0
  16. package/ai-path/cli.js +535 -0
  17. package/ai-path/connect.js +884 -0
  18. package/ai-path/discover.js +178 -0
  19. package/ai-path/environment.js +266 -0
  20. package/ai-path/errors.js +86 -0
  21. package/ai-path/examples/autonomous-agent.mjs +220 -0
  22. package/ai-path/examples/multi-region.mjs +174 -0
  23. package/ai-path/examples/one-shot.mjs +31 -0
  24. package/ai-path/index.js +60 -0
  25. package/ai-path/pricing.js +136 -0
  26. package/ai-path/recommend.js +413 -0
  27. package/ai-path/run-admin.vbs +25 -0
  28. package/ai-path/setup.js +291 -0
  29. package/ai-path/wallet.js +137 -0
  30. package/app-helpers.js +363 -0
  31. package/app-settings.js +95 -0
  32. package/app-types.js +267 -0
  33. package/audit.js +847 -0
  34. package/batch.js +293 -0
  35. package/bin/setup.js +376 -0
  36. package/chain/authz.js +109 -0
  37. package/chain/broadcast.js +472 -0
  38. package/chain/client.js +160 -0
  39. package/chain/fee-grants.js +305 -0
  40. package/chain/index.js +891 -0
  41. package/chain/lcd.js +313 -0
  42. package/chain/queries.js +547 -0
  43. package/chain/rpc.js +408 -0
  44. package/chain/wallet.js +141 -0
  45. package/cli/config.js +143 -0
  46. package/cli/index.js +463 -0
  47. package/cli/output.js +182 -0
  48. package/cli.js +491 -0
  49. package/client/index.js +251 -0
  50. package/client.js +271 -0
  51. package/config/index.js +255 -0
  52. package/connection/connect.js +849 -0
  53. package/connection/disconnect.js +180 -0
  54. package/connection/discovery.js +321 -0
  55. package/connection/index.js +76 -0
  56. package/connection/proxy.js +148 -0
  57. package/connection/resilience.js +428 -0
  58. package/connection/security.js +232 -0
  59. package/connection/state.js +369 -0
  60. package/connection/tunnel.js +691 -0
  61. package/consumer.js +132 -0
  62. package/cosmjs-setup.js +1884 -0
  63. package/defaults.js +366 -0
  64. package/disk-cache.js +107 -0
  65. package/dist/client.d.ts +108 -0
  66. package/dist/client.d.ts.map +1 -0
  67. package/dist/client.js +400 -0
  68. package/dist/client.js.map +1 -0
  69. package/dist/index.d.ts +8 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +8 -0
  72. package/dist/index.js.map +1 -0
  73. package/errors/index.js +112 -0
  74. package/errors.js +218 -0
  75. package/examples/README.md +64 -0
  76. package/examples/connect-direct.mjs +106 -0
  77. package/examples/connect-plan.mjs +125 -0
  78. package/examples/error-handling.mjs +109 -0
  79. package/examples/query-nodes.mjs +94 -0
  80. package/examples/wallet-basics.mjs +61 -0
  81. package/generated/amino/amino.ts +9 -0
  82. package/generated/cosmos/base/v1beta1/coin.ts +365 -0
  83. package/generated/cosmos_proto/cosmos.ts +323 -0
  84. package/generated/gogoproto/gogo.ts +9 -0
  85. package/generated/google/protobuf/descriptor.ts +7601 -0
  86. package/generated/google/protobuf/duration.ts +208 -0
  87. package/generated/google/protobuf/timestamp.ts +238 -0
  88. package/generated/sentinel/lease/v1/events.ts +924 -0
  89. package/generated/sentinel/lease/v1/lease.ts +292 -0
  90. package/generated/sentinel/lease/v1/msg.ts +949 -0
  91. package/generated/sentinel/lease/v1/params.ts +164 -0
  92. package/generated/sentinel/node/v3/events.ts +881 -0
  93. package/generated/sentinel/node/v3/msg.ts +1002 -0
  94. package/generated/sentinel/node/v3/node.ts +263 -0
  95. package/generated/sentinel/node/v3/params.ts +183 -0
  96. package/generated/sentinel/plan/v3/events.ts +675 -0
  97. package/generated/sentinel/plan/v3/msg.ts +1191 -0
  98. package/generated/sentinel/plan/v3/plan.ts +283 -0
  99. package/generated/sentinel/provider/v2/events.ts +171 -0
  100. package/generated/sentinel/provider/v2/msg.ts +480 -0
  101. package/generated/sentinel/provider/v2/params.ts +131 -0
  102. package/generated/sentinel/provider/v2/provider.ts +246 -0
  103. package/generated/sentinel/session/v3/events.ts +480 -0
  104. package/generated/sentinel/session/v3/msg.ts +616 -0
  105. package/generated/sentinel/session/v3/params.ts +260 -0
  106. package/generated/sentinel/session/v3/proof.ts +180 -0
  107. package/generated/sentinel/session/v3/session.ts +384 -0
  108. package/generated/sentinel/subscription/v3/events.ts +1181 -0
  109. package/generated/sentinel/subscription/v3/msg.ts +1305 -0
  110. package/generated/sentinel/subscription/v3/params.ts +167 -0
  111. package/generated/sentinel/subscription/v3/subscription.ts +315 -0
  112. package/generated/sentinel/types/v1/bandwidth.ts +124 -0
  113. package/generated/sentinel/types/v1/price.ts +149 -0
  114. package/generated/sentinel/types/v1/renewal.ts +87 -0
  115. package/generated/sentinel/types/v1/status.ts +54 -0
  116. package/generated/typeRegistry.ts +27 -0
  117. package/index.js +486 -0
  118. package/node-connect.js +3015 -0
  119. package/operator.js +134 -0
  120. package/package.json +113 -0
  121. package/plan-operations.js +199 -0
  122. package/preflight.js +352 -0
  123. package/pricing/index.js +262 -0
  124. package/proto/amino/amino.proto +84 -0
  125. package/proto/cosmos/base/v1beta1/coin.proto +61 -0
  126. package/proto/cosmos_proto/cosmos.proto +112 -0
  127. package/proto/gogoproto/gogo.proto +145 -0
  128. package/proto/google/api/annotations.proto +31 -0
  129. package/proto/google/api/http.proto +370 -0
  130. package/proto/google/protobuf/any.proto +106 -0
  131. package/proto/google/protobuf/duration.proto +115 -0
  132. package/proto/google/protobuf/timestamp.proto +145 -0
  133. package/proto/sentinel/lease/v1/events.proto +52 -0
  134. package/proto/sentinel/lease/v1/genesis.proto +15 -0
  135. package/proto/sentinel/lease/v1/lease.proto +25 -0
  136. package/proto/sentinel/lease/v1/msg.proto +62 -0
  137. package/proto/sentinel/lease/v1/params.proto +17 -0
  138. package/proto/sentinel/node/v3/events.proto +50 -0
  139. package/proto/sentinel/node/v3/genesis.proto +15 -0
  140. package/proto/sentinel/node/v3/msg.proto +63 -0
  141. package/proto/sentinel/node/v3/node.proto +27 -0
  142. package/proto/sentinel/node/v3/params.proto +21 -0
  143. package/proto/sentinel/node/v3/querier.proto +63 -0
  144. package/proto/sentinel/plan/v3/events.proto +41 -0
  145. package/proto/sentinel/plan/v3/genesis.proto +21 -0
  146. package/proto/sentinel/plan/v3/msg.proto +83 -0
  147. package/proto/sentinel/plan/v3/plan.proto +32 -0
  148. package/proto/sentinel/plan/v3/querier.proto +53 -0
  149. package/proto/sentinel/provider/v2/events.proto +16 -0
  150. package/proto/sentinel/provider/v2/genesis.proto +15 -0
  151. package/proto/sentinel/provider/v2/msg.proto +35 -0
  152. package/proto/sentinel/provider/v2/params.proto +17 -0
  153. package/proto/sentinel/provider/v2/provider.proto +24 -0
  154. package/proto/sentinel/provider/v3/genesis.proto +15 -0
  155. package/proto/sentinel/provider/v3/params.proto +13 -0
  156. package/proto/sentinel/session/v3/events.proto +30 -0
  157. package/proto/sentinel/session/v3/genesis.proto +15 -0
  158. package/proto/sentinel/session/v3/msg.proto +50 -0
  159. package/proto/sentinel/session/v3/params.proto +25 -0
  160. package/proto/sentinel/session/v3/proof.proto +25 -0
  161. package/proto/sentinel/session/v3/querier.proto +100 -0
  162. package/proto/sentinel/session/v3/session.proto +50 -0
  163. package/proto/sentinel/subscription/v2/allocation.proto +21 -0
  164. package/proto/sentinel/subscription/v2/payout.proto +22 -0
  165. package/proto/sentinel/subscription/v3/events.proto +65 -0
  166. package/proto/sentinel/subscription/v3/genesis.proto +17 -0
  167. package/proto/sentinel/subscription/v3/msg.proto +83 -0
  168. package/proto/sentinel/subscription/v3/params.proto +21 -0
  169. package/proto/sentinel/subscription/v3/subscription.proto +33 -0
  170. package/proto/sentinel/types/v1/bandwidth.proto +19 -0
  171. package/proto/sentinel/types/v1/price.proto +21 -0
  172. package/proto/sentinel/types/v1/renewal.proto +21 -0
  173. package/proto/sentinel/types/v1/status.proto +16 -0
  174. package/protocol/encoding.js +341 -0
  175. package/protocol/events.js +361 -0
  176. package/protocol/handshake.js +297 -0
  177. package/protocol/index.js +15 -0
  178. package/protocol/messages.js +346 -0
  179. package/protocol/plans.js +199 -0
  180. package/protocol/v2ray.js +268 -0
  181. package/protocol/v3.js +723 -0
  182. package/protocol/wireguard.js +125 -0
  183. package/security/index.js +132 -0
  184. package/session-manager.js +329 -0
  185. package/session-tracker.js +80 -0
  186. package/setup.js +376 -0
  187. package/speedtest/index.js +528 -0
  188. package/speedtest.js +567 -0
  189. package/src/client.ts +502 -0
  190. package/src/index.ts +20 -0
  191. package/state/index.js +347 -0
  192. package/state.js +516 -0
  193. package/test-all-chain-ops.js +493 -0
  194. package/test-all-logic.js +199 -0
  195. package/test-all-msg-types.js +292 -0
  196. package/test-every-connection.js +208 -0
  197. package/test-feegrant-connect.js +98 -0
  198. package/test-logic.js +148 -0
  199. package/test-mainnet.js +176 -0
  200. package/test-plan-lifecycle.js +335 -0
  201. package/tls-trust.js +132 -0
  202. package/tsconfig.build.json +20 -0
  203. package/tsconfig.json +34 -0
  204. package/types/chain.d.ts +746 -0
  205. package/types/connection.d.ts +425 -0
  206. package/types/errors.d.ts +174 -0
  207. package/types/index.d.ts +1380 -0
  208. package/types/nodes.d.ts +187 -0
  209. package/types/pricing.d.ts +156 -0
  210. package/types/protocol.d.ts +332 -0
  211. package/types/session.d.ts +236 -0
  212. package/types/settings.d.ts +192 -0
  213. package/v3protocol.js +1053 -0
  214. package/wallet/index.js +153 -0
  215. package/wireguard.js +307 -0
@@ -0,0 +1,1555 @@
1
+ # End-to-End Flow: AI Agent to Decentralized VPN
2
+
3
+ > The definitive technical reference. Every step from SDK discovery to encrypted traffic flowing through a decentralized VPN tunnel. Verified against 837+ mainnet node tests, 8 consumer apps, and 165+ hours of production debugging. Enriched with findings from Handshake dVPN (27 problems), Test2 proving ground, Node Tester (780+ nodes), and 22 undiagnosed failure patterns. Every timeout, every field name, every byte count is exact.
4
+
5
+ ---
6
+
7
+ ## Phase 1: Discovery & Installation
8
+
9
+ ### Package
10
+
11
+ ```
12
+ npm install sentinel-dvpn-sdk
13
+ ```
14
+
15
+ Package name: `sentinel-dvpn-sdk`
16
+ License: MIT
17
+ Entry point: `index.js` (ESM only -- `"type": "module"`)
18
+ Exports: 160+ functions from single entry point
19
+
20
+ ### Node.js Requirement
21
+
22
+ **Node.js 20.0.0 or higher.** The SDK uses:
23
+ - ES Modules (`import`/`export`) exclusively
24
+ - `crypto.randomUUID()` (Node 19+)
25
+ - Native `fetch` adapter override via `axios.defaults.adapter = 'http'` (Node 18+ uses undici internally, which produces opaque "fetch failed" errors on self-signed certs -- the SDK forces the classic HTTP adapter)
26
+
27
+ ### Postinstall: V2Ray Download
28
+
29
+ `npm install` triggers `node setup.js` which:
30
+
31
+ 1. Detects platform: `${process.platform}-${process.arch}` (e.g., `win32-x64`, `linux-x64`, `darwin-arm64`)
32
+ 2. Downloads V2Ray v5.2.1 from `https://github.com/v2fly/v2ray-core/releases/download/v5.2.1/v2ray-{platform}.zip`
33
+ 3. Verifies SHA256 checksum against hardcoded digests:
34
+ - `win32-x64`: `d9791f911b603437a34219488b0111ae9913f38abe22c0103abce330537dabd6`
35
+ - `win32-ia32`: `dc9f37dbeb32221e62b9a52b79f1842a217f049675872b334e1e5fd96121d0d2`
36
+ - `linux-x64`: `56eb8d4727b058d10f8ff830bb0121381386b0695171767f38ba410f2613fc9a`
37
+ - `linux-arm64`: `63958429e93f24f10f34a64701f70b4f42dfa0bc8120e1c0a426c6161bd2a3c9`
38
+ - `darwin-x64`: `edbb0b94c05570d39a4549186927369853542649eb6b703dd432bda300c5d51a`
39
+ - `darwin-arm64`: `e18c17a79c4585d963395ae6ddafffb18c5d22777f7ac5938c1b40563db88d56`
40
+ 4. Extracts to `bin/` directory: `v2ray.exe` (or `v2ray`), `geoip.dat`, `geosite.dat`
41
+ 5. If download fails, prints warning but does not block install (V2Ray is only needed for V2Ray nodes)
42
+
43
+ **CRITICAL: V2Ray must be exactly v5.2.1.** Versions 5.44.1+ have observatory/balancer bugs that break multi-outbound configs. The SDK's `verifyDependencies()` function checks the version at connect time and refuses incompatible versions.
44
+
45
+ ### WireGuard (Optional)
46
+
47
+ WireGuard is not downloaded by setup. It must be pre-installed:
48
+ - **Windows:** `https://download.wireguard.com/windows-client/wireguard-installer.exe` -- installs to `C:\Program Files\WireGuard\wireguard.exe`
49
+ - **Linux:** `apt install wireguard` or equivalent
50
+ - **macOS:** `brew install wireguard-tools`
51
+
52
+ WireGuard requires **Administrator/root privileges** for tunnel installation. The SDK detects admin status at module load via `IS_ADMIN` export. Approximately 30% of Sentinel nodes are WireGuard; the rest are V2Ray.
53
+
54
+ ### File Structure After Install
55
+
56
+ ```
57
+ node_modules/sentinel-dvpn-sdk/
58
+ index.js # Single entry point (160+ exports)
59
+ index.d.ts # TypeScript definitions
60
+ defaults.js # Chain IDs, endpoints, timeouts, transport rates
61
+ errors.js # 33 typed error codes with severity classification
62
+ cosmjs-setup.js # Wallet, registry, broadcast, LCD queries
63
+ v3protocol.js # Handshake, protobuf encoders, WireGuard/V2Ray config
64
+ node-connect.js # Connection orchestration (connectDirect, connectAuto, disconnect)
65
+ wireguard.js # Cross-platform WireGuard tunnel management
66
+ speedtest.js # SOCKS5 and direct speed testing
67
+ plan-operations.js # Plan/provider/lease message encoders
68
+ batch.js # Batch session operations (operator/testing)
69
+ preflight.js # Pre-flight system checks
70
+ tls-trust.js # TOFU TLS for self-signed node certificates
71
+ state.js # Crash recovery state persistence
72
+ disk-cache.js # Generic disk cache with age tracking
73
+ session-tracker.js # Payment mode persistence per session
74
+ session-manager.js # Session lifecycle management
75
+ app-settings.js # VPN settings persistence
76
+ app-types.js # App type framework (peer-to-peer, plan-based, all-in-one)
77
+ app-helpers.js # Country map (183 entries), pricing display, UX helpers
78
+ client.js # SentinelClient class (per-instance DI)
79
+ audit.js # Network audit and node testing
80
+ setup.js # Binary download script
81
+ bin/
82
+ v2ray.exe # V2Ray 5.2.1 binary
83
+ geoip.dat # V2Ray GeoIP database
84
+ geosite.dat # V2Ray GeoSite database
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Phase 2: Wallet Setup
90
+
91
+ ### Key Generation
92
+
93
+ ```javascript
94
+ import { generateWallet, createWallet, privKeyFromMnemonic } from 'sentinel-dvpn-sdk';
95
+
96
+ // Generate a NEW wallet (random mnemonic)
97
+ const { mnemonic, wallet, account } = await generateWallet(128); // 128=12 words, 256=24 words
98
+
99
+ // Restore from existing mnemonic
100
+ const { wallet, account } = await createWallet('your twelve word mnemonic phrase here ...');
101
+
102
+ // Derive raw private key (needed for handshake signatures)
103
+ const privKey = await privKeyFromMnemonic('your twelve word mnemonic ...');
104
+ // privKey is a 32-byte Buffer (secp256k1 private key)
105
+ ```
106
+
107
+ ### Cryptographic Details
108
+
109
+ | Property | Value |
110
+ |----------|-------|
111
+ | Mnemonic standard | BIP39, English wordlist |
112
+ | Word count | 12 (128-bit entropy) or 24 (256-bit entropy) |
113
+ | Entropy source | `@cosmjs/crypto Random.getBytes()` (CSPRNG) |
114
+ | Seed derivation | BIP39 mnemonic-to-seed (PBKDF2, 2048 rounds) |
115
+ | HD derivation | SLIP-10 (not BIP32) with curve `Secp256k1` |
116
+ | HD path | `m/44'/118'/0'/0/0` (Cosmos standard, `makeCosmoshubPath(0)`) |
117
+ | Key type | secp256k1 (same as Bitcoin, Ethereum, Cosmos) |
118
+ | Address format | Bech32 with prefix `sent` (e.g., `sent1abc...xyz`) |
119
+ | Address length | 47 characters total (5 prefix + 1 separator + 38 data + 6 checksum) |
120
+ | Node address format | Bech32 with prefix `sentnode` (e.g., `sentnode1abc...xyz`) |
121
+ | Provider address format | Bech32 with prefix `sentprov` |
122
+
123
+ ### Security Requirements
124
+
125
+ - **Zero private key after use:** `privKey.fill(0)` -- the SDK does this automatically in `connectInternal()` via `try/finally`
126
+ - **NEVER log or store mnemonic in source code** -- use environment variables or secure storage
127
+ - **Wallet cache:** The SDK caches wallet derivation by SHA256(mnemonic) to avoid repeated 300ms BIP39 derivation. Call `clearWalletCache()` after disconnect to release key material from memory.
128
+
129
+ ### Address Conversion
130
+
131
+ Same key, different Bech32 prefix. The SDK provides conversion functions:
132
+
133
+ ```javascript
134
+ import { sentToSentnode, sentToSentprov, sentprovToSent } from 'sentinel-dvpn-sdk';
135
+
136
+ sentToSentnode('sent1abc...') // -> 'sentnode1abc...'
137
+ sentToSentprov('sent1abc...') // -> 'sentprov1abc...'
138
+ sentprovToSent('sentprov1abc...') // -> 'sent1abc...'
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Phase 3: Funding
144
+
145
+ ### Token
146
+
147
+ | Property | Value |
148
+ |----------|-------|
149
+ | Display name | **P2P** (NOT "DVPN" in user-facing text) |
150
+ | Chain denom | `udvpn` (micro-DVPN, 1 P2P = 1,000,000 udvpn) |
151
+ | Format function | `formatP2P(amount)` returns "X.XX P2P" |
152
+ | Chain ID | `sentinelhub-2` |
153
+ | Gas price | `0.2udvpn` (chain minimum) |
154
+
155
+ ### CRITICAL: BaseValue vs QuoteValue Pricing
156
+
157
+ *(Source: Handshake dVPN -- 3 hours wasted debugging 18-decimal garbage)*
158
+
159
+ Node prices come with TWO values. Use the WRONG one and your UI shows `52573.099722991367791000000000/GB`:
160
+
161
+ | Field | Format | Use? | Example |
162
+ |-------|--------|------|---------|
163
+ | `base_value` | Cosmos `sdk.Dec` (scaled by 10^18) | **NEVER for display** | `"5500000000000000000000000"` |
164
+ | `quote_value` | Integer (`sdk.Int`) in `udvpn` | **ALWAYS for display** | `"40152030"` |
165
+
166
+ **Rule:** Always use `quote_value` for display pricing. Use `base_value` only when constructing the `max_price` protobuf field for `MsgStartSessionRequest`.
167
+
168
+ **Price formatting function:**
169
+ ```javascript
170
+ function formatP2PPrice(udvpnStr) {
171
+ const p2p = parseInt(udvpnStr) / 1_000_000;
172
+ if (p2p >= 100) return `${Math.round(p2p)} P2P`;
173
+ if (p2p >= 10) return `${p2p.toFixed(1).replace(/\.0$/, '')} P2P`;
174
+ if (p2p >= 1) return `${p2p.toFixed(2).replace(/0$/, '').replace(/\.$/, '')} P2P`;
175
+ return `${p2p.toFixed(4)} P2P`;
176
+ }
177
+ // formatP2PPrice("40152030") -> "40.15 P2P"
178
+ ```
179
+
180
+ ### Minimum Balance
181
+
182
+ | Item | Cost |
183
+ |------|------|
184
+ | Minimum for any operation | 1.0 P2P (1,000,000 udvpn) — covers gas + cheapest node |
185
+ | 1 GB (cheapest, varies) | ~0.68 P2P — node operators set their own prices |
186
+ | 1 GB (median, varies) | ~40 P2P — use `estimateCost()` for live pricing |
187
+ | Gas per transaction | ~0.04 P2P (40,000 udvpn) |
188
+ | End session TX | Fixed 0.02 P2P gas (20,000 udvpn, 200,000 gas) |
189
+
190
+ The SDK checks balance before session payment:
191
+ ```javascript
192
+ if (bal.udvpn < 1000000) {
193
+ throw new ChainError('INSUFFICIENT_BALANCE',
194
+ `Wallet has ${bal.p2p} P2P -- need at least 1.0 P2P`);
195
+ }
196
+ ```
197
+
198
+ ### Where to Buy P2P Tokens
199
+
200
+ - **Osmosis DEX:** Swap USDT/USDC/ATOM to P2P
201
+ - **CEX listings:** Check CoinGecko for current exchanges
202
+ - Fund the `sent1...` address shown in `account.address`
203
+
204
+ ### Balance Check
205
+
206
+ ```javascript
207
+ import { getBalance, createWallet, createClient, DEFAULT_RPC } from 'sentinel-dvpn-sdk';
208
+
209
+ const { wallet, account } = await createWallet(mnemonic);
210
+ const client = await createClient(DEFAULT_RPC, wallet);
211
+ const { udvpn, dvpn } = await getBalance(client, account.address);
212
+ // udvpn = raw micro amount (integer), dvpn = human-readable (float)
213
+ ```
214
+
215
+ **LCD endpoint for direct query:**
216
+ ```
217
+ GET /cosmos/bank/v1beta1/balances/{sent1...}
218
+ ```
219
+
220
+ ### LCD Failover Chain
221
+
222
+ The SDK tries these endpoints in order. On failure, it automatically falls back to the next:
223
+
224
+ | Priority | URL | Name |
225
+ |----------|-----|------|
226
+ | 1 | `https://lcd.sentinel.co` | Sentinel Official |
227
+ | 2 | `https://sentinel-api.polkachu.com` | Polkachu |
228
+ | 3 | `https://api.sentinel.quokkastake.io` | QuokkaStake |
229
+ | 4 | `https://sentinel-rest.publicnode.com` | PublicNode |
230
+
231
+ **RPC Failover Chain** (for TX broadcast):
232
+
233
+ | Priority | URL | Name |
234
+ |----------|-----|------|
235
+ | 1 | `https://rpc.sentinel.co:443` | Sentinel Official |
236
+ | 2 | `https://sentinel-rpc.polkachu.com` | Polkachu |
237
+ | 3 | `https://rpc.mathnodes.com` | MathNodes |
238
+ | 4 | `https://sentinel-rpc.publicnode.com` | PublicNode |
239
+ | 5 | `https://rpc.sentinel.quokkastake.io` | QuokkaStake |
240
+
241
+ ---
242
+
243
+ ## Phase 4: Node Discovery
244
+
245
+ ### Fetching Active Nodes
246
+
247
+ ```javascript
248
+ import { queryOnlineNodes, fetchAllNodes } from 'sentinel-dvpn-sdk';
249
+
250
+ // Fast: LCD-only, no per-node checks (900+ nodes, instant)
251
+ const allNodes = await fetchAllNodes();
252
+
253
+ // Thorough: checks each node's online status + quality scoring
254
+ const onlineNodes = await queryOnlineNodes({
255
+ serviceType: 'v2ray', // 'wireguard' | 'v2ray' | null (both)
256
+ maxNodes: 100, // max nodes to probe
257
+ concurrency: 20, // parallel online checks
258
+ });
259
+ ```
260
+
261
+ ### LCD Endpoint for Active Nodes
262
+
263
+ ```
264
+ GET /sentinel/node/v3/nodes?status=1&pagination.limit=5000
265
+ ```
266
+
267
+ ### CRITICAL v3 Rules
268
+
269
+ | Rule | Detail |
270
+ |------|--------|
271
+ | **Use v3 paths, NOT v2** | v2 returns "Not Implemented" for ALL endpoints except provider |
272
+ | **Provider is v2 ONLY** | `/sentinel/provider/v2/providers/{sentprov1...}` -- NOT v3 |
273
+ | **Status filter** | `status=1` (integer), NOT `status=STATUS_ACTIVE` (string) |
274
+ | **Node type field** | `service_type` (NOT `type`) -- values: `1` (V2Ray) or `2` (WireGuard) |
275
+ | **Remote URL field** | `remote_addrs` (array of `"IP:PORT"` strings), NOT `remote_url` (string) |
276
+ | **Account address** | `acc_address` (NOT `address`) |
277
+ | **Session nesting** | Session data is under `base_session` -- always use `session.base_session || session` |
278
+
279
+ ### CRITICAL: Pagination is Broken
280
+
281
+ *(Source: Test2 confirmed, Node Tester confirmed with 921+ node scans)*
282
+
283
+ **NEVER trust `count_total` or `next_key`** on Sentinel LCD endpoints:
284
+ - Some endpoints return `min(actual_count, limit)` as `count_total`
285
+ - Some endpoints return `null` for `next_key` even when more data exists
286
+ - Test2 verified: `count_total` returned 500 when 921 nodes existed
287
+ - **Solution:** Single request with `limit=5000` -- returns all data in one call
288
+
289
+ ### Tiered Caching Pattern
290
+
291
+ *(Source: Handshake dVPN -- bandwidth optimization for consumer apps)*
292
+
293
+ Node data should be cached at multiple tiers to minimize chain queries:
294
+
295
+ | Data | Show From | Refresh When | TTL |
296
+ |------|-----------|-------------|-----|
297
+ | Node list | Disk cache on login | Background after login | 30 min |
298
+ | Node status (peers, location) | Memory from last probe | On user Refresh click | Per session |
299
+ | Balance | Last known value | Every 5 min | -- |
300
+ | Session allocation | Chain query | Every 120s when connected | -- |
301
+ | Country flags | Disk permanent | Never | Forever |
302
+ | Settings | Disk permanent | On save | Forever |
303
+
304
+ **Key rules from Handshake dVPN:**
305
+ - Load on app open -- probing starts immediately, before login
306
+ - Single probe per session -- cache in memory, reuse across login/logout
307
+ - Only re-probe on explicit Refresh button -- user controls when to re-query
308
+ - Progress logged every 200 nodes, not every 100 (reduces log spam)
309
+ - 30 parallel workers, 6s timeout each, ~25s for 1000+ nodes
310
+ - ChainClient usable without wallet (for node loading before login)
311
+
312
+ ### Node Object Shape (from LCD)
313
+
314
+ ```json
315
+ {
316
+ "address": "sentnode1...",
317
+ "remote_addrs": ["1.2.3.4:8585"],
318
+ "gigabyte_prices": [
319
+ { "denom": "udvpn", "base_value": "5500000", "quote_value": "40152030" }
320
+ ],
321
+ "hourly_prices": [
322
+ { "denom": "udvpn", "base_value": "1000000", "quote_value": "0" }
323
+ ],
324
+ "service_type": 1
325
+ }
326
+ ```
327
+
328
+ ### Resolving Node URL
329
+
330
+ LCD returns `remote_addrs` as an array of bare `"IP:PORT"` strings (no protocol prefix). The SDK's `resolveNodeUrl()` handles both v2 and v3 formats:
331
+
332
+ ```javascript
333
+ import { resolveNodeUrl } from 'sentinel-dvpn-sdk';
334
+ const url = resolveNodeUrl(node); // Returns "https://1.2.3.4:8585"
335
+ ```
336
+
337
+ **NEVER access `node.remote_url` directly** -- it is undefined in v3. Always use `resolveNodeUrl()`.
338
+
339
+ ### Country Data
340
+
341
+ Country/city information is NOT on the LCD. It requires querying each node's own status API:
342
+
343
+ ```javascript
344
+ import { nodeStatusV3 } from 'sentinel-dvpn-sdk';
345
+ const status = await nodeStatusV3('https://1.2.3.4:8585');
346
+ // status.location = { city, country, country_code, latitude, longitude }
347
+ // status.moniker = "MyNode"
348
+ // status.type = "wireguard" | "v2ray"
349
+ // status.peers = 3
350
+ // status.clockDriftSec = -2 (seconds, negative = node behind)
351
+ ```
352
+
353
+ Use `enrichNodes()` to batch-probe all nodes for country data, or `buildNodeIndex()` to create a geographic lookup.
354
+
355
+ ---
356
+
357
+ ## Phase 5: Node Selection
358
+
359
+ ### Service Type Success Rates (from 780-node mainnet scan)
360
+
361
+ | Service Type | Success Rate | Admin Required | Notes |
362
+ |-------------|-------------|----------------|-------|
363
+ | WireGuard | ~100% (when reachable) | Yes | Simpler protocol, fewer failure modes |
364
+ | V2Ray | ~95.6% | No | Multiple transport fallback options |
365
+
366
+ ### V2Ray Transport Reliability (from 780-node scan)
367
+
368
+ | Transport | Success Rate | Sample | Notes |
369
+ |-----------|-------------|--------|-------|
370
+ | `tcp` | 100% | 274 | **Best. Always first choice.** |
371
+ | `websocket` | 100% | 23 | Second choice |
372
+ | `http` | 100% | 4 | Third choice |
373
+ | `gun` | 100% | 10 | gun(2) and grpc(3) are DIFFERENT enum values but use same V2Ray config |
374
+ | `mkcp` | 100% | 5 | |
375
+ | `grpc/none` | 87% | 81 | Fixed by serverName TLS fix |
376
+ | `quic` | 0% | 4 | Fixed (security: 'none'), but low node count |
377
+ | `grpc/tls` | **0%** | 0 | **ALWAYS FAILS. Filter before paying.** |
378
+
379
+ ### Undiagnosed Failure Categories
380
+
381
+ *(Source: Node Tester -- 22 nodes with active peers that fail for us)*
382
+
383
+ Even after all fixes, ~2.8% of nodes with active peers still fail. These are OUR bugs, not node-side issues. Categories:
384
+
385
+ | Category | Count | Symptom | Likely Cause (Our Side) |
386
+ |----------|-------|---------|------------------------|
387
+ | TCP Port Unreachable | 10 | Pre-check says port closed, but 3-7 peers connected | Probe timeout too short, DNS resolution differs, ISP blocking from our IP |
388
+ | SOCKS5 No Connectivity | 5 | Handshake OK, V2Ray starts, SOCKS5 binds, no internet | V2Ray 5.2.1 grpc/quic bugs, egress policy blocks our test targets |
389
+ | Clock Drift VMess Skip | 4 | >120s drift detected, we skip, but 4-6 peers work fine | Our measurement may be inaccurate, node may have VLess (unaffected) we fail to detect |
390
+ | V2 Format Metadata | 1 | 48 peers, but metadata has v2 fields not v3 | Dual-format node, we should support v2 fallback |
391
+ | Handshake Failures | 2 | ECONNRESET or 30s timeout with 3-4 peers | TLS cipher mismatch, timeout too short for distant nodes |
392
+
393
+ **Iron rule:** Any node with peers > 0 that fails = our bug. NEVER say "node-side" or "can't fix."
394
+
395
+ ### Quality Scoring
396
+
397
+ The SDK scores nodes 0-100 based on:
398
+ - **+20** for WireGuard type
399
+ - **-40** for clock drift >120s (VMess AEAD failure zone)
400
+ - **-15** for clock drift >60s
401
+ - **-5** for clock drift >30s
402
+ - **+10** for 0 peers (empty node = fast)
403
+ - **+5** for <5 peers
404
+ - **-10** for >20 peers
405
+
406
+ ### Circuit Breaker
407
+
408
+ The SDK tracks node failures and skips nodes that fail repeatedly:
409
+ - Default: 3 failures within 5 minutes = circuit open (node skipped)
410
+ - Configurable via `configureCircuitBreaker({ threshold: 3, ttlMs: 300000 })`
411
+ - Auto-resets after TTL expires
412
+ - Cleared on successful connection
413
+
414
+ ### Pre-Selection Checks
415
+
416
+ Before paying for a session, verify:
417
+ 1. Node accepts `udvpn` denom: `node.gigabyte_prices.some(p => p.denom === 'udvpn')`
418
+ 2. Node has reachable remote URL: `resolveNodeUrl(node)` does not return null
419
+ 3. Node is not in the circuit breaker
420
+ 4. Node address matches remote URL (prevents address mismatch -- check `nodeStatusV3().address`)
421
+ 5. Clock drift <120s for V2Ray nodes (VMess AEAD tolerance)
422
+ 6. If WireGuard: admin privileges available (`IS_ADMIN === true`)
423
+ 7. If V2Ray: `v2ray.exe` v5.2.1 exists
424
+
425
+ ---
426
+
427
+ ## Phase 6: Session Creation (Blockchain TX)
428
+
429
+ ### Message Type
430
+
431
+ ```
432
+ /sentinel.node.v3.MsgStartSessionRequest
433
+ ```
434
+
435
+ ### Protobuf Fields
436
+
437
+ | Field # | Name | Type | Description |
438
+ |---------|------|------|-------------|
439
+ | 1 | `from` | string | Signer's `sent1...` address |
440
+ | 2 | `node_address` | string | Target `sentnode1...` address |
441
+ | 3 | `gigabytes` | int64 | Bandwidth to purchase (0 if hourly) |
442
+ | 4 | `hours` | int64 | Duration to purchase (0 if per-GB) |
443
+ | 5 | `max_price` | Price | Maximum price user will pay (from node's gigabyte_prices or hourly_prices) |
444
+
445
+ **Price sub-message:**
446
+ | Field # | Name | Type | Description |
447
+ |---------|------|------|-------------|
448
+ | 1 | `denom` | string | `"udvpn"` |
449
+ | 2 | `base_value` | string | sdk.Dec scaled by 10^18 (e.g., `"5500000"` from LCD) |
450
+ | 3 | `quote_value` | string | sdk.Int (e.g., `"40152030"` from LCD) |
451
+
452
+ ### SDK Usage
453
+
454
+ ```javascript
455
+ import { connectDirect, registerCleanupHandlers } from 'sentinel-dvpn-sdk';
456
+
457
+ // REQUIRED: Register cleanup handlers BEFORE any connection
458
+ registerCleanupHandlers();
459
+
460
+ const result = await connectDirect({
461
+ mnemonic: process.env.MNEMONIC,
462
+ nodeAddress: 'sentnode1...',
463
+ gigabytes: 1,
464
+ // Optional:
465
+ rpcUrl: 'https://rpc.sentinel.co:443',
466
+ lcdUrl: 'https://lcd.sentinel.co',
467
+ v2rayExePath: './bin/v2ray.exe',
468
+ fullTunnel: true, // Route ALL traffic through VPN
469
+ systemProxy: false, // Set Windows system SOCKS proxy
470
+ killSwitch: false, // Block all traffic if tunnel drops
471
+ onProgress: (step, detail) => console.log(`[${step}] ${detail}`),
472
+ });
473
+ ```
474
+
475
+ ### CRITICAL: ForceNewSession for Consumer Apps
476
+
477
+ *(Source: Handshake dVPN -- stale session 404 errors)*
478
+
479
+ Consumer apps should use `forceNewSession: true` to avoid stale session 404 errors. Without it, the SDK attempts to reuse existing sessions which may have expired or become corrupted on the node side, producing confusing 404 "session does not exist" errors.
480
+
481
+ ```javascript
482
+ const result = await connectDirect({
483
+ mnemonic: process.env.MNEMONIC,
484
+ nodeAddress: 'sentnode1...',
485
+ gigabytes: 1,
486
+ forceNewSession: true, // RECOMMENDED for consumer apps
487
+ });
488
+ ```
489
+
490
+ **Exception:** For node testing tools that test hundreds of nodes, session reuse saves tokens. Only use `forceNewSession: false` when you have logic to validate the existing session is still active.
491
+
492
+ ### CRITICAL: Fee Grant Detection
493
+
494
+ *(Source: Handshake dVPN -- wrong fee grant auto-detection for direct-connect apps)*
495
+
496
+ The SDK's fee grant auto-detection assumes plan-based subscriptions (where the plan owner pays gas). For direct-connect (P2P) apps where the user pays their own gas, fee grant detection must be **disabled entirely**. Otherwise the SDK incorrectly tries to use a non-existent fee grant and the TX fails.
497
+
498
+ - **Plan-based apps:** Fee grant auto-detection is correct (plan owner grants gas to subscribers)
499
+ - **Direct-connect apps:** Set `feeGrant: false` or remove fee grant logic entirely
500
+ - **All-in-one apps:** Detect payment mode and toggle accordingly
501
+
502
+ ### Payment Is Locked Upfront
503
+
504
+ Tokens are escrowed on session creation. They are NOT refundable if the connection fails after payment. This is why pre-verification (Phase 5) is critical.
505
+
506
+ ### Sequence Retry Logic
507
+
508
+ The SDK uses `createSafeBroadcaster()` for production apps, which handles:
509
+ - **Error code 32 (sequence mismatch):** Up to 5 retry attempts with exponential backoff (2s, 4s, 6s, 6s)
510
+ - Each retry creates a fresh SigningStargateClient (fresh sequence number from RPC)
511
+ - Broadcasts are serialized through a mutex (one TX at a time)
512
+
513
+ ### Code 105 Retry (Node Inactive)
514
+
515
+ If the chain returns code 105 ("invalid status inactive"), it means the node went offline between the LCD query and the payment TX. The SDK:
516
+ 1. Waits 15 seconds (LCD data may be stale)
517
+ 2. Retries the broadcast once
518
+ 3. If still code 105, throws `NodeError('NODE_INACTIVE')`
519
+
520
+ ### Session ID Extraction
521
+
522
+ After successful broadcast, the session ID is extracted from TX ABCI events:
523
+ ```javascript
524
+ const sessionId = extractId(txResult, /session/i, ['session_id', 'id']);
525
+ ```
526
+
527
+ Event attributes may be base64-encoded (depends on CosmJS version). The SDK handles both string and base64-encoded keys/values. The extracted ID is a string representation of a uint64.
528
+
529
+ **CRITICAL:** `sessionId` is a BigInt internally. Convert to string before JSON serialization: `sessionId.toString()`. `JSON.stringify({ sessionId: 123n })` throws `TypeError`.
530
+
531
+ ### Inter-TX Spacing
532
+
533
+ **Minimum 7 seconds between transactions.** Rapid TX submission causes sequence mismatch cascades. NEVER run parallel chain operations -- rate limits will kill your internet connectivity.
534
+
535
+ ---
536
+
537
+ ## Phase 7: Index Wait
538
+
539
+ After the session TX confirms on-chain, the node needs time to see the block and index the session.
540
+
541
+ ### Wait Sequence
542
+
543
+ | Step | Delay | Purpose |
544
+ |------|-------|---------|
545
+ | Post-payment wait | **5 seconds** | Node indexes session from new block |
546
+ | Chain lag retry (handshake 404) | **10 seconds** | Node still processing; retry once |
547
+ | Already-exists retry 1 (409) | **15 seconds** | Session indexing race condition |
548
+ | Already-exists retry 2 (409) | **20 seconds** | Final attempt before fresh payment |
549
+
550
+ ### What Happens Without the Wait
551
+
552
+ Without the 5-second post-payment delay:
553
+ - **404 "session does not exist"** -- node hasn't seen the block yet
554
+ - **409 "already exists"** -- node is still indexing the previous state
555
+
556
+ The SDK handles both automatically with retries, but the initial 5s delay prevents most failures.
557
+
558
+ ### Inactive Pending Status
559
+
560
+ After TX confirms, the session may be in `inactive_pending` status. The SDK's `waitForSessionActive()` polls every 2 seconds for up to 20 seconds until the status transitions to `active`.
561
+
562
+ ---
563
+
564
+ ## Phase 8: V3 Handshake
565
+
566
+ ### Protocol
567
+
568
+ Single HTTPS POST to the node's remote URL (from `remote_addrs`).
569
+
570
+ ### Endpoint
571
+
572
+ ```
573
+ POST https://{node_ip}:{node_port}/
574
+ Content-Type: application/json
575
+ ```
576
+
577
+ ### TLS
578
+
579
+ All Sentinel nodes use **self-signed certificates**. The SDK uses a TOFU (Trust-On-First-Use) model:
580
+ - First connection: accept any certificate, save fingerprint
581
+ - Subsequent connections: reject if certificate fingerprint changed (possible MITM)
582
+ - Configurable via `tlsTrust: 'tofu'` (default) or `'none'` (insecure)
583
+
584
+ ### Request Timeout
585
+
586
+ **90 seconds.** Overloaded nodes can take 60-90s to respond.
587
+
588
+ ### Request Body
589
+
590
+ ```json
591
+ {
592
+ "data": "<base64_encoded_peer_data>",
593
+ "id": 12345,
594
+ "pub_key": "secp256k1:<base64_compressed_pubkey>",
595
+ "signature": "<base64_signature>"
596
+ }
597
+ ```
598
+
599
+ ### Field Construction Details
600
+
601
+ **`data` field (base64-encoded JSON):**
602
+
603
+ For WireGuard:
604
+ ```json
605
+ {"public_key": "<base64_x25519_public_key>"}
606
+ ```
607
+
608
+ For V2Ray:
609
+ ```json
610
+ {"uuid": [byte, byte, byte, ...16 bytes]}
611
+ ```
612
+
613
+ The `data` field is `base64(JSON.stringify(peer_request))`.
614
+
615
+ **`id` field:**
616
+ - Session ID as a JavaScript Number (NOT BigInt)
617
+ - CRITICAL: Must pass `Number.isSafeInteger()` check (max 2^53 - 1 = 9007199254740991)
618
+ - The SDK throws if sessionId exceeds safe integer range
619
+
620
+ **`pub_key` field:**
621
+ - Format: `"secp256k1:" + base64(compressed_secp256k1_pubkey)`
622
+ - Compressed public key is 33 bytes (prefix 0x02 or 0x03 + 32 bytes X coordinate)
623
+
624
+ **`signature` field:**
625
+
626
+ ```
627
+ signature = base64(secp256k1_sign(SHA256(message))[0:64])
628
+ ```
629
+
630
+ Where `message` is the concatenation of:
631
+ 1. **BigEndian uint64 (8 bytes)** of the session ID
632
+ 2. **Raw bytes** of the `data` field (the base64-decoded peer JSON bytes)
633
+
634
+ **CRITICAL: Sign the RAW JSON bytes, NOT the base64 string.**
635
+
636
+ The signature is:
637
+ - SHA256 hash of the concatenated message
638
+ - secp256k1 signature (deterministic, RFC 6979)
639
+ - **Exactly 64 bytes** (r + s, NO recovery byte) -- Go's `VerifySignature` requires `len == 64`
640
+ - The SDK takes the first 64 bytes of the 65-byte `toFixedLength()` output
641
+
642
+ ### WireGuard Key Generation
643
+
644
+ ```javascript
645
+ import { generateWgKeyPair } from 'sentinel-dvpn-sdk';
646
+ const { privateKey, publicKey } = generateWgKeyPair();
647
+ // privateKey: Buffer(32), publicKey: Buffer(32)
648
+ ```
649
+
650
+ Key generation:
651
+ 1. Generate 32 random bytes (CSPRNG)
652
+ 2. Apply WireGuard bit clamping: `priv[0] &= 248; priv[31] &= 127; priv[31] |= 64;`
653
+ 3. Derive public key via X25519 scalar base multiplication
654
+
655
+ ### V2Ray UUID Generation
656
+
657
+ ```javascript
658
+ import { generateV2RayUUID } from 'sentinel-dvpn-sdk';
659
+ const uuid = generateV2RayUUID(); // crypto.randomUUID(), e.g., "550e8400-e29b-41d4-a716-446655440000"
660
+ ```
661
+
662
+ The UUID is sent to the node as an integer byte array (16 bytes) in the peer request.
663
+
664
+ ### Response (WireGuard)
665
+
666
+ The response `result.data` is base64-encoded JSON containing:
667
+ ```json
668
+ {
669
+ "addrs": ["10.8.0.2/24"],
670
+ "metadata": [{ "port": 51820, "public_key": "<base64_server_wg_pubkey>" }]
671
+ }
672
+ ```
673
+
674
+ - `result.addrs` = node's WireGuard listening addresses (`["IP:PORT", ...]`)
675
+ - `addPeerResp.addrs` = our assigned IPs (e.g., `["10.8.0.2/24"]`)
676
+ - `addPeerResp.metadata[0].public_key` = server's WireGuard public key
677
+ - `addPeerResp.metadata[0].port` = server's WireGuard port (default 51820)
678
+
679
+ ### Response (V2Ray)
680
+
681
+ The response `result.data` is base64-encoded JSON containing V2Ray outbound configuration with metadata entries. Each metadata entry describes one transport option:
682
+
683
+ ```json
684
+ {
685
+ "metadata": [
686
+ {
687
+ "proxy_protocol": 1,
688
+ "transport_protocol": 7,
689
+ "transport_security": 0,
690
+ "port": 443
691
+ }
692
+ ]
693
+ }
694
+ ```
695
+
696
+ - `proxy_protocol`: 1=VLess, 2=VMess
697
+ - `transport_protocol`: 1=domainsocket, 2=gun, 3=grpc, 4=http, 5=mkcp, 6=quic, 7=tcp, 8=websocket
698
+ - `transport_security`: 0=none, 2=tls
699
+ - `port`: server port for this transport
700
+
701
+ ### Handshake Error Handling
702
+
703
+ | HTTP Status | Body Pattern | Meaning | Action |
704
+ |-------------|-------------|---------|--------|
705
+ | 404 | "does not exist" | Chain lag -- node hasn't indexed session | Wait 10s, retry once |
706
+ | 409 | "already exists" | Session indexing race | Wait 15s/20s, retry; then pay fresh |
707
+ | 500 | "no such table" / "database is locked" | Corrupted node database | Throw `NODE_DATABASE_CORRUPT`, skip node |
708
+ | Other | Any | Node-level failure | Throw `NODE_OFFLINE` |
709
+
710
+ ### Response Validation
711
+
712
+ The SDK validates every handshake response field:
713
+ - Server public key must be non-empty
714
+ - Server port must be 1-65535
715
+ - Assigned addresses must be valid CIDR (IPv4 or IPv6)
716
+ - At least one assigned address must be returned
717
+ - At least one server endpoint address must be returned
718
+
719
+ ---
720
+
721
+ ## Phase 9: Tunnel Installation
722
+
723
+ ### CRITICAL: WireGuard Pre-Cleanup Before Every Connect
724
+
725
+ *(Source: Handshake dVPN -- tunnel orphan crashes)*
726
+
727
+ **Always uninstall stale WireGuard tunnels BEFORE attempting a new connection.** A leftover `wgsent0` tunnel from a crash, force-quit, or previous session will cause the new install to fail silently.
728
+
729
+ ```
730
+ # Windows: run before every new connection
731
+ wireguard.exe /uninstalltunnelservice wgsent0
732
+ ```
733
+
734
+ The SDK's `registerCleanupHandlers()` does this at startup via `emergencyCleanupSync()`, but consumer apps with dedicated test VPN instances must also clean up independently. For node testing scenarios, clean up BEFORE and AFTER each test node.
735
+
736
+ ### WireGuard Path
737
+
738
+ #### Config Generation
739
+
740
+ ```ini
741
+ [Interface]
742
+ PrivateKey = <base64_client_private_key>
743
+ Address = 10.8.0.2/24
744
+ MTU = 1420
745
+ DNS = 103.196.38.38, 103.196.38.39
746
+
747
+ [Peer]
748
+ PublicKey = <base64_server_public_key>
749
+ Endpoint = 1.2.3.4:51820
750
+ AllowedIPs = 0.0.0.0/0, ::/0
751
+ PersistentKeepalive = 25
752
+ ```
753
+
754
+ #### Config Values
755
+
756
+ | Setting | Value | Why |
757
+ |---------|-------|-----|
758
+ | MTU | `1420` (default, configurable) | WireGuard standard for IPv4. Use `1280` for IPv6/restrictive networks. Sentinel nodes historically used 1280; current SDK defaults to 1420. |
759
+ | DNS | `103.196.38.38, 103.196.38.39` (default: Handshake DNS) | Configurable via `dns` option. Presets: `handshake`, `google` (8.8.8.8), `cloudflare` (1.1.1.1). Only set for full tunnel; split tunnel uses system DNS. |
760
+ | PersistentKeepalive | `25` (default, configurable) | NAT traversal. 15-25s is safe for all NAT routers. |
761
+ | AllowedIPs (full tunnel) | `0.0.0.0/0, ::/0` | Routes ALL traffic. **Kills internet if tunnel fails.** |
762
+ | AllowedIPs (split tunnel) | Explicit IP list | Only routes specified IPs. Safe for testing. |
763
+
764
+ #### Config File Location
765
+
766
+ - **Windows:** `C:\ProgramData\sentinel-wg\wgsent0.conf` (SYSTEM-readable; user temp dirs are not)
767
+ - **Linux/macOS:** `/tmp/sentinel-wg/wgsent0.conf`
768
+
769
+ **Security:** Directory ACL is set BEFORE writing the file (closes the race window where the private key would be world-readable). On Windows, `icacls` restricts to current user + SYSTEM only.
770
+
771
+ #### Verify-Before-Capture (Critical Safety Pattern)
772
+
773
+ The SDK uses a two-phase install to prevent killing the user's internet:
774
+
775
+ 1. **Phase 1: Safe install** -- Install with split IPs (`1.1.1.1/32, 1.0.0.1/32`) that only capture verification traffic
776
+ 2. **Verify** -- HTTP GET to `https://1.1.1.1` and `https://1.0.0.1` through the tunnel
777
+ 3. **Phase 2: Full capture** -- If verified, reinstall with `AllowedIPs = 0.0.0.0/0, ::/0`
778
+
779
+ Without this pattern, a broken node causes ~78 seconds of dead internet while verification loops fail.
780
+
781
+ #### Installation (Windows)
782
+
783
+ ```
784
+ wireguard.exe /installtunnelservice C:\ProgramData\sentinel-wg\wgsent0.conf
785
+ ```
786
+
787
+ **NEVER use `/installmanagerservice`** -- that starts the WireGuard GUI which takes over tunnel management and conflicts with programmatic control.
788
+
789
+ #### Installation Retry
790
+
791
+ The SDK tries tunnel installation 3 times with escalating delays:
792
+ 1. Wait 1.5s, try install
793
+ 2. Wait 1.5s, try install
794
+ 3. Wait 2.0s, try install (final attempt)
795
+
796
+ This gives the node time to register the peer (most do so within 1-2 seconds).
797
+
798
+ #### Connectivity Verification
799
+
800
+ After installation, the SDK verifies actual traffic flow:
801
+ - 1 attempt, 2 targets (`https://1.1.1.1`, `https://www.cloudflare.com`), 5s timeout each
802
+ - Maximum exposure: ~10 seconds
803
+ - If verification fails: immediately tear down tunnel, throw `WG_NO_CONNECTIVITY`
804
+ - A RUNNING service does NOT guarantee traffic flows
805
+
806
+ ### V2Ray Path
807
+
808
+ #### Post-Handshake Delay
809
+
810
+ **Wait 5 seconds after handshake before starting V2Ray.** The node needs time to register the UUID internally. Without this delay, ~8% of V2Ray nodes will reject connections.
811
+
812
+ #### Sequential Outbound Fallback
813
+
814
+ The SDK does NOT use V2Ray's built-in balancer (it's buggy in v5.2.1). Instead, it implements its own sequential fallback:
815
+
816
+ 1. Parse all metadata entries from handshake response
817
+ 2. Sort outbounds by transport reliability (tcp > websocket > http > gun > mkcp > grpc/none)
818
+ 3. For each outbound, one at a time:
819
+ a. Write V2Ray config with single outbound
820
+ b. Spawn `v2ray run -config <path>`
821
+ c. Wait for SOCKS5 port to accept connections (`waitForPort`, 10s timeout)
822
+ d. Test SOCKS5 connectivity via HTTP GET through proxy (see pre-check below)
823
+ e. If connected: keep this outbound, break
824
+ f. If failed: kill V2Ray process, try next outbound
825
+
826
+ #### CRITICAL: V2Ray SOCKS5 Connectivity Pre-Check
827
+
828
+ *(Source: Node Tester -- 411/411 V2Ray speed tests failing until this was fixed)*
829
+ *(Source: Complete Integration Spec -- exact retry pattern)*
830
+
831
+ V2Ray SOCKS5 binding is asynchronous. The proxy may not be ready even after the port accepts TCP connections. The pre-check MUST use retries:
832
+
833
+ ```
834
+ Try up to 3 attempts with 5-second pause between:
835
+ For each target in [google.com, cloudflare.com, 1.1.1.1/cdn-cgi/trace,
836
+ httpbin.org/ip, ifconfig.me, ip-api.com/json]:
837
+ HTTP GET via SOCKS5 proxy (15s timeout)
838
+ If ANY target returns HTTP 200 -> tunnel is working, proceed
839
+
840
+ If all 6 targets fail -> wait 5 seconds -> try again (up to 3 attempts)
841
+
842
+ If ALL 3 attempts fail -> throw "SOCKS5 tunnel has no internet connectivity"
843
+ ```
844
+
845
+ **CRITICAL:** Create a FRESH `SocksProxyAgent` / `HttpClient` per request. V2Ray SOCKS5 connections SILENTLY FAIL with connection reuse. In C#, never reuse `HttpClient` for SOCKS5 -- connection pooling returns stale/empty responses.
846
+
847
+ #### V2Ray Config Rules (Silent Failures If Wrong)
848
+
849
+ | Setting | Correct | Wrong (Fails Silently) |
850
+ |---------|---------|----------------------|
851
+ | VLess encryption | `"none"` | Any other value |
852
+ | VLess flow | **Omit entirely** | Any value including `""` |
853
+ | VMess alterId | `0` | Any other value |
854
+ | VMess user security | **Omit entirely** | Any value |
855
+ | UUID field name | `"uuid"` | `"id"` |
856
+ | UUID format | String `"550e8400-..."` | Array or other format |
857
+ | grpc serviceName | `grpcSettings: { serviceName: '' }` | Omitting grpcSettings |
858
+ | QUIC security | `security: 'none'` | `security: 'chacha20-poly1305'` |
859
+ | Per-outbound transport | **No per-outbound streamSettings** | Putting streamSettings inside outbound |
860
+
861
+ #### SOCKS5 Port Assignment
862
+
863
+ Each outbound attempt gets a unique SOCKS5 port to avoid Windows TIME_WAIT conflicts:
864
+ ```
865
+ basePort = 10800 + random(0, 999)
866
+ port_for_outbound_i = basePort + i
867
+ ```
868
+
869
+ TIME_WAIT on Windows lasts ~120 seconds. Reusing the same port across fallback attempts guarantees failure.
870
+
871
+ #### SOCKS5 Authentication
872
+
873
+ V2Ray SOCKS5 inbound uses username/password authentication by default. **Windows system proxy cannot pass SOCKS5 credentials.** When `systemProxy: true`, the SDK patches SOCKS5 inbound to `noauth`.
874
+
875
+ #### SOCKS5 Testing
876
+
877
+ ```javascript
878
+ // MUST use axios with adapter='http' for SOCKS5 testing
879
+ // Native fetch silently ignores the proxy agent
880
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
881
+ const agent = new SocksProxyAgent(`socks5://user:pass@127.0.0.1:${socksPort}`);
882
+ await axios.get('https://www.google.com', {
883
+ httpAgent: agent,
884
+ httpsAgent: agent,
885
+ timeout: 10000,
886
+ });
887
+ ```
888
+
889
+ ### 7-Layer Speed Test Fallback Chain
890
+
891
+ *(Source: Node Tester -- discovered through 780+ node tests, Complete Integration Spec)*
892
+
893
+ Speed testing must handle a wide range of tunnel capabilities. The SDK uses a 7-layer fallback chain:
894
+
895
+ #### For WireGuard (Direct Through Tunnel)
896
+
897
+ ```
898
+ 1. PRE-RESOLVE DNS before tunnel install
899
+ Resolve speed.cloudflare.com, proof.ovh.net, speedtest.tele2.net to IP
900
+ Cache resolved IPs for 5 minutes (DNS fails through many WireGuard tunnels)
901
+
902
+ 2. PROBE: 1MB download (try in order, 30s timeout each)
903
+ a. Cloudflare via cached IP: https://{cached_cf_ip}/__down?bytes=1048576
904
+ b. Cloudflare via hostname: https://speed.cloudflare.com/__down?bytes=1048576
905
+ c. OVH fallback: https://proof.ovh.net/files/1Mb.dat
906
+ d. Tele2 fallback: https://speedtest.tele2.net/1MB.zip
907
+ e. RESCUE: Cloudflare with 60s keep-alive timeout
908
+
909
+ If ALL fail -> return "speed test failed"
910
+ If probeMbps < 3 -> return { mbps: probeMbps, method: "probe-only" }
911
+ If probeMbps >= 3 -> proceed to multi-request
912
+
913
+ 3. MULTI-REQUEST: 5 x 1MB sequential downloads
914
+ FRESH TCP+TLS connection per download (no connection reuse)
915
+ Return average: { mbps: totalMbps, method: "multi-request", chunks: 5 }
916
+ If fails but probe worked -> return { mbps: probeMbps, method: "probe-fallback" }
917
+ ```
918
+
919
+ #### For V2Ray (Through SOCKS5 Proxy)
920
+
921
+ Same fallback chain but through SOCKS5, plus two additional layers:
922
+
923
+ ```
924
+ 4-5. Same as WireGuard layers 2-3 but via SOCKS5 proxy
925
+ CRITICAL: Fresh SocksProxyAgent per request
926
+
927
+ 6. GOOGLE FALLBACK: If all speed targets fail, time a Google HEAD request
928
+ Estimate speed from latency: { mbps: estimated, method: "google-fallback" }
929
+
930
+ 7. CONNECTED-NO-THROUGHPUT: If connectivity check passed but ALL speed methods fail
931
+ Return: { mbps: 0.01, method: "connected-no-throughput" }
932
+ (Node is reachable but too slow for meaningful speed measurement)
933
+ ```
934
+
935
+ #### Speed Test Constants
936
+
937
+ ```javascript
938
+ SPEED_THRESHOLDS = {
939
+ PROBE_CUTOFF_MBPS: 3, // Below this, skip multi-request
940
+ PASS_10_MBPS: 10, // SLA threshold ("FAST")
941
+ PASS_15_MBPS: 15, // High quality threshold
942
+ BASELINE_MIN: 30, // Minimum baseline for SLA applicability
943
+ ISP_BOTTLENECK_PCT: 0.85, // 85% of baseline = ISP bottleneck
944
+ };
945
+
946
+ TIMEOUTS = {
947
+ PROBE_MS: 30000,
948
+ RESCUE_MS: 60000,
949
+ GOOGLE_MS: 15000,
950
+ MULTI_CHUNK_MS: 30000,
951
+ CONNECTIVITY_MS: 15000,
952
+ };
953
+ ```
954
+
955
+ ---
956
+
957
+ ## Phase 10: Verification
958
+
959
+ ### IP Verification
960
+
961
+ ```javascript
962
+ // Check your VPN IP
963
+ const response = await axios.get('https://api.ipify.org?format=json', { timeout: 10000 });
964
+ console.log(response.data.ip); // Should be the VPN node's IP, not your real IP
965
+ ```
966
+
967
+ ### DNS Leak Check
968
+
969
+ For full-tunnel WireGuard: **Pre-resolve ALL hostnames BEFORE installing full tunnel.** DNS resolution fails through many WireGuard full tunnels because:
970
+ - The tunnel routes ALL traffic (including DNS)
971
+ - The node's internal DNS (10.8.0.1) may not resolve all domains
972
+ - External DNS servers may be unreachable through the tunnel
973
+
974
+ The SDK's `resolveSpeedtestIPs()` pre-resolves speed test hostnames before tunnel installation.
975
+
976
+ ### CRITICAL: Fresh HttpClient Per V2Ray SOCKS5 Request
977
+
978
+ *(Source: Handshake dVPN -- 2 hours debugging; Node Tester -- 411 V2Ray tests failing)*
979
+
980
+ **Never reuse HTTP clients for V2Ray SOCKS5 proxy requests.** This is a platform-wide gotcha:
981
+
982
+ | Platform | Problem | Fix |
983
+ |----------|---------|-----|
984
+ | **Node.js** | `SocksProxyAgent` connection reuse causes TLS failures ("socket disconnected before secure connection") | Create fresh `SocksProxyAgent` per request, call `agent.destroy()` after |
985
+ | **C# (.NET)** | `HttpClient` connection pooling silently returns stale/empty responses through SOCKS5 | Create fresh `HttpClient(new HttpClientHandler { Proxy = ... })` per request |
986
+ | **Node.js fetch** | Native `fetch` (undici) **silently ignores** the `agent` option for SOCKS5 | Must use `axios` with `httpAgent`/`httpsAgent`, never native fetch |
987
+
988
+ This applies to ALL V2Ray SOCKS5 operations: connectivity pre-check, speed test, IP verification, Google check.
989
+
990
+ ### Connection Verification Function
991
+
992
+ ```javascript
993
+ import { verifyConnection } from 'sentinel-dvpn-sdk';
994
+
995
+ const result = await verifyConnection({
996
+ socksPort: 10800, // V2Ray only
997
+ timeout: 10000,
998
+ });
999
+ // result = { connected: true, vpnIp: '1.2.3.4', latencyMs: 150 }
1000
+ ```
1001
+
1002
+ ---
1003
+
1004
+ ## Phase 11: Active Connection
1005
+
1006
+ ### V2Ray (SOCKS5 Proxy)
1007
+
1008
+ After successful connection, a SOCKS5 proxy is available at:
1009
+ ```
1010
+ 127.0.0.1:<socksPort>
1011
+ ```
1012
+
1013
+ Route application traffic through this proxy:
1014
+ ```javascript
1015
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
1016
+ const agent = new SocksProxyAgent(`socks5://127.0.0.1:${result.socksPort}`);
1017
+ const response = await axios.get('https://example.com', {
1018
+ httpAgent: agent,
1019
+ httpsAgent: agent,
1020
+ });
1021
+ ```
1022
+
1023
+ System-wide proxy (optional): `setSystemProxy(socksPort)` modifies Windows registry / macOS networksetup / Linux gsettings.
1024
+
1025
+ ### WireGuard (Full Tunnel)
1026
+
1027
+ All system traffic is routed through the VPN. No application configuration needed. The tunnel is managed as a Windows service (`WireGuardTunnel$wgsent0`).
1028
+
1029
+ ### Kill Switch
1030
+
1031
+ Blocks all non-tunnel traffic using Windows firewall rules (`netsh advfirewall`):
1032
+ ```javascript
1033
+ import { enableKillSwitch, disableKillSwitch, isKillSwitchEnabled } from 'sentinel-dvpn-sdk';
1034
+ enableKillSwitch(serverEndpoint); // Blocks all traffic except to VPN server
1035
+ disableKillSwitch(); // Restores normal routing
1036
+ ```
1037
+
1038
+ The kill switch state is persisted to disk. On crash, `recoverOrphans()` at next startup detects and cleans orphaned firewall rules.
1039
+
1040
+ ### Auto-Reconnect
1041
+
1042
+ ```javascript
1043
+ import { autoReconnect } from 'sentinel-dvpn-sdk';
1044
+ autoReconnect({
1045
+ mnemonic: process.env.MNEMONIC,
1046
+ maxRetries: 5,
1047
+ onReconnecting: (attempt) => console.log(`Reconnecting (${attempt})...`),
1048
+ });
1049
+ ```
1050
+
1051
+ ### Events
1052
+
1053
+ ```javascript
1054
+ import { events } from 'sentinel-dvpn-sdk';
1055
+ events.on('connected', ({ sessionId, serviceType, nodeAddress }) => { });
1056
+ events.on('disconnected', ({ nodeAddress, serviceType, reason }) => { });
1057
+ events.on('progress', ({ event, detail, ts }) => { });
1058
+ events.on('error', (err) => { });
1059
+ events.on('sessionEnded', ({ txHash }) => { });
1060
+ events.on('sessionEndFailed', ({ error }) => { });
1061
+ ```
1062
+
1063
+ ---
1064
+
1065
+ ## Phase 12: Disconnect
1066
+
1067
+ ### SDK Disconnect
1068
+
1069
+ ```javascript
1070
+ import { disconnect } from 'sentinel-dvpn-sdk';
1071
+ await disconnect();
1072
+ ```
1073
+
1074
+ Or use the cleanup function returned by `connectDirect()`:
1075
+ ```javascript
1076
+ const conn = await connectDirect({ ... });
1077
+ // ... use VPN ...
1078
+ await conn.cleanup();
1079
+ ```
1080
+
1081
+ ### Disconnect Sequence
1082
+
1083
+ 1. **Signal abort:** Set `_abortConnect = true` to stop any running `connectAuto()` retry loop
1084
+ 2. **Release connection lock:** Set `_connectLock = false` so user can reconnect
1085
+ 3. **Disable kill switch:** Remove firewall rules (if enabled)
1086
+ 4. **Clear system proxy:** Restore previous proxy settings (Windows registry / macOS / Linux)
1087
+ 5. **Kill V2Ray:** `process.kill()` on the V2Ray child process
1088
+ 6. **Uninstall WireGuard:** `wireguard.exe /uninstalltunnelservice wgsent0`
1089
+ 7. **End session on chain:** `MsgCancelSessionRequest` (fire-and-forget, best-effort)
1090
+ 8. **Zero mnemonic:** `state._mnemonic = null`
1091
+ 9. **Clear connection state:** `state.connection = null`
1092
+ 10. **Clear persisted state:** Remove crash recovery files
1093
+ 11. **Flush DNS cache:** Clear stale speed test DNS entries
1094
+ 12. **Emit event:** `events.emit('disconnected', { ... })`
1095
+
1096
+ ### End Session On-Chain
1097
+
1098
+ Message type: `/sentinel.session.v3.MsgCancelSessionRequest`
1099
+
1100
+ | Field # | Name | Type |
1101
+ |---------|------|------|
1102
+ | 1 | `from` | string (signer address) |
1103
+ | 2 | `id` | uint64 (session ID) |
1104
+
1105
+ Gas: Fixed 200,000 gas, 20,000 udvpn fee.
1106
+
1107
+ This is fire-and-forget (never blocks disconnect). If the TX fails, it logs a warning. Unended sessions eventually expire on-chain, but ending them promptly is good practice.
1108
+
1109
+ ### CRITICAL: Session Tracker (Chain Doesn't Store Payment Mode)
1110
+
1111
+ *(Source: Handshake dVPN -- built SessionTracker from scratch because this was undocumented)*
1112
+
1113
+ The Sentinel chain does NOT store whether a session was created with GB-based or hourly payment. After disconnect, there is no way to query the chain and determine the payment mode. Consumer apps MUST track this locally.
1114
+
1115
+ **What must be persisted per session:**
1116
+
1117
+ | Field | Source | Why |
1118
+ |-------|--------|-----|
1119
+ | `sessionId` | TX event extraction | Identify the session |
1120
+ | `nodeAddress` | User selection | Reconnection |
1121
+ | `paymentMode` | User choice ("gb" or "hourly") | Chain doesn't expose this |
1122
+ | `gigabytes` or `hours` | User input | Allocation display |
1123
+ | `priceUdvpn` | Node's `quote_value` | Cost tracking |
1124
+ | `createdAt` | Local timestamp | Session age |
1125
+
1126
+ The SDK provides `session-tracker.js` for this. If not using the SDK module, persist to disk (e.g., `%LocalAppData%/AppName/sessions.json`). Clear on successful end-session TX.
1127
+
1128
+ **Why this matters:** Without local tracking, the app cannot:
1129
+ - Show "Per GB" vs "Per Hour" on the session card
1130
+ - Calculate remaining allocation correctly (GB remaining vs time remaining)
1131
+ - Display cost information after reconnection
1132
+
1133
+ ### Crash Cleanup
1134
+
1135
+ The SDK registers process handlers via `registerCleanupHandlers()`:
1136
+
1137
+ | Signal | Action |
1138
+ |--------|--------|
1139
+ | `exit` | Kill switch off, clear proxy, kill V2Ray, cleanup WireGuard |
1140
+ | `SIGINT` (Ctrl+C) | Same + `process.exit(130)` |
1141
+ | `SIGTERM` | Same + `process.exit(143)` |
1142
+ | `uncaughtException` | Same + `process.exit(1)` |
1143
+
1144
+ On startup, `registerCleanupHandlers()` also:
1145
+ - Calls `recoverOrphans()` to clean state-tracked orphans from previous crashes
1146
+ - Calls `emergencyCleanupSync()` to remove any stale `wgsent*` WireGuard services
1147
+ - Calls `killOrphanV2Ray()` to terminate abandoned V2Ray processes
1148
+
1149
+ **CRITICAL: `registerCleanupHandlers()` MUST be called before any `connect*()` function.** The SDK throws `INVALID_OPTIONS` if cleanup handlers are not registered, because an unregistered crash leaves WireGuard capturing all traffic with no way to recover.
1150
+
1151
+ ---
1152
+
1153
+ ## Appendix A: Every LCD Endpoint Path
1154
+
1155
+ ### Sentinel v3 Endpoints
1156
+
1157
+ | Query | Method | Path | Notes |
1158
+ |-------|--------|------|-------|
1159
+ | Active nodes | GET | `/sentinel/node/v3/nodes?status=1&pagination.limit=5000` | Use `status=1`, not `STATUS_ACTIVE` |
1160
+ | Single node | GET | `/sentinel/node/v3/nodes/{sentnode1...}` | Direct lookup, no pagination |
1161
+ | Plan nodes | GET | `/sentinel/node/v3/plans/{planId}/nodes?pagination.limit=5000` | Pagination broken (next_key always null) |
1162
+ | Plan by ID | GET | `/sentinel/plan/v3/plans/{planId}` | May return 501 on some endpoints |
1163
+ | Plan subscribers | GET | `/sentinel/plan/v3/plans/{planId}/subscribers?pagination.limit=5000` | |
1164
+ | Subscriptions (by account) | GET | `/sentinel/subscription/v3/accounts/{sent1...}/subscriptions` | **Account-scoped!** |
1165
+ | Subscription by ID | GET | `/sentinel/subscription/v3/subscriptions/{id}` | |
1166
+ | Sessions (by account) | GET | `/sentinel/session/v3/accounts/{sent1...}/sessions` | Add `&status=1` for active only |
1167
+ | Session allocation | GET | `/sentinel/session/v3/sessions/{sessionId}/allocations` | May 404 for expired sessions |
1168
+
1169
+ ### Sentinel v2 Endpoints (Still Active)
1170
+
1171
+ | Query | Method | Path | Notes |
1172
+ |-------|--------|------|-------|
1173
+ | Provider | GET | `/sentinel/provider/v2/providers/{sentprov1...}` | **Only provider remains v2** |
1174
+
1175
+ ### Cosmos Standard Endpoints
1176
+
1177
+ | Query | Method | Path |
1178
+ |-------|--------|------|
1179
+ | Balance | GET | `/cosmos/bank/v1beta1/balances/{sent1...}` |
1180
+ | Fee grants (for address) | GET | `/cosmos/feegrant/v1beta1/allowances/{sent1...}` |
1181
+ | Authz grants | GET | `/cosmos/authz/v1beta1/grants?granter={addr}&grantee={addr}` |
1182
+
1183
+ ### Node Direct API
1184
+
1185
+ | Query | Method | URL | Notes |
1186
+ |-------|--------|-----|-------|
1187
+ | Node status | GET | `https://{ip}:{port}/` | Returns moniker, location, peers, bandwidth |
1188
+ | Handshake | POST | `https://{ip}:{port}/` | Session handshake (see Phase 8) |
1189
+
1190
+ ---
1191
+
1192
+ ## Appendix B: Every Error Code with Action
1193
+
1194
+ ### Error Classes
1195
+
1196
+ | Class | Base | When |
1197
+ |-------|------|------|
1198
+ | `SentinelError` | `Error` | Generic SDK errors |
1199
+ | `ValidationError` | `SentinelError` | Input validation failures |
1200
+ | `NodeError` | `SentinelError` | Node-level failures |
1201
+ | `ChainError` | `SentinelError` | Chain/transaction failures |
1202
+ | `TunnelError` | `SentinelError` | Tunnel setup failures |
1203
+ | `SecurityError` | `SentinelError` | Security policy violations |
1204
+
1205
+ ### All 33 Error Codes
1206
+
1207
+ | Code | Severity | Trigger | User Message | Action |
1208
+ |------|----------|---------|-------------|--------|
1209
+ | `INVALID_OPTIONS` | fatal | Bad options object | "Invalid connection options provided." | Fix input, do not retry |
1210
+ | `INVALID_MNEMONIC` | fatal | Bad mnemonic (<12 words) | "Invalid wallet phrase. Must be 12 or 24 words." | Fix input, do not retry |
1211
+ | `INVALID_NODE_ADDRESS` | fatal | Bad sentnode1... address | "Invalid node address." | Fix input, do not retry |
1212
+ | `INVALID_GIGABYTES` | fatal | gigabytes < 1 or > 100 | "Invalid bandwidth amount. Must be a positive number." | Fix input, do not retry |
1213
+ | `INVALID_URL` | fatal | Malformed URL | "Invalid URL format." | Fix input, do not retry |
1214
+ | `INVALID_PLAN_ID` | fatal | Non-numeric plan ID | "Invalid plan ID." | Fix input, do not retry |
1215
+ | `NODE_OFFLINE` | retryable | Node unreachable or handshake failed | "This node is offline. Try a different server." | Try different node |
1216
+ | `NODE_NO_UDVPN` | retryable | Node doesn't accept udvpn denom | "This node does not accept P2P tokens." | Try different node |
1217
+ | `NODE_NOT_FOUND` | retryable | Node not on chain / address mismatch | "Node not found on chain. It may be inactive." | Try different node |
1218
+ | `NODE_CLOCK_DRIFT` | retryable | VMess node with >120s clock drift | "Node clock is out of sync. Try a different server." | Try different node (or VLess node) |
1219
+ | `NODE_INACTIVE` | retryable | Code 105 after retry | "Node went inactive. Try a different server." | Try different node |
1220
+ | `NODE_DATABASE_CORRUPT` | retryable | HTTP 500 with sqlite errors | "Node has a corrupted database. Try a different server." | Try different node |
1221
+ | `INVALID_ASSIGNED_IP` | retryable | Handshake returned bad IP/CIDR | "Node returned an invalid IP address during handshake. Try a different server." | Try different node |
1222
+ | `INSUFFICIENT_BALANCE` | fatal | Wallet < 1.0 P2P | "Not enough P2P tokens. Fund your wallet to continue." | Fund wallet, do not retry |
1223
+ | `BROADCAST_FAILED` | retryable | TX broadcast network error | "Transaction failed. Check your balance and try again." | Retry after delay |
1224
+ | `TX_FAILED` | retryable | TX returned non-zero code | "Chain transaction rejected. Check balance and gas." | Check error, retry |
1225
+ | `LCD_ERROR` | retryable | LCD query returned error code | "Chain query failed. Try again later." | Retry with fallback LCD |
1226
+ | `UNKNOWN_MSG_TYPE` | fatal | Unregistered protobuf type | "Unknown message type. Check SDK version compatibility." | Update SDK |
1227
+ | `ALL_ENDPOINTS_FAILED` | retryable | All LCD/RPC endpoints down | "All chain endpoints are unreachable. Try again later." | Wait and retry |
1228
+ | `SESSION_EXISTS` | recoverable | Active session found for wallet+node | "An active session already exists. Use recoverSession() to resume." | Call `recoverSession()` |
1229
+ | `SESSION_EXTRACT_FAILED` | recoverable | TX OK but no session ID in events | "Session creation succeeded but ID extraction failed. Use recoverSession()." | Call `recoverSession()` |
1230
+ | `SESSION_POISONED` | fatal | Previously failed session reuse attempt | "Session is poisoned (previously failed). Start a new session." | Use `forceNewSession: true` |
1231
+ | `V2RAY_NOT_FOUND` | infrastructure | v2ray.exe missing | "V2Ray binary not found. Check your installation." | Run `npm run setup` |
1232
+ | `V2RAY_ALL_FAILED` | retryable | All V2Ray transports failed | "Could not establish tunnel. Node may be overloaded." | Try different node |
1233
+ | `WG_NOT_AVAILABLE` | fatal | WireGuard not installed | "WireGuard is not available. Install it or use V2Ray nodes." | Install WireGuard |
1234
+ | `WG_NO_CONNECTIVITY` | retryable | Tunnel installed but no traffic | "VPN tunnel has no internet connectivity." | Try different node |
1235
+ | `TUNNEL_SETUP_FAILED` | retryable | Generic tunnel error | "Tunnel setup failed. Try again or pick another server." | Try different node |
1236
+ | `TLS_CERT_CHANGED` | infrastructure | TOFU certificate mismatch | "Node certificate changed unexpectedly. This could indicate a security issue." | Investigate; clear TOFU store if intentional |
1237
+ | `ABORTED` | -- | AbortController signal / disconnect during connect | "Connection was cancelled." | User-initiated; no action |
1238
+ | `ALL_NODES_FAILED` | retryable | connectAuto exhausted all candidates | "All servers failed. Check your network connection." | Check network; increase maxAttempts |
1239
+ | `ALREADY_CONNECTED` | -- | connect() called while connected | "Already connected. Disconnect first." | Call `disconnect()` first |
1240
+ | `PARTIAL_CONNECTION_FAILED` | recoverable | Payment OK, tunnel failed | "Payment succeeded but connection failed. Use recoverSession() to retry." | Call `recoverSession()` |
1241
+ | `CHAIN_LAG` | retryable | Session not confirmed on node | "Session not yet confirmed on node. Wait a moment and try again." | Wait 10s, retry |
1242
+
1243
+ ### Severity Classification
1244
+
1245
+ | Severity | Meaning | Retry? | Action |
1246
+ |----------|---------|--------|--------|
1247
+ | `fatal` | User action required | No | Fix input, fund wallet, install dependency |
1248
+ | `retryable` | Transient failure | Yes, different node | Use `connectAuto()` for automatic fallback |
1249
+ | `recoverable` | Partial success | Yes, same session | Call `recoverSession()` to resume |
1250
+ | `infrastructure` | System issue | No | Fix system state (install binary, check certs) |
1251
+
1252
+ ### Usage
1253
+
1254
+ ```javascript
1255
+ import { ErrorCodes, ERROR_SEVERITY, isRetryable, userMessage } from 'sentinel-dvpn-sdk';
1256
+
1257
+ try {
1258
+ await connectDirect({ ... });
1259
+ } catch (err) {
1260
+ console.log(err.code); // 'V2RAY_ALL_FAILED'
1261
+ console.log(err.name); // 'TunnelError'
1262
+ console.log(userMessage(err)); // 'Could not establish tunnel...'
1263
+ console.log(ERROR_SEVERITY[err.code]); // 'retryable'
1264
+ console.log(isRetryable(err)); // true
1265
+ console.log(err.details); // { sessionId, nodeAddress, failedAt }
1266
+ }
1267
+ ```
1268
+
1269
+ ---
1270
+
1271
+ ## Appendix C: Configuration That Silently Fails If Wrong
1272
+
1273
+ These are settings where an incorrect value produces NO error message -- the connection simply doesn't work, traffic doesn't flow, or data is silently wrong. Every one of these has caused real production failures.
1274
+
1275
+ ### Protocol Configuration
1276
+
1277
+ | # | Setting | Correct Value | Wrong Value | What Happens |
1278
+ |---|---------|--------------|-------------|-------------|
1279
+ | 1 | VLess encryption | `"none"` | Any other string | Connection accepted but zero traffic flows |
1280
+ | 2 | VLess flow field | **Omit entirely** | `""` or `"xtls-rprx-vision"` | V2Ray silently drops to fallback |
1281
+ | 3 | VMess alterId | `0` | Any non-zero value | Authentication fails silently |
1282
+ | 4 | VMess user security | **Omit from user object** | `"auto"` or `"aes-128-gcm"` | Config parsing fails silently |
1283
+ | 5 | UUID field name | `"uuid"` | `"id"` | V2Ray cannot match incoming connection |
1284
+ | 6 | grpc serviceName | `grpcSettings: { serviceName: '' }` | Omitting grpcSettings entirely | gRPC transport silently fails |
1285
+ | 7 | QUIC security | `security: 'none'` | `security: 'chacha20-poly1305'` | QUIC handshake fails silently |
1286
+ | 8 | gun vs grpc in V2Ray config | Both use `"network": "grpc"` with grpcSettings | Using `"network": "gun"` | V2Ray does not recognize "gun" network |
1287
+
1288
+ ### Signature & Handshake
1289
+
1290
+ | # | Setting | Correct Value | Wrong Value | What Happens |
1291
+ |---|---------|--------------|-------------|-------------|
1292
+ | 9 | Signature input | `SHA256(BigEndian_uint64(sessionId) + raw_data_bytes)` | Using base64-encoded data string | Signature verification fails, 401 |
1293
+ | 10 | Signature length | Exactly 64 bytes (r + s) | 65 bytes (with recovery byte) | Go `VerifySignature` returns false |
1294
+ | 11 | Session ID in POST body | JavaScript Number (not BigInt) | BigInt | `JSON.stringify` throws TypeError |
1295
+ | 12 | Public key encoding | `"secp256k1:" + base64(compressed_33_bytes)` | Uncompressed key (65 bytes) | Key format mismatch, rejected |
1296
+
1297
+ ### Chain Queries
1298
+
1299
+ | # | Setting | Correct Value | Wrong Value | What Happens |
1300
+ |---|---------|--------------|-------------|-------------|
1301
+ | 13 | LCD API version | v3 paths (`/sentinel/node/v3/...`) | v2 paths | Returns "Not Implemented" |
1302
+ | 14 | Provider API version | v2 path (`/sentinel/provider/v2/...`) | v3 path | Returns 501 |
1303
+ | 15 | Status filter | `status=1` (integer) | `status=STATUS_ACTIVE` (string) | Returns wrong or empty results |
1304
+ | 16 | Node type field | `service_type` (v3) | `type` (v2) | `undefined` -- no error, just missing data |
1305
+ | 17 | Remote URL field | `remote_addrs` (array) | `remote_url` (string) | `undefined` -- all connections fail |
1306
+ | 18 | Session data | `session.base_session.id` | `session.id` | `undefined` -- silent null propagation |
1307
+ | 19 | Account address field | `acc_address` (v3) | `address` (v2) | `undefined` -- subscription parsing fails |
1308
+ | 20 | Pagination | `limit=5000` single request | Trust `count_total` or `next_key` | Missing 400+ nodes silently |
1309
+
1310
+ ### Tunnel Configuration
1311
+
1312
+ | # | Setting | Correct Value | Wrong Value | What Happens |
1313
+ |---|---------|--------------|-------------|-------------|
1314
+ | 21 | WireGuard config path (Windows) | `C:\ProgramData\sentinel-wg\` | User temp dir | SYSTEM service cannot read file -- silent fail |
1315
+ | 22 | axios SOCKS5 adapter | `axios` with `adapter: 'http'` | Native `fetch` | Proxy agent silently ignored, direct connection |
1316
+ | 23 | System proxy + SOCKS5 auth | `noauth` when systemProxy is true | Username/password auth | System proxy cannot pass credentials, zero traffic |
1317
+ | 24 | SOCKS5 port reuse in fallback | Unique port per outbound | Same port | TIME_WAIT (120s on Windows) blocks connection |
1318
+ | 25 | Full tunnel default in dev | `fullTunnel: false` for development | `fullTunnel: true` | AI's own internet dies (RPC/LCD/npm unreachable) |
1319
+ | 26 | DNS before full tunnel | Pre-resolve hostnames BEFORE installing tunnel | Resolve after tunnel up | DNS fails through tunnel, speed test returns 0 |
1320
+ | 27 | Cleanup handler registration | Call `registerCleanupHandlers()` before connect | Skip it | Crash leaves WireGuard 0.0.0.0/0 route -- dead internet |
1321
+
1322
+ ### Consumer App Integration (from project findings)
1323
+
1324
+ *(Source: Handshake dVPN, Test2, Node Tester -- 22 discovered patterns)*
1325
+
1326
+ | # | Setting | Correct Value | Wrong Value | What Happens |
1327
+ |---|---------|--------------|-------------|-------------|
1328
+ | 28 | Price display field | `quote_value` (integer udvpn) | `base_value` (18-decimal sdk.Dec) | UI shows `52573.099722991367791000000000/GB` |
1329
+ | 29 | Fee grant for direct-connect | Disabled / `feeGrant: false` | Auto-detect (assumes plan-based) | TX fails with invalid fee grant |
1330
+ | 30 | Session creation for consumer apps | `forceNewSession: true` | Default (reuse existing) | Stale session 404 "does not exist" errors |
1331
+ | 31 | V2Ray SOCKS5 HttpClient reuse | Fresh client per request | Reuse connections | Stale/empty responses, TLS disconnects |
1332
+ | 32 | WireGuard pre-connect cleanup | Uninstall `wgsent0` before connect | Skip cleanup | New tunnel install fails silently |
1333
+ | 33 | CancellationToken for speed test (C#) | `CancellationToken.None` | Pass parent CT | Speed test cancelled prematurely |
1334
+ | 34 | Shared VPN client for testing | Dedicated test VPN instance | Share with main app | State corruption, tunnel leftovers |
1335
+ | 35 | V2Ray "context canceled" after success | Ignore (normal cleanup) | Treat as error | False failure reporting |
1336
+ | 36 | WPF emoji country flags | PNG images from flagcdn.com | Unicode emoji | Invisible flags (Windows limitation) |
1337
+ | 37 | Country name normalization | 120+ variant map with fuzzy match | Exact string match | "The Netherlands" != "Netherlands" -> no flag |
1338
+ | 38 | Plan endpoint v3 | May return 501 on some LCD endpoints | Assume always works | Unhandled 501, no plan data |
1339
+ | 39 | JSON BigInt serialization | `sessionId.toString()` before JSON | `JSON.stringify({ id: 123n })` | `TypeError: Do not know how to serialize a BigInt` |
1340
+ | 40 | Progress counter on error | Increment on ALL paths (success, error, cancel) | Only on success | UI freezes, appears stuck |
1341
+ | 41 | Background refresh during test | Cancel refresh before starting test | Let refresh run | Chain client contention, connection timeouts |
1342
+ | 42 | Node status check without wallet | ChainClient usable pre-login | Require wallet for all queries | Cannot load nodes before login |
1343
+ | 43 | `PreferHourly` SDK option | Silently creates GB sessions (known bug) | Trust it works | Wrong session type, wrong billing |
1344
+
1345
+ ---
1346
+
1347
+ ## Appendix D: Complete Message Type URLs
1348
+
1349
+ All 22 registered protobuf message types in the CosmJS Registry:
1350
+
1351
+ ### Consumer App Messages
1352
+
1353
+ | Operation | Type URL |
1354
+ |-----------|----------|
1355
+ | Start direct session | `/sentinel.node.v3.MsgStartSessionRequest` |
1356
+ | End/cancel session | `/sentinel.session.v3.MsgCancelSessionRequest` |
1357
+ | Start subscription | `/sentinel.subscription.v3.MsgStartSubscriptionRequest` |
1358
+ | Start session via subscription | `/sentinel.subscription.v3.MsgStartSessionRequest` |
1359
+ | Start session via plan | `/sentinel.plan.v3.MsgStartSessionRequest` |
1360
+ | Cancel subscription | `/sentinel.subscription.v3.MsgCancelSubscriptionRequest` |
1361
+ | Renew subscription | `/sentinel.subscription.v3.MsgRenewSubscriptionRequest` |
1362
+ | Share subscription | `/sentinel.subscription.v3.MsgShareSubscriptionRequest` |
1363
+ | Update subscription | `/sentinel.subscription.v3.MsgUpdateSubscriptionRequest` |
1364
+ | Update session | `/sentinel.session.v3.MsgUpdateSessionRequest` |
1365
+
1366
+ ### Operator/Provider Messages
1367
+
1368
+ | Operation | Type URL |
1369
+ |-----------|----------|
1370
+ | Register provider | `/sentinel.provider.v3.MsgRegisterProviderRequest` |
1371
+ | Update provider details | `/sentinel.provider.v3.MsgUpdateProviderDetailsRequest` |
1372
+ | Update provider status | `/sentinel.provider.v3.MsgUpdateProviderStatusRequest` |
1373
+ | Create plan | `/sentinel.plan.v3.MsgCreatePlanRequest` |
1374
+ | Update plan status | `/sentinel.plan.v3.MsgUpdatePlanStatusRequest` |
1375
+ | Update plan details | `/sentinel.plan.v3.MsgUpdatePlanDetailsRequest` |
1376
+ | Link node to plan | `/sentinel.plan.v3.MsgLinkNodeRequest` |
1377
+ | Unlink node from plan | `/sentinel.plan.v3.MsgUnlinkNodeRequest` |
1378
+ | Register node | `/sentinel.node.v3.MsgRegisterNodeRequest` |
1379
+ | Update node details | `/sentinel.node.v3.MsgUpdateNodeDetailsRequest` |
1380
+ | Update node status | `/sentinel.node.v3.MsgUpdateNodeStatusRequest` |
1381
+
1382
+ ### Lease Messages (v1)
1383
+
1384
+ | Operation | Type URL |
1385
+ |-----------|----------|
1386
+ | Start lease | `/sentinel.lease.v1.MsgStartLeaseRequest` |
1387
+ | End lease | `/sentinel.lease.v1.MsgEndLeaseRequest` |
1388
+
1389
+ ---
1390
+
1391
+ ## Appendix E: Timeouts Reference
1392
+
1393
+ | Operation | Timeout | Configurable | Notes |
1394
+ |-----------|---------|-------------|-------|
1395
+ | Handshake POST | 90,000ms | `timeouts.handshake` | Overloaded nodes need 60-90s |
1396
+ | Node status GET | 12,000ms | `timeouts.nodeStatus` | Quick health check |
1397
+ | LCD query | 15,000ms | `timeouts.lcdQuery` | Chain REST API |
1398
+ | V2Ray port ready | 10,000ms | `timeouts.v2rayReady` | SOCKS5 port acceptance |
1399
+ | WireGuard verify | 5,000ms per target | Hardcoded | 2 targets, 1 attempt |
1400
+ | SOCKS5 connectivity test | 10,000ms | Hardcoded | Through V2Ray proxy |
1401
+ | Post-payment wait | 5,000ms | Hardcoded | Node session indexing |
1402
+ | Chain lag retry | 10,000ms | Hardcoded | Handshake 404 retry |
1403
+ | Already-exists retry 1 | 15,000ms | Hardcoded | Session indexing race |
1404
+ | Already-exists retry 2 | 20,000ms | Hardcoded | Final retry before fresh payment |
1405
+ | V2Ray UUID registration | 5,000ms | Hardcoded | Post-handshake warmup |
1406
+ | WireGuard peer registration | 1,500ms / 1,500ms / 2,000ms | Hardcoded | Exponential install retry |
1407
+ | Sequence mismatch retry | 2,000ms / 4,000ms / 6,000ms / 6,000ms / 4,000ms | Hardcoded | Up to 5 retries + final |
1408
+ | Node inactive retry | 15,000ms | Hardcoded | Code 105 stale LCD data |
1409
+ | Circuit breaker TTL | 300,000ms (5 min) | `configureCircuitBreaker()` | Node skip duration |
1410
+ | Node cache TTL | 300,000ms (5 min) | Hardcoded | Background refresh |
1411
+ | Dynamic rate TTL | 604,800,000ms (7 days) | Hardcoded | Transport success rates |
1412
+ | Inter-TX spacing | 7,000ms minimum | Manual | Prevent sequence mismatch |
1413
+
1414
+ ---
1415
+
1416
+ ## Appendix F: Quick Start (Minimal Working Code)
1417
+
1418
+ ```javascript
1419
+ import {
1420
+ registerCleanupHandlers,
1421
+ quickConnect,
1422
+ disconnect,
1423
+ events,
1424
+ } from 'sentinel-dvpn-sdk';
1425
+
1426
+ // Listen to events
1427
+ events.on('progress', ({ event, detail }) => console.log(`[${event}] ${detail}`));
1428
+ events.on('connected', ({ serviceType, nodeAddress }) => {
1429
+ console.log(`Connected via ${serviceType} to ${nodeAddress}`);
1430
+ });
1431
+
1432
+ // One-call connection (handles everything)
1433
+ const conn = await quickConnect({
1434
+ mnemonic: process.env.SENTINEL_MNEMONIC,
1435
+ countries: ['DE', 'NL'], // Preferred countries
1436
+ serviceType: 'v2ray', // 'wireguard' | 'v2ray' | null
1437
+ maxAttempts: 3, // Try up to 3 nodes
1438
+ fullTunnel: true, // Route all traffic through VPN
1439
+ onProgress: (step, detail) => console.log(`[${step}] ${detail}`),
1440
+ });
1441
+
1442
+ console.log(`Session: ${conn.sessionId}`);
1443
+ console.log(`Type: ${conn.serviceType}`);
1444
+ if (conn.socksPort) console.log(`SOCKS5: 127.0.0.1:${conn.socksPort}`);
1445
+ if (conn.vpnIp) console.log(`VPN IP: ${conn.vpnIp}`);
1446
+
1447
+ // ... use VPN ...
1448
+
1449
+ // Disconnect
1450
+ await disconnect();
1451
+ ```
1452
+
1453
+ `quickConnect()` automatically:
1454
+ 1. Calls `registerCleanupHandlers()` (idempotent)
1455
+ 2. Verifies dependencies (V2Ray, WireGuard)
1456
+ 3. Selects best node based on quality scoring
1457
+ 4. Connects with automatic fallback
1458
+ 5. Verifies IP changed
1459
+
1460
+ For more control, use `connectDirect()` (specific node) or `connectAuto()` (auto-fallback without quickConnect's dependency check overhead).
1461
+
1462
+ ---
1463
+
1464
+ ## Appendix G: Consumer App Integration Patterns
1465
+
1466
+ *(Source: Handshake dVPN retrospective, Test2 proving ground, Node Tester 780+ node scans)*
1467
+
1468
+ These patterns were discovered through 165+ hours of production debugging across 8 consumer apps. Every pattern listed here caused real failures.
1469
+
1470
+ ### Dedicated Test VPN Instance (CRITICAL)
1471
+
1472
+ *(Source: Handshake dVPN -- 2 hours debugging state corruption)*
1473
+
1474
+ **NEVER share the main VPN client with a test/audit feature.** Create a fresh VPN client per test node:
1475
+ - Main app VPN: user's active connection, managed by UI
1476
+ - Test VPN: dedicated instance per test, disposed after each node
1477
+ - The two must share NO state (no shared chain client, no shared tunnel, no shared session)
1478
+
1479
+ In C#: `new SentinelVpnClient()` per test. In JS: separate `connectDirect()` call with independent options.
1480
+
1481
+ ### CancellationToken Architecture (C#)
1482
+
1483
+ *(Source: Handshake dVPN -- 1 hour debugging premature cancellation)*
1484
+
1485
+ | Operation | CancellationToken | Why |
1486
+ |-----------|------------------|-----|
1487
+ | Outer test loop (between nodes) | Pass `ct` | Check between nodes, stop cleanly |
1488
+ | Speed test (once started) | `CancellationToken.None` | Let it complete, don't waste the session |
1489
+ | Google connectivity check | `CancellationToken.None` | Let it complete |
1490
+ | Background refresh | Cancel BEFORE starting test | Prevent chain client contention |
1491
+
1492
+ Use `volatile bool _stopRequested` as an additional stop signal -- CancellationToken alone is NOT sufficient because SDK async operations don't respond to cancellation mid-flight.
1493
+
1494
+ ### In-App Node Testing (Level 2)
1495
+
1496
+ *(Source: Handshake dVPN AI-NODE-TEST-INTEGRATION spec)*
1497
+
1498
+ Two levels of testing exist:
1499
+ - **Level 1 (CLI/Browser):** Tests raw protocol (handshake, V2Ray config, transport). Finds SDK bugs.
1500
+ - **Level 2 (In-App):** Tests the APP's own `connect()`/`disconnect()`. Finds integration bugs.
1501
+
1502
+ Level 2 is a thin orchestrator:
1503
+ 1. Gets node list via the app's own API
1504
+ 2. For each node: calls the app's `ConnectDirectAsync()` (not raw SDK)
1505
+ 3. Runs connectivity check + speed test through the tunnel
1506
+ 4. Calls the app's `Disconnect()`
1507
+ 5. Records the result
1508
+
1509
+ **It does NOT reimplement handshake/tunnel logic.** The app is the black box.
1510
+
1511
+ ### Bandwidth Optimization for Consumer Apps
1512
+
1513
+ *(Source: Handshake dVPN -- measured actual bandwidth)*
1514
+
1515
+ | Poll Type | Interval | Cost | Notes |
1516
+ |-----------|----------|------|-------|
1517
+ | Status poll (is tunnel alive?) | 3s | FREE (in-memory) | No chain call |
1518
+ | Allocation check (bytes remaining) | 120s | ~2KB chain query | Node posts to chain every ~5min |
1519
+ | IP check (am I still on VPN?) | 60s | ~0.5KB to ipify.org | Detect tunnel drops |
1520
+ | Balance check | 5min | ~2KB chain query | Doesn't change mid-session |
1521
+ | Total daily overhead when connected | -- | ~3MB | Negligible |
1522
+
1523
+ ### The 6 Questions Every Feature Must Answer
1524
+
1525
+ *(Source: Handshake dVPN retrospective)*
1526
+
1527
+ 1. Does it work on first use? (New user, no data)
1528
+ 2. Does it work on return visit? (Data exists from previous session)
1529
+ 3. Does it work after restart? (App closed, reopened)
1530
+ 4. Does it work after failure? (Crash, network error, cancel)
1531
+ 5. Can the user share the output? (Export, copy)
1532
+ 6. Can the user investigate issues? (Click, expand, filter, sort)
1533
+
1534
+ ### Country Flag Rendering
1535
+
1536
+ *(Source: Handshake dVPN -- 2 hours discovering WPF limitation)*
1537
+
1538
+ | Platform | Method | Notes |
1539
+ |----------|--------|-------|
1540
+ | Web/Electron | `String.fromCodePoint()` emoji | Works in all browsers |
1541
+ | WPF (.NET) | PNG images from `flagcdn.com/w40/{code}.png` | WPF CANNOT render emoji flags |
1542
+ | macOS/iOS (Swift) | Native emoji in NSImage/UILabel | Works natively |
1543
+
1544
+ For WPF: three-layer cache (memory -> disk -> download). Cache permanently (flags don't change). Requires 120+ country name variant map with fuzzy matching (e.g., "The Netherlands" -> NL, "Turkiye"/"Turkey" -> TR).
1545
+
1546
+ ### Results Must Survive Restarts
1547
+
1548
+ *(Source: Handshake dVPN, Node Tester -- both had blank dashboards after restart)*
1549
+
1550
+ - On app startup, load cached results from disk BEFORE rendering any UI
1551
+ - Save results to disk every N nodes (5 recommended) AND on completion AND on stop
1552
+ - Users will force-kill your app -- don't lose 50 results because you only save at the end
1553
+ - NEVER show "No results yet" when results exist on disk
1554
+ - Handle corruption gracefully (reset to empty on parse error, don't crash)
1555
+ - Cap stored results at 2000 entries to prevent unbounded growth