@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.
package/src/errors.js ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Custom error classes for Neurai DePIN Terminal
3
+ * @module errors
4
+ */
5
+
6
+ /**
7
+ * Base error class for DePIN Terminal
8
+ * @extends Error
9
+ */
10
+ export class DepinError extends Error {
11
+ /**
12
+ * @param {string} message - Error message
13
+ * @param {string} [code] - Error code
14
+ */
15
+ constructor(message, code) {
16
+ super(message);
17
+ this.name = this.constructor.name;
18
+ this.code = code;
19
+ Error.captureStackTrace(this, this.constructor);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Configuration-related errors
25
+ * @extends DepinError
26
+ */
27
+ export class ConfigError extends DepinError {
28
+ /**
29
+ * @param {string} message - Error message
30
+ * @param {string} [code] - Error code
31
+ */
32
+ constructor(message, code = 'CONFIG_ERROR') {
33
+ super(message, code);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Password validation errors
39
+ * @extends DepinError
40
+ */
41
+ export class PasswordError extends DepinError {
42
+ /**
43
+ * @param {string} message - Error message
44
+ * @param {string} [code] - Error code
45
+ */
46
+ constructor(message, code = 'PASSWORD_ERROR') {
47
+ super(message, code);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Wallet-related errors
53
+ * @extends DepinError
54
+ */
55
+ export class WalletError extends DepinError {
56
+ /**
57
+ * @param {string} message - Error message
58
+ * @param {string} [code] - Error code
59
+ */
60
+ constructor(message, code = 'WALLET_ERROR') {
61
+ super(message, code);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * RPC connection and call errors
67
+ * @extends DepinError
68
+ */
69
+ export class RpcError extends DepinError {
70
+ /**
71
+ * @param {string} message - Error message
72
+ * @param {string} [code] - Error code
73
+ */
74
+ constructor(message, code = 'RPC_ERROR') {
75
+ super(message, code);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Message sending/receiving errors
81
+ * @extends DepinError
82
+ */
83
+ export class MessageError extends DepinError {
84
+ /**
85
+ * @param {string} message - Error message
86
+ * @param {string} [code] - Error code
87
+ */
88
+ constructor(message, code = 'MESSAGE_ERROR') {
89
+ super(message, code);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Encryption/decryption errors
95
+ * @extends DepinError
96
+ */
97
+ export class EncryptionError extends DepinError {
98
+ /**
99
+ * @param {string} message - Error message
100
+ * @param {string} [code] - Error code
101
+ */
102
+ constructor(message, code = 'ENCRYPTION_ERROR') {
103
+ super(message, code);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Library loading errors
109
+ * @extends DepinError
110
+ */
111
+ export class LibraryError extends DepinError {
112
+ /**
113
+ * @param {string} message - Error message
114
+ * @param {string} [code] - Error code
115
+ */
116
+ constructor(message, code = 'LIBRARY_ERROR') {
117
+ super(message, code);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Extract a user-friendly error message from various error formats
123
+ * @param {Error|string|Object} error - Error object, string, or error response
124
+ * @param {string} [fallback='Unknown error'] - Fallback message
125
+ * @returns {string} Extracted error message
126
+ */
127
+ export function extractErrorMessage(error, fallback = 'Unknown error') {
128
+ if (!error) {
129
+ return fallback;
130
+ }
131
+
132
+ if (typeof error === 'string' && error.trim()) {
133
+ return error;
134
+ }
135
+
136
+ if (error.message && error.message.trim()) {
137
+ return error.message;
138
+ }
139
+
140
+ if (error.error && error.error.message) {
141
+ return error.error.message;
142
+ }
143
+
144
+ if (error.code) {
145
+ return `Error code: ${error.code}`;
146
+ }
147
+
148
+ return fallback;
149
+ }
package/src/index.js ADDED
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Neurai DePIN Terminal - Main Entry Point
5
+ * A command-line terminal interface for sending and receiving DePIN messages
6
+ * @module index
7
+ */
8
+
9
+ import { ConfigManager } from './config/ConfigManager.js';
10
+ import { loadDepinMsgLibrary } from './lib/depinMsgLoader.js';
11
+ import { WalletManager } from './wallet/WalletManager.js';
12
+ import { RpcService } from './services/RpcService.js';
13
+ import { MessageStore } from './messaging/MessageStore.js';
14
+ import { MessagePoller } from './messaging/MessagePoller.js';
15
+ import { MessageSender } from './messaging/MessageSender.js';
16
+ import { TerminalUI } from './ui/TerminalUI.js';
17
+ import {
18
+ INFO_MESSAGES,
19
+ SUCCESS_MESSAGES,
20
+ ERROR_MESSAGES,
21
+ WARNING_MESSAGES,
22
+ MESSAGE,
23
+ HASH,
24
+ ICONS
25
+ } from './constants.js';
26
+ import { extractErrorMessage } from './errors.js';
27
+
28
+ /**
29
+ * Global UI instance for cleanup on exit
30
+ * @type {TerminalUI|null}
31
+ */
32
+ let uiInstance = null;
33
+
34
+ /**
35
+ * Initialize configuration
36
+ * @returns {Promise<Object>} Configuration object
37
+ */
38
+ async function initializeConfig() {
39
+ console.log(INFO_MESSAGES.LOADING_CONFIG);
40
+ const configManager = new ConfigManager();
41
+ const config = await configManager.load();
42
+ console.log(SUCCESS_MESSAGES.CONFIG_LOADED);
43
+ console.log('');
44
+ return config;
45
+ }
46
+
47
+ /**
48
+ * Load DePIN library
49
+ * @returns {Promise<Object>} DePIN library instance
50
+ */
51
+ async function initializeLibrary() {
52
+ console.log(INFO_MESSAGES.LOADING_LIBRARY);
53
+ const neuraiDepinMsg = await loadDepinMsgLibrary();
54
+ console.log(SUCCESS_MESSAGES.LIBRARY_LOADED);
55
+ console.log('');
56
+ return neuraiDepinMsg;
57
+ }
58
+
59
+ /**
60
+ * Initialize wallet (keys only)
61
+ * @param {Object} config - Configuration object
62
+ * @returns {Promise<WalletManager>} Wallet manager instance
63
+ */
64
+ async function initializeWallet(config) {
65
+ console.log(INFO_MESSAGES.INITIALIZING_WALLET);
66
+ const walletManager = new WalletManager(config);
67
+ await walletManager.initialize();
68
+ console.log('');
69
+ return walletManager;
70
+ }
71
+
72
+ /**
73
+ * Initialize RPC service
74
+ * @param {Object} config - Configuration object
75
+ * @returns {Promise<RpcService>} RPC service instance
76
+ */
77
+ async function initializeRpc(config) {
78
+ console.log(INFO_MESSAGES.CONNECTING);
79
+ const rpcService = new RpcService(config);
80
+ await rpcService.initialize();
81
+ console.log('');
82
+ return rpcService;
83
+ }
84
+
85
+ /**
86
+ * Initialize messaging components
87
+ * @param {Object} config - Configuration object
88
+ * @param {WalletManager} walletManager - Wallet manager instance
89
+ * @param {RpcService} rpcService - RPC service instance
90
+ * @param {Object} neuraiDepinMsg - DePIN library instance
91
+ * @returns {Object} Messaging components (store, poller, sender)
92
+ */
93
+ function initializeMessaging(config, walletManager, rpcService, neuraiDepinMsg) {
94
+ const messageStore = new MessageStore();
95
+ const messagePoller = new MessagePoller(config, rpcService, messageStore, neuraiDepinMsg, walletManager);
96
+ const messageSender = new MessageSender(config, walletManager, rpcService, neuraiDepinMsg);
97
+
98
+ return { messageStore, messagePoller, messageSender };
99
+ }
100
+
101
+ /**
102
+ * Connect poller events to UI
103
+ * @param {MessagePoller} messagePoller - Message poller instance
104
+ * @param {TerminalUI} ui - Terminal UI instance
105
+ * @param {RpcService} rpcService - RPC service instance
106
+ */
107
+ function connectPollerToUI(messagePoller, ui, rpcService, onRpcDown) {
108
+ const onMessage = (msg) => {
109
+ ui.addMessage(msg);
110
+ };
111
+
112
+ const onPollComplete = (status) => {
113
+ // Update pool info if available
114
+ if (status.poolInfo) {
115
+ ui.updatePoolInfo(status.poolInfo);
116
+ }
117
+
118
+ ui.updateTopBar({
119
+ connected: rpcService.isConnected(),
120
+ lastPoll: status.date
121
+ });
122
+
123
+ // Clear error status if connection is successful
124
+ if (rpcService.isConnected()) {
125
+ ui.clearSendStatus();
126
+ }
127
+ };
128
+
129
+ const onError = (error) => {
130
+ const errorMsg = extractErrorMessage(error, 'Connection error');
131
+
132
+ ui.updateSendStatus(`Polling error: ${errorMsg}`, 'error');
133
+ ui.updateTopBar({
134
+ connected: false,
135
+ lastPoll: new Date()
136
+ });
137
+
138
+ // Stop polling while disconnected and delegate retry scheduling
139
+ messagePoller.stop();
140
+ if (typeof onRpcDown === 'function') {
141
+ onRpcDown(error);
142
+ }
143
+ };
144
+
145
+ const onReconnected = () => {
146
+ ui.showSuccess('Reconnected to RPC server!');
147
+ ui.updateSendStatus('Connected to server', 'success');
148
+ };
149
+
150
+ messagePoller.on('message', onMessage);
151
+ messagePoller.on('poll-complete', onPollComplete);
152
+ messagePoller.on('error', onError);
153
+ messagePoller.on('reconnected', onReconnected);
154
+
155
+ return () => {
156
+ messagePoller.off('message', onMessage);
157
+ messagePoller.off('poll-complete', onPollComplete);
158
+ messagePoller.off('error', onError);
159
+ messagePoller.off('reconnected', onReconnected);
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Connect UI send action to message sender
165
+ * @param {TerminalUI} ui - Terminal UI instance
166
+ * @param {MessageSender} messageSender - Message sender instance
167
+ * @param {MessagePoller} messagePoller - Message poller instance
168
+ */
169
+ function connectSenderToUI(ui, messageSender, getMessagePoller) {
170
+ ui.onSend(async (message) => {
171
+ ui.updateSendStatus(INFO_MESSAGES.SENDING, 'info');
172
+
173
+ try {
174
+ const result = await messageSender.send(message);
175
+
176
+ ui.updateSendStatus(
177
+ `Message sent to ${result.recipients} recipients. Hash: ${result.hash.slice(0, HASH.DISPLAY_LENGTH)}...`,
178
+ 'success'
179
+ );
180
+
181
+ // Force a poll after sending to see the message
182
+ setTimeout(() => {
183
+ const poller = getMessagePoller();
184
+ if (poller) {
185
+ poller.poll();
186
+ }
187
+ }, MESSAGE.FORCE_POLL_DELAY);
188
+ } catch (error) {
189
+ const errorMsg = extractErrorMessage(error);
190
+ ui.updateSendStatus(`Error: ${errorMsg}`, 'error');
191
+ }
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Perform initial connection check and update UI
197
+ * @param {RpcService} rpcService - RPC service instance
198
+ * @param {TerminalUI} ui - Terminal UI instance
199
+ */
200
+ async function performInitialConnectionCheck(rpcService, ui) {
201
+ if (rpcService.isConnected()) {
202
+ try {
203
+ const poolInfo = await rpcService.call('depingetmsginfo', []);
204
+ ui.updatePoolInfo(poolInfo);
205
+ ui.updateTopBar({
206
+ connected: true,
207
+ lastPoll: null
208
+ });
209
+ ui.showSuccess(SUCCESS_MESSAGES.CONNECTED);
210
+ } catch (error) {
211
+ // Pool info check failed, continue without it
212
+ ui.updateTopBar({
213
+ connected: false,
214
+ lastPoll: null
215
+ });
216
+ ui.updateSendStatus('Connecting to server...', 'info');
217
+ }
218
+ } else {
219
+ // Not connected initially
220
+ ui.updateTopBar({
221
+ connected: false,
222
+ lastPoll: null
223
+ });
224
+ ui.showInfo(INFO_MESSAGES.CONNECTING);
225
+ ui.updateSendStatus(INFO_MESSAGES.RECONNECTING, 'error');
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Start verification loop for Token and PubKey
231
+ * @param {RpcService} rpcService - RPC service
232
+ * @param {WalletManager} walletManager - Wallet manager
233
+ * @param {Object} config - Configuration
234
+ * @param {TerminalUI} ui - UI instance
235
+ * @param {MessagePoller} messagePoller - Message poller instance
236
+ */
237
+ function startVerificationLoop(rpcService, walletManager, config, ui, getMessagePoller, resetMessagingAfterReconnect) {
238
+ const RETRY_MS = 30000;
239
+ let timeoutId = null;
240
+ let hadBlockingErrors = false;
241
+
242
+ const scheduleNext = (ms) => {
243
+ if (timeoutId) {
244
+ clearTimeout(timeoutId);
245
+ }
246
+ timeoutId = setTimeout(() => {
247
+ verify();
248
+ }, ms);
249
+ };
250
+
251
+ const verify = async () => {
252
+ const errors = [];
253
+ const address = walletManager.getAddress();
254
+
255
+ const messagePoller = getMessagePoller();
256
+
257
+ // 1. Check / (re)connect RPC
258
+ // Only happens when this verification runs (aligned with overlay countdown).
259
+ let isConnected = false;
260
+ if (!rpcService.isConnected()) {
261
+ isConnected = await rpcService.attemptReconnect(true);
262
+ } else {
263
+ isConnected = await rpcService.testConnection(true);
264
+ }
265
+
266
+ if (!isConnected) {
267
+ errors.push('RPC: Unable to connect to RPC server or Node.');
268
+ if (messagePoller) {
269
+ messagePoller.wasDisconnected = true;
270
+ }
271
+ } else {
272
+ try {
273
+ // 2. Verify Token
274
+ const hasToken = await rpcService.verifyTokenOwnership(address, config.token);
275
+ if (!hasToken) {
276
+ errors.push(`Token: You do not have the configured token (${config.token}).`);
277
+ }
278
+
279
+ // 3. Verify Public Key
280
+ const isRevealed = await rpcService.checkPubKeyRevealed(address);
281
+ if (!isRevealed) {
282
+ errors.push('PubKey: Not available on the blockchain.');
283
+ }
284
+ } catch (err) {
285
+ // If verification fails due to RPC error
286
+ errors.push(`RPC: Error verifying data (${err.message})`);
287
+ }
288
+ }
289
+
290
+ // Update UI
291
+ if (errors.length > 0) {
292
+ hadBlockingErrors = true;
293
+ ui.showBlockingErrors(errors);
294
+ if (messagePoller) {
295
+ messagePoller.stop();
296
+ }
297
+ scheduleNext(RETRY_MS);
298
+ } else {
299
+ const shouldFullSync = hadBlockingErrors;
300
+ hadBlockingErrors = false;
301
+ ui.clearBlockingErrors();
302
+
303
+ if (shouldFullSync && typeof resetMessagingAfterReconnect === 'function') {
304
+ await resetMessagingAfterReconnect();
305
+ } else if (messagePoller) {
306
+ messagePoller.start();
307
+ try {
308
+ await messagePoller.poll();
309
+ } catch (e) {
310
+ // Poller error handler will surface this and reschedule.
311
+ }
312
+ }
313
+
314
+ scheduleNext(RETRY_MS);
315
+ }
316
+ };
317
+
318
+ const notifyRpcDown = () => {
319
+ // Start a fresh 30s countdown and retry schedule
320
+ hadBlockingErrors = true;
321
+ ui.showBlockingErrors(['RPC: Unable to connect to RPC server or Node.']);
322
+ const messagePoller = getMessagePoller();
323
+ if (messagePoller) {
324
+ messagePoller.wasDisconnected = true;
325
+ messagePoller.stop();
326
+ }
327
+ scheduleNext(RETRY_MS);
328
+ };
329
+
330
+ const start = () => {
331
+ verify();
332
+ };
333
+
334
+ return { notifyRpcDown, start };
335
+ }
336
+
337
+ /**
338
+ * Main application entry point
339
+ * Orchestrates initialization and starts the application
340
+ */
341
+ async function main() {
342
+ try {
343
+ console.log('Neurai DePIN Terminal');
344
+ console.log('=====================\n');
345
+
346
+ // 1. Load configuration
347
+ const config = await initializeConfig();
348
+
349
+ // 2. Load DePIN library
350
+ const neuraiDepinMsg = await initializeLibrary();
351
+
352
+ // 3. Initialize wallet
353
+ const walletManager = await initializeWallet(config);
354
+
355
+ // 4. Initialize RPC
356
+ const rpcService = await initializeRpc(config);
357
+
358
+ // 5. Initialize messaging components
359
+ const { messageStore, messagePoller, messageSender } = initializeMessaging(
360
+ config,
361
+ walletManager,
362
+ rpcService,
363
+ neuraiDepinMsg
364
+ );
365
+
366
+ // Mutable messaging refs to allow reset on reconnect
367
+ const messaging = {
368
+ messageStore,
369
+ messagePoller,
370
+ messageSender,
371
+ detachPollerUi: null
372
+ };
373
+
374
+ // 6. Initialize UI
375
+ console.log(INFO_MESSAGES.STARTING_UI);
376
+ console.log('');
377
+ const ui = new TerminalUI(config, walletManager, rpcService);
378
+ uiInstance = ui;
379
+
380
+ // 7. Get initial pool info and check connection
381
+ await performInitialConnectionCheck(rpcService, ui);
382
+
383
+ let onRpcDownHandler = null;
384
+
385
+ const attachCurrentPollerToUI = () => {
386
+ if (messaging.detachPollerUi) {
387
+ messaging.detachPollerUi();
388
+ messaging.detachPollerUi = null;
389
+ }
390
+ messaging.detachPollerUi = connectPollerToUI(
391
+ messaging.messagePoller,
392
+ ui,
393
+ rpcService,
394
+ (err) => {
395
+ if (typeof onRpcDownHandler === 'function') {
396
+ onRpcDownHandler(err);
397
+ }
398
+ }
399
+ );
400
+ };
401
+
402
+ const resetMessagingAfterReconnect = async () => {
403
+ // Make reconnection behave like initial startup: new store + new poller + listeners.
404
+ if (messaging.detachPollerUi) {
405
+ messaging.detachPollerUi();
406
+ messaging.detachPollerUi = null;
407
+ }
408
+ if (messaging.messagePoller) {
409
+ messaging.messagePoller.stop();
410
+ messaging.messagePoller.removeAllListeners();
411
+ }
412
+
413
+ messaging.messageStore = new MessageStore();
414
+ messaging.messagePoller = new MessagePoller(
415
+ config,
416
+ rpcService,
417
+ messaging.messageStore,
418
+ neuraiDepinMsg,
419
+ walletManager
420
+ );
421
+
422
+ // Mark as disconnected so the first poll is a full sync
423
+ messaging.messagePoller.wasDisconnected = true;
424
+
425
+ attachCurrentPollerToUI();
426
+
427
+ // Refresh pool info like at startup
428
+ await performInitialConnectionCheck(rpcService, ui);
429
+
430
+ messaging.messagePoller.start();
431
+ await messaging.messagePoller.poll();
432
+ };
433
+
434
+ const getMessagePoller = () => messaging.messagePoller;
435
+
436
+ // 8. Create verification loop (Single retry mechanism)
437
+ const verification = startVerificationLoop(
438
+ rpcService,
439
+ walletManager,
440
+ config,
441
+ ui,
442
+ getMessagePoller,
443
+ resetMessagingAfterReconnect
444
+ );
445
+ onRpcDownHandler = verification.notifyRpcDown;
446
+
447
+ // 9. Connect poller events to UI
448
+ attachCurrentPollerToUI();
449
+
450
+ // 10. Connect message sending from UI
451
+ connectSenderToUI(ui, messaging.messageSender, getMessagePoller);
452
+
453
+ // 11. Start verification loop (after wiring listeners)
454
+ verification.start();
455
+
456
+ // 12. Mark as disconnected if starting without connection
457
+ if (!rpcService.isConnected()) {
458
+ messaging.messagePoller.wasDisconnected = true;
459
+ }
460
+
461
+ // 13. Show instructions
462
+ ui.showInfo(INFO_MESSAGES.PRESS_CTRL_C);
463
+
464
+ } catch (error) {
465
+ if (uiInstance) {
466
+ uiInstance.cleanup();
467
+ }
468
+
469
+ const errorMsg = extractErrorMessage(error, 'Unknown error');
470
+ console.error('Fatal error:', errorMsg);
471
+
472
+ if (error.stack) {
473
+ console.error(error.stack);
474
+ }
475
+
476
+ process.exit(1);
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Handle unhandled promise rejections
482
+ */
483
+ process.on('unhandledRejection', (error) => {
484
+ if (uiInstance) {
485
+ uiInstance.cleanup();
486
+ }
487
+
488
+ const errorMsg = extractErrorMessage(error, 'Unknown error');
489
+ console.error('Unhandled error:', errorMsg);
490
+
491
+ if (error && error.stack) {
492
+ console.error(error.stack);
493
+ }
494
+
495
+ process.exit(1);
496
+ });
497
+
498
+ /**
499
+ * Handle SIGINT (Ctrl+C)
500
+ */
501
+ process.on('SIGINT', () => {
502
+ if (uiInstance) {
503
+ uiInstance.cleanup();
504
+ }
505
+ process.exit(0);
506
+ });
507
+
508
+ /**
509
+ * Handle SIGTERM
510
+ */
511
+ process.on('SIGTERM', () => {
512
+ if (uiInstance) {
513
+ uiInstance.cleanup();
514
+ }
515
+ process.exit(0);
516
+ });
517
+
518
+ /**
519
+ * Handle process exit
520
+ */
521
+ process.on('exit', () => {
522
+ if (uiInstance) {
523
+ uiInstance.cleanup();
524
+ }
525
+ });
526
+
527
+ // Execute main function
528
+ main();