@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/LICENSE +21 -0
- package/README.md +195 -0
- package/config.example.json +10 -0
- package/dist/index.cjs +218 -0
- package/package.json +61 -0
- package/src/config/ConfigManager.js +363 -0
- package/src/constants.js +208 -0
- package/src/errors.js +149 -0
- package/src/index.js +528 -0
- package/src/lib/depinMsgLoader.js +73 -0
- package/src/lib/empty.js +2 -0
- package/src/messaging/MessagePoller.js +300 -0
- package/src/messaging/MessageSender.js +194 -0
- package/src/messaging/MessageStore.js +99 -0
- package/src/services/RpcService.js +200 -0
- package/src/ui/TerminalUI.js +272 -0
- package/src/ui/components/ErrorOverlay.js +99 -0
- package/src/ui/components/InputBox.js +63 -0
- package/src/ui/components/MessageBox.js +51 -0
- package/src/ui/components/StatusBar.js +32 -0
- package/src/ui/components/TopBar.js +63 -0
- package/src/utils.js +309 -0
- package/src/wallet/WalletManager.js +94 -0
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();
|