@neuraiproject/neurai-depin-terminal 1.0.1 → 2.0.1
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/README.md +27 -8
- package/dist/index.cjs +143 -188
- package/package.json +12 -11
- package/src/config/ConfigManager.js +49 -29
- package/src/constants.js +18 -5
- package/src/domain/messageTypes.js +26 -0
- package/src/errors.js +17 -0
- package/src/index.js +129 -30
- package/src/messaging/MessagePoller.js +132 -3
- package/src/messaging/MessageSender.js +99 -41
- package/src/messaging/MessageStore.js +26 -0
- package/src/messaging/RecipientDirectory.js +186 -0
- package/src/ui/CharsmUI.js +690 -0
- package/src/ui/RecipientSelector.js +114 -0
- package/src/ui/TabManager.js +162 -0
- package/src/ui/render.js +173 -0
- package/src/utils.js +304 -43
- package/src/ui/TerminalUI.js +0 -272
- package/src/ui/components/ErrorOverlay.js +0 -99
- package/src/ui/components/InputBox.js +0 -63
- package/src/ui/components/MessageBox.js +0 -51
- package/src/ui/components/StatusBar.js +0 -32
- package/src/ui/components/TopBar.js +0 -63
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neuraiproject/neurai-depin-terminal",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Neurai DePIN terminal messaging client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"start": "node src/index.js",
|
|
25
|
-
"bundle": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --minify --alias:term.js=./src/lib/empty.js --alias:pty.js=./src/lib/empty.js --log-override:empty-import-meta=silent",
|
|
25
|
+
"bundle": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --minify --external:charsm --alias:term.js=./src/lib/empty.js --alias:pty.js=./src/lib/empty.js --log-override:empty-import-meta=silent",
|
|
26
26
|
"prepublishOnly": "npm run bundle",
|
|
27
27
|
"build": "npm run bundle && NODE_NO_WARNINGS=1 pkg dist/index.cjs --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output bin/neurai-depin-terminal --compress GZip"
|
|
28
28
|
},
|
|
29
29
|
"pkg": {
|
|
30
30
|
"assets": [
|
|
31
|
-
"node_modules/@neuraiproject/neurai-depin-msg/dist/neurai-depin-msg.js"
|
|
31
|
+
"node_modules/@neuraiproject/neurai-depin-msg/dist/neurai-depin-msg.js",
|
|
32
|
+
"node_modules/charsm/dist/lip.wasm",
|
|
33
|
+
"node_modules/charsm/dist/binary/**/*"
|
|
32
34
|
],
|
|
33
35
|
"scripts": [
|
|
34
|
-
"node_modules/
|
|
35
|
-
"node_modules/
|
|
36
|
+
"node_modules/charsm/dist/**/*.js",
|
|
37
|
+
"node_modules/charsm/dist/**/*.mjs"
|
|
36
38
|
]
|
|
37
39
|
},
|
|
38
40
|
"keywords": [
|
|
@@ -45,16 +47,15 @@
|
|
|
45
47
|
"author": "Neurai Community",
|
|
46
48
|
"license": "MIT",
|
|
47
49
|
"dependencies": {
|
|
48
|
-
"@neuraiproject/neurai-depin-msg": "^2.1.
|
|
49
|
-
"@neuraiproject/neurai-jswallet": "0.12.
|
|
50
|
-
"@neuraiproject/neurai-key": "^2.8.
|
|
51
|
-
"blessed": "^0.1.81",
|
|
52
|
-
"blessed-contrib": "^4.11.0",
|
|
50
|
+
"@neuraiproject/neurai-depin-msg": "^2.1.3",
|
|
51
|
+
"@neuraiproject/neurai-jswallet": "0.12.10",
|
|
52
|
+
"@neuraiproject/neurai-key": "^2.8.7",
|
|
53
53
|
"chalk": "^5.3.0",
|
|
54
|
+
"charsm": "^0.2.0",
|
|
54
55
|
"readline": "^1.3.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"esbuild": "^0.27.2",
|
|
58
59
|
"pkg": "^5.8.1"
|
|
59
60
|
}
|
|
60
|
-
}
|
|
61
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
SUCCESS_MESSAGES
|
|
19
19
|
} from '../constants.js';
|
|
20
20
|
import { ConfigError, PasswordError, EncryptionError } from '../errors.js';
|
|
21
|
-
import { readPassword, validatePassword, isValidUrl, isValidTimezone } from '../utils.js';
|
|
21
|
+
import { readPassword, validatePassword, isValidUrl, isValidTimezone, drainInput } from '../utils.js';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Manages application configuration with encrypted private key storage
|
|
@@ -36,22 +36,28 @@ export class ConfigManager {
|
|
|
36
36
|
* Encrypt private key using AES-256-GCM
|
|
37
37
|
* @param {string} privateKey - Plain text private key in WIF format
|
|
38
38
|
* @param {string} password - Password for encryption
|
|
39
|
-
* @returns {string} Encrypted data in format: salt:iv:authTag:encrypted (hex)
|
|
39
|
+
* @returns {Promise<string>} Encrypted data in format: salt:iv:authTag:encrypted (hex)
|
|
40
40
|
* @throws {EncryptionError} If encryption fails
|
|
41
41
|
*/
|
|
42
|
-
encryptPrivateKey(privateKey, password) {
|
|
42
|
+
async encryptPrivateKey(privateKey, password) {
|
|
43
43
|
try {
|
|
44
44
|
const salt = crypto.randomBytes(ENCRYPTION.SALT_LENGTH);
|
|
45
|
-
const key =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
const key = await new Promise((resolve, reject) => {
|
|
46
|
+
crypto.scrypt(
|
|
47
|
+
password,
|
|
48
|
+
salt,
|
|
49
|
+
ENCRYPTION.KEY_LENGTH,
|
|
50
|
+
{
|
|
51
|
+
N: ENCRYPTION.SCRYPT_COST,
|
|
52
|
+
r: ENCRYPTION.SCRYPT_BLOCK_SIZE,
|
|
53
|
+
p: ENCRYPTION.SCRYPT_PARALLELIZATION
|
|
54
|
+
},
|
|
55
|
+
(err, derivedKey) => {
|
|
56
|
+
if (err) reject(err);
|
|
57
|
+
else resolve(derivedKey);
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
});
|
|
55
61
|
const iv = crypto.randomBytes(ENCRYPTION.IV_LENGTH);
|
|
56
62
|
const cipher = crypto.createCipheriv(ENCRYPTION.ALGORITHM, key, iv);
|
|
57
63
|
|
|
@@ -69,10 +75,10 @@ export class ConfigManager {
|
|
|
69
75
|
* Decrypt private key using AES-256-GCM
|
|
70
76
|
* @param {string} encryptedData - Encrypted data in format: salt:iv:authTag:encrypted
|
|
71
77
|
* @param {string} password - Password for decryption
|
|
72
|
-
* @returns {string} Decrypted private key in WIF format
|
|
78
|
+
* @returns {Promise<string>} Decrypted private key in WIF format
|
|
73
79
|
* @throws {EncryptionError} If decryption fails or password is incorrect
|
|
74
80
|
*/
|
|
75
|
-
decryptPrivateKey(encryptedData, password) {
|
|
81
|
+
async decryptPrivateKey(encryptedData, password) {
|
|
76
82
|
try {
|
|
77
83
|
const parts = encryptedData.split(':');
|
|
78
84
|
if (parts.length !== 4) {
|
|
@@ -84,16 +90,22 @@ export class ConfigManager {
|
|
|
84
90
|
const authTag = Buffer.from(parts[2], 'hex');
|
|
85
91
|
const encrypted = parts[3];
|
|
86
92
|
|
|
87
|
-
const key =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
const key = await new Promise((resolve, reject) => {
|
|
94
|
+
crypto.scrypt(
|
|
95
|
+
password,
|
|
96
|
+
salt,
|
|
97
|
+
ENCRYPTION.KEY_LENGTH,
|
|
98
|
+
{
|
|
99
|
+
N: ENCRYPTION.SCRYPT_COST,
|
|
100
|
+
r: ENCRYPTION.SCRYPT_BLOCK_SIZE,
|
|
101
|
+
p: ENCRYPTION.SCRYPT_PARALLELIZATION
|
|
102
|
+
},
|
|
103
|
+
(err, derivedKey) => {
|
|
104
|
+
if (err) reject(err);
|
|
105
|
+
else resolve(derivedKey);
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
});
|
|
97
109
|
const decipher = crypto.createDecipheriv(ENCRYPTION.ALGORITHM, key, iv);
|
|
98
110
|
decipher.setAuthTag(authTag);
|
|
99
111
|
|
|
@@ -126,10 +138,13 @@ export class ConfigManager {
|
|
|
126
138
|
const password = await readPassword('Enter password to decrypt private key: ');
|
|
127
139
|
|
|
128
140
|
try {
|
|
129
|
-
|
|
141
|
+
process.stdout.write('Verifying password...');
|
|
142
|
+
privateKey = await this.decryptPrivateKey(this.config.privateKey, password);
|
|
143
|
+
process.stdout.write('\r\x1b[K'); // Clear the "Verifying..." line
|
|
130
144
|
decrypted = true;
|
|
131
145
|
console.log('✓ Private key decrypted successfully\n');
|
|
132
146
|
} catch (error) {
|
|
147
|
+
process.stdout.write('\r\x1b[K'); // Clear the "Verifying..." line
|
|
133
148
|
if (attempts < maxAttempts) {
|
|
134
149
|
console.log(`✗ Incorrect password. ${maxAttempts - attempts} attempts remaining.\n`);
|
|
135
150
|
} else {
|
|
@@ -210,6 +225,9 @@ export class ConfigManager {
|
|
|
210
225
|
* @returns {Promise<void>}
|
|
211
226
|
*/
|
|
212
227
|
async runWizard() {
|
|
228
|
+
// Ensure input buffer is clean before starting wizard
|
|
229
|
+
await drainInput(process.stdin);
|
|
230
|
+
|
|
213
231
|
const rl = readline.createInterface({
|
|
214
232
|
input: process.stdin,
|
|
215
233
|
output: process.stdout
|
|
@@ -258,7 +276,9 @@ export class ConfigManager {
|
|
|
258
276
|
|
|
259
277
|
// Get password and encrypt private key
|
|
260
278
|
const password = await this.promptForPasswordCreation();
|
|
261
|
-
|
|
279
|
+
process.stdout.write('Encrypting private key...');
|
|
280
|
+
const encryptedPrivateKey = await this.encryptPrivateKey(privateKey, password);
|
|
281
|
+
process.stdout.write('\r\x1b[K'); // Clear the "Encrypting..." line
|
|
262
282
|
console.log('✓ Private key encrypted successfully\n');
|
|
263
283
|
|
|
264
284
|
// Create new readline for remaining questions
|
|
@@ -279,9 +299,9 @@ export class ConfigManager {
|
|
|
279
299
|
rl2,
|
|
280
300
|
'Timezone offset (e.g., +1, -5, +5.5, UTC) [default: UTC]: '
|
|
281
301
|
);
|
|
282
|
-
|
|
302
|
+
|
|
283
303
|
const candidate = input || 'UTC';
|
|
284
|
-
|
|
304
|
+
|
|
285
305
|
if (isValidTimezone(candidate)) {
|
|
286
306
|
timezone = candidate;
|
|
287
307
|
} else {
|
package/src/constants.js
CHANGED
|
@@ -48,6 +48,11 @@ export const MESSAGE = {
|
|
|
48
48
|
FORCE_POLL_DELAY: 2000
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
// Recipient cache
|
|
52
|
+
export const RECIPIENT_CACHE = {
|
|
53
|
+
REFRESH_MS: 60000
|
|
54
|
+
};
|
|
55
|
+
|
|
51
56
|
// Token Validation
|
|
52
57
|
export const TOKEN = {
|
|
53
58
|
PREFIX: '&',
|
|
@@ -57,9 +62,10 @@ export const TOKEN = {
|
|
|
57
62
|
// UI Layout
|
|
58
63
|
export const UI = {
|
|
59
64
|
TOP_BAR_HEIGHT: 2,
|
|
65
|
+
TAB_BAR_HEIGHT: 1,
|
|
60
66
|
INPUT_BOX_HEIGHT: 3,
|
|
61
67
|
STATUS_BAR_HEIGHT: 1,
|
|
62
|
-
MESSAGE_BOX_OFFSET:
|
|
68
|
+
MESSAGE_BOX_OFFSET: 7, // top bar + tab bar + input + status
|
|
63
69
|
SCROLLBAR_CHAR: ' '
|
|
64
70
|
};
|
|
65
71
|
|
|
@@ -94,11 +100,13 @@ export const RPC_METHODS = {
|
|
|
94
100
|
DEPIN_SUBMIT_MSG: 'depinsubmitmsg',
|
|
95
101
|
DEPIN_GET_MSG_INFO: 'depingetmsginfo',
|
|
96
102
|
LIST_ADDRESSES_BY_ASSET: 'listaddressesbyasset',
|
|
97
|
-
GET_PUBKEY: 'getpubkey'
|
|
103
|
+
GET_PUBKEY: 'getpubkey',
|
|
104
|
+
LIST_DEPIN_ADDRESSES: 'listdepinaddresses'
|
|
98
105
|
};
|
|
99
106
|
|
|
100
107
|
// Terminal Control Sequences
|
|
101
108
|
export const TERMINAL = {
|
|
109
|
+
ENTER_ALT_SCREEN: '\x1b[?1049h',
|
|
102
110
|
EXIT_ALT_SCREEN: '\x1b[?1049l',
|
|
103
111
|
SHOW_CURSOR: '\x1b[?25h',
|
|
104
112
|
RESET_ATTRIBUTES: '\x1b[0m',
|
|
@@ -143,7 +151,9 @@ export const ERROR_MESSAGES = {
|
|
|
143
151
|
INVALID_RPC_URL: 'Invalid RPC URL',
|
|
144
152
|
CONNECTION_ERROR: 'Connection error',
|
|
145
153
|
TOKEN_NOT_OWNED: 'This address does not own the configured token',
|
|
146
|
-
PUBKEY_NOT_REVEALED: 'Public key not revealed on blockchain'
|
|
154
|
+
PUBKEY_NOT_REVEALED: 'Public key not revealed on blockchain',
|
|
155
|
+
INVALID_PRIVATE_MESSAGE_FORMAT: 'Private message format: @address message',
|
|
156
|
+
RECIPIENT_PUBKEY_NOT_REVEALED: 'Recipient public key not revealed on blockchain'
|
|
147
157
|
};
|
|
148
158
|
|
|
149
159
|
// Success Messages
|
|
@@ -162,10 +172,10 @@ export const INFO_MESSAGES = {
|
|
|
162
172
|
LOADING_LIBRARY: 'Loading DePIN library...',
|
|
163
173
|
INITIALIZING_WALLET: 'Initializing wallet...',
|
|
164
174
|
STARTING_UI: 'Starting terminal interface...',
|
|
165
|
-
PRESS_CTRL_C: 'Press
|
|
175
|
+
PRESS_CTRL_C: 'Press ESC or CTRL+C to exit.',
|
|
166
176
|
CONNECTING: 'Attempting to connect to DePIN server...',
|
|
167
177
|
RECONNECTING: 'Reconnecting, check server configuration',
|
|
168
|
-
SENDING: 'Sending message
|
|
178
|
+
SENDING: 'Sending message...',
|
|
169
179
|
VERIFYING_TOKEN: 'Verifying token ownership...',
|
|
170
180
|
VERIFYING_PUBKEY: 'Verifying public key...'
|
|
171
181
|
};
|
|
@@ -192,6 +202,9 @@ export const TIME = {
|
|
|
192
202
|
export const BLESSED_KEYS = {
|
|
193
203
|
QUIT: ['C-c', 'escape'],
|
|
194
204
|
SEND: ['enter', 'C-s'],
|
|
205
|
+
TAB_NEXT: ['C-right'],
|
|
206
|
+
TAB_PREV: ['C-left'],
|
|
207
|
+
TAB_CLOSE: ['C-w'],
|
|
195
208
|
SCROLL_UP: ['up'],
|
|
196
209
|
SCROLL_DOWN: ['down']
|
|
197
210
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared message type helpers
|
|
3
|
+
* @module domain/messageTypes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const MESSAGE_TYPES = {
|
|
7
|
+
GROUP: 'group',
|
|
8
|
+
PRIVATE: 'private'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const normalizeMessageType = (rawType) => {
|
|
12
|
+
if (typeof rawType === 'string') {
|
|
13
|
+
const lowered = rawType.toLowerCase();
|
|
14
|
+
if (lowered === MESSAGE_TYPES.PRIVATE) {
|
|
15
|
+
return MESSAGE_TYPES.PRIVATE;
|
|
16
|
+
}
|
|
17
|
+
if (lowered === MESSAGE_TYPES.GROUP) {
|
|
18
|
+
return MESSAGE_TYPES.GROUP;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return MESSAGE_TYPES.GROUP;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const isPrivateMessage = (rawType) => {
|
|
25
|
+
return normalizeMessageType(rawType) === MESSAGE_TYPES.PRIVATE;
|
|
26
|
+
};
|
package/src/errors.js
CHANGED
|
@@ -147,3 +147,20 @@ export function extractErrorMessage(error, fallback = 'Unknown error') {
|
|
|
147
147
|
|
|
148
148
|
return fallback;
|
|
149
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if an error is a known/expected application error
|
|
153
|
+
* @param {Error} error - Error object
|
|
154
|
+
* @returns {boolean} True if error is a known application error
|
|
155
|
+
*/
|
|
156
|
+
export function isKnownError(error) {
|
|
157
|
+
return error instanceof DepinError;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if debug mode is enabled
|
|
162
|
+
* @returns {boolean} True if debug mode is enabled
|
|
163
|
+
*/
|
|
164
|
+
export function isDebugMode() {
|
|
165
|
+
return process.env.DEPIN_DEBUG === '1' || process.env.NODE_ENV === 'development';
|
|
166
|
+
}
|
package/src/index.js
CHANGED
|
@@ -13,21 +13,25 @@ import { RpcService } from './services/RpcService.js';
|
|
|
13
13
|
import { MessageStore } from './messaging/MessageStore.js';
|
|
14
14
|
import { MessagePoller } from './messaging/MessagePoller.js';
|
|
15
15
|
import { MessageSender } from './messaging/MessageSender.js';
|
|
16
|
-
import {
|
|
16
|
+
import { RecipientDirectory } from './messaging/RecipientDirectory.js';
|
|
17
|
+
import { CharsmUI } from './ui/CharsmUI.js';
|
|
17
18
|
import {
|
|
18
19
|
INFO_MESSAGES,
|
|
19
20
|
SUCCESS_MESSAGES,
|
|
20
21
|
ERROR_MESSAGES,
|
|
21
22
|
WARNING_MESSAGES,
|
|
22
23
|
MESSAGE,
|
|
24
|
+
RECIPIENT_CACHE,
|
|
23
25
|
HASH,
|
|
24
26
|
ICONS
|
|
25
27
|
} from './constants.js';
|
|
26
|
-
import { extractErrorMessage } from './errors.js';
|
|
28
|
+
import { extractErrorMessage, isKnownError, isDebugMode } from './errors.js';
|
|
29
|
+
import { MESSAGE_TYPES } from './domain/messageTypes.js';
|
|
30
|
+
import { emergencyTerminalCleanup, drainInput } from './utils.js';
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Global UI instance for cleanup on exit
|
|
30
|
-
* @type {
|
|
34
|
+
* @type {CharsmUI|null}
|
|
31
35
|
*/
|
|
32
36
|
let uiInstance = null;
|
|
33
37
|
|
|
@@ -91,17 +95,31 @@ async function initializeRpc(config) {
|
|
|
91
95
|
* @returns {Object} Messaging components (store, poller, sender)
|
|
92
96
|
*/
|
|
93
97
|
function initializeMessaging(config, walletManager, rpcService, neuraiDepinMsg) {
|
|
98
|
+
const recipientDirectory = new RecipientDirectory(config, rpcService, neuraiDepinMsg);
|
|
94
99
|
const messageStore = new MessageStore();
|
|
95
|
-
const messagePoller = new MessagePoller(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
const messagePoller = new MessagePoller(
|
|
101
|
+
config,
|
|
102
|
+
rpcService,
|
|
103
|
+
messageStore,
|
|
104
|
+
neuraiDepinMsg,
|
|
105
|
+
walletManager,
|
|
106
|
+
recipientDirectory
|
|
107
|
+
);
|
|
108
|
+
const messageSender = new MessageSender(
|
|
109
|
+
config,
|
|
110
|
+
walletManager,
|
|
111
|
+
rpcService,
|
|
112
|
+
neuraiDepinMsg,
|
|
113
|
+
recipientDirectory
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return { messageStore, messagePoller, messageSender, recipientDirectory };
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
/**
|
|
102
120
|
* Connect poller events to UI
|
|
103
121
|
* @param {MessagePoller} messagePoller - Message poller instance
|
|
104
|
-
* @param {
|
|
122
|
+
* @param {CharsmUI} ui - Terminal UI instance
|
|
105
123
|
* @param {RpcService} rpcService - RPC service instance
|
|
106
124
|
*/
|
|
107
125
|
function connectPollerToUI(messagePoller, ui, rpcService, onRpcDown) {
|
|
@@ -162,21 +180,39 @@ function connectPollerToUI(messagePoller, ui, rpcService, onRpcDown) {
|
|
|
162
180
|
|
|
163
181
|
/**
|
|
164
182
|
* Connect UI send action to message sender
|
|
165
|
-
* @param {
|
|
183
|
+
* @param {CharsmUI} ui - Terminal UI instance
|
|
166
184
|
* @param {MessageSender} messageSender - Message sender instance
|
|
167
185
|
* @param {MessagePoller} messagePoller - Message poller instance
|
|
168
186
|
*/
|
|
169
|
-
function connectSenderToUI(ui, messageSender, getMessagePoller) {
|
|
187
|
+
function connectSenderToUI(ui, messageSender, getMessageStore, getMessagePoller) {
|
|
170
188
|
ui.onSend(async (message) => {
|
|
171
189
|
ui.updateSendStatus(INFO_MESSAGES.SENDING, 'info');
|
|
172
190
|
|
|
173
191
|
try {
|
|
174
192
|
const result = await messageSender.send(message);
|
|
193
|
+
const hashPreview = result.hash
|
|
194
|
+
? `${result.hash.slice(0, HASH.DISPLAY_LENGTH)}...`
|
|
195
|
+
: 'N/A';
|
|
196
|
+
|
|
197
|
+
if (result.messageType === MESSAGE_TYPES.PRIVATE && result.messageHash && result.recipientAddress) {
|
|
198
|
+
const store = getMessageStore();
|
|
199
|
+
if (store) {
|
|
200
|
+
store.registerOutgoingPrivateMessage(result.messageHash, result.recipientAddress);
|
|
201
|
+
}
|
|
202
|
+
ui.openPrivateTab(result.recipientAddress, true);
|
|
203
|
+
}
|
|
175
204
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
205
|
+
if (result.messageType === MESSAGE_TYPES.PRIVATE) {
|
|
206
|
+
ui.updateSendStatus(
|
|
207
|
+
`Private message sent to ${result.recipientAddress}. Hash: ${hashPreview}`,
|
|
208
|
+
'success'
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
ui.updateSendStatus(
|
|
212
|
+
`Message sent to ${result.recipients} recipients. Hash: ${hashPreview}`,
|
|
213
|
+
'success'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
180
216
|
|
|
181
217
|
// Force a poll after sending to see the message
|
|
182
218
|
setTimeout(() => {
|
|
@@ -195,7 +231,7 @@ function connectSenderToUI(ui, messageSender, getMessagePoller) {
|
|
|
195
231
|
/**
|
|
196
232
|
* Perform initial connection check and update UI
|
|
197
233
|
* @param {RpcService} rpcService - RPC service instance
|
|
198
|
-
* @param {
|
|
234
|
+
* @param {CharsmUI} ui - Terminal UI instance
|
|
199
235
|
*/
|
|
200
236
|
async function performInitialConnectionCheck(rpcService, ui) {
|
|
201
237
|
if (rpcService.isConnected()) {
|
|
@@ -206,7 +242,10 @@ async function performInitialConnectionCheck(rpcService, ui) {
|
|
|
206
242
|
connected: true,
|
|
207
243
|
lastPoll: null
|
|
208
244
|
});
|
|
209
|
-
ui.showSuccess(SUCCESS_MESSAGES.CONNECTED);
|
|
245
|
+
const connectedMsgHash = ui.showSuccess(SUCCESS_MESSAGES.CONNECTED);
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
ui.removeMessage(connectedMsgHash);
|
|
248
|
+
}, 10000);
|
|
210
249
|
} catch (error) {
|
|
211
250
|
// Pool info check failed, continue without it
|
|
212
251
|
ui.updateTopBar({
|
|
@@ -231,7 +270,7 @@ async function performInitialConnectionCheck(rpcService, ui) {
|
|
|
231
270
|
* @param {RpcService} rpcService - RPC service
|
|
232
271
|
* @param {WalletManager} walletManager - Wallet manager
|
|
233
272
|
* @param {Object} config - Configuration
|
|
234
|
-
* @param {
|
|
273
|
+
* @param {CharsmUI} ui - UI instance
|
|
235
274
|
* @param {MessagePoller} messagePoller - Message poller instance
|
|
236
275
|
*/
|
|
237
276
|
function startVerificationLoop(rpcService, walletManager, config, ui, getMessagePoller, resetMessagingAfterReconnect) {
|
|
@@ -340,12 +379,28 @@ function startVerificationLoop(rpcService, walletManager, config, ui, getMessage
|
|
|
340
379
|
*/
|
|
341
380
|
async function main() {
|
|
342
381
|
try {
|
|
382
|
+
// Emergency cleanup to ensure terminal is in a clean state
|
|
383
|
+
emergencyTerminalCleanup();
|
|
384
|
+
|
|
343
385
|
console.log('Neurai DePIN Terminal');
|
|
344
386
|
console.log('=====================\n');
|
|
345
387
|
|
|
346
388
|
// 1. Load configuration
|
|
347
389
|
const config = await initializeConfig();
|
|
348
390
|
|
|
391
|
+
// Comprehensive stdin cleanup after password prompt
|
|
392
|
+
if (process.stdin.isTTY) {
|
|
393
|
+
process.stdin.removeAllListeners('data');
|
|
394
|
+
process.stdin.removeAllListeners('keypress');
|
|
395
|
+
process.stdin.setRawMode(false);
|
|
396
|
+
|
|
397
|
+
// Use shared robust flushing logic
|
|
398
|
+
await drainInput(process.stdin);
|
|
399
|
+
|
|
400
|
+
// Wait one tick for everything to stabilize
|
|
401
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
402
|
+
}
|
|
403
|
+
|
|
349
404
|
// 2. Load DePIN library
|
|
350
405
|
const neuraiDepinMsg = await initializeLibrary();
|
|
351
406
|
|
|
@@ -356,7 +411,7 @@ async function main() {
|
|
|
356
411
|
const rpcService = await initializeRpc(config);
|
|
357
412
|
|
|
358
413
|
// 5. Initialize messaging components
|
|
359
|
-
const { messageStore, messagePoller, messageSender } = initializeMessaging(
|
|
414
|
+
const { messageStore, messagePoller, messageSender, recipientDirectory } = initializeMessaging(
|
|
360
415
|
config,
|
|
361
416
|
walletManager,
|
|
362
417
|
rpcService,
|
|
@@ -368,17 +423,36 @@ async function main() {
|
|
|
368
423
|
messageStore,
|
|
369
424
|
messagePoller,
|
|
370
425
|
messageSender,
|
|
371
|
-
|
|
426
|
+
recipientDirectory,
|
|
427
|
+
detachPollerUi: null,
|
|
428
|
+
recipientRefreshInterval: null
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const refreshRecipientCache = async (force = false) => {
|
|
432
|
+
try {
|
|
433
|
+
await messaging.messageSender.refreshRecipientCache(force);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
// Non-fatal: keep existing cache if any
|
|
436
|
+
}
|
|
372
437
|
};
|
|
373
438
|
|
|
374
439
|
// 6. Initialize UI
|
|
375
440
|
console.log(INFO_MESSAGES.STARTING_UI);
|
|
376
441
|
console.log('');
|
|
377
|
-
const ui =
|
|
442
|
+
const ui = await CharsmUI.create(config, walletManager, rpcService);
|
|
378
443
|
uiInstance = ui;
|
|
444
|
+
ui.setRecipientProvider(
|
|
445
|
+
() => messaging.messageSender.getPrivateRecipientAddresses(),
|
|
446
|
+
() => messaging.messageSender.getCachedPrivateRecipientAddresses()
|
|
447
|
+
);
|
|
379
448
|
|
|
380
449
|
// 7. Get initial pool info and check connection
|
|
381
450
|
await performInitialConnectionCheck(rpcService, ui);
|
|
451
|
+
refreshRecipientCache(true);
|
|
452
|
+
messaging.recipientRefreshInterval = setInterval(
|
|
453
|
+
() => refreshRecipientCache(true),
|
|
454
|
+
RECIPIENT_CACHE.REFRESH_MS
|
|
455
|
+
);
|
|
382
456
|
|
|
383
457
|
let onRpcDownHandler = null;
|
|
384
458
|
|
|
@@ -416,7 +490,8 @@ async function main() {
|
|
|
416
490
|
rpcService,
|
|
417
491
|
messaging.messageStore,
|
|
418
492
|
neuraiDepinMsg,
|
|
419
|
-
walletManager
|
|
493
|
+
walletManager,
|
|
494
|
+
messaging.recipientDirectory
|
|
420
495
|
);
|
|
421
496
|
|
|
422
497
|
// Mark as disconnected so the first poll is a full sync
|
|
@@ -427,11 +502,13 @@ async function main() {
|
|
|
427
502
|
// Refresh pool info like at startup
|
|
428
503
|
await performInitialConnectionCheck(rpcService, ui);
|
|
429
504
|
|
|
505
|
+
await refreshRecipientCache(true);
|
|
430
506
|
messaging.messagePoller.start();
|
|
431
507
|
await messaging.messagePoller.poll();
|
|
432
508
|
};
|
|
433
509
|
|
|
434
510
|
const getMessagePoller = () => messaging.messagePoller;
|
|
511
|
+
const getMessageStore = () => messaging.messageStore;
|
|
435
512
|
|
|
436
513
|
// 8. Create verification loop (Single retry mechanism)
|
|
437
514
|
const verification = startVerificationLoop(
|
|
@@ -448,7 +525,7 @@ async function main() {
|
|
|
448
525
|
attachCurrentPollerToUI();
|
|
449
526
|
|
|
450
527
|
// 10. Connect message sending from UI
|
|
451
|
-
connectSenderToUI(ui, messaging.messageSender, getMessagePoller);
|
|
528
|
+
connectSenderToUI(ui, messaging.messageSender, getMessageStore, getMessagePoller);
|
|
452
529
|
|
|
453
530
|
// 11. Start verification loop (after wiring listeners)
|
|
454
531
|
verification.start();
|
|
@@ -459,7 +536,10 @@ async function main() {
|
|
|
459
536
|
}
|
|
460
537
|
|
|
461
538
|
// 13. Show instructions
|
|
462
|
-
ui.showInfo(INFO_MESSAGES.PRESS_CTRL_C);
|
|
539
|
+
const exitMsgHash = ui.showInfo(INFO_MESSAGES.PRESS_CTRL_C);
|
|
540
|
+
setTimeout(() => {
|
|
541
|
+
ui.removeMessage(exitMsgHash);
|
|
542
|
+
}, 10000);
|
|
463
543
|
|
|
464
544
|
} catch (error) {
|
|
465
545
|
if (uiInstance) {
|
|
@@ -467,10 +547,20 @@ async function main() {
|
|
|
467
547
|
}
|
|
468
548
|
|
|
469
549
|
const errorMsg = extractErrorMessage(error, 'Unknown error');
|
|
470
|
-
console.error('Fatal error:', errorMsg);
|
|
471
550
|
|
|
472
|
-
|
|
473
|
-
|
|
551
|
+
// For known errors, show a clean message. For unknown errors or debug mode, show stack trace
|
|
552
|
+
if (isKnownError(error)) {
|
|
553
|
+
console.error('\n✗ Error:', errorMsg);
|
|
554
|
+
if (isDebugMode() && error.stack) {
|
|
555
|
+
console.error('\nStack trace:');
|
|
556
|
+
console.error(error.stack);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
console.error('\n✗ Fatal error:', errorMsg);
|
|
560
|
+
if (error.stack) {
|
|
561
|
+
console.error('\nStack trace:');
|
|
562
|
+
console.error(error.stack);
|
|
563
|
+
}
|
|
474
564
|
}
|
|
475
565
|
|
|
476
566
|
process.exit(1);
|
|
@@ -486,10 +576,19 @@ process.on('unhandledRejection', (error) => {
|
|
|
486
576
|
}
|
|
487
577
|
|
|
488
578
|
const errorMsg = extractErrorMessage(error, 'Unknown error');
|
|
489
|
-
console.error('Unhandled error:', errorMsg);
|
|
490
579
|
|
|
491
|
-
if (error
|
|
492
|
-
console.error(
|
|
580
|
+
if (isKnownError(error)) {
|
|
581
|
+
console.error('\n✗ Error:', errorMsg);
|
|
582
|
+
if (isDebugMode() && error && error.stack) {
|
|
583
|
+
console.error('\nStack trace:');
|
|
584
|
+
console.error(error.stack);
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
console.error('\n✗ Unhandled error:', errorMsg);
|
|
588
|
+
if (error && error.stack) {
|
|
589
|
+
console.error('\nStack trace:');
|
|
590
|
+
console.error(error.stack);
|
|
591
|
+
}
|
|
493
592
|
}
|
|
494
593
|
|
|
495
594
|
process.exit(1);
|
|
@@ -525,4 +624,4 @@ process.on('exit', () => {
|
|
|
525
624
|
});
|
|
526
625
|
|
|
527
626
|
// Execute main function
|
|
528
|
-
main();
|
|
627
|
+
main();
|