@neuraiproject/neurai-depin-terminal 1.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.
@@ -0,0 +1,63 @@
1
+ import blessed from 'blessed';
2
+ import { UI, COLORS, ICONS } from '../../constants.js';
3
+ import { parseRpcHost, formatTimestamp } from '../../utils.js';
4
+
5
+ export class TopBar {
6
+ constructor(screen, config, myAddress) {
7
+ this.screen = screen;
8
+ this.config = config;
9
+ this.myAddress = myAddress;
10
+ this.totalMessages = 0;
11
+ this.encryptionType = 'N/A';
12
+
13
+ this.component = blessed.box({
14
+ top: 0,
15
+ left: 0,
16
+ width: '100%',
17
+ height: UI.TOP_BAR_HEIGHT,
18
+ content: 'Loading...',
19
+ tags: true,
20
+ style: {
21
+ fg: COLORS.FG_WHITE,
22
+ bg: COLORS.BG_BLUE
23
+ }
24
+ });
25
+
26
+ this.screen.append(this.component);
27
+ }
28
+
29
+ update(status) {
30
+ const rpcUrl = parseRpcHost(this.config.rpc_url);
31
+ const connectedIndicator = status.connected ?
32
+ `{${COLORS.CONNECTED}}${ICONS.CONNECTED}{/${COLORS.CONNECTED}}` :
33
+ `{${COLORS.DISCONNECTED}}${ICONS.DISCONNECTED}{/${COLORS.DISCONNECTED}}`;
34
+
35
+ const lastPollStr = status.lastPoll ?
36
+ formatTimestamp(status.lastPoll, this.config.timezone) :
37
+ '--:--:--';
38
+
39
+ // Format timezone display
40
+ let timezoneDisplay = this.config.timezone || 'UTC';
41
+ if (timezoneDisplay !== 'UTC') {
42
+ if (!timezoneDisplay.startsWith('+') && !timezoneDisplay.startsWith('-')) {
43
+ timezoneDisplay = `+${timezoneDisplay}`;
44
+ }
45
+ timezoneDisplay = `UTC${timezoneDisplay}`;
46
+ }
47
+
48
+ this.component.setContent(
49
+ `Neurai DePIN | ${connectedIndicator} RPC: ${rpcUrl} | Token: ${this.config.token} | Time: ${timezoneDisplay}\n` +
50
+ `Address: ${this.myAddress} | Total: ${this.totalMessages} | Encryption: ${this.encryptionType} | Last poll: ${lastPollStr}`
51
+ );
52
+
53
+ this.screen.render();
54
+ }
55
+
56
+ setTotalMessages(count) {
57
+ this.totalMessages = count;
58
+ }
59
+
60
+ setEncryptionType(type) {
61
+ this.encryptionType = type;
62
+ }
63
+ }
package/src/utils.js ADDED
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Utility functions for Neurai DePIN Terminal
3
+ * @module utils
4
+ */
5
+
6
+ import { KEY_CODES, TERMINAL } from './constants.js';
7
+
8
+ /**
9
+ * Sleep for a specified duration
10
+ * @param {number} ms - Milliseconds to sleep
11
+ * @returns {Promise<void>}
12
+ */
13
+ export function sleep(ms) {
14
+ return new Promise(resolve => setTimeout(resolve, ms));
15
+ }
16
+
17
+ /**
18
+ * Validate a URL string
19
+ * @param {string} url - URL to validate
20
+ * @returns {boolean} True if valid URL
21
+ */
22
+ export function isValidUrl(url) {
23
+ try {
24
+ new URL(url);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Validate a timezone string (UTC or numeric offset)
33
+ * @param {string} timezone - Timezone to validate
34
+ * @returns {boolean} True if valid timezone
35
+ */
36
+ export function isValidTimezone(timezone) {
37
+ if (timezone === 'UTC') return true;
38
+ // Allow numeric offsets like +1, -5, +5.5, -3.5, 2, -2
39
+ const offsetRegex = /^[+-]?\d+(\.\d+)?$/;
40
+ return offsetRegex.test(timezone);
41
+ }
42
+
43
+ /**
44
+ * Ensure RPC URL has the correct format
45
+ * @param {string} url - Base RPC URL
46
+ * @param {string} suffix - Suffix to append (default: '/rpc')
47
+ * @returns {string} Formatted RPC URL
48
+ */
49
+ export function formatRpcUrl(url, suffix = '/rpc') {
50
+ if (!url.endsWith(suffix)) {
51
+ return url + suffix;
52
+ }
53
+ return url;
54
+ }
55
+
56
+ /**
57
+ * Truncate a string with ellipsis
58
+ * @param {string} str - String to truncate
59
+ * @param {number} length - Maximum length
60
+ * @param {string} [suffix='...'] - Suffix to append
61
+ * @returns {string} Truncated string
62
+ */
63
+ export function truncate(str, length, suffix = '...') {
64
+ if (!str || str.length <= length) {
65
+ return str;
66
+ }
67
+ return str.slice(0, length) + suffix;
68
+ }
69
+
70
+ /**
71
+ * Format timestamp to locale time string
72
+ * @param {number|Date} timestamp - Unix timestamp in seconds or Date object
73
+ * @param {string} [timezone='UTC'] - Timezone offset (e.g., 'UTC', '+1', '-5')
74
+ * @returns {string} Formatted time string
75
+ */
76
+ export function formatTimestamp(timestamp, timezone = 'UTC') {
77
+ const date = timestamp instanceof Date ? timestamp : new Date(timestamp * 1000);
78
+
79
+ // Handle UTC explicitly
80
+ if (timezone === 'UTC') {
81
+ return date.toLocaleTimeString('en-US', {
82
+ timeZone: 'UTC',
83
+ hour12: false,
84
+ hour: '2-digit',
85
+ minute: '2-digit',
86
+ second: '2-digit'
87
+ });
88
+ }
89
+
90
+ // Handle numeric offset
91
+ const offset = parseFloat(timezone);
92
+ if (!isNaN(offset)) {
93
+ // Create a new date shifted by the offset hours
94
+ // We use UTC for display to avoid local system timezone interference
95
+ const shiftedDate = new Date(date.getTime() + (offset * 60 * 60 * 1000));
96
+ return shiftedDate.toLocaleTimeString('en-US', {
97
+ timeZone: 'UTC',
98
+ hour12: false,
99
+ hour: '2-digit',
100
+ minute: '2-digit',
101
+ second: '2-digit'
102
+ });
103
+ }
104
+
105
+ // Fallback to UTC
106
+ return date.toLocaleTimeString('en-US', {
107
+ timeZone: 'UTC',
108
+ hour12: false,
109
+ hour: '2-digit',
110
+ minute: '2-digit',
111
+ second: '2-digit'
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Create a deduplication key from message hash and signature
117
+ * @param {string} hash - Message hash
118
+ * @param {string} signature - Message signature
119
+ * @returns {string} Deduplication key
120
+ */
121
+ export function createMessageKey(hash, signature) {
122
+ return `${hash}|${signature}`;
123
+ }
124
+
125
+ /**
126
+ * Read password from stdin with character masking
127
+ * @param {string} prompt - Prompt to display
128
+ * @param {string} [maskChar='*'] - Character to display for each typed character
129
+ * @returns {Promise<string>} The entered password
130
+ */
131
+ export function readPassword(prompt, maskChar = '*') {
132
+ return new Promise((resolve) => {
133
+ process.stdout.write(prompt);
134
+ const stdin = process.stdin;
135
+ stdin.setRawMode(true);
136
+ stdin.resume();
137
+ stdin.setEncoding('utf8');
138
+ let password = '';
139
+
140
+ const onData = (char) => {
141
+ switch (char) {
142
+ case KEY_CODES.ENTER:
143
+ case KEY_CODES.CARRIAGE_RETURN:
144
+ case KEY_CODES.CTRL_D:
145
+ stdin.setRawMode(false);
146
+ stdin.pause();
147
+ stdin.removeListener('data', onData);
148
+ process.stdout.write('\n');
149
+ resolve(password);
150
+ break;
151
+
152
+ case KEY_CODES.CTRL_C:
153
+ process.stdout.write('\n');
154
+ process.exit(0);
155
+ break;
156
+
157
+ case KEY_CODES.BACKSPACE:
158
+ case KEY_CODES.BACKSPACE_ALT:
159
+ if (password.length > 0) {
160
+ password = password.slice(0, -1);
161
+ process.stdout.write(TERMINAL.BACKSPACE);
162
+ }
163
+ break;
164
+
165
+ default:
166
+ password += char;
167
+ process.stdout.write(maskChar);
168
+ break;
169
+ }
170
+ };
171
+
172
+ stdin.on('data', onData);
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Suppress all console output during a function execution
178
+ * @param {Function} fn - Function to execute with suppressed console
179
+ * @returns {Promise<*>} Result of the function
180
+ */
181
+ export async function withSuppressedConsole(fn) {
182
+ const originalLog = console.log;
183
+ const originalWarn = console.warn;
184
+ const originalError = console.error;
185
+ const originalInfo = console.info;
186
+ const originalStdoutWrite = process.stdout.write;
187
+ const originalStderrWrite = process.stderr.write;
188
+
189
+ console.log = () => {};
190
+ console.warn = () => {};
191
+ console.error = () => {};
192
+ console.info = () => {};
193
+ process.stdout.write = () => {};
194
+ process.stderr.write = () => {};
195
+
196
+ try {
197
+ return await fn();
198
+ } finally {
199
+ console.log = originalLog;
200
+ console.warn = originalWarn;
201
+ console.error = originalError;
202
+ console.info = originalInfo;
203
+ process.stdout.write = originalStdoutWrite;
204
+ process.stderr.write = originalStderrWrite;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Reset terminal to normal state
210
+ * Useful for cleanup on exit
211
+ */
212
+ export function resetTerminal() {
213
+ if (process.stdout.isTTY) {
214
+ try {
215
+ process.stdout.write(TERMINAL.EXIT_ALT_SCREEN);
216
+ process.stdout.write(TERMINAL.SHOW_CURSOR);
217
+ process.stdout.write(TERMINAL.RESET_ATTRIBUTES);
218
+ process.stdout.write(TERMINAL.NEW_LINE);
219
+ } catch (err) {
220
+ // Ignore errors during terminal reset
221
+ }
222
+ }
223
+
224
+ if (process.stdin.isTTY) {
225
+ try {
226
+ process.stdin.setRawMode(false);
227
+ process.stdin.pause();
228
+ } catch (err) {
229
+ // Ignore errors during stdin reset
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Parse RPC host from URL for display
236
+ * @param {string} url - Full RPC URL
237
+ * @returns {string} Hostname:port
238
+ */
239
+ export function parseRpcHost(url) {
240
+ try {
241
+ const urlObj = new URL(url);
242
+ return urlObj.host;
243
+ } catch {
244
+ return url;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Validate password meets requirements
250
+ * @param {string} password - Password to validate
251
+ * @param {number} minLength - Minimum length
252
+ * @param {number} maxLength - Maximum length
253
+ * @returns {{valid: boolean, error: string|null}} Validation result
254
+ */
255
+ export function validatePassword(password, minLength, maxLength) {
256
+ if (!password || password.length < minLength) {
257
+ return {
258
+ valid: false,
259
+ error: `Password must be at least ${minLength} characters`
260
+ };
261
+ }
262
+
263
+ if (password.length > maxLength) {
264
+ return {
265
+ valid: false,
266
+ error: `Password must be at most ${maxLength} characters`
267
+ };
268
+ }
269
+
270
+ return { valid: true, error: null };
271
+ }
272
+
273
+ /**
274
+ * Check if a public key has been revealed
275
+ * @param {Object} pubkeyResponse - Response from getpubkey RPC call
276
+ * @returns {boolean} True if pubkey is revealed and valid
277
+ */
278
+ export function isPubkeyRevealed(pubkeyResponse) {
279
+ return pubkeyResponse?.pubkey &&
280
+ pubkeyResponse?.revealed === 1 &&
281
+ pubkeyResponse.pubkey.trim().length > 0;
282
+ }
283
+
284
+ /**
285
+ * Normalize public key to lowercase hex
286
+ * @param {string} pubkey - Public key to normalize
287
+ * @returns {string} Normalized public key
288
+ */
289
+ export function normalizePubkey(pubkey) {
290
+ return pubkey.trim().toLowerCase();
291
+ }
292
+
293
+ /**
294
+ * Check if server has privacy layer enabled
295
+ * @param {Object} msgInfo - Response from depingetmsginfo
296
+ * @returns {boolean} True if privacy layer is enabled
297
+ */
298
+ export function hasPrivacyLayer(msgInfo) {
299
+ return msgInfo?.depinpoolpkey && msgInfo.depinpoolpkey !== '0';
300
+ }
301
+
302
+ /**
303
+ * Check if response indicates encrypted privacy layer
304
+ * @param {Object} response - RPC response
305
+ * @returns {boolean} True if response is encrypted
306
+ */
307
+ export function isEncryptedResponse(response) {
308
+ return Boolean(response?.encrypted);
309
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Wallet manager for Neurai DePIN Terminal
3
+ * Handles wallet initialization and key derivation
4
+ * @module WalletManager
5
+ */
6
+
7
+ import NeuraiKey from '@neuraiproject/neurai-key';
8
+ import secp256k1 from 'secp256k1';
9
+ import {
10
+ ADDRESS,
11
+ ERROR_MESSAGES
12
+ } from '../constants.js';
13
+ import { WalletError } from '../errors.js';
14
+ import { withSuppressedConsole } from '../utils.js';
15
+
16
+ /**
17
+ * Manages wallet operations
18
+ */
19
+ export class WalletManager {
20
+ /**
21
+ * Create a new WalletManager instance
22
+ * @param {Object} config - Configuration object
23
+ * @param {string} config.network - Network name (xna)
24
+ * @param {string} config.privateKey - WIF private key
25
+ */
26
+ constructor(config) {
27
+ this.config = config;
28
+ this.address = null;
29
+ this.publicKey = null;
30
+ this.privateKeyHex = null;
31
+ }
32
+
33
+ /**
34
+ * Derive address and public key from WIF private key
35
+ * Uses NeuraiKey to get address and secp256k1 to derive compressed public key
36
+ * @returns {Promise<void>}
37
+ * @throws {WalletError} If key derivation fails
38
+ */
39
+ async deriveKeysFromWif() {
40
+ try {
41
+ // Suppress NeuraiKey console output
42
+ const keyInfo = await withSuppressedConsole(() => {
43
+ return NeuraiKey.getAddressByWIF(this.config.network, this.config.privateKey);
44
+ });
45
+
46
+ this.address = keyInfo.address;
47
+ this.privateKeyHex = keyInfo.privateKey;
48
+
49
+ // Derive compressed public key from private key
50
+ const privateKeyBuffer = Buffer.from(keyInfo.privateKey, 'hex');
51
+ const publicKeyBuffer = secp256k1.publicKeyCreate(privateKeyBuffer, true);
52
+ this.publicKey = Buffer.from(publicKeyBuffer).toString('hex');
53
+ } catch (error) {
54
+ throw new WalletError(`${ERROR_MESSAGES.INVALID_WIF}: ${error.message}`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Initialize wallet: derive keys
60
+ * @returns {Promise<void>}
61
+ * @throws {WalletError} If key derivation fails
62
+ */
63
+ async initialize() {
64
+ // Derive keys from WIF (fatal if fails)
65
+ await this.deriveKeysFromWif();
66
+
67
+ console.log(`DePIN Address: ${this.address}`);
68
+ console.log(`Public Key: ${this.publicKey.slice(0, ADDRESS.PUBKEY_DISPLAY_LENGTH)}...`);
69
+ }
70
+
71
+ /**
72
+ * Get wallet address
73
+ * @returns {string} Neurai address
74
+ */
75
+ getAddress() {
76
+ return this.address;
77
+ }
78
+
79
+ /**
80
+ * Get public key
81
+ * @returns {string} Compressed public key in hex format
82
+ */
83
+ getPublicKey() {
84
+ return this.publicKey;
85
+ }
86
+
87
+ /**
88
+ * Get private key in hex format
89
+ * @returns {string} Private key in hex format
90
+ */
91
+ getPrivateKeyHex() {
92
+ return this.privateKeyHex;
93
+ }
94
+ }