@neuraiproject/neurai-depin-terminal 1.0.0 → 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/src/utils.js CHANGED
@@ -75,10 +75,10 @@ export function truncate(str, length, suffix = '...') {
75
75
  */
76
76
  export function formatTimestamp(timestamp, timezone = 'UTC') {
77
77
  const date = timestamp instanceof Date ? timestamp : new Date(timestamp * 1000);
78
-
78
+
79
79
  // Handle UTC explicitly
80
80
  if (timezone === 'UTC') {
81
- return date.toLocaleTimeString('en-US', {
81
+ return date.toLocaleTimeString('en-US', {
82
82
  timeZone: 'UTC',
83
83
  hour12: false,
84
84
  hour: '2-digit',
@@ -93,7 +93,7 @@ export function formatTimestamp(timestamp, timezone = 'UTC') {
93
93
  // Create a new date shifted by the offset hours
94
94
  // We use UTC for display to avoid local system timezone interference
95
95
  const shiftedDate = new Date(date.getTime() + (offset * 60 * 60 * 1000));
96
- return shiftedDate.toLocaleTimeString('en-US', {
96
+ return shiftedDate.toLocaleTimeString('en-US', {
97
97
  timeZone: 'UTC',
98
98
  hour12: false,
99
99
  hour: '2-digit',
@@ -103,7 +103,7 @@ export function formatTimestamp(timestamp, timezone = 'UTC') {
103
103
  }
104
104
 
105
105
  // Fallback to UTC
106
- return date.toLocaleTimeString('en-US', {
106
+ return date.toLocaleTimeString('en-US', {
107
107
  timeZone: 'UTC',
108
108
  hour12: false,
109
109
  hour: '2-digit',
@@ -123,53 +123,260 @@ export function createMessageKey(hash, signature) {
123
123
  }
124
124
 
125
125
  /**
126
- * Read password from stdin with character masking
126
+ * Drain stdin until silence is detected
127
+ * @param {Object} stdin - Stdin stream
128
+ * @param {number} silenceMs - Milliseconds of silence to wait for (default: 300)
129
+ * @param {number} maxWaitMs - Maximum time to wait in total (default: 2000)
130
+ * @returns {Promise<void>}
131
+ */
132
+ export async function drainInput(stdin, silenceMs = 300, maxWaitMs = 2000) {
133
+ // If not TTY, we can't really drain in the same way, but we can try small read
134
+ if (!stdin.isTTY) {
135
+ if (stdin.readableLength > 0) stdin.read();
136
+ return;
137
+ }
138
+
139
+ // Enable raw mode to catch all chars
140
+ try {
141
+ stdin.setRawMode(true);
142
+ } catch (e) {
143
+ // Ignore
144
+ }
145
+
146
+ await new Promise(resolve => {
147
+ stdin.resume();
148
+
149
+ let silenceTimer;
150
+ let maxTimer;
151
+
152
+ const cleanup = () => {
153
+ if (silenceTimer) clearTimeout(silenceTimer);
154
+ if (maxTimer) clearTimeout(maxTimer);
155
+ stdin.removeAllListeners('data');
156
+
157
+ // Pause so we don't eat future input intended for others
158
+ stdin.pause();
159
+
160
+ try {
161
+ stdin.setRawMode(false);
162
+ } catch (e) {
163
+ // Ignore
164
+ }
165
+ resolve();
166
+ };
167
+
168
+ const resetSilenceTimer = () => {
169
+ if (silenceTimer) clearTimeout(silenceTimer);
170
+ silenceTimer = setTimeout(cleanup, silenceMs);
171
+ };
172
+
173
+ // If we hit the max wait time, just force proceed
174
+ maxTimer = setTimeout(cleanup, maxWaitMs);
175
+
176
+ // Listen for ANY data and drain it
177
+ stdin.on('data', () => {
178
+ resetSilenceTimer();
179
+ });
180
+
181
+ // Start the initial timer
182
+ resetSilenceTimer();
183
+ });
184
+
185
+ // Final sanity check drain
186
+ if (stdin.readableLength > 0) {
187
+ stdin.read();
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Read password from stdin with character masking (pure implementation without readline)
127
193
  * @param {string} prompt - Prompt to display
128
194
  * @param {string} [maskChar='*'] - Character to display for each typed character
129
195
  * @returns {Promise<string>} The entered password
130
196
  */
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);
197
+ export async function readPassword(prompt, maskChar = '*') {
198
+ const stdin = process.stdin;
199
+
200
+ // Use robust drain before starting
201
+ await drainInput(stdin, 200, 1000);
202
+
203
+ return new Promise((resolve, reject) => {
204
+ let onDataHandler = null;
205
+
206
+ // Comprehensive cleanup function to ensure stdin is in pristine state
207
+ const cleanup = (removeListener = true) => {
208
+ if (removeListener && onDataHandler) {
209
+ stdin.removeListener('data', onDataHandler);
210
+ onDataHandler = null;
211
+ }
212
+
213
+ // Remove ALL listeners to avoid any residual state
214
+ stdin.removeAllListeners('data');
215
+
216
+ if (stdin.isTTY) {
217
+ try {
218
+ stdin.setRawMode(false);
219
+ } catch (error) {
220
+ // Ignore raw mode reset failures
221
+ }
222
+ }
223
+
224
+ // DON'T pause stdin - leave it ready for next use
225
+ // CharsmUI will manage its own resume/pause
226
+ };
227
+
228
+ // Ensure stdin is in correct initial state
229
+ if (!stdin.isTTY) {
230
+ reject(new Error('stdin is not a TTY'));
231
+ return;
232
+ }
233
+
234
+ // Start fresh
235
+ stdin.removeAllListeners('data');
236
+ stdin.removeAllListeners('keypress');
136
237
  stdin.resume();
238
+
239
+ // Set raw mode for character-by-character input
240
+ try {
241
+ stdin.setRawMode(true);
242
+ } catch (error) {
243
+ reject(new Error(`Failed to set raw mode: ${error.message}`));
244
+ return;
245
+ }
246
+
137
247
  stdin.setEncoding('utf8');
248
+
249
+ // Always mask password (show asterisks)
250
+ const shouldMask = true;
138
251
  let password = '';
252
+ let done = false;
253
+ let escapeState = 'normal';
139
254
 
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;
255
+ const finish = () => {
256
+ if (done) {
257
+ return;
258
+ }
259
+ done = true;
260
+ cleanup(true);
261
+ process.stdout.write('\n');
262
+ resolve(password);
263
+ };
151
264
 
152
- case KEY_CODES.CTRL_C:
153
- process.stdout.write('\n');
154
- process.exit(0);
265
+ onDataHandler = (chunk) => {
266
+ for (const ch of chunk) {
267
+ if (done) {
155
268
  break;
269
+ }
270
+
271
+ const codePoint = ch.charCodeAt(0);
156
272
 
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);
273
+ // State machine for filtering ANSI escape sequences
274
+ if (escapeState === 'esc') {
275
+ if (ch === '[') {
276
+ escapeState = 'csi';
277
+ } else if (ch === ']') {
278
+ escapeState = 'osc';
279
+ } else {
280
+ escapeState = 'normal';
162
281
  }
163
- break;
282
+ continue;
283
+ }
164
284
 
165
- default:
166
- password += char;
167
- process.stdout.write(maskChar);
168
- break;
285
+ if (escapeState === 'csi') {
286
+ if (ch >= '@' && ch <= '~') {
287
+ escapeState = 'normal';
288
+ }
289
+ continue;
290
+ }
291
+
292
+ if (escapeState === 'osc') {
293
+ if (ch === '\x07') {
294
+ escapeState = 'normal';
295
+ } else if (codePoint === 0x9c) {
296
+ escapeState = 'normal';
297
+ } else if (ch === '\x1b') {
298
+ escapeState = 'osc-esc';
299
+ }
300
+ continue;
301
+ }
302
+
303
+ if (escapeState === 'osc-esc') {
304
+ if (ch === '\\') {
305
+ escapeState = 'normal';
306
+ } else if (ch !== '\x1b') {
307
+ escapeState = 'osc';
308
+ }
309
+ continue;
310
+ }
311
+
312
+ // Process actual characters
313
+ switch (ch) {
314
+ case KEY_CODES.ENTER:
315
+ case KEY_CODES.CARRIAGE_RETURN:
316
+ case KEY_CODES.CTRL_D:
317
+ finish();
318
+ return;
319
+
320
+ case KEY_CODES.CTRL_C:
321
+ cleanup(true);
322
+ process.stdout.write('\n');
323
+ resetTerminal();
324
+ process.exit(0);
325
+
326
+ case KEY_CODES.BACKSPACE:
327
+ case KEY_CODES.BACKSPACE_ALT:
328
+ if (password.length > 0) {
329
+ password = password.slice(0, -1);
330
+ if (shouldMask) {
331
+ process.stdout.write(TERMINAL.BACKSPACE);
332
+ }
333
+ }
334
+ break;
335
+
336
+ default:
337
+ // Start of escape sequence
338
+ if (ch === '\x1b') {
339
+ escapeState = 'esc';
340
+ break;
341
+ }
342
+
343
+ // C1 control characters (ignore)
344
+ if (codePoint === 0x9b) {
345
+ escapeState = 'csi';
346
+ break;
347
+ }
348
+ if (codePoint === 0x9d) {
349
+ escapeState = 'osc';
350
+ break;
351
+ }
352
+ if (codePoint >= 0x80 && codePoint <= 0x9f) {
353
+ // Ignore C1 control characters
354
+ break;
355
+ }
356
+
357
+ // C0 control characters (ignore except Enter, Backspace, Ctrl+C handled above)
358
+ if (codePoint < 0x20) {
359
+ // Ignore C0 control characters
360
+ break;
361
+ }
362
+
363
+ // Valid printable character
364
+ password += ch;
365
+ if (shouldMask) {
366
+ process.stdout.write(maskChar);
367
+ }
368
+ break;
369
+ }
169
370
  }
170
371
  };
171
372
 
172
- stdin.on('data', onData);
373
+ // Attach listener FIRST so it can filter any incoming ANSI sequences
374
+ stdin.on('data', onDataHandler);
375
+
376
+ // Show prompt after a tiny delay
377
+ setTimeout(() => {
378
+ process.stdout.write(prompt);
379
+ }, 10);
173
380
  });
174
381
  }
175
382
 
@@ -186,12 +393,12 @@ export async function withSuppressedConsole(fn) {
186
393
  const originalStdoutWrite = process.stdout.write;
187
394
  const originalStderrWrite = process.stderr.write;
188
395
 
189
- console.log = () => {};
190
- console.warn = () => {};
191
- console.error = () => {};
192
- console.info = () => {};
193
- process.stdout.write = () => {};
194
- process.stderr.write = () => {};
396
+ console.log = () => { };
397
+ console.warn = () => { };
398
+ console.error = () => { };
399
+ console.info = () => { };
400
+ process.stdout.write = () => { };
401
+ process.stderr.write = () => { };
195
402
 
196
403
  try {
197
404
  return await fn();
@@ -213,6 +420,12 @@ export function resetTerminal() {
213
420
  if (process.stdout.isTTY) {
214
421
  try {
215
422
  process.stdout.write(TERMINAL.EXIT_ALT_SCREEN);
423
+ // Disable mouse reporting (1000, 1002, 1003, 1006, 1015)
424
+ process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l');
425
+ // Disable bracketed paste (2004)
426
+ process.stdout.write('\x1b[?2004l');
427
+ // Disable focus tracking (1004)
428
+ process.stdout.write('\x1b[?1004l');
216
429
  process.stdout.write(TERMINAL.SHOW_CURSOR);
217
430
  process.stdout.write(TERMINAL.RESET_ATTRIBUTES);
218
431
  process.stdout.write(TERMINAL.NEW_LINE);
@@ -231,6 +444,54 @@ export function resetTerminal() {
231
444
  }
232
445
  }
233
446
 
447
+ /**
448
+ * Emergency terminal cleanup
449
+ * Called at startup to ensure terminal is in a clean state
450
+ */
451
+ export function emergencyTerminalCleanup() {
452
+ if (process.stdin.isTTY) {
453
+ try {
454
+ // Remove any stale listeners
455
+ process.stdin.removeAllListeners('keypress');
456
+ process.stdin.removeAllListeners('data');
457
+
458
+ // Ensure raw mode is off
459
+ process.stdin.setRawMode(false);
460
+
461
+ // Resume stdin to allow reading buffered data
462
+ process.stdin.resume();
463
+
464
+ // Aggressively flush all buffered data (may need multiple reads)
465
+ let flushed = 0;
466
+ while (process.stdin.readableLength > 0 && flushed < 10) {
467
+ process.stdin.read();
468
+ flushed++;
469
+ }
470
+
471
+ // Now pause for normal operation
472
+ process.stdin.pause();
473
+ } catch (err) {
474
+ // Ignore errors during cleanup
475
+ }
476
+ }
477
+
478
+ if (process.stdout.isTTY) {
479
+ try {
480
+ // Reset terminal attributes
481
+ process.stdout.write(TERMINAL.RESET_ATTRIBUTES);
482
+ // Disable mouse reporting (1000, 1002, 1003, 1006, 1015)
483
+ process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l');
484
+ // Disable bracketed paste (2004)
485
+ process.stdout.write('\x1b[?2004l');
486
+ // Disable focus tracking (1004)
487
+ process.stdout.write('\x1b[?1004l');
488
+ process.stdout.write(TERMINAL.SHOW_CURSOR);
489
+ } catch (err) {
490
+ // Ignore errors
491
+ }
492
+ }
493
+ }
494
+
234
495
  /**
235
496
  * Parse RPC host from URL for display
236
497
  * @param {string} url - Full RPC URL
@@ -277,8 +538,8 @@ export function validatePassword(password, minLength, maxLength) {
277
538
  */
278
539
  export function isPubkeyRevealed(pubkeyResponse) {
279
540
  return pubkeyResponse?.pubkey &&
280
- pubkeyResponse?.revealed === 1 &&
281
- pubkeyResponse.pubkey.trim().length > 0;
541
+ pubkeyResponse?.revealed === 1 &&
542
+ pubkeyResponse.pubkey.trim().length > 0;
282
543
  }
283
544
 
284
545
  /**
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import NeuraiKey from '@neuraiproject/neurai-key';
8
- import secp256k1 from 'secp256k1';
9
8
  import {
10
9
  ADDRESS,
11
10
  ERROR_MESSAGES
@@ -32,7 +31,7 @@ export class WalletManager {
32
31
 
33
32
  /**
34
33
  * Derive address and public key from WIF private key
35
- * Uses NeuraiKey to get address and secp256k1 to derive compressed public key
34
+ * Uses NeuraiKey to get address and derive compressed public key
36
35
  * @returns {Promise<void>}
37
36
  * @throws {WalletError} If key derivation fails
38
37
  */
@@ -46,10 +45,10 @@ export class WalletManager {
46
45
  this.address = keyInfo.address;
47
46
  this.privateKeyHex = keyInfo.privateKey;
48
47
 
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');
48
+ // Derive compressed public key from WIF
49
+ this.publicKey = await withSuppressedConsole(() => {
50
+ return NeuraiKey.getPubkeyByWIF(this.config.network, this.config.privateKey);
51
+ });
53
52
  } catch (error) {
54
53
  throw new WalletError(`${ERROR_MESSAGES.INVALID_WIF}: ${error.message}`);
55
54
  }