@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/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
+ }
@@ -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
+ };