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,255 @@
1
+ /**
2
+ * Sentinel dVPN SDK — Hardcoded Defaults & Recommended Values
3
+ *
4
+ * SINGLE SOURCE OF TRUTH for all values that may go stale.
5
+ * When the RPC query server is built, this file gets replaced by live lookups.
6
+ *
7
+ * ┌──────────────────────────────────────────────────────────────────────┐
8
+ * │ LAST VERIFIED: 2026-03-08T00:00:00Z │
9
+ * │ VERIFIED BY: 708-node scan + manual LCD/RPC checks │
10
+ * │ CHAIN: sentinelhub-2 (sentinelhub v12.0.0, Cosmos 0.47.17)│
11
+ * │ │
12
+ * │ These values are HARDCODED for easy cold-start setup. │
13
+ * │ A future RPC query server will replace static defaults with live │
14
+ * │ endpoint health checks, node scoring, and price feeds. │
15
+ * └──────────────────────────────────────────────────────────────────────┘
16
+ */
17
+
18
+ // ─── Axios adapter fix (MUST run before any HTTP requests) ──────────────────
19
+ // Node.js 18+ uses undici internally. Without this, ~40% of HTTP requests
20
+ // fail with opaque "fetch failed" errors (no stack trace, no errno).
21
+ import axios from 'axios';
22
+ axios.defaults.adapter = 'http';
23
+
24
+ // ─── SDK Version ─────────────────────────────────────────────────────────────
25
+ // This is the npm/semver version for consumers. Internal development iterations
26
+ // (v20, v21, v22, etc.) track feature milestones and are not exposed as exports.
27
+
28
+ export const SDK_VERSION = '1.0.0';
29
+
30
+ // ─── Timestamps ──────────────────────────────────────────────────────────────
31
+
32
+ /** When these defaults were last verified against the live chain */
33
+ export const LAST_VERIFIED = '2026-03-08T00:00:00Z';
34
+
35
+ /** Human-readable note for builders */
36
+ export const HARDCODED_NOTE = 'Static defaults — no RPC query server yet. Verify endpoints are live before production use. See README.md "Hardcoded Defaults" section.';
37
+
38
+ // ─── Chain ───────────────────────────────────────────────────────────────────
39
+
40
+ export const CHAIN_ID = 'sentinelhub-2';
41
+ export const DENOM = 'udvpn';
42
+ export const GAS_PRICE = '0.2udvpn'; // Chain minimum as of 2026-03-08
43
+ export const CHAIN_VERSION = 'v12.0.0'; // sentinelhub version
44
+ export const COSMOS_SDK_VERSION = '0.47.17';
45
+
46
+ // ─── RPC Endpoints (TX broadcast) ────────────────────────────────────────────
47
+ // Ordered by reliability. Primary is tried first, fallbacks on failure.
48
+ // Verified reachable 2026-03-08.
49
+
50
+ export const RPC_ENDPOINTS = [
51
+ { url: 'https://rpc.sentinel.co:443', name: 'Sentinel Official', verified: '2026-03-08' },
52
+ { url: 'https://sentinel-rpc.polkachu.com', name: 'Polkachu', verified: '2026-03-08' },
53
+ { url: 'https://rpc.mathnodes.com', name: 'MathNodes', verified: '2026-03-08' },
54
+ { url: 'https://sentinel-rpc.publicnode.com', name: 'PublicNode', verified: '2026-03-08' },
55
+ { url: 'https://rpc.sentinel.quokkastake.io', name: 'QuokkaStake', verified: '2026-03-08' },
56
+ ];
57
+
58
+ export const DEFAULT_RPC = RPC_ENDPOINTS[0].url;
59
+
60
+ // ─── LCD Endpoints (REST queries) ────────────────────────────────────────────
61
+ // Ordered by reliability. All have same limitations (v3 providers = 501, plan details = 501).
62
+ // Verified reachable 2026-03-08.
63
+
64
+ export const LCD_ENDPOINTS = [
65
+ { url: 'https://lcd.sentinel.co', name: 'Sentinel Official', verified: '2026-03-08' },
66
+ { url: 'https://sentinel-api.polkachu.com', name: 'Polkachu', verified: '2026-03-08' },
67
+ { url: 'https://api.sentinel.quokkastake.io', name: 'QuokkaStake', verified: '2026-03-08' },
68
+ { url: 'https://sentinel-rest.publicnode.com', name: 'PublicNode', verified: '2026-03-08' },
69
+ ];
70
+
71
+ export const DEFAULT_LCD = LCD_ENDPOINTS[0].url;
72
+
73
+ // ─── V2Ray ───────────────────────────────────────────────────────────────────
74
+
75
+ export const V2RAY_VERSION = '5.2.1';
76
+ /** WARNING: v5.44.1 has observatory bugs. Do NOT upgrade past 5.2.1. Verified 2026-03-08. */
77
+ export const V2RAY_VERSION_WARNING = 'v5.2.1 exactly — v5.44.1+ has observatory/balancer bugs that break multi-outbound configs';
78
+
79
+ // ─── Transport Success Rates (from 780-node scan, 2026-03-09) ────────────────
80
+ // Used by buildV2RayClientConfig() to sort outbounds by reliability.
81
+ // Dynamic rates (in-memory) override these when available — see below.
82
+
83
+ export const TRANSPORT_SUCCESS_RATES = {
84
+ 'tcp': { rate: 1.00, sample: 274, note: 'Best — always first choice' },
85
+ 'websocket': { rate: 1.00, sample: 23, note: 'Second choice' },
86
+ 'http': { rate: 1.00, sample: 4, note: '' },
87
+ 'gun': { rate: 1.00, sample: 10, note: 'gun(2) ≠ grpc(3) — different protocols' },
88
+ 'mkcp': { rate: 1.00, sample: 5, note: '' },
89
+ 'grpc/none': { rate: 0.87, sample: 81, note: '70/81 pass. serverName TLS fix applied.' },
90
+ 'quic': { rate: 0.00, sample: 4, note: '0/4 — chacha20 mismatch fixed, low node count' },
91
+ 'grpc/tls': { rate: 0.00, sample: 0, note: 'serverName TLS fix applied. No test nodes available.' },
92
+ };
93
+
94
+ // ─── Dynamic Transport Rate Tracking (in-memory) ────────────────────────────
95
+ // Runtime success/failure tracking per transport type. Overrides hardcoded
96
+ // TRANSPORT_SUCCESS_RATES when enough samples exist. Resets on process restart.
97
+ // A future version will persist these to disk (see suggestions/dynamic-transport-rates.md).
98
+
99
+ const _dynamicRates = new Map(); // transportKey -> { success, fail }
100
+
101
+ /** Record a transport connection result. Called automatically by setupV2Ray. */
102
+ export function recordTransportResult(transportKey, success) {
103
+ const entry = _dynamicRates.get(transportKey) || { success: 0, fail: 0 };
104
+ if (success) entry.success++; else entry.fail++;
105
+ _dynamicRates.set(transportKey, entry);
106
+ }
107
+
108
+ /**
109
+ * Get the dynamic success rate for a transport. Returns null if < 2 samples.
110
+ * Used by transportSortKey() in v3protocol.js to prioritize transports.
111
+ */
112
+ export function getDynamicRate(transportKey) {
113
+ const entry = _dynamicRates.get(transportKey);
114
+ if (!entry) return null;
115
+ const total = entry.success + entry.fail;
116
+ if (total < 2) return null;
117
+ return entry.success / total;
118
+ }
119
+
120
+ /** Get all dynamic rates as { transportKey: { rate, sample } }. */
121
+ export function getDynamicRates() {
122
+ const result = {};
123
+ for (const [key, entry] of _dynamicRates) {
124
+ const total = entry.success + entry.fail;
125
+ if (total > 0) result[key] = { rate: entry.success / total, sample: total };
126
+ }
127
+ return result;
128
+ }
129
+
130
+ /** Clear all dynamic rate data. */
131
+ export function resetDynamicRates() {
132
+ _dynamicRates.clear();
133
+ }
134
+
135
+ // ─── Recommended Starter Nodes ───────────────────────────────────────────────
136
+ // High-reliability nodes from 708-node scan (2026-03-08).
137
+ // These had 100% connection success, low drift, WireGuard or proven V2Ray transports.
138
+ //
139
+ // ⚠ STALE WARNING: Nodes go offline. These are starting points, NOT guarantees.
140
+ // No hardcoded node list — nodes go offline unpredictably.
141
+ // Use queryOnlineNodes() for live, scored results:
142
+ // const nodes = await queryOnlineNodes({ lcdUrl: DEFAULT_LCD, maxNodes: 50 });
143
+ // // Returns nodes sorted by quality score (WG preferred, clock drift penalized)
144
+
145
+ // ─── Known Broken Nodes (blacklist) ──────────────────────────────────────────
146
+ // Nodes with confirmed bugs. Skip these to avoid wasting P2P.
147
+ // Verified 2026-03-08.
148
+
149
+ export const BROKEN_NODES = [
150
+ { address: 'sentnode1qqktst6793vdxknvvkewfcmtv9edh7vvdvavrj', reason: 'nil UUID state — handshake always fails', verified: '2026-03-08' },
151
+ { address: 'sentnode1qx2p7kyep6m44ae47yh9zf3cfxrzrv5zt9vdnj', reason: 'handshake OK but proxy always fails (0 bytes)', verified: '2026-03-08' },
152
+ ];
153
+
154
+ // ─── Pricing Reference (from chain data, 2026-03-08) ─────────────────────────
155
+ // Typical values — actual prices vary per node. These are for estimation only.
156
+
157
+ export const PRICING_REFERENCE = {
158
+ verified: '2026-03-08',
159
+ note: 'Approximate values for cost estimation. Actual prices vary per node and change over time.',
160
+ session: {
161
+ typicalCostDvpn: 0.1, // ~0.04-0.15 P2P per 1GB session
162
+ minBalanceDvpn: 1, // Minimum recommended wallet balance
163
+ minBalanceUdvpn: 1_000_000, // Same in micro-denom
164
+ },
165
+ gasPerMsg: {
166
+ startSession: 200_000, // ~200k gas for MsgStartSession
167
+ startSubscription: 250_000, // ~250k gas for subscription + session
168
+ createPlan: 300_000, // ~300k gas
169
+ startLease: 250_000, // ~250k gas
170
+ batchOf5: 800_000, // ~800k gas for 5 MsgStartSession batch
171
+ },
172
+ averageNodePrices: {
173
+ gigabyteQuoteValue: '40152030', // Average udvpn per GB (0.04 P2P)
174
+ hourlyQuoteValue: '18384000', // Average udvpn per hour (0.018 P2P)
175
+ baseValue: '0.003000000000000000', // Typical base_value (sdk.Dec)
176
+ },
177
+ };
178
+
179
+ // ─── Shared Utilities ─────────────────────────────────────────────────────────
180
+
181
+ /** Promise-based delay. Used across node-connect, wireguard, speedtest. */
182
+ export function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
183
+
184
+ /** Convert bytes transferred over seconds to Mbps. */
185
+ export function bytesToMbps(bytes, seconds, decimals = null) {
186
+ if (!seconds || seconds <= 0) return 0;
187
+ const mbps = (bytes * 8) / seconds / 1_000_000;
188
+ return decimals !== null ? parseFloat(mbps.toFixed(decimals)) : mbps;
189
+ }
190
+
191
+ // ─── Connection Timeouts (ms) ────────────────────────────────────────────────
192
+
193
+ /** Default timeout values used during node connection. Override via opts.timeouts. */
194
+ export const DEFAULT_TIMEOUTS = {
195
+ handshake: 30000, // Max time for WireGuard/V2Ray handshake with node
196
+ nodeStatus: 12000, // Max time to fetch node status from remote URL
197
+ lcdQuery: 15000, // Max time for LCD chain queries
198
+ v2rayReady: 10000, // Max time waiting for V2Ray SOCKS proxy to be ready
199
+ };
200
+
201
+ // ─── Endpoint Health Check ────────────────────────────────────────────────────
202
+
203
+ /**
204
+ * Ping endpoints and return them sorted by latency (fastest first).
205
+ * Unreachable endpoints are moved to the end.
206
+ * @param {Array<{url: string, name: string}>} endpoints
207
+ * @param {number} timeoutMs - Per-endpoint timeout (default 5000ms)
208
+ * @returns {Promise<Array<{url: string, name: string, latencyMs: number|null}>>}
209
+ */
210
+ export async function checkEndpointHealth(endpoints, timeoutMs = 5000) {
211
+ const results = await Promise.all(endpoints.map(async (ep) => {
212
+ const start = Date.now();
213
+ try {
214
+ const controller = new AbortController();
215
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
216
+ await axios.get(`${ep.url}/status`, { signal: controller.signal, timeout: timeoutMs });
217
+ clearTimeout(timer);
218
+ return { ...ep, latencyMs: Date.now() - start };
219
+ } catch {
220
+ return { ...ep, latencyMs: null };
221
+ }
222
+ }));
223
+ // Sort: reachable first (by latency), unreachable last
224
+ return results.sort((a, b) => {
225
+ if (a.latencyMs != null && b.latencyMs != null) return a.latencyMs - b.latencyMs;
226
+ if (a.latencyMs != null) return -1;
227
+ if (b.latencyMs != null) return 1;
228
+ return 0;
229
+ });
230
+ }
231
+
232
+ // ─── Helper: Try endpoints with fallback ─────────────────────────────────────
233
+
234
+ /**
235
+ * Try an async operation against multiple endpoints, returning the first success.
236
+ * Use this for RPC/LCD operations that should fall back to alternatives.
237
+ *
238
+ * @param {Array<{url: string, name: string}>} endpoints - Ordered list of endpoints
239
+ * @param {function} operation - async (url) => result
240
+ * @param {string} label - For error messages (e.g. 'LCD query', 'RPC connect')
241
+ * @returns {Promise<{result: any, endpoint: string}>}
242
+ */
243
+ export async function tryWithFallback(endpoints, operation, label = 'operation') {
244
+ const errors = [];
245
+ for (const ep of endpoints) {
246
+ try {
247
+ const result = await operation(ep.url);
248
+ return { result, endpoint: ep.url, endpointName: ep.name };
249
+ } catch (err) {
250
+ errors.push({ endpoint: ep.url, name: ep.name, error: err.message });
251
+ }
252
+ }
253
+ const tried = errors.map(e => ` ${e.name} (${e.endpoint}): ${e.error}`).join('\n');
254
+ throw new Error(`${label} failed on all ${endpoints.length} endpoints (verified ${LAST_VERIFIED}):\n${tried}\n\nAll endpoints may be down, or your network may be blocking HTTPS. Try curl-ing the URLs manually.`);
255
+ }