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.
- package/CHANGELOG.md +446 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/ai-path/ADMIN-ELEVATION.md +116 -0
- package/ai-path/AI-MANIFESTO.md +185 -0
- package/ai-path/BREAKING.md +74 -0
- package/ai-path/CHECKLIST.md +619 -0
- package/ai-path/CONNECTION-STEPS.md +724 -0
- package/ai-path/DECISION-TREE.md +378 -0
- package/ai-path/DEPENDENCIES.md +459 -0
- package/ai-path/E2E-FLOW.md +1555 -0
- package/ai-path/FAILURES.md +403 -0
- package/ai-path/GUIDE.md +1217 -0
- package/ai-path/README.md +558 -0
- package/ai-path/SPLIT-TUNNEL.md +266 -0
- package/ai-path/cli.js +535 -0
- package/ai-path/connect.js +884 -0
- package/ai-path/discover.js +178 -0
- package/ai-path/environment.js +266 -0
- package/ai-path/errors.js +86 -0
- package/ai-path/examples/autonomous-agent.mjs +220 -0
- package/ai-path/examples/multi-region.mjs +174 -0
- package/ai-path/examples/one-shot.mjs +31 -0
- package/ai-path/index.js +60 -0
- package/ai-path/pricing.js +136 -0
- package/ai-path/recommend.js +413 -0
- package/ai-path/run-admin.vbs +25 -0
- package/ai-path/setup.js +291 -0
- package/ai-path/wallet.js +137 -0
- package/app-helpers.js +363 -0
- package/app-settings.js +95 -0
- package/app-types.js +267 -0
- package/audit.js +847 -0
- package/batch.js +293 -0
- package/bin/setup.js +376 -0
- package/chain/authz.js +109 -0
- package/chain/broadcast.js +472 -0
- package/chain/client.js +160 -0
- package/chain/fee-grants.js +305 -0
- package/chain/index.js +891 -0
- package/chain/lcd.js +313 -0
- package/chain/queries.js +547 -0
- package/chain/rpc.js +408 -0
- package/chain/wallet.js +141 -0
- package/cli/config.js +143 -0
- package/cli/index.js +463 -0
- package/cli/output.js +182 -0
- package/cli.js +491 -0
- package/client/index.js +251 -0
- package/client.js +271 -0
- package/config/index.js +255 -0
- package/connection/connect.js +849 -0
- package/connection/disconnect.js +180 -0
- package/connection/discovery.js +321 -0
- package/connection/index.js +76 -0
- package/connection/proxy.js +148 -0
- package/connection/resilience.js +428 -0
- package/connection/security.js +232 -0
- package/connection/state.js +369 -0
- package/connection/tunnel.js +691 -0
- package/consumer.js +132 -0
- package/cosmjs-setup.js +1884 -0
- package/defaults.js +366 -0
- package/disk-cache.js +107 -0
- package/dist/client.d.ts +108 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +400 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/errors/index.js +112 -0
- package/errors.js +218 -0
- package/examples/README.md +64 -0
- package/examples/connect-direct.mjs +106 -0
- package/examples/connect-plan.mjs +125 -0
- package/examples/error-handling.mjs +109 -0
- package/examples/query-nodes.mjs +94 -0
- package/examples/wallet-basics.mjs +61 -0
- package/generated/amino/amino.ts +9 -0
- package/generated/cosmos/base/v1beta1/coin.ts +365 -0
- package/generated/cosmos_proto/cosmos.ts +323 -0
- package/generated/gogoproto/gogo.ts +9 -0
- package/generated/google/protobuf/descriptor.ts +7601 -0
- package/generated/google/protobuf/duration.ts +208 -0
- package/generated/google/protobuf/timestamp.ts +238 -0
- package/generated/sentinel/lease/v1/events.ts +924 -0
- package/generated/sentinel/lease/v1/lease.ts +292 -0
- package/generated/sentinel/lease/v1/msg.ts +949 -0
- package/generated/sentinel/lease/v1/params.ts +164 -0
- package/generated/sentinel/node/v3/events.ts +881 -0
- package/generated/sentinel/node/v3/msg.ts +1002 -0
- package/generated/sentinel/node/v3/node.ts +263 -0
- package/generated/sentinel/node/v3/params.ts +183 -0
- package/generated/sentinel/plan/v3/events.ts +675 -0
- package/generated/sentinel/plan/v3/msg.ts +1191 -0
- package/generated/sentinel/plan/v3/plan.ts +283 -0
- package/generated/sentinel/provider/v2/events.ts +171 -0
- package/generated/sentinel/provider/v2/msg.ts +480 -0
- package/generated/sentinel/provider/v2/params.ts +131 -0
- package/generated/sentinel/provider/v2/provider.ts +246 -0
- package/generated/sentinel/session/v3/events.ts +480 -0
- package/generated/sentinel/session/v3/msg.ts +616 -0
- package/generated/sentinel/session/v3/params.ts +260 -0
- package/generated/sentinel/session/v3/proof.ts +180 -0
- package/generated/sentinel/session/v3/session.ts +384 -0
- package/generated/sentinel/subscription/v3/events.ts +1181 -0
- package/generated/sentinel/subscription/v3/msg.ts +1305 -0
- package/generated/sentinel/subscription/v3/params.ts +167 -0
- package/generated/sentinel/subscription/v3/subscription.ts +315 -0
- package/generated/sentinel/types/v1/bandwidth.ts +124 -0
- package/generated/sentinel/types/v1/price.ts +149 -0
- package/generated/sentinel/types/v1/renewal.ts +87 -0
- package/generated/sentinel/types/v1/status.ts +54 -0
- package/generated/typeRegistry.ts +27 -0
- package/index.js +486 -0
- package/node-connect.js +3015 -0
- package/operator.js +134 -0
- package/package.json +113 -0
- package/plan-operations.js +199 -0
- package/preflight.js +352 -0
- package/pricing/index.js +262 -0
- package/proto/amino/amino.proto +84 -0
- package/proto/cosmos/base/v1beta1/coin.proto +61 -0
- package/proto/cosmos_proto/cosmos.proto +112 -0
- package/proto/gogoproto/gogo.proto +145 -0
- package/proto/google/api/annotations.proto +31 -0
- package/proto/google/api/http.proto +370 -0
- package/proto/google/protobuf/any.proto +106 -0
- package/proto/google/protobuf/duration.proto +115 -0
- package/proto/google/protobuf/timestamp.proto +145 -0
- package/proto/sentinel/lease/v1/events.proto +52 -0
- package/proto/sentinel/lease/v1/genesis.proto +15 -0
- package/proto/sentinel/lease/v1/lease.proto +25 -0
- package/proto/sentinel/lease/v1/msg.proto +62 -0
- package/proto/sentinel/lease/v1/params.proto +17 -0
- package/proto/sentinel/node/v3/events.proto +50 -0
- package/proto/sentinel/node/v3/genesis.proto +15 -0
- package/proto/sentinel/node/v3/msg.proto +63 -0
- package/proto/sentinel/node/v3/node.proto +27 -0
- package/proto/sentinel/node/v3/params.proto +21 -0
- package/proto/sentinel/node/v3/querier.proto +63 -0
- package/proto/sentinel/plan/v3/events.proto +41 -0
- package/proto/sentinel/plan/v3/genesis.proto +21 -0
- package/proto/sentinel/plan/v3/msg.proto +83 -0
- package/proto/sentinel/plan/v3/plan.proto +32 -0
- package/proto/sentinel/plan/v3/querier.proto +53 -0
- package/proto/sentinel/provider/v2/events.proto +16 -0
- package/proto/sentinel/provider/v2/genesis.proto +15 -0
- package/proto/sentinel/provider/v2/msg.proto +35 -0
- package/proto/sentinel/provider/v2/params.proto +17 -0
- package/proto/sentinel/provider/v2/provider.proto +24 -0
- package/proto/sentinel/provider/v3/genesis.proto +15 -0
- package/proto/sentinel/provider/v3/params.proto +13 -0
- package/proto/sentinel/session/v3/events.proto +30 -0
- package/proto/sentinel/session/v3/genesis.proto +15 -0
- package/proto/sentinel/session/v3/msg.proto +50 -0
- package/proto/sentinel/session/v3/params.proto +25 -0
- package/proto/sentinel/session/v3/proof.proto +25 -0
- package/proto/sentinel/session/v3/querier.proto +100 -0
- package/proto/sentinel/session/v3/session.proto +50 -0
- package/proto/sentinel/subscription/v2/allocation.proto +21 -0
- package/proto/sentinel/subscription/v2/payout.proto +22 -0
- package/proto/sentinel/subscription/v3/events.proto +65 -0
- package/proto/sentinel/subscription/v3/genesis.proto +17 -0
- package/proto/sentinel/subscription/v3/msg.proto +83 -0
- package/proto/sentinel/subscription/v3/params.proto +21 -0
- package/proto/sentinel/subscription/v3/subscription.proto +33 -0
- package/proto/sentinel/types/v1/bandwidth.proto +19 -0
- package/proto/sentinel/types/v1/price.proto +21 -0
- package/proto/sentinel/types/v1/renewal.proto +21 -0
- package/proto/sentinel/types/v1/status.proto +16 -0
- package/protocol/encoding.js +341 -0
- package/protocol/events.js +361 -0
- package/protocol/handshake.js +297 -0
- package/protocol/index.js +15 -0
- package/protocol/messages.js +346 -0
- package/protocol/plans.js +199 -0
- package/protocol/v2ray.js +268 -0
- package/protocol/v3.js +723 -0
- package/protocol/wireguard.js +125 -0
- package/security/index.js +132 -0
- package/session-manager.js +329 -0
- package/session-tracker.js +80 -0
- package/setup.js +376 -0
- package/speedtest/index.js +528 -0
- package/speedtest.js +567 -0
- package/src/client.ts +502 -0
- package/src/index.ts +20 -0
- package/state/index.js +347 -0
- package/state.js +516 -0
- package/test-all-chain-ops.js +493 -0
- package/test-all-logic.js +199 -0
- package/test-all-msg-types.js +292 -0
- package/test-every-connection.js +208 -0
- package/test-feegrant-connect.js +98 -0
- package/test-logic.js +148 -0
- package/test-mainnet.js +176 -0
- package/test-plan-lifecycle.js +335 -0
- package/tls-trust.js +132 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +34 -0
- package/types/chain.d.ts +746 -0
- package/types/connection.d.ts +425 -0
- package/types/errors.d.ts +174 -0
- package/types/index.d.ts +1380 -0
- package/types/nodes.d.ts +187 -0
- package/types/pricing.d.ts +156 -0
- package/types/protocol.d.ts +332 -0
- package/types/session.d.ts +236 -0
- package/types/settings.d.ts +192 -0
- package/v3protocol.js +1053 -0
- package/wallet/index.js +153 -0
- 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
|
+
}
|
package/chain/wallet.js
ADDED
|
@@ -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 };
|