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/chain/rpc.js ADDED
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Sentinel SDK — Chain / RPC Query Module
3
+ *
4
+ * RPC-based chain queries via CosmJS QueryClient + ABCI.
5
+ * ~912x faster than LCD for bulk queries. Uses protobuf transport.
6
+ *
7
+ * Falls back to LCD if RPC connection fails.
8
+ *
9
+ * Usage:
10
+ * import { createRpcQueryClient, rpcQueryNodes, rpcQueryNode } from './chain/rpc.js';
11
+ * const rpcClient = await createRpcQueryClient('https://rpc.sentinel.co:443');
12
+ * const nodes = await rpcQueryNodes(rpcClient, { status: 1, limit: 100 });
13
+ */
14
+
15
+ import { Tendermint37Client } from '@cosmjs/tendermint-rpc';
16
+ import { QueryClient, createProtobufRpcClient } from '@cosmjs/stargate';
17
+ import { RPC_ENDPOINTS } from '../defaults.js';
18
+
19
+ // ─── RPC Client Creation ────────────────────────────────────────────────────
20
+
21
+ let _cachedRpcClient = null;
22
+ let _cachedRpcUrl = null;
23
+
24
+ /**
25
+ * Create or return cached RPC query client with ABCI protobuf support.
26
+ * Tries RPC endpoints in order until one connects.
27
+ *
28
+ * @param {string} [rpcUrl] - RPC endpoint URL (defaults to first in RPC_ENDPOINTS)
29
+ * @returns {Promise<{ queryClient: QueryClient, rpc: ReturnType<typeof createProtobufRpcClient>, tmClient: Tendermint37Client }>}
30
+ */
31
+ export async function createRpcQueryClient(rpcUrl) {
32
+ const url = rpcUrl || RPC_ENDPOINTS[0]?.url || 'https://rpc.sentinel.co:443';
33
+
34
+ if (_cachedRpcClient && _cachedRpcUrl === url) return _cachedRpcClient;
35
+
36
+ const tmClient = await Tendermint37Client.connect(url);
37
+ const queryClient = QueryClient.withExtensions(tmClient);
38
+ const rpc = createProtobufRpcClient(queryClient);
39
+
40
+ _cachedRpcClient = { queryClient, rpc, tmClient };
41
+ _cachedRpcUrl = url;
42
+ return _cachedRpcClient;
43
+ }
44
+
45
+ /**
46
+ * Try connecting to RPC endpoints in order, return first success.
47
+ * @returns {Promise<{ queryClient: QueryClient, rpc: ReturnType<typeof createProtobufRpcClient>, tmClient: Tendermint37Client, url: string }>}
48
+ */
49
+ export async function createRpcQueryClientWithFallback() {
50
+ const errors = [];
51
+ for (const ep of RPC_ENDPOINTS) {
52
+ try {
53
+ const client = await createRpcQueryClient(ep.url);
54
+ return { ...client, url: ep.url };
55
+ } catch (err) {
56
+ errors.push({ url: ep.url, error: err.message });
57
+ }
58
+ }
59
+ throw new Error(`All RPC endpoints failed: ${errors.map(e => `${e.url}: ${e.error}`).join('; ')}`);
60
+ }
61
+
62
+ /**
63
+ * Disconnect and clear cached RPC client.
64
+ */
65
+ export function disconnectRpc() {
66
+ if (_cachedRpcClient?.tmClient) {
67
+ _cachedRpcClient.tmClient.disconnect();
68
+ }
69
+ _cachedRpcClient = null;
70
+ _cachedRpcUrl = null;
71
+ }
72
+
73
+ // ─── ABCI Query Helper ─────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Raw ABCI query — sends protobuf-encoded request to a gRPC service path.
77
+ * This is the low-level primitive used by all typed query functions below.
78
+ *
79
+ * @param {QueryClient} queryClient - CosmJS QueryClient
80
+ * @param {string} path - gRPC method path (e.g., '/sentinel.node.v3.QueryService/QueryNodes')
81
+ * @param {Uint8Array} requestBytes - Protobuf-encoded request
82
+ * @returns {Promise<Uint8Array>} Protobuf-encoded response
83
+ */
84
+ async function abciQuery(queryClient, path, requestBytes) {
85
+ const result = await queryClient.queryAbci(path, requestBytes);
86
+ return result.value;
87
+ }
88
+
89
+ // ─── Protobuf Encoding Helpers (minimal, for query requests) ────────────────
90
+
91
+ function encodeVarint(value) {
92
+ let n = BigInt(value);
93
+ const bytes = [];
94
+ do {
95
+ let b = Number(n & 0x7fn);
96
+ n >>= 7n;
97
+ if (n > 0n) b |= 0x80;
98
+ bytes.push(b);
99
+ } while (n > 0n);
100
+ return new Uint8Array(bytes);
101
+ }
102
+
103
+ function encodeString(fieldNum, str) {
104
+ if (!str) return new Uint8Array(0);
105
+ const encoder = new TextEncoder();
106
+ const b = encoder.encode(str);
107
+ const tag = encodeVarint((BigInt(fieldNum) << 3n) | 2n);
108
+ const len = encodeVarint(b.length);
109
+ const result = new Uint8Array(tag.length + len.length + b.length);
110
+ result.set(tag, 0);
111
+ result.set(len, tag.length);
112
+ result.set(b, tag.length + len.length);
113
+ return result;
114
+ }
115
+
116
+ function encodeUint64(fieldNum, value) {
117
+ if (!value) return new Uint8Array(0);
118
+ const tag = encodeVarint((BigInt(fieldNum) << 3n) | 0n);
119
+ const val = encodeVarint(value);
120
+ const result = new Uint8Array(tag.length + val.length);
121
+ result.set(tag, 0);
122
+ result.set(val, tag.length);
123
+ return result;
124
+ }
125
+
126
+ function encodeEnum(fieldNum, value) {
127
+ return encodeUint64(fieldNum, value);
128
+ }
129
+
130
+ function encodeEmbedded(fieldNum, bytes) {
131
+ if (!bytes || bytes.length === 0) return new Uint8Array(0);
132
+ const tag = encodeVarint((BigInt(fieldNum) << 3n) | 2n);
133
+ const len = encodeVarint(bytes.length);
134
+ const result = new Uint8Array(tag.length + len.length + bytes.length);
135
+ result.set(tag, 0);
136
+ result.set(len, tag.length);
137
+ result.set(bytes, tag.length + len.length);
138
+ return result;
139
+ }
140
+
141
+ function encodePagination({ limit = 100, key, countTotal = false, reverse = false } = {}) {
142
+ const parts = [];
143
+ if (key) parts.push(encodeString(1, key)); // key
144
+ parts.push(encodeUint64(2, limit)); // limit
145
+ if (countTotal) parts.push(encodeEnum(4, 1)); // count_total
146
+ if (reverse) parts.push(encodeEnum(5, 1)); // reverse
147
+ return concat(parts);
148
+ }
149
+
150
+ function concat(arrays) {
151
+ const totalLen = arrays.reduce((sum, a) => sum + a.length, 0);
152
+ const result = new Uint8Array(totalLen);
153
+ let offset = 0;
154
+ for (const arr of arrays) {
155
+ result.set(arr, offset);
156
+ offset += arr.length;
157
+ }
158
+ return result;
159
+ }
160
+
161
+ // ─── Protobuf Decoding Helpers (minimal, for query responses) ───────────────
162
+
163
+ /**
164
+ * Decode a protobuf message into a field map.
165
+ * Returns { fieldNumber: { wireType, value } } for each field.
166
+ * Wire types: 0=varint, 2=length-delimited
167
+ */
168
+ function decodeProto(buf) {
169
+ const fields = {};
170
+ let i = 0;
171
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
172
+
173
+ while (i < buf.length) {
174
+ // Read tag
175
+ let tag = 0n;
176
+ let shift = 0n;
177
+ while (i < buf.length) {
178
+ const b = buf[i++];
179
+ tag |= BigInt(b & 0x7f) << shift;
180
+ shift += 7n;
181
+ if (!(b & 0x80)) break;
182
+ }
183
+
184
+ const fieldNum = Number(tag >> 3n);
185
+ const wireType = Number(tag & 0x7n);
186
+
187
+ if (wireType === 0) {
188
+ // Varint
189
+ let val = 0n;
190
+ let s = 0n;
191
+ while (i < buf.length) {
192
+ const b = buf[i++];
193
+ val |= BigInt(b & 0x7f) << s;
194
+ s += 7n;
195
+ if (!(b & 0x80)) break;
196
+ }
197
+ if (!fields[fieldNum]) fields[fieldNum] = [];
198
+ fields[fieldNum].push({ wireType, value: val });
199
+ } else if (wireType === 2) {
200
+ // Length-delimited
201
+ let len = 0n;
202
+ let s = 0n;
203
+ while (i < buf.length) {
204
+ const b = buf[i++];
205
+ len |= BigInt(b & 0x7f) << s;
206
+ s += 7n;
207
+ if (!(b & 0x80)) break;
208
+ }
209
+ const numLen = Number(len);
210
+ const data = buf.slice(i, i + numLen);
211
+ i += numLen;
212
+ if (!fields[fieldNum]) fields[fieldNum] = [];
213
+ fields[fieldNum].push({ wireType, value: data });
214
+ } else if (wireType === 5) {
215
+ // 32-bit fixed
216
+ i += 4;
217
+ } else if (wireType === 1) {
218
+ // 64-bit fixed
219
+ i += 8;
220
+ }
221
+ }
222
+
223
+ return fields;
224
+ }
225
+
226
+ function decodeString(data) {
227
+ return new TextDecoder().decode(data);
228
+ }
229
+
230
+ function decodeRepeatedMessages(fieldEntries) {
231
+ if (!fieldEntries) return [];
232
+ return fieldEntries.map(entry => decodeProto(entry.value));
233
+ }
234
+
235
+ // ─── Node Decoder ───────────────────────────────────────────────────────────
236
+
237
+ function decodePrice(fields) {
238
+ return {
239
+ denom: fields[1]?.[0] ? decodeString(fields[1][0].value) : '',
240
+ base_value: fields[2]?.[0] ? decodeString(fields[2][0].value) : '0',
241
+ quote_value: fields[3]?.[0] ? decodeString(fields[3][0].value) : '0',
242
+ };
243
+ }
244
+
245
+ function decodeNode(fields) {
246
+ return {
247
+ address: fields[1]?.[0] ? decodeString(fields[1][0].value) : '',
248
+ gigabyte_prices: (fields[2] || []).map(f => decodePrice(decodeProto(f.value))),
249
+ hourly_prices: (fields[3] || []).map(f => decodePrice(decodeProto(f.value))),
250
+ remote_addrs: (fields[4] || []).map(f => decodeString(f.value)),
251
+ status: fields[6]?.[0] ? Number(fields[6][0].value) : 0,
252
+ };
253
+ }
254
+
255
+ // ─── Typed Query Functions ──────────────────────────────────────────────────
256
+
257
+ /**
258
+ * Query active nodes via RPC.
259
+ *
260
+ * @param {{ queryClient: QueryClient }} client - From createRpcQueryClient()
261
+ * @param {{ status?: number, limit?: number }} [opts]
262
+ * @returns {Promise<Array<{ address: string, gigabyte_prices: Array, hourly_prices: Array, remote_addrs: string[], status: number }>>}
263
+ */
264
+ export async function rpcQueryNodes(client, { status = 1, limit = 500 } = {}) {
265
+ const path = '/sentinel.node.v3.QueryService/QueryNodes';
266
+ const request = concat([
267
+ encodeEnum(1, status), // status field
268
+ encodeEmbedded(2, encodePagination({ limit })), // pagination field
269
+ ]);
270
+
271
+ const response = await abciQuery(client.queryClient, path, request);
272
+ const fields = decodeProto(new Uint8Array(response));
273
+
274
+ // Field 1 = repeated Node
275
+ const nodes = (fields[1] || []).map(entry => decodeNode(decodeProto(entry.value)));
276
+ return nodes;
277
+ }
278
+
279
+ /**
280
+ * Query a single node by address via RPC.
281
+ *
282
+ * @param {{ queryClient: QueryClient }} client
283
+ * @param {string} address - sentnode1... address
284
+ * @returns {Promise<object|null>}
285
+ */
286
+ export async function rpcQueryNode(client, address) {
287
+ const path = '/sentinel.node.v3.QueryService/QueryNode';
288
+ const request = encodeString(1, address);
289
+
290
+ try {
291
+ const response = await abciQuery(client.queryClient, path, request);
292
+ const fields = decodeProto(new Uint8Array(response));
293
+ // Field 1 = Node
294
+ if (!fields[1]?.[0]) return null;
295
+ return decodeNode(decodeProto(fields[1][0].value));
296
+ } catch {
297
+ return null;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Query nodes linked to a plan via RPC.
303
+ *
304
+ * @param {{ queryClient: QueryClient }} client
305
+ * @param {number|bigint} planId
306
+ * @param {{ status?: number, limit?: number }} [opts]
307
+ * @returns {Promise<Array>}
308
+ */
309
+ export async function rpcQueryNodesForPlan(client, planId, { status = 1, limit = 500 } = {}) {
310
+ const path = '/sentinel.node.v3.QueryService/QueryNodesForPlan';
311
+ const request = concat([
312
+ encodeUint64(1, planId), // id
313
+ encodeEnum(2, status), // status
314
+ encodeEmbedded(3, encodePagination({ limit })), // pagination
315
+ ]);
316
+
317
+ const response = await abciQuery(client.queryClient, path, request);
318
+ const fields = decodeProto(new Uint8Array(response));
319
+ return (fields[1] || []).map(entry => decodeNode(decodeProto(entry.value)));
320
+ }
321
+
322
+ /**
323
+ * Query sessions for an account via RPC.
324
+ *
325
+ * @param {{ queryClient: QueryClient }} client
326
+ * @param {string} address - sent1... address
327
+ * @param {{ limit?: number }} [opts]
328
+ * @returns {Promise<Array<Uint8Array>>} Raw session Any-encoded bytes (need type-specific decoding)
329
+ */
330
+ export async function rpcQuerySessionsForAccount(client, address, { limit = 100 } = {}) {
331
+ const path = '/sentinel.session.v3.QueryService/QuerySessionsForAccount';
332
+ const request = concat([
333
+ encodeString(1, address),
334
+ encodeEmbedded(2, encodePagination({ limit })),
335
+ ]);
336
+
337
+ const response = await abciQuery(client.queryClient, path, request);
338
+ const fields = decodeProto(new Uint8Array(response));
339
+ // Field 1 = repeated google.protobuf.Any (sessions)
340
+ return (fields[1] || []).map(entry => entry.value);
341
+ }
342
+
343
+ /**
344
+ * Query subscriptions for an account via RPC.
345
+ *
346
+ * @param {{ queryClient: QueryClient }} client
347
+ * @param {string} address - sent1... address
348
+ * @param {{ limit?: number }} [opts]
349
+ * @returns {Promise<Array<Uint8Array>>} Raw subscription bytes
350
+ */
351
+ export async function rpcQuerySubscriptionsForAccount(client, address, { limit = 100 } = {}) {
352
+ const path = '/sentinel.subscription.v3.QueryService/QuerySubscriptionsForAccount';
353
+ const request = concat([
354
+ encodeString(1, address),
355
+ encodeEmbedded(2, encodePagination({ limit })),
356
+ ]);
357
+
358
+ const response = await abciQuery(client.queryClient, path, request);
359
+ const fields = decodeProto(new Uint8Array(response));
360
+ return (fields[1] || []).map(entry => entry.value);
361
+ }
362
+
363
+ /**
364
+ * Query a single plan by ID via RPC.
365
+ *
366
+ * @param {{ queryClient: QueryClient }} client
367
+ * @param {number|bigint} planId
368
+ * @returns {Promise<Uint8Array|null>} Raw plan bytes
369
+ */
370
+ export async function rpcQueryPlan(client, planId) {
371
+ const path = '/sentinel.plan.v3.QueryService/QueryPlan';
372
+ const request = encodeUint64(1, planId);
373
+
374
+ try {
375
+ const response = await abciQuery(client.queryClient, path, request);
376
+ const fields = decodeProto(new Uint8Array(response));
377
+ return fields[1]?.[0]?.value || null;
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Query wallet balance via RPC (uses cosmos bank module).
385
+ *
386
+ * @param {{ queryClient: QueryClient }} client
387
+ * @param {string} address - sent1... address
388
+ * @param {string} [denom='udvpn']
389
+ * @returns {Promise<{ denom: string, amount: string }>}
390
+ */
391
+ export async function rpcQueryBalance(client, address, denom = 'udvpn') {
392
+ const path = '/cosmos.bank.v1beta1.Query/Balance';
393
+ const request = concat([
394
+ encodeString(1, address),
395
+ encodeString(2, denom),
396
+ ]);
397
+
398
+ const response = await abciQuery(client.queryClient, path, request);
399
+ const fields = decodeProto(new Uint8Array(response));
400
+
401
+ // Field 1 = Coin (embedded)
402
+ if (!fields[1]?.[0]) return { denom, amount: '0' };
403
+ const coinFields = decodeProto(fields[1][0].value);
404
+ return {
405
+ denom: coinFields[1]?.[0] ? decodeString(coinFields[1][0].value) : denom,
406
+ amount: coinFields[2]?.[0] ? decodeString(coinFields[2][0].value) : '0',
407
+ };
408
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Sentinel SDK — Chain / Wallet Module
3
+ *
4
+ * Wallet creation, mnemonic validation, private key derivation,
5
+ * and bech32 address prefix conversion.
6
+ *
7
+ * Usage:
8
+ * import { createWallet, generateWallet, privKeyFromMnemonic } from './chain/wallet.js';
9
+ * const { wallet, account } = await createWallet(mnemonic);
10
+ */
11
+
12
+ import { Bip39, EnglishMnemonic, Slip10, Slip10Curve, Random } from '@cosmjs/crypto';
13
+ import { makeCosmoshubPath } from '@cosmjs/amino';
14
+ import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
15
+ import { fromBech32, toBech32 } from '@cosmjs/encoding';
16
+ import { ValidationError, ErrorCodes } from '../errors.js';
17
+
18
+ // ─── Input Validation Helpers ────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Validate a BIP39 mnemonic string against the English wordlist.
22
+ * Returns true if valid (12/15/18/21/24 words, all in BIP39 list, valid checksum).
23
+ * Use this to enable/disable a "Connect" button in your UI.
24
+ *
25
+ * @param {string} mnemonic - The mnemonic to validate
26
+ * @returns {boolean} True if the mnemonic is a valid BIP39 English mnemonic
27
+ *
28
+ * @example
29
+ * if (isMnemonicValid(userInput)) showConnectButton();
30
+ */
31
+ export function isMnemonicValid(mnemonic) {
32
+ if (typeof mnemonic !== 'string') return false;
33
+ const trimmed = mnemonic.trim();
34
+ if (trimmed.split(/\s+/).length < 12) return false;
35
+ try {
36
+ // EnglishMnemonic constructor validates word count, wordlist membership, and checksum
37
+ new EnglishMnemonic(trimmed);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ export function validateMnemonic(mnemonic, fnName) {
45
+ if (typeof mnemonic !== 'string') {
46
+ throw new ValidationError(ErrorCodes.INVALID_MNEMONIC,
47
+ `${fnName}(): mnemonic must be a string`, { wordCount: 0 });
48
+ }
49
+ const words = mnemonic.trim().split(/\s+/);
50
+ if (words.length < 12) {
51
+ throw new ValidationError(ErrorCodes.INVALID_MNEMONIC,
52
+ `${fnName}(): mnemonic must have at least 12 words`,
53
+ { wordCount: words.length });
54
+ }
55
+ if (!isMnemonicValid(mnemonic)) {
56
+ throw new ValidationError(ErrorCodes.INVALID_MNEMONIC,
57
+ `${fnName}(): mnemonic contains invalid BIP39 words or failed checksum`,
58
+ { wordCount: words.length });
59
+ }
60
+ }
61
+
62
+ export function validateAddress(addr, prefix, fnName) {
63
+ if (typeof addr !== 'string' || !addr.startsWith(prefix)) {
64
+ throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS,
65
+ `${fnName}(): address must be a valid ${prefix}... bech32 string`,
66
+ { value: addr });
67
+ }
68
+ }
69
+
70
+ // ─── Wallet ──────────────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Create a Sentinel wallet from a BIP39 mnemonic.
74
+ * Returns { wallet, account } where account.address is the sent1... address.
75
+ */
76
+ export async function createWallet(mnemonic) {
77
+ validateMnemonic(mnemonic, 'createWallet');
78
+ const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'sent' });
79
+ const [account] = await wallet.getAccounts();
80
+ return { wallet, account };
81
+ }
82
+
83
+ /**
84
+ * Generate a new wallet with a fresh random BIP39 mnemonic.
85
+ * @param {number} strength - 128 for 12 words, 256 for 24 words (default: 128)
86
+ * @returns {{ mnemonic: string, wallet: DirectSecp256k1HdWallet, account: { address: string } }}
87
+ */
88
+ export async function generateWallet(strength = 128) {
89
+ const entropy = Random.getBytes(strength / 8);
90
+ const mnemonic = Bip39.encode(entropy).toString();
91
+ const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'sent' });
92
+ const [account] = await wallet.getAccounts();
93
+ return { mnemonic, wallet, account };
94
+ }
95
+
96
+ /**
97
+ * Derive the raw secp256k1 private key from a mnemonic.
98
+ * Needed for handshake signatures (node-handshake protocol).
99
+ */
100
+ export async function privKeyFromMnemonic(mnemonic) {
101
+ validateMnemonic(mnemonic, 'privKeyFromMnemonic');
102
+ const seed = await Bip39.mnemonicToSeed(new EnglishMnemonic(mnemonic));
103
+ const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, makeCosmoshubPath(0));
104
+ return Buffer.from(privkey);
105
+ }
106
+
107
+ // ─── Address Prefix Conversion ───────────────────────────────────────────────
108
+ // Same key, different bech32 prefix. See address-prefixes.md.
109
+
110
+ export function sentToSentprov(sentAddr) {
111
+ validateAddress(sentAddr, 'sent', 'sentToSentprov');
112
+ const { data } = fromBech32(sentAddr);
113
+ return toBech32('sentprov', data);
114
+ }
115
+
116
+ export function sentToSentnode(sentAddr) {
117
+ validateAddress(sentAddr, 'sent', 'sentToSentnode');
118
+ const { data } = fromBech32(sentAddr);
119
+ return toBech32('sentnode', data);
120
+ }
121
+
122
+ export function sentprovToSent(provAddr) {
123
+ validateAddress(provAddr, 'sentprov', 'sentprovToSent');
124
+ const { data } = fromBech32(provAddr);
125
+ return toBech32('sent', data);
126
+ }
127
+
128
+ /**
129
+ * Compare two addresses across different bech32 prefixes (sent1, sentprov1, sentnode1).
130
+ * Returns true if they derive from the same public key.
131
+ * @param {string} addr1
132
+ * @param {string} addr2
133
+ * @returns {boolean}
134
+ */
135
+ export function isSameKey(addr1, addr2) {
136
+ try {
137
+ const { data: d1 } = fromBech32(addr1);
138
+ const { data: d2 } = fromBech32(addr2);
139
+ return Buffer.from(d1).equals(Buffer.from(d2));
140
+ } catch { return false; }
141
+ }
package/cli/config.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Sentinel CLI — Configuration Management
3
+ *
4
+ * Reads/writes ~/.sentinel/config.json for persistent CLI settings.
5
+ * Prompts for mnemonic on first run if not configured.
6
+ *
7
+ * No external dependencies — uses Node.js built-ins only.
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
11
+ import { homedir } from 'os';
12
+ import { join } from 'path';
13
+ import { createInterface } from 'readline';
14
+
15
+ // ─── Paths ───────────────────────────────────────────────────────────────────
16
+
17
+ const CONFIG_DIR = join(homedir(), '.sentinel');
18
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
19
+
20
+ // ─── Defaults ────────────────────────────────────────────────────────────────
21
+
22
+ const DEFAULT_CONFIG = {
23
+ mnemonic: '',
24
+ rpc: 'https://rpc.sentinel.co:443',
25
+ lcd: 'https://lcd.sentinel.co',
26
+ gigabytes: 1,
27
+ denom: 'udvpn',
28
+ };
29
+
30
+ // ─── Read/Write ──────────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Load config from ~/.sentinel/config.json.
34
+ * Returns defaults merged with any saved values.
35
+ * @returns {object}
36
+ */
37
+ export function loadConfig() {
38
+ try {
39
+ if (existsSync(CONFIG_FILE)) {
40
+ const raw = readFileSync(CONFIG_FILE, 'utf8');
41
+ const saved = JSON.parse(raw);
42
+ return { ...DEFAULT_CONFIG, ...saved };
43
+ }
44
+ } catch {
45
+ // Corrupted config — fall back to defaults
46
+ }
47
+ return { ...DEFAULT_CONFIG };
48
+ }
49
+
50
+ /**
51
+ * Save config to ~/.sentinel/config.json.
52
+ * Creates ~/.sentinel/ directory if it does not exist.
53
+ * @param {object} config
54
+ */
55
+ export function saveConfig(config) {
56
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
57
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
58
+ }
59
+
60
+ /**
61
+ * Get a single config value, with CLI flag override.
62
+ * Priority: flag > config file > default.
63
+ * @param {string} key
64
+ * @param {*} flagValue - Value from CLI flag (undefined if not set)
65
+ * @returns {*}
66
+ */
67
+ export function getConfigValue(key, flagValue) {
68
+ if (flagValue !== undefined && flagValue !== null) return flagValue;
69
+ const config = loadConfig();
70
+ return config[key] ?? DEFAULT_CONFIG[key];
71
+ }
72
+
73
+ // ─── Interactive Prompt ──────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Prompt user for input on stdin. Used for first-run mnemonic setup.
77
+ * @param {string} question
78
+ * @param {boolean} [hidden=false] - If true, input is not echoed (for secrets)
79
+ * @returns {Promise<string>}
80
+ */
81
+ export function prompt(question, hidden = false) {
82
+ return new Promise((resolve) => {
83
+ const rl = createInterface({
84
+ input: process.stdin,
85
+ output: process.stderr,
86
+ });
87
+
88
+ if (hidden) {
89
+ process.stderr.write(question);
90
+ const onData = (char) => {
91
+ const c = char.toString();
92
+ if (c === '\n' || c === '\r') {
93
+ process.stdin.removeListener('data', onData);
94
+ process.stderr.write('\n');
95
+ rl.close();
96
+ resolve(rl.line || '');
97
+ return;
98
+ }
99
+ // Suppress echo by not writing back
100
+ };
101
+ process.stdin.setRawMode?.(true);
102
+ process.stdin.resume();
103
+ process.stdin.on('data', onData);
104
+ } else {
105
+ rl.question(question, (answer) => {
106
+ rl.close();
107
+ resolve(answer.trim());
108
+ });
109
+ }
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Ensure mnemonic is available. Checks config, then prompts user.
115
+ * Saves to config on first entry so subsequent runs skip the prompt.
116
+ * @returns {Promise<string>}
117
+ */
118
+ export async function ensureMnemonic() {
119
+ const config = loadConfig();
120
+ if (config.mnemonic) return config.mnemonic;
121
+
122
+ // Check environment variable as fallback
123
+ if (process.env.MNEMONIC) return process.env.MNEMONIC;
124
+
125
+ process.stderr.write('\n No mnemonic configured.\n');
126
+ process.stderr.write(' Enter your BIP39 mnemonic to get started.\n');
127
+ process.stderr.write(' It will be saved to ~/.sentinel/config.json\n\n');
128
+
129
+ const mnemonic = await prompt(' Mnemonic: ', true); // hidden=true: suppress echo
130
+ if (!mnemonic) {
131
+ process.stderr.write(' No mnemonic provided. Exiting.\n');
132
+ process.exit(1);
133
+ }
134
+
135
+ config.mnemonic = mnemonic;
136
+ saveConfig(config);
137
+ process.stderr.write(' Saved.\n\n');
138
+ return mnemonic;
139
+ }
140
+
141
+ // ─── Config Path Export ──────────────────────────────────────────────────────
142
+
143
+ export { CONFIG_DIR, CONFIG_FILE, DEFAULT_CONFIG };