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
package/preflight.js ADDED
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Sentinel SDK — Pre-Flight System Check
3
+ *
4
+ * Run before any connection attempt. Detects:
5
+ * - Missing binaries (WireGuard, V2Ray) with install instructions
6
+ * - Admin/root permissions
7
+ * - Orphaned tunnels from previous crashes
8
+ * - Orphaned V2Ray processes
9
+ * - Conflicting VPN software
10
+ * - Port conflicts
11
+ *
12
+ * Returns actionable steps — not just pass/fail.
13
+ */
14
+
15
+ import { execSync, execFileSync } from 'child_process';
16
+ import { existsSync } from 'fs';
17
+ import { WG_EXE, WG_QUICK, WG_AVAILABLE, IS_ADMIN, emergencyCleanupSync } from './wireguard.js';
18
+
19
+ // ─── Orphaned Tunnel Detection ──────────────────────────────────────────────
20
+
21
+ /**
22
+ * Check for orphaned WireGuard tunnels (left over from crashes).
23
+ * @returns {{ found: boolean, tunnels: string[], cleaned: boolean }}
24
+ */
25
+ export function checkOrphanedTunnels() {
26
+ const result = { found: false, tunnels: [], cleaned: false };
27
+
28
+ if (process.platform === 'win32') {
29
+ try {
30
+ const services = execSync('sc query type= service state= all', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
31
+ const matches = services.match(/WireGuardTunnel\$wgsent\S*/g) || [];
32
+ if (matches.length > 0) {
33
+ result.found = true;
34
+ result.tunnels = matches.map(s => s.replace('WireGuardTunnel$', ''));
35
+ }
36
+ } catch { /* sc query may fail */ }
37
+ } else {
38
+ // Linux/macOS: check for wgsent* interfaces
39
+ try {
40
+ const ifaces = execSync('ip link show 2>/dev/null || ifconfig 2>/dev/null', { encoding: 'utf8', timeout: 3000, stdio: 'pipe' });
41
+ const matches = ifaces.match(/wgsent\d+/g) || [];
42
+ if (matches.length > 0) {
43
+ result.found = true;
44
+ result.tunnels = [...new Set(matches)];
45
+ }
46
+ } catch { /* ip/ifconfig may not exist */ }
47
+ }
48
+
49
+ return result;
50
+ }
51
+
52
+ /**
53
+ * Clean up orphaned WireGuard tunnels.
54
+ * @returns {{ cleaned: number, errors: string[] }}
55
+ */
56
+ export function cleanOrphanedTunnels() {
57
+ const before = checkOrphanedTunnels();
58
+ if (!before.found) return { cleaned: 0, errors: [] };
59
+
60
+ emergencyCleanupSync();
61
+
62
+ const after = checkOrphanedTunnels();
63
+ const cleaned = before.tunnels.length - after.tunnels.length;
64
+ const errors = after.found
65
+ ? [`${after.tunnels.length} tunnel(s) could not be removed: ${after.tunnels.join(', ')}`]
66
+ : [];
67
+
68
+ return { cleaned, errors };
69
+ }
70
+
71
+ // ─── V2Ray Orphan Detection ─────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Check for orphaned V2Ray processes.
75
+ * @returns {{ found: boolean, pids: number[] }}
76
+ */
77
+ export function checkOrphanedV2Ray() {
78
+ const result = { found: false, pids: [] };
79
+
80
+ try {
81
+ if (process.platform === 'win32') {
82
+ const out = execSync('tasklist /FI "IMAGENAME eq v2ray.exe" /NH /FO CSV', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
83
+ const lines = out.split('\n').filter(l => l.includes('v2ray.exe'));
84
+ for (const line of lines) {
85
+ const match = line.match(/"v2ray\.exe","(\d+)"/);
86
+ if (match) result.pids.push(parseInt(match[1], 10));
87
+ }
88
+ } else {
89
+ const out = execSync('pgrep -x v2ray 2>/dev/null || true', { encoding: 'utf8', timeout: 3000, stdio: 'pipe' });
90
+ for (const line of out.trim().split('\n')) {
91
+ const pid = parseInt(line, 10);
92
+ if (!isNaN(pid)) result.pids.push(pid);
93
+ }
94
+ }
95
+ } catch { /* process listing may fail */ }
96
+
97
+ result.found = result.pids.length > 0;
98
+ return result;
99
+ }
100
+
101
+ // ─── Conflicting VPN Detection ──────────────────────────────────────────────
102
+
103
+ /** Known VPN processes that conflict with WireGuard routing. */
104
+ const KNOWN_VPN_PROCESSES = [
105
+ { name: 'NordVPN', process: 'nordvpn', service: 'nordvpn-service' },
106
+ { name: 'ExpressVPN', process: 'expressvpn', service: 'ExpressVpnService' },
107
+ { name: 'Surfshark', process: 'surfshark', service: 'Surfshark' },
108
+ { name: 'ProtonVPN', process: 'protonvpn', service: 'ProtonVPN Service' },
109
+ { name: 'Mullvad', process: 'mullvad-vpn', service: 'mullvad' },
110
+ { name: 'CyberGhost', process: 'cyberghost', service: 'CyberGhostVPN' },
111
+ { name: 'PIA', process: 'pia-client', service: 'PrivateInternetAccessService' },
112
+ { name: 'Windscribe', process: 'windscribe', service: 'WindscribeService' },
113
+ { name: 'TunnelBear', process: 'tunnelbear', service: 'TunnelBearService' },
114
+ { name: 'OpenVPN', process: 'openvpn', service: 'OpenVPNService' },
115
+ ];
116
+
117
+ /**
118
+ * Check for running VPN software that may conflict.
119
+ * @returns {{ conflicts: Array<{ name: string, running: boolean }> }}
120
+ */
121
+ export function checkVpnConflicts() {
122
+ const conflicts = [];
123
+
124
+ if (process.platform === 'win32') {
125
+ try {
126
+ const tasks = execSync('tasklist /NH /FO CSV', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).toLowerCase();
127
+ for (const vpn of KNOWN_VPN_PROCESSES) {
128
+ if (tasks.includes(vpn.process.toLowerCase())) {
129
+ conflicts.push({ name: vpn.name, running: true });
130
+ }
131
+ }
132
+ } catch { /* tasklist may fail */ }
133
+ } else {
134
+ try {
135
+ const ps = execSync('ps aux 2>/dev/null || ps -ef', { encoding: 'utf8', timeout: 3000, stdio: 'pipe' }).toLowerCase();
136
+ for (const vpn of KNOWN_VPN_PROCESSES) {
137
+ if (ps.includes(vpn.process.toLowerCase())) {
138
+ conflicts.push({ name: vpn.name, running: true });
139
+ }
140
+ }
141
+ } catch { /* ps may fail */ }
142
+ }
143
+
144
+ return { conflicts };
145
+ }
146
+
147
+ // ─── Port Conflict Detection ────────────────────────────────────────────────
148
+
149
+ /**
150
+ * Check if common V2Ray SOCKS5 ports are in use.
151
+ * @returns {{ conflicts: Array<{ port: number, inUse: boolean }> }}
152
+ */
153
+ export function checkPortConflicts() {
154
+ const portsToCheck = [10808, 10809, 10810]; // common V2Ray SOCKS ports
155
+ const conflicts = [];
156
+
157
+ try {
158
+ if (process.platform === 'win32') {
159
+ const netstat = execSync('netstat -ano', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
160
+ for (const port of portsToCheck) {
161
+ if (netstat.includes(`:${port} `)) {
162
+ conflicts.push({ port, inUse: true });
163
+ }
164
+ }
165
+ } else {
166
+ for (const port of portsToCheck) {
167
+ try {
168
+ execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf8', timeout: 3000, stdio: 'pipe' });
169
+ conflicts.push({ port, inUse: true });
170
+ } catch { /* port free */ }
171
+ }
172
+ }
173
+ } catch { /* netstat may fail */ }
174
+
175
+ return { conflicts };
176
+ }
177
+
178
+ // ─── Main Preflight Check ───────────────────────────────────────────────────
179
+
180
+ /**
181
+ * Complete pre-flight system check. Run at app startup before any connection.
182
+ *
183
+ * Returns a structured report with:
184
+ * - ok: boolean (true if everything is ready to connect)
185
+ * - issues: array of { severity, message, action, autoFix }
186
+ * - Each issue has a human-readable message and an actionable fix
187
+ *
188
+ * @param {object} [opts]
189
+ * @param {boolean} [opts.autoClean=false] - Auto-clean orphaned tunnels/processes
190
+ * @param {string} [opts.v2rayExePath] - Explicit V2Ray path
191
+ * @returns {object} Pre-flight report
192
+ */
193
+ export function preflight(opts = {}) {
194
+ const issues = [];
195
+
196
+ // ── 1. WireGuard ──
197
+ if (!WG_AVAILABLE) {
198
+ issues.push({
199
+ severity: 'warning',
200
+ component: 'wireguard',
201
+ message: 'WireGuard is not installed.',
202
+ detail: 'WireGuard nodes (faster, more reliable) will not work. V2Ray nodes still work without it.',
203
+ action: process.platform === 'win32'
204
+ ? 'Download and install from: https://download.wireguard.com/windows-client/wireguard-installer.exe'
205
+ : process.platform === 'darwin'
206
+ ? 'Run: brew install wireguard-tools'
207
+ : 'Run: sudo apt install wireguard (Ubuntu/Debian) or sudo dnf install wireguard-tools (Fedora)',
208
+ autoFix: false,
209
+ });
210
+ } else if (!IS_ADMIN) {
211
+ issues.push({
212
+ severity: 'warning',
213
+ component: 'wireguard',
214
+ message: 'WireGuard requires administrator privileges.',
215
+ detail: 'WireGuard is installed but the app is not running as admin. WireGuard nodes will fail. V2Ray nodes still work.',
216
+ action: process.platform === 'win32'
217
+ ? 'Right-click your app → "Run as administrator", or add a manifest with requireAdministrator.'
218
+ : 'Run with: sudo node your-app.js',
219
+ autoFix: false,
220
+ });
221
+ }
222
+
223
+ // ── 2. V2Ray ──
224
+ const findV2Ray = () => {
225
+ const paths = [
226
+ opts.v2rayExePath,
227
+ process.env.V2RAY_PATH,
228
+ './bin/v2ray.exe', './bin/v2ray',
229
+ '../bin/v2ray.exe', '../bin/v2ray',
230
+ ].filter(Boolean);
231
+ for (const p of paths) { if (existsSync(p)) return p; }
232
+ try {
233
+ const cmd = process.platform === 'win32' ? 'where v2ray.exe' : 'which v2ray';
234
+ return execSync(cmd, { encoding: 'utf8', stdio: 'pipe', timeout: 3000 }).trim().split('\n')[0];
235
+ } catch { return null; }
236
+ };
237
+
238
+ const v2path = findV2Ray();
239
+ if (!v2path) {
240
+ issues.push({
241
+ severity: 'warning',
242
+ component: 'v2ray',
243
+ message: 'V2Ray binary not found.',
244
+ detail: 'V2Ray nodes will not work. WireGuard nodes still work without it.',
245
+ action: 'Run: node js-sdk/setup.js (auto-downloads V2Ray 5.2.1), or place v2ray.exe + geoip.dat + geosite.dat in a bin/ folder.',
246
+ autoFix: false,
247
+ });
248
+ }
249
+
250
+ // ── 3. Neither installed ──
251
+ if (!WG_AVAILABLE && !v2path) {
252
+ // Upgrade to error — no protocol available at all
253
+ issues.push({
254
+ severity: 'error',
255
+ component: 'protocols',
256
+ message: 'No VPN protocol available. Cannot connect to any node.',
257
+ detail: 'Neither WireGuard nor V2Ray is installed. You need at least one.',
258
+ action: 'Install WireGuard (recommended) and/or run: node js-sdk/setup.js to download V2Ray.',
259
+ autoFix: false,
260
+ });
261
+ }
262
+
263
+ // ── 4. Orphaned WireGuard tunnels ──
264
+ const orphanedWg = checkOrphanedTunnels();
265
+ if (orphanedWg.found) {
266
+ if (opts.autoClean) {
267
+ const cleaned = cleanOrphanedTunnels();
268
+ if (cleaned.errors.length > 0) {
269
+ issues.push({
270
+ severity: 'warning',
271
+ component: 'wireguard',
272
+ message: `Found ${orphanedWg.tunnels.length} orphaned tunnel(s), cleaned ${cleaned.cleaned}. ${cleaned.errors[0]}`,
273
+ detail: 'Stale tunnels from a previous crash. Some could not be removed automatically.',
274
+ action: process.platform === 'win32'
275
+ ? `Run as admin: sc stop WireGuardTunnel$${orphanedWg.tunnels[0]} && sc delete WireGuardTunnel$${orphanedWg.tunnels[0]}`
276
+ : `Run: sudo wg-quick down ${orphanedWg.tunnels[0]}`,
277
+ autoFix: false,
278
+ });
279
+ }
280
+ // If all cleaned, no issue to report
281
+ } else {
282
+ issues.push({
283
+ severity: 'warning',
284
+ component: 'wireguard',
285
+ message: `Found ${orphanedWg.tunnels.length} orphaned WireGuard tunnel(s): ${orphanedWg.tunnels.join(', ')}`,
286
+ detail: 'Left over from a previous crash or app exit. Will block new connections. Set autoClean: true to fix automatically.',
287
+ action: 'Call preflight({ autoClean: true }) or cleanOrphanedTunnels() to remove them.',
288
+ autoFix: true,
289
+ });
290
+ }
291
+ }
292
+
293
+ // ── 5. Orphaned V2Ray processes ──
294
+ const orphanedV2 = checkOrphanedV2Ray();
295
+ if (orphanedV2.found) {
296
+ issues.push({
297
+ severity: 'info',
298
+ component: 'v2ray',
299
+ message: `Found ${orphanedV2.pids.length} V2Ray process(es) running: PIDs ${orphanedV2.pids.join(', ')}`,
300
+ detail: 'May be from a previous session or another application. These consume SOCKS5 ports.',
301
+ action: 'If these are unexpected, they will be replaced on next connection. No action needed unless ports conflict.',
302
+ autoFix: false,
303
+ });
304
+ }
305
+
306
+ // ── 6. Conflicting VPN software ──
307
+ const vpnCheck = checkVpnConflicts();
308
+ if (vpnCheck.conflicts.length > 0) {
309
+ const names = vpnCheck.conflicts.map(c => c.name).join(', ');
310
+ issues.push({
311
+ severity: 'warning',
312
+ component: 'system',
313
+ message: `Other VPN software detected: ${names}`,
314
+ detail: 'Running multiple VPNs simultaneously can cause routing conflicts, DNS leaks, or connection failures. Disconnect the other VPN before connecting.',
315
+ action: `Disconnect ${names} before using this app.`,
316
+ autoFix: false,
317
+ });
318
+ }
319
+
320
+ // ── 7. Port conflicts ──
321
+ const portCheck = checkPortConflicts();
322
+ if (portCheck.conflicts.length > 0) {
323
+ const ports = portCheck.conflicts.map(c => c.port).join(', ');
324
+ issues.push({
325
+ severity: 'info',
326
+ component: 'v2ray',
327
+ message: `SOCKS5 port(s) already in use: ${ports}`,
328
+ detail: 'V2Ray will use a random port to avoid conflicts. This is usually fine.',
329
+ action: 'No action needed — SDK uses random ports. If you need a specific port, close the process using it.',
330
+ autoFix: false,
331
+ });
332
+ }
333
+
334
+ // ── Summary ──
335
+ const errors = issues.filter(i => i.severity === 'error');
336
+ const warnings = issues.filter(i => i.severity === 'warning');
337
+
338
+ return {
339
+ ok: errors.length === 0,
340
+ ready: {
341
+ wireguard: WG_AVAILABLE && IS_ADMIN,
342
+ v2ray: !!v2path,
343
+ anyProtocol: (WG_AVAILABLE && IS_ADMIN) || !!v2path,
344
+ },
345
+ issues,
346
+ summary: errors.length === 0 && warnings.length === 0
347
+ ? 'All checks passed. Ready to connect.'
348
+ : errors.length > 0
349
+ ? `${errors.length} error(s), ${warnings.length} warning(s). Fix errors before connecting.`
350
+ : `${warnings.length} warning(s). Can still connect with available protocols.`,
351
+ };
352
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Sentinel SDK — Pricing, Display & Filtering Module
3
+ *
4
+ * Extracted from cosmjs-setup.js during domain-module refactor.
5
+ * Provides node price lookups, network overview, display formatting,
6
+ * node filtering, and BigInt-safe serialization.
7
+ *
8
+ * Usage:
9
+ * import { getNodePrices, formatDvpn, filterNodes } from './pricing/index.js';
10
+ * const prices = await getNodePrices('sentnode1abc...');
11
+ * console.log(formatDvpn(prices.gigabyte.udvpn)); // "0.04 P2P"
12
+ */
13
+
14
+ import { lcd } from '../chain/index.js';
15
+ import { fetchActiveNodes } from '../chain/index.js';
16
+ import { LCD_ENDPOINTS, tryWithFallback } from '../config/index.js';
17
+ import { ValidationError, NodeError, ErrorCodes } from '../errors/index.js';
18
+
19
+ // ─── Node Price Lookup ──────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Get standardized prices for a node — abstracts V3 LCD price parsing entirely.
23
+ *
24
+ * Solves the common "NaN / GB" problem by defensively extracting quote_value,
25
+ * base_value, or amount from the nested LCD response structure.
26
+ *
27
+ * @param {string} nodeAddress - sentnode1... address
28
+ * @param {string} [lcdUrl] - LCD endpoint URL (default: cascading fallback across all endpoints)
29
+ * @returns {Promise<{ gigabyte: { dvpn: number, udvpn: number, raw: object|null }, hourly: { dvpn: number, udvpn: number, raw: object|null }, denom: string, nodeAddress: string }>}
30
+ *
31
+ * @example
32
+ * const prices = await getNodePrices('sentnode1abc...');
33
+ * console.log(`${prices.gigabyte.dvpn} P2P/GB, ${prices.hourly.dvpn} P2P/hr`);
34
+ * // Use prices.gigabyte.raw for the full { denom, base_value, quote_value } object
35
+ * // needed by encodeMsgStartSession's max_price field.
36
+ */
37
+ export async function getNodePrices(nodeAddress, lcdUrl) {
38
+ if (typeof nodeAddress !== 'string' || !/^sentnode1[a-z0-9]{38}$/.test(nodeAddress)) {
39
+ throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be a valid sentnode1... bech32 address (46 characters)', { value: nodeAddress });
40
+ }
41
+
42
+ const fetchNode = async (baseUrl) => {
43
+ let nextKey = null;
44
+ let pages = 0;
45
+ do {
46
+ const keyParam = nextKey ? `&pagination.key=${encodeURIComponent(nextKey)}` : '';
47
+ const data = await lcd(baseUrl, `/sentinel/node/v3/nodes?status=1&pagination.limit=500${keyParam}`);
48
+ const nodes = data.nodes || [];
49
+ const found = nodes.find(n => n.address === nodeAddress);
50
+ if (found) return found;
51
+ nextKey = data.pagination?.next_key || null;
52
+ pages++;
53
+ } while (nextKey && pages < 20);
54
+ return null;
55
+ };
56
+
57
+ let node;
58
+ if (lcdUrl) {
59
+ node = await fetchNode(lcdUrl);
60
+ } else {
61
+ const result = await tryWithFallback(LCD_ENDPOINTS, fetchNode, 'getNodePrices');
62
+ node = result.result;
63
+ }
64
+
65
+ if (!node) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found on LCD (may be inactive or deregistered)`, { nodeAddress });
66
+
67
+ function extractPrice(priceArray) {
68
+ if (!Array.isArray(priceArray)) return { dvpn: 0, udvpn: 0, raw: null };
69
+ const entry = priceArray.find(p => p.denom === 'udvpn');
70
+ if (!entry) return { dvpn: 0, udvpn: 0, raw: null };
71
+ // Defensive fallback chain: quote_value (V3 current) -> base_value -> amount (legacy)
72
+ const rawVal = entry.quote_value || entry.base_value || entry.amount || '0';
73
+ const udvpn = parseInt(rawVal, 10) || 0;
74
+ return { dvpn: parseFloat((udvpn / 1_000_000).toFixed(6)), udvpn, raw: entry };
75
+ }
76
+
77
+ return {
78
+ gigabyte: extractPrice(node.gigabyte_prices),
79
+ hourly: extractPrice(node.hourly_prices),
80
+ denom: 'P2P',
81
+ nodeAddress,
82
+ };
83
+ }
84
+
85
+ // ─── Network Overview ───────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Get a quick network overview — total nodes, counts by country and service type, average prices.
89
+ * Perfect for dashboard UIs, onboarding screens, and network health displays.
90
+ *
91
+ * @param {string} [lcdUrl] - LCD endpoint (default: cascading fallback)
92
+ * @returns {Promise<{ totalNodes: number, byCountry: Array<{country: string, count: number}>, byType: {wireguard: number, v2ray: number, unknown: number}, averagePrice: {gigabyteDvpn: number, hourlyDvpn: number}, nodes: Array }>}
93
+ *
94
+ * @example
95
+ * const overview = await getNetworkOverview();
96
+ * console.log(`${overview.totalNodes} nodes across ${overview.byCountry.length} countries`);
97
+ * console.log(`Average: ${overview.averagePrice.gigabyteDvpn.toFixed(3)} P2P/GB`);
98
+ */
99
+ export async function getNetworkOverview(lcdUrl) {
100
+ const fetchFn = async (url) => fetchActiveNodes(url);
101
+ let nodes;
102
+ if (lcdUrl) {
103
+ nodes = await fetchFn(lcdUrl);
104
+ } else {
105
+ const result = await tryWithFallback(LCD_ENDPOINTS, fetchFn, 'getNetworkOverview');
106
+ nodes = result.result;
107
+ }
108
+
109
+ // Filter to nodes that accept udvpn
110
+ const active = nodes.filter(n => n.remote_url && (n.gigabyte_prices || []).some(p => p.denom === 'udvpn'));
111
+
112
+ // Count by country (from LCD metadata, limited — enrichNodes gives better data)
113
+ const countryMap = {};
114
+ for (const n of active) {
115
+ const c = n.location?.country || n.country || 'Unknown';
116
+ countryMap[c] = (countryMap[c] || 0) + 1;
117
+ }
118
+ const byCountry = Object.entries(countryMap)
119
+ .map(([country, count]) => ({ country, count }))
120
+ .sort((a, b) => b.count - a.count);
121
+
122
+ // Count by type (type not in LCD — estimate from service_type field if present)
123
+ const byType = { wireguard: 0, v2ray: 0, unknown: 0 };
124
+ for (const n of active) {
125
+ const t = n.service_type || n.type;
126
+ if (t === 'wireguard' || t === 1) byType.wireguard++;
127
+ else if (t === 'v2ray' || t === 2) byType.v2ray++;
128
+ else byType.unknown++;
129
+ }
130
+
131
+ // Average prices
132
+ let gbTotal = 0, gbCount = 0, hrTotal = 0, hrCount = 0;
133
+ for (const n of active) {
134
+ const gb = (n.gigabyte_prices || []).find(p => p.denom === 'udvpn');
135
+ if (gb?.quote_value) { gbTotal += parseInt(gb.quote_value, 10); gbCount++; }
136
+ const hr = (n.hourly_prices || []).find(p => p.denom === 'udvpn');
137
+ if (hr?.quote_value) { hrTotal += parseInt(hr.quote_value, 10); hrCount++; }
138
+ }
139
+
140
+ return {
141
+ totalNodes: active.length,
142
+ byCountry,
143
+ byType,
144
+ averagePrice: {
145
+ gigabyteDvpn: gbCount > 0 ? (gbTotal / gbCount) / 1_000_000 : 0,
146
+ hourlyDvpn: hrCount > 0 ? (hrTotal / hrCount) / 1_000_000 : 0,
147
+ },
148
+ nodes: active,
149
+ };
150
+ }
151
+
152
+ // ─── Display Formatting ─────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Format a micro-denom (udvpn) amount as a human-readable P2P string.
156
+ *
157
+ * @param {number|string} udvpn - Amount in micro-denom (1 P2P = 1,000,000 udvpn)
158
+ * @param {number} [decimals=2] - Decimal places to show
159
+ * @returns {string} e.g., "0.04 P2P", "47.69 P2P"
160
+ *
161
+ * @example
162
+ * formatDvpn(40152030); // "40.15 P2P"
163
+ * formatDvpn('1000000', 0); // "1 P2P"
164
+ * formatDvpn(500000, 4); // "0.5000 P2P"
165
+ */
166
+ export function formatDvpn(udvpn, decimals = 2) {
167
+ const val = Number(udvpn) / 1_000_000;
168
+ if (isNaN(val)) return '? P2P';
169
+ return `${val.toFixed(decimals)} P2P`;
170
+ }
171
+
172
+ /** Alias for formatDvpn — uses the new P2P token ticker. */
173
+ export const formatP2P = formatDvpn;
174
+
175
+ // ─── Node Filtering ─────────────────────────────────────────────────────────
176
+
177
+ /**
178
+ * Filter a node list by country, service type, and/or max price.
179
+ * Works with results from listNodes(), enrichNodes(), or fetchAllNodes().
180
+ *
181
+ * @param {Array} nodes - Array of node objects
182
+ * @param {object} criteria
183
+ * @param {string} [criteria.country] - Country name (case-insensitive partial match)
184
+ * @param {string} [criteria.serviceType] - 'wireguard' or 'v2ray'
185
+ * @param {number} [criteria.maxPriceDvpn] - Maximum GB price in P2P (e.g., 0.1)
186
+ * @param {number} [criteria.minScore] - Minimum quality score (0-100)
187
+ * @returns {Array} Filtered nodes
188
+ *
189
+ * @example
190
+ * const cheap = filterNodes(nodes, { maxPriceDvpn: 0.05, serviceType: 'v2ray' });
191
+ * const german = filterNodes(nodes, { country: 'Germany' });
192
+ */
193
+ export function filterNodes(nodes, criteria = {}) {
194
+ if (!Array.isArray(nodes)) return [];
195
+ return nodes.filter(node => {
196
+ if (criteria.country) {
197
+ const c = (node.country || node.location?.country || '').toLowerCase();
198
+ if (!c.includes(criteria.country.toLowerCase())) return false;
199
+ }
200
+ if (criteria.serviceType) {
201
+ const t = node.serviceType || node.type || '';
202
+ if (t !== criteria.serviceType) return false;
203
+ }
204
+ if (criteria.maxPriceDvpn != null) {
205
+ const prices = node.gigabytePrices || node.gigabyte_prices || [];
206
+ const entry = prices.find(p => p.denom === 'udvpn');
207
+ if (entry) {
208
+ const dvpn = parseInt(entry.quote_value || entry.base_value || entry.amount || '0', 10) / 1_000_000;
209
+ if (dvpn > criteria.maxPriceDvpn) return false;
210
+ }
211
+ }
212
+ if (criteria.minScore != null && node.qualityScore != null) {
213
+ if (node.qualityScore < criteria.minScore) return false;
214
+ }
215
+ return true;
216
+ });
217
+ }
218
+
219
+ // ─── Serialization ──────────────────────────────────────────────────────────
220
+
221
+ /**
222
+ * Serialize a ConnectResult for JSON APIs. Handles BigInt -> string conversion.
223
+ * Without this, JSON.stringify(connectResult) throws "BigInt can't be serialized".
224
+ *
225
+ * @param {object} result - ConnectResult from connectDirect/connectAuto/connectViaPlan
226
+ * @returns {object} JSON-safe object with sessionId as string
227
+ *
228
+ * @example
229
+ * const conn = await connectDirect(opts);
230
+ * res.json(serializeResult(conn)); // Safe for Express response
231
+ */
232
+ export function serializeResult(result) {
233
+ if (!result || typeof result !== 'object') return result;
234
+ const out = {};
235
+ for (const [key, val] of Object.entries(result)) {
236
+ if (typeof val === 'bigint') out[key] = String(val);
237
+ else if (typeof val === 'function') continue; // skip cleanup()
238
+ else out[key] = val;
239
+ }
240
+ return out;
241
+ }
242
+
243
+ // ─── PriceResolver Class ────────────────────────────────────────────────────
244
+
245
+ /**
246
+ * Static class providing a unified API for all pricing operations.
247
+ * Convenience wrapper around the standalone functions above.
248
+ *
249
+ * @example
250
+ * const prices = await PriceResolver.getNodePrices('sentnode1abc...');
251
+ * const overview = await PriceResolver.getNetworkOverview();
252
+ * const display = PriceResolver.format(prices.gigabyte.udvpn);
253
+ * const filtered = PriceResolver.filter(overview.nodes, { country: 'Germany' });
254
+ * const safe = PriceResolver.serialize(connectResult);
255
+ */
256
+ export class PriceResolver {
257
+ static async getNodePrices(nodeAddress, lcdUrl) { return getNodePrices(nodeAddress, lcdUrl); }
258
+ static async getNetworkOverview(lcdUrl) { return getNetworkOverview(lcdUrl); }
259
+ static format(udvpn, decimals) { return formatDvpn(udvpn, decimals); }
260
+ static filter(nodes, criteria) { return filterNodes(nodes, criteria); }
261
+ static serialize(result) { return serializeResult(result); }
262
+ }