@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/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neuraiproject/neurai-depin-terminal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Neurai DePIN terminal messaging client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"neurai-depin-terminal": "dist/index.cjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"src/",
|
|
13
|
+
"config.example.json",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node src/index.js",
|
|
25
|
+
"bundle": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --minify --external:secp256k1 --alias:term.js=./src/lib/empty.js --alias:pty.js=./src/lib/empty.js --log-override:empty-import-meta=silent",
|
|
26
|
+
"prepublishOnly": "npm run bundle",
|
|
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
|
+
},
|
|
29
|
+
"pkg": {
|
|
30
|
+
"assets": [
|
|
31
|
+
"node_modules/@neuraiproject/neurai-depin-msg/dist/neurai-depin-msg.js"
|
|
32
|
+
],
|
|
33
|
+
"scripts": [
|
|
34
|
+
"node_modules/blessed/lib/**/*.js",
|
|
35
|
+
"node_modules/blessed-contrib/lib/**/*.js"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"neurai",
|
|
40
|
+
"depin",
|
|
41
|
+
"messaging",
|
|
42
|
+
"terminal",
|
|
43
|
+
"cli"
|
|
44
|
+
],
|
|
45
|
+
"author": "Neurai Community",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@neuraiproject/neurai-depin-msg": "^2.1.1",
|
|
49
|
+
"@neuraiproject/neurai-jswallet": "0.12.8",
|
|
50
|
+
"@neuraiproject/neurai-key": "^2.8.5",
|
|
51
|
+
"blessed": "^0.1.81",
|
|
52
|
+
"blessed-contrib": "^4.11.0",
|
|
53
|
+
"chalk": "^5.3.0",
|
|
54
|
+
"readline": "^1.3.0",
|
|
55
|
+
"secp256k1": "^4.0.4"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"esbuild": "^0.27.2",
|
|
59
|
+
"pkg": "^5.8.1"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration manager for Neurai DePIN Terminal
|
|
3
|
+
* Handles loading, creating, validating, and encrypting configuration
|
|
4
|
+
* @module ConfigManager
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import readline from 'readline';
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
import {
|
|
12
|
+
CONFIG,
|
|
13
|
+
ENCRYPTION,
|
|
14
|
+
PASSWORD,
|
|
15
|
+
NETWORK,
|
|
16
|
+
POLLING,
|
|
17
|
+
ERROR_MESSAGES,
|
|
18
|
+
SUCCESS_MESSAGES
|
|
19
|
+
} from '../constants.js';
|
|
20
|
+
import { ConfigError, PasswordError, EncryptionError } from '../errors.js';
|
|
21
|
+
import { readPassword, validatePassword, isValidUrl, isValidTimezone } from '../utils.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manages application configuration with encrypted private key storage
|
|
25
|
+
*/
|
|
26
|
+
export class ConfigManager {
|
|
27
|
+
/**
|
|
28
|
+
* Create a new ConfigManager instance
|
|
29
|
+
*/
|
|
30
|
+
constructor() {
|
|
31
|
+
this.configPath = path.join(process.cwd(), CONFIG.FILE_NAME);
|
|
32
|
+
this.config = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Encrypt private key using AES-256-GCM
|
|
37
|
+
* @param {string} privateKey - Plain text private key in WIF format
|
|
38
|
+
* @param {string} password - Password for encryption
|
|
39
|
+
* @returns {string} Encrypted data in format: salt:iv:authTag:encrypted (hex)
|
|
40
|
+
* @throws {EncryptionError} If encryption fails
|
|
41
|
+
*/
|
|
42
|
+
encryptPrivateKey(privateKey, password) {
|
|
43
|
+
try {
|
|
44
|
+
const salt = crypto.randomBytes(ENCRYPTION.SALT_LENGTH);
|
|
45
|
+
const key = crypto.scryptSync(
|
|
46
|
+
password,
|
|
47
|
+
salt,
|
|
48
|
+
ENCRYPTION.KEY_LENGTH,
|
|
49
|
+
{
|
|
50
|
+
N: ENCRYPTION.SCRYPT_COST,
|
|
51
|
+
r: ENCRYPTION.SCRYPT_BLOCK_SIZE,
|
|
52
|
+
p: ENCRYPTION.SCRYPT_PARALLELIZATION
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
const iv = crypto.randomBytes(ENCRYPTION.IV_LENGTH);
|
|
56
|
+
const cipher = crypto.createCipheriv(ENCRYPTION.ALGORITHM, key, iv);
|
|
57
|
+
|
|
58
|
+
let encrypted = cipher.update(privateKey, 'utf8', 'hex');
|
|
59
|
+
encrypted += cipher.final('hex');
|
|
60
|
+
const authTag = cipher.getAuthTag();
|
|
61
|
+
|
|
62
|
+
return `${salt.toString('hex')}:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new EncryptionError(`Failed to encrypt private key: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Decrypt private key using AES-256-GCM
|
|
70
|
+
* @param {string} encryptedData - Encrypted data in format: salt:iv:authTag:encrypted
|
|
71
|
+
* @param {string} password - Password for decryption
|
|
72
|
+
* @returns {string} Decrypted private key in WIF format
|
|
73
|
+
* @throws {EncryptionError} If decryption fails or password is incorrect
|
|
74
|
+
*/
|
|
75
|
+
decryptPrivateKey(encryptedData, password) {
|
|
76
|
+
try {
|
|
77
|
+
const parts = encryptedData.split(':');
|
|
78
|
+
if (parts.length !== 4) {
|
|
79
|
+
throw new EncryptionError('Invalid encrypted data format');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const salt = Buffer.from(parts[0], 'hex');
|
|
83
|
+
const iv = Buffer.from(parts[1], 'hex');
|
|
84
|
+
const authTag = Buffer.from(parts[2], 'hex');
|
|
85
|
+
const encrypted = parts[3];
|
|
86
|
+
|
|
87
|
+
const key = crypto.scryptSync(
|
|
88
|
+
password,
|
|
89
|
+
salt,
|
|
90
|
+
ENCRYPTION.KEY_LENGTH,
|
|
91
|
+
{
|
|
92
|
+
N: ENCRYPTION.SCRYPT_COST,
|
|
93
|
+
r: ENCRYPTION.SCRYPT_BLOCK_SIZE,
|
|
94
|
+
p: ENCRYPTION.SCRYPT_PARALLELIZATION
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
const decipher = crypto.createDecipheriv(ENCRYPTION.ALGORITHM, key, iv);
|
|
98
|
+
decipher.setAuthTag(authTag);
|
|
99
|
+
|
|
100
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
101
|
+
decrypted += decipher.final('utf8');
|
|
102
|
+
|
|
103
|
+
return decrypted;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof EncryptionError) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
throw new EncryptionError(ERROR_MESSAGES.INVALID_PASSWORD);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Prompt user for password with retry logic
|
|
114
|
+
* @param {number} [maxAttempts=3] - Maximum number of attempts
|
|
115
|
+
* @returns {Promise<string>} Decrypted private key
|
|
116
|
+
* @throws {PasswordError} If max attempts exceeded
|
|
117
|
+
*/
|
|
118
|
+
async promptForDecryption(maxAttempts = PASSWORD.MAX_ATTEMPTS) {
|
|
119
|
+
console.log('\n🔐 Your private key is encrypted.');
|
|
120
|
+
let decrypted = false;
|
|
121
|
+
let attempts = 0;
|
|
122
|
+
let privateKey = null;
|
|
123
|
+
|
|
124
|
+
while (!decrypted && attempts < maxAttempts) {
|
|
125
|
+
attempts++;
|
|
126
|
+
const password = await readPassword('Enter password to decrypt private key: ');
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
privateKey = this.decryptPrivateKey(this.config.privateKey, password);
|
|
130
|
+
decrypted = true;
|
|
131
|
+
console.log('✓ Private key decrypted successfully\n');
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (attempts < maxAttempts) {
|
|
134
|
+
console.log(`✗ Incorrect password. ${maxAttempts - attempts} attempts remaining.\n`);
|
|
135
|
+
} else {
|
|
136
|
+
throw new PasswordError(ERROR_MESSAGES.MAX_ATTEMPTS_REACHED);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return privateKey;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Prompt user to create and confirm a password
|
|
146
|
+
* @returns {Promise<string>} Validated password
|
|
147
|
+
*/
|
|
148
|
+
async promptForPasswordCreation() {
|
|
149
|
+
console.log('\n🔐 To protect your private key, it will be encrypted with a password.');
|
|
150
|
+
|
|
151
|
+
while (true) {
|
|
152
|
+
const password = await readPassword(`Enter password (${PASSWORD.MIN_LENGTH}-${PASSWORD.MAX_LENGTH} characters): `);
|
|
153
|
+
|
|
154
|
+
const validation = validatePassword(password, PASSWORD.MIN_LENGTH, PASSWORD.MAX_LENGTH);
|
|
155
|
+
if (!validation.valid) {
|
|
156
|
+
console.log(`✗ ${validation.error}\n`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const passwordConfirm = await readPassword('Confirm password: ');
|
|
161
|
+
|
|
162
|
+
if (password !== passwordConfirm) {
|
|
163
|
+
console.log(`✗ ${ERROR_MESSAGES.PASSWORDS_DONT_MATCH}\n`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return password;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Prompt user for input using readline
|
|
173
|
+
* @param {readline.Interface} rl - Readline interface
|
|
174
|
+
* @param {string} prompt - Prompt message
|
|
175
|
+
* @returns {Promise<string>} User input
|
|
176
|
+
*/
|
|
177
|
+
async promptInput(rl, prompt) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
rl.question(prompt, resolve);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Load configuration from file or run wizard if not found
|
|
185
|
+
* @returns {Promise<Object>} Configuration object
|
|
186
|
+
* @throws {ConfigError} If config is invalid
|
|
187
|
+
*/
|
|
188
|
+
async load() {
|
|
189
|
+
if (!fs.existsSync(this.configPath)) {
|
|
190
|
+
console.log('config.json not found. Let\'s create it.');
|
|
191
|
+
await this.runWizard();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const configData = fs.readFileSync(this.configPath, 'utf-8');
|
|
196
|
+
this.config = JSON.parse(configData);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw new ConfigError(`Failed to load config: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Decrypt private key
|
|
202
|
+
this.config.privateKey = await this.promptForDecryption();
|
|
203
|
+
|
|
204
|
+
this.validate();
|
|
205
|
+
return this.config;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Run interactive configuration wizard
|
|
210
|
+
* @returns {Promise<void>}
|
|
211
|
+
*/
|
|
212
|
+
async runWizard() {
|
|
213
|
+
const rl = readline.createInterface({
|
|
214
|
+
input: process.stdin,
|
|
215
|
+
output: process.stdout
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
console.log('\n================================');
|
|
219
|
+
console.log('Welcome to Neurai DePIN Client');
|
|
220
|
+
console.log('================================\n');
|
|
221
|
+
|
|
222
|
+
// Collect RPC server URL (required)
|
|
223
|
+
let rpc_url = '';
|
|
224
|
+
while (!rpc_url) {
|
|
225
|
+
rpc_url = await this.promptInput(rl, 'RPC Server URL (e.g., https://rpc-depin.neurai.org): ');
|
|
226
|
+
if (!rpc_url) {
|
|
227
|
+
console.log('Error: RPC server is required');
|
|
228
|
+
} else if (!isValidUrl(rpc_url)) {
|
|
229
|
+
console.log('Error: Invalid URL format');
|
|
230
|
+
rpc_url = '';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Collect optional RPC credentials
|
|
235
|
+
const rpc_username = await this.promptInput(rl, 'RPC Username (optional, press Enter to skip): ') || '';
|
|
236
|
+
const rpc_password = await this.promptInput(rl, 'RPC Password (optional, press Enter to skip): ') || '';
|
|
237
|
+
|
|
238
|
+
// Collect token (required)
|
|
239
|
+
let token = '';
|
|
240
|
+
while (!token) {
|
|
241
|
+
token = await this.promptInput(rl, 'DePIN Token (asset name): ');
|
|
242
|
+
if (!token) {
|
|
243
|
+
console.log('Error: Token is required');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Collect private key (required)
|
|
248
|
+
let privateKey = '';
|
|
249
|
+
while (!privateKey) {
|
|
250
|
+
privateKey = await this.promptInput(rl, 'Private Key (WIF format): ');
|
|
251
|
+
if (!privateKey) {
|
|
252
|
+
console.log('Error: Private key is required');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Close readline before password input (uses raw mode)
|
|
257
|
+
rl.close();
|
|
258
|
+
|
|
259
|
+
// Get password and encrypt private key
|
|
260
|
+
const password = await this.promptForPasswordCreation();
|
|
261
|
+
const encryptedPrivateKey = this.encryptPrivateKey(privateKey, password);
|
|
262
|
+
console.log('✓ Private key encrypted successfully\n');
|
|
263
|
+
|
|
264
|
+
// Create new readline for remaining questions
|
|
265
|
+
const rl2 = readline.createInterface({
|
|
266
|
+
input: process.stdin,
|
|
267
|
+
output: process.stdout
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const pollIntervalStr = await this.promptInput(
|
|
271
|
+
rl2,
|
|
272
|
+
`Polling interval in ms [${POLLING.DEFAULT_INTERVAL}]: `
|
|
273
|
+
) || String(POLLING.DEFAULT_INTERVAL);
|
|
274
|
+
|
|
275
|
+
// Collect timezone (optional, default UTC)
|
|
276
|
+
let timezone = '';
|
|
277
|
+
while (!timezone) {
|
|
278
|
+
const input = await this.promptInput(
|
|
279
|
+
rl2,
|
|
280
|
+
'Timezone offset (e.g., +1, -5, +5.5, UTC) [default: UTC]: '
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const candidate = input || 'UTC';
|
|
284
|
+
|
|
285
|
+
if (isValidTimezone(candidate)) {
|
|
286
|
+
timezone = candidate;
|
|
287
|
+
} else {
|
|
288
|
+
console.log('Error: Invalid timezone. Please use numeric offset (e.g., +1, -5) or "UTC".');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
rl2.close();
|
|
293
|
+
|
|
294
|
+
// Build configuration object
|
|
295
|
+
const config = {
|
|
296
|
+
rpc_url,
|
|
297
|
+
rpc_username,
|
|
298
|
+
rpc_password,
|
|
299
|
+
token,
|
|
300
|
+
privateKey: encryptedPrivateKey,
|
|
301
|
+
network: NETWORK.DEFAULT,
|
|
302
|
+
pollInterval: parseInt(pollIntervalStr, 10),
|
|
303
|
+
timezone
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Save to file
|
|
307
|
+
try {
|
|
308
|
+
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
|
309
|
+
console.log('Saving configuration to config.json...');
|
|
310
|
+
console.log('✓ Configuration saved\n');
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw new ConfigError(`Failed to save config: ${error.message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Validate configuration object
|
|
318
|
+
* @throws {ConfigError} If validation fails
|
|
319
|
+
*/
|
|
320
|
+
validate() {
|
|
321
|
+
if (!this.config) {
|
|
322
|
+
throw new ConfigError('Config not loaded');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!this.config.rpc_url) {
|
|
326
|
+
throw new ConfigError('rpc_url is required in config.json');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!isValidUrl(this.config.rpc_url)) {
|
|
330
|
+
throw new ConfigError(ERROR_MESSAGES.INVALID_RPC_URL);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!this.config.token) {
|
|
334
|
+
throw new ConfigError('token is required in config.json');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!this.config.privateKey) {
|
|
338
|
+
throw new ConfigError('privateKey is required in config.json');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Force network to xna (mainnet only)
|
|
342
|
+
this.config.network = NETWORK.DEFAULT;
|
|
343
|
+
|
|
344
|
+
// Validate and adjust poll interval
|
|
345
|
+
if (!this.config.pollInterval || this.config.pollInterval < POLLING.MIN_INTERVAL) {
|
|
346
|
+
console.warn(`Warning: pollInterval too low, setting to ${POLLING.DEFAULT_INTERVAL}ms`);
|
|
347
|
+
this.config.pollInterval = POLLING.DEFAULT_INTERVAL;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (this.config.pollInterval > POLLING.MAX_INTERVAL) {
|
|
351
|
+
console.warn(`Warning: pollInterval too high, setting to ${POLLING.MAX_INTERVAL}ms`);
|
|
352
|
+
this.config.pollInterval = POLLING.MAX_INTERVAL;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get the loaded configuration
|
|
358
|
+
* @returns {Object} Configuration object
|
|
359
|
+
*/
|
|
360
|
+
get() {
|
|
361
|
+
return this.config;
|
|
362
|
+
}
|
|
363
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-wide constants for Neurai DePIN Terminal
|
|
3
|
+
* @module constants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Network Configuration
|
|
7
|
+
export const NETWORK = {
|
|
8
|
+
XNA: 'xna',
|
|
9
|
+
DEFAULT: 'xna'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Encryption
|
|
13
|
+
export const ENCRYPTION = {
|
|
14
|
+
ALGORITHM: 'aes-256-gcm',
|
|
15
|
+
SALT_LENGTH: 32,
|
|
16
|
+
IV_LENGTH: 16,
|
|
17
|
+
KEY_LENGTH: 32,
|
|
18
|
+
SCRYPT_COST: 16384,
|
|
19
|
+
SCRYPT_BLOCK_SIZE: 8,
|
|
20
|
+
SCRYPT_PARALLELIZATION: 1
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Password Validation
|
|
24
|
+
export const PASSWORD = {
|
|
25
|
+
MIN_LENGTH: 4,
|
|
26
|
+
MAX_LENGTH: 30,
|
|
27
|
+
MAX_ATTEMPTS: 3
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Polling Configuration
|
|
31
|
+
export const POLLING = {
|
|
32
|
+
DEFAULT_INTERVAL: 10000, // 10 seconds in milliseconds
|
|
33
|
+
MIN_INTERVAL: 1000,
|
|
34
|
+
MAX_INTERVAL: 60000
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// RPC Configuration
|
|
38
|
+
export const RPC = {
|
|
39
|
+
ENDPOINT_SUFFIX: '/rpc',
|
|
40
|
+
DEFAULT_URL: 'https://rpc-depin.neurai.org',
|
|
41
|
+
TIMEOUT: 30000,
|
|
42
|
+
DUMMY_MNEMONIC: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Message Deduplication
|
|
46
|
+
export const MESSAGE = {
|
|
47
|
+
SEPARATOR: '|',
|
|
48
|
+
FORCE_POLL_DELAY: 2000
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Token Validation
|
|
52
|
+
export const TOKEN = {
|
|
53
|
+
PREFIX: '&',
|
|
54
|
+
MIN_LENGTH: 2
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// UI Layout
|
|
58
|
+
export const UI = {
|
|
59
|
+
TOP_BAR_HEIGHT: 2,
|
|
60
|
+
INPUT_BOX_HEIGHT: 3,
|
|
61
|
+
STATUS_BAR_HEIGHT: 1,
|
|
62
|
+
MESSAGE_BOX_OFFSET: 6, // top bar + input + status
|
|
63
|
+
SCROLLBAR_CHAR: ' '
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// UI Colors
|
|
67
|
+
export const COLORS = {
|
|
68
|
+
CONNECTED: 'green-fg',
|
|
69
|
+
DISCONNECTED: 'red-fg',
|
|
70
|
+
MY_MESSAGE: 'cyan-fg',
|
|
71
|
+
OTHER_MESSAGE: 'green-fg',
|
|
72
|
+
ERROR: 'red-fg',
|
|
73
|
+
SUCCESS: 'green-fg',
|
|
74
|
+
INFO: 'yellow-fg',
|
|
75
|
+
BORDER: 'cyan',
|
|
76
|
+
BG_BLUE: 'blue',
|
|
77
|
+
BG_BLACK: 'black',
|
|
78
|
+
FG_WHITE: 'white'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// UI Status Icons
|
|
82
|
+
export const ICONS = {
|
|
83
|
+
CONNECTED: '●',
|
|
84
|
+
DISCONNECTED: '●',
|
|
85
|
+
SUCCESS: '✓',
|
|
86
|
+
ERROR: '✗',
|
|
87
|
+
LOADING: '⟳'
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// RPC Methods
|
|
91
|
+
export const RPC_METHODS = {
|
|
92
|
+
GET_BLOCKCHAIN_INFO: 'getblockchaininfo',
|
|
93
|
+
DEPIN_RECEIVE_MSG: 'depinreceivemsg',
|
|
94
|
+
DEPIN_SUBMIT_MSG: 'depinsubmitmsg',
|
|
95
|
+
DEPIN_GET_MSG_INFO: 'depingetmsginfo',
|
|
96
|
+
LIST_ADDRESSES_BY_ASSET: 'listaddressesbyasset',
|
|
97
|
+
GET_PUBKEY: 'getpubkey'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Terminal Control Sequences
|
|
101
|
+
export const TERMINAL = {
|
|
102
|
+
EXIT_ALT_SCREEN: '\x1b[?1049l',
|
|
103
|
+
SHOW_CURSOR: '\x1b[?25h',
|
|
104
|
+
RESET_ATTRIBUTES: '\x1b[0m',
|
|
105
|
+
NEW_LINE: '\r\n',
|
|
106
|
+
BACKSPACE: '\b \b'
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Config File
|
|
110
|
+
export const CONFIG = {
|
|
111
|
+
FILE_NAME: 'config.json',
|
|
112
|
+
EXAMPLE_FILE_NAME: 'config.example.json',
|
|
113
|
+
ENCRYPTED_KEY: 'privateKeyEncrypted',
|
|
114
|
+
PLAIN_KEY: 'privateKey'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Special Key Codes
|
|
118
|
+
export const KEY_CODES = {
|
|
119
|
+
ENTER: '\n',
|
|
120
|
+
CARRIAGE_RETURN: '\r',
|
|
121
|
+
CTRL_D: '\u0004',
|
|
122
|
+
CTRL_C: '\u0003',
|
|
123
|
+
BACKSPACE: '\u007f',
|
|
124
|
+
BACKSPACE_ALT: '\b'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Error Messages
|
|
128
|
+
export const ERROR_MESSAGES = {
|
|
129
|
+
CONFIG_NOT_FOUND: 'Configuration file not found',
|
|
130
|
+
INVALID_CONFIG: 'Invalid configuration',
|
|
131
|
+
INVALID_PASSWORD: 'Invalid password',
|
|
132
|
+
PASSWORD_TOO_SHORT: `Password must be at least ${PASSWORD.MIN_LENGTH} characters`,
|
|
133
|
+
PASSWORD_TOO_LONG: `Password must be at most ${PASSWORD.MAX_LENGTH} characters`,
|
|
134
|
+
PASSWORDS_DONT_MATCH: 'Passwords do not match',
|
|
135
|
+
MAX_ATTEMPTS_REACHED: `Maximum password attempts (${PASSWORD.MAX_ATTEMPTS}) reached`,
|
|
136
|
+
WALLET_INIT_FAILED: 'Failed to initialize wallet',
|
|
137
|
+
RPC_NOT_INITIALIZED: 'RPC client not initialized',
|
|
138
|
+
NO_TOKEN_HOLDERS: 'No token holders found',
|
|
139
|
+
NO_RECIPIENTS: 'No recipients found with revealed public key',
|
|
140
|
+
LIBRARY_LOAD_FAILED: 'Failed to load neuraiDepinMsg library',
|
|
141
|
+
INVALID_WIF: 'Invalid WIF private key format',
|
|
142
|
+
INVALID_TOKEN: `Token must start with "${TOKEN.PREFIX}"`,
|
|
143
|
+
INVALID_RPC_URL: 'Invalid RPC URL',
|
|
144
|
+
CONNECTION_ERROR: 'Connection error',
|
|
145
|
+
TOKEN_NOT_OWNED: 'This address does not own the configured token',
|
|
146
|
+
PUBKEY_NOT_REVEALED: 'Public key not revealed on blockchain'
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Success Messages
|
|
150
|
+
export const SUCCESS_MESSAGES = {
|
|
151
|
+
CONFIG_LOADED: '✓ Configuration loaded',
|
|
152
|
+
LIBRARY_LOADED: '✓ DePIN library loaded',
|
|
153
|
+
RPC_CONNECTED: '✓ Connected to RPC server',
|
|
154
|
+
TOKEN_VERIFIED: '✓ Token ownership verified',
|
|
155
|
+
PUBKEY_VERIFIED: '✓ Public key revealed',
|
|
156
|
+
CONNECTED: 'Connected! Type your message and press Enter to send.'
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Info Messages
|
|
160
|
+
export const INFO_MESSAGES = {
|
|
161
|
+
LOADING_CONFIG: 'Loading configuration...',
|
|
162
|
+
LOADING_LIBRARY: 'Loading DePIN library...',
|
|
163
|
+
INITIALIZING_WALLET: 'Initializing wallet...',
|
|
164
|
+
STARTING_UI: 'Starting terminal interface...',
|
|
165
|
+
PRESS_CTRL_C: 'Press Ctrl+C to exit.',
|
|
166
|
+
CONNECTING: 'Attempting to connect to DePIN server...',
|
|
167
|
+
RECONNECTING: 'Reconnecting, check server configuration',
|
|
168
|
+
SENDING: 'Sending message to all token holders...',
|
|
169
|
+
VERIFYING_TOKEN: 'Verifying token ownership...',
|
|
170
|
+
VERIFYING_PUBKEY: 'Verifying public key...'
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Warning Messages
|
|
174
|
+
export const WARNING_MESSAGES = {
|
|
175
|
+
RPC_CONNECTION_FAILED: '⚠ Could not connect to RPC server. Will retry during polling.',
|
|
176
|
+
RPC_INIT_FAILED: '⚠ RPC client initialization failed. Will retry during polling.'
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Privacy Layer
|
|
180
|
+
export const PRIVACY = {
|
|
181
|
+
NO_KEY_VALUE: '0',
|
|
182
|
+
DEFAULT_ENCRYPTION: 'N/A'
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Time Formats
|
|
186
|
+
export const TIME = {
|
|
187
|
+
LOCALE_TIME: 'toLocaleTimeString',
|
|
188
|
+
PLACEHOLDER: '--:--:--'
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Blessed Keys
|
|
192
|
+
export const BLESSED_KEYS = {
|
|
193
|
+
QUIT: ['C-c', 'escape'],
|
|
194
|
+
SEND: ['enter', 'C-s'],
|
|
195
|
+
SCROLL_UP: ['up'],
|
|
196
|
+
SCROLL_DOWN: ['down']
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Address Display
|
|
200
|
+
export const ADDRESS = {
|
|
201
|
+
TRUNCATE_LENGTH: 10,
|
|
202
|
+
PUBKEY_DISPLAY_LENGTH: 20
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Hash Display
|
|
206
|
+
export const HASH = {
|
|
207
|
+
DISPLAY_LENGTH: 16
|
|
208
|
+
};
|