@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.
@@ -0,0 +1,200 @@
1
+ /**
2
+ * RPC Service for Neurai DePIN Terminal
3
+ * Handles RPC connection, initialization, and method calls
4
+ * @module RpcService
5
+ */
6
+
7
+ import neuraiJsWallet from '@neuraiproject/neurai-jswallet';
8
+ import {
9
+ RPC,
10
+ RPC_METHODS,
11
+ WARNING_MESSAGES,
12
+ SUCCESS_MESSAGES,
13
+ ERROR_MESSAGES
14
+ } from '../constants.js';
15
+ import { RpcError } from '../errors.js';
16
+ import { formatRpcUrl } from '../utils.js';
17
+
18
+ const { Wallet } = neuraiJsWallet;
19
+
20
+ /**
21
+ * Manages RPC connectivity and method calls
22
+ */
23
+ export class RpcService {
24
+ /**
25
+ * Create a new RpcService instance
26
+ * @param {Object} config - Configuration object
27
+ * @param {string} config.network - Network name (xna)
28
+ * @param {string} config.rpc_url - RPC server URL
29
+ * @param {string} [config.rpc_username] - Optional RPC username
30
+ * @param {string} [config.rpc_password] - Optional RPC password
31
+ */
32
+ constructor(config) {
33
+ this.config = config;
34
+ this.wallet = null;
35
+ this.connected = false;
36
+ }
37
+
38
+ /**
39
+ * Initialize RPC wallet client
40
+ * Creates a wallet instance for RPC access
41
+ * @returns {Promise<void>}
42
+ */
43
+ async initialize() {
44
+ const rpcUrl = formatRpcUrl(this.config.rpc_url, RPC.ENDPOINT_SUFFIX);
45
+
46
+ try {
47
+ this.wallet = new Wallet();
48
+ await this.wallet.init({
49
+ mnemonic: RPC.DUMMY_MNEMONIC,
50
+ network: this.config.network,
51
+ rpc_url: rpcUrl,
52
+ rpc_username: this.config.rpc_username || undefined,
53
+ rpc_password: this.config.rpc_password || undefined,
54
+ offlineMode: false,
55
+ minAmountOfAddresses: 1
56
+ });
57
+
58
+ // Test connection
59
+ await this.testConnection();
60
+ } catch (error) {
61
+ this.connected = false;
62
+ this.wallet = null;
63
+ console.warn(WARNING_MESSAGES.RPC_INIT_FAILED);
64
+ console.warn(` Error: ${error.message || 'Unknown error'}`);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Test RPC connection by calling getblockchaininfo
70
+ * @param {boolean} [silent=false] - If true, suppress console output
71
+ * @returns {Promise<boolean>} True if connected, false otherwise
72
+ */
73
+ async testConnection(silent = false) {
74
+ try {
75
+ await this.wallet.rpc(RPC_METHODS.GET_BLOCKCHAIN_INFO, []);
76
+ this.connected = true;
77
+ if (!silent) console.log(SUCCESS_MESSAGES.RPC_CONNECTED);
78
+ return true;
79
+ } catch (error) {
80
+ this.connected = false;
81
+ if (!silent) console.warn(WARNING_MESSAGES.RPC_CONNECTION_FAILED);
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Execute an RPC method
88
+ * @param {string} method - RPC method name
89
+ * @param {Array} [params=[]] - RPC parameters
90
+ * @returns {Promise<any>} RPC result
91
+ * @throws {RpcError} If RPC call fails or not initialized
92
+ */
93
+ async call(method, params = []) {
94
+ if (!this.wallet) {
95
+ throw new RpcError(ERROR_MESSAGES.RPC_NOT_INITIALIZED);
96
+ }
97
+ try {
98
+ const result = await this.wallet.rpc(method, params);
99
+ this.connected = true;
100
+ return result;
101
+ } catch (error) {
102
+ this.connected = false;
103
+ throw new RpcError(error.message);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Verify if an address holds a specific token
109
+ * @param {string} address - Address to check
110
+ * @param {string} token - Token name
111
+ * @returns {Promise<boolean>} True if address holds the token
112
+ */
113
+ async verifyTokenOwnership(address, token) {
114
+ try {
115
+ const result = await this.call(RPC_METHODS.LIST_ADDRESSES_BY_ASSET, [token]);
116
+ // Result is an object where keys are addresses and values are balances
117
+ return Object.prototype.hasOwnProperty.call(result, address) && result[address] > 0;
118
+ } catch (error) {
119
+ console.warn(`Failed to verify token ownership: ${error.message}`);
120
+ return false;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Check if the public key for an address is revealed on the blockchain
126
+ * @param {string} address - Address to check
127
+ * @returns {Promise<boolean>} True if public key is revealed
128
+ */
129
+ async checkPubKeyRevealed(address) {
130
+ try {
131
+ const result = await this.call(RPC_METHODS.GET_PUBKEY, [address]);
132
+ return result && (result.revealed === 1 || result.revealed === true);
133
+ } catch (error) {
134
+ console.warn(`Failed to check pubkey: ${error.message}`);
135
+ return false;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Check if currently connected to RPC server
141
+ * @returns {boolean} Connection status
142
+ */
143
+ isConnected() {
144
+ return this.connected;
145
+ }
146
+
147
+ /**
148
+ * Attempt to reconnect to RPC server
149
+ * Tries to reinitialize the wallet if not connected
150
+ * @param {boolean} [silent=true] - If true, suppress console output
151
+ * @returns {Promise<boolean>} True if reconnection successful, false otherwise
152
+ */
153
+ async attemptReconnect(silent = true) {
154
+ // If already connected, no need to reconnect
155
+ if (this.connected && this.wallet) {
156
+ return true;
157
+ }
158
+
159
+ // If wallet exists, just test the connection
160
+ if (this.wallet) {
161
+ try {
162
+ await this.wallet.rpc(RPC_METHODS.GET_BLOCKCHAIN_INFO, []);
163
+ this.connected = true;
164
+ return true;
165
+ } catch (error) {
166
+ this.connected = false;
167
+ }
168
+ }
169
+
170
+ // Wallet doesn't exist or check failed, need to reinitialize RPC client
171
+ try {
172
+ const rpcUrl = formatRpcUrl(this.config.rpc_url, RPC.ENDPOINT_SUFFIX);
173
+
174
+ this.wallet = new Wallet();
175
+ await this.wallet.init({
176
+ mnemonic: RPC.DUMMY_MNEMONIC,
177
+ network: this.config.network,
178
+ rpc_url: rpcUrl,
179
+ rpc_username: this.config.rpc_username || undefined,
180
+ rpc_password: this.config.rpc_password || undefined,
181
+ offlineMode: false,
182
+ minAmountOfAddresses: 1
183
+ });
184
+
185
+ // Test connection
186
+ await this.wallet.rpc(RPC_METHODS.GET_BLOCKCHAIN_INFO, []);
187
+ this.connected = true;
188
+
189
+ if (!silent) {
190
+ console.log('✓ Reconnected to RPC server');
191
+ }
192
+
193
+ return true;
194
+ } catch (error) {
195
+ this.connected = false;
196
+ this.wallet = null;
197
+ return false;
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Terminal UI for Neurai DePIN Terminal
3
+ * Full-screen blessed-based interface with top bar, message area, input, and status bar
4
+ * @module TerminalUI
5
+ */
6
+
7
+ import blessed from 'blessed';
8
+ import {
9
+ COLORS,
10
+ ICONS,
11
+ BLESSED_KEYS,
12
+ ADDRESS,
13
+ PRIVACY,
14
+ TIME
15
+ } from '../constants.js';
16
+ import { formatTimestamp, resetTerminal } from '../utils.js';
17
+ import { TopBar } from './components/TopBar.js';
18
+ import { MessageBox } from './components/MessageBox.js';
19
+ import { InputBox } from './components/InputBox.js';
20
+ import { StatusBar } from './components/StatusBar.js';
21
+ import { ErrorOverlay } from './components/ErrorOverlay.js';
22
+
23
+ /**
24
+ * Terminal UI manager using blessed library
25
+ */
26
+ export class TerminalUI {
27
+ /**
28
+ * Create a new TerminalUI instance
29
+ * @param {Object} config - Configuration object
30
+ * @param {string} config.rpc_url - RPC server URL
31
+ * @param {string} config.token - DePIN token name
32
+ * @param {WalletManager} walletManager - Wallet manager instance
33
+ * @param {RpcService} rpcService - RPC service instance
34
+ */
35
+ constructor(config, walletManager, rpcService) {
36
+ this.config = config;
37
+ this.walletManager = walletManager;
38
+ this.rpcService = rpcService;
39
+ this.myAddress = walletManager.getAddress();
40
+ this.sendCallback = null;
41
+ this.displayedMessages = [];
42
+ this.hasPrivacy = false;
43
+ this.encryptionType = PRIVACY.DEFAULT_ENCRYPTION;
44
+ this.totalMessages = 0;
45
+ this.lastConnectionStatus = false;
46
+ this.lastPollTime = null;
47
+
48
+ this.initializeScreen();
49
+ this.createComponents();
50
+ this.setupKeybindings();
51
+ this.inputBox.focus();
52
+ this.screen.render();
53
+ this.updateTopBar({ connected: false, lastPoll: null });
54
+ }
55
+
56
+ /**
57
+ * Initialize blessed screen
58
+ */
59
+ initializeScreen() {
60
+ this.screen = blessed.screen({
61
+ smartCSR: true,
62
+ title: 'Neurai DePIN Terminal'
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Create UI components (top bar, message box, input box, status bar)
68
+ */
69
+ createComponents() {
70
+ this.topBar = new TopBar(this.screen, this.config, this.myAddress);
71
+ this.messageBox = new MessageBox(this.screen);
72
+ this.inputBox = new InputBox(this.screen, (msg) => this.handleSend(msg));
73
+ this.statusBar = new StatusBar(this.screen);
74
+ this.errorOverlay = new ErrorOverlay(this.screen);
75
+ }
76
+
77
+ /**
78
+ * Show blocking error overlay
79
+ * @param {string[]} errors - List of error messages
80
+ */
81
+ showBlockingErrors(errors) {
82
+ this.inputBox.disable();
83
+ this.errorOverlay.show(errors);
84
+ }
85
+
86
+ /**
87
+ * Clear blocking error overlay
88
+ */
89
+ clearBlockingErrors() {
90
+ if (this.errorOverlay.isVisible()) {
91
+ this.errorOverlay.hide();
92
+ this.inputBox.enable();
93
+ this.inputBox.focus();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Setup keyboard bindings
99
+ */
100
+ setupKeybindings() {
101
+ // Exit on Ctrl+C or Escape
102
+ this.screen.key(BLESSED_KEYS.QUIT, () => {
103
+ this.cleanup();
104
+ process.exit(0);
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Update top bar with connection status and info
110
+ * @param {Object} status - Status object
111
+ * @param {boolean} status.connected - Connection status
112
+ * @param {Date|null} status.lastPoll - Last poll time
113
+ */
114
+ updateTopBar(status) {
115
+ this.lastConnectionStatus = status.connected;
116
+
117
+ if (status.lastPoll) {
118
+ this.lastPollTime = status.lastPoll;
119
+ }
120
+
121
+ this.topBar.update({
122
+ connected: status.connected,
123
+ lastPoll: status.lastPoll
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Format message line for display
129
+ * @param {Object} msg - Message object
130
+ * @returns {string} Formatted message line with blessed tags
131
+ */
132
+ formatMessageLine(msg) {
133
+ const time = formatTimestamp(msg.timestamp, this.config.timezone);
134
+ const isMe = msg.sender === this.myAddress;
135
+ const senderLabel = isMe ? 'YOU' : msg.sender.slice(0, ADDRESS.TRUNCATE_LENGTH);
136
+ const color = isMe ? COLORS.MY_MESSAGE : COLORS.OTHER_MESSAGE;
137
+
138
+ return `{${color}}[${time}] ${senderLabel}: ${msg.message}{/}`;
139
+ }
140
+
141
+ /**
142
+ * Add a new message to the display
143
+ * Maintains chronological order (oldest to newest)
144
+ * @param {Object} msg - Message object
145
+ * @param {string} msg.sender - Sender address
146
+ * @param {string} msg.message - Message content
147
+ * @param {number} msg.timestamp - Unix timestamp in seconds
148
+ * @param {string} msg.hash - Message hash
149
+ */
150
+ addMessage(msg) {
151
+ this.displayedMessages.push(msg);
152
+
153
+ // Sort by timestamp (oldest to newest)
154
+ this.displayedMessages.sort((a, b) => a.timestamp - b.timestamp);
155
+
156
+ // Redraw all messages
157
+ this.redrawMessages();
158
+ }
159
+
160
+ /**
161
+ * Redraw all messages in the message box
162
+ */
163
+ redrawMessages() {
164
+ const formattedMessages = this.displayedMessages.map(msg => this.formatMessageLine(msg));
165
+ // Clear and rebuild message box content
166
+ this.messageBox.component.setContent(formattedMessages.join('\n'));
167
+ this.messageBox.component.setScrollPerc(100);
168
+ this.screen.render();
169
+ }
170
+
171
+ /**
172
+ * Show error message in message box
173
+ * @param {string} errorMsg - Error message
174
+ */
175
+ showError(errorMsg) {
176
+ const line = `{${COLORS.ERROR}}[ERROR] ${errorMsg}{/}`;
177
+ this.messageBox.addMessage(line);
178
+ }
179
+
180
+ /**
181
+ * Show info message in message box
182
+ * @param {string} infoMsg - Info message
183
+ */
184
+ showInfo(infoMsg) {
185
+ const line = `{${COLORS.INFO}}[INFO] ${infoMsg}{/}`;
186
+ this.messageBox.addMessage(line);
187
+ }
188
+
189
+ /**
190
+ * Show success message in message box
191
+ * @param {string} successMsg - Success message
192
+ */
193
+ showSuccess(successMsg) {
194
+ const line = `{${COLORS.SUCCESS}}[${ICONS.SUCCESS}] ${successMsg}{/}`;
195
+ this.messageBox.addMessage(line);
196
+ }
197
+
198
+ /**
199
+ * Update status bar with send status
200
+ * @param {string} message - Status message
201
+ * @param {string} [type='info'] - Message type: 'success', 'error', or 'info'
202
+ */
203
+ updateSendStatus(message, type = 'info') {
204
+ this.statusBar.update(message, type);
205
+ }
206
+
207
+ /**
208
+ * Clear status bar
209
+ */
210
+ clearSendStatus() {
211
+ this.statusBar.update('');
212
+ }
213
+
214
+ /**
215
+ * Handle send message action
216
+ * @param {string} message - Message to send
217
+ */
218
+ handleSend(message) {
219
+ if (!message) return;
220
+
221
+ if (this.sendCallback) {
222
+ this.sendCallback(message);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Register callback for send action
228
+ * @param {Function} callback - Callback function(message)
229
+ */
230
+ onSend(callback) {
231
+ this.sendCallback = callback;
232
+ }
233
+
234
+ /**
235
+ * Update pool information from depingetmsginfo RPC call
236
+ * @param {Object} poolInfo - Pool information
237
+ * @param {number} [poolInfo.messages] - Total messages in pool
238
+ * @param {string} [poolInfo.cipher] - Encryption cipher name
239
+ * @param {string} [poolInfo.depinpoolpkey] - Server privacy public key
240
+ */
241
+ updatePoolInfo(poolInfo) {
242
+ if (poolInfo) {
243
+ this.totalMessages = poolInfo.messages || 0;
244
+ this.encryptionType = poolInfo.cipher || PRIVACY.DEFAULT_ENCRYPTION;
245
+ this.hasPrivacy = poolInfo.depinpoolpkey && poolInfo.depinpoolpkey !== PRIVACY.NO_KEY_VALUE;
246
+
247
+ this.topBar.setTotalMessages(this.totalMessages);
248
+ this.topBar.setEncryptionType(this.encryptionType);
249
+
250
+ // Refresh top bar
251
+ this.updateTopBar({
252
+ connected: this.lastConnectionStatus,
253
+ lastPoll: this.lastPollTime
254
+ });
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Cleanup terminal state
260
+ * Removes listeners, destroys screen, resets terminal
261
+ */
262
+ cleanup() {
263
+ if (this.screen) {
264
+ try {
265
+ this.screen.destroy();
266
+ } catch (err) {
267
+ // Ignore cleanup errors
268
+ }
269
+ resetTerminal();
270
+ }
271
+ }
272
+ }
@@ -0,0 +1,99 @@
1
+ import blessed from 'blessed';
2
+ import { COLORS } from '../../constants.js';
3
+
4
+ export class ErrorOverlay {
5
+ constructor(screen) {
6
+ this.screen = screen;
7
+ this.timer = null;
8
+ this.timeLeft = 30;
9
+ this.currentErrors = [];
10
+
11
+ this.component = blessed.box({
12
+ top: 'center',
13
+ left: 'center',
14
+ width: '50%',
15
+ height: 'shrink',
16
+ content: '',
17
+ tags: true,
18
+ border: {
19
+ type: 'line'
20
+ },
21
+ style: {
22
+ fg: COLORS.FG_WHITE,
23
+ bg: COLORS.ERROR,
24
+ border: {
25
+ fg: COLORS.FG_WHITE
26
+ }
27
+ },
28
+ hidden: true
29
+ });
30
+
31
+ this.screen.append(this.component);
32
+ }
33
+
34
+ /**
35
+ * Show blocking error overlay
36
+ * @param {string[]} errors - List of error messages
37
+ */
38
+ show(errors) {
39
+ if (!errors || errors.length === 0) {
40
+ this.hide();
41
+ return;
42
+ }
43
+
44
+ this.currentErrors = errors;
45
+ this.timeLeft = 30;
46
+
47
+ // Clear existing timer if any
48
+ if (this.timer) {
49
+ clearInterval(this.timer);
50
+ }
51
+
52
+ this.updateContent();
53
+ this.component.show();
54
+ this.component.setFront();
55
+ this.screen.render();
56
+
57
+ // Start countdown
58
+ this.timer = setInterval(() => {
59
+ this.timeLeft--;
60
+ if (this.timeLeft < 0) this.timeLeft = 0;
61
+ this.updateContent();
62
+ }, 1000);
63
+ }
64
+
65
+ /**
66
+ * Update overlay content with current timer
67
+ */
68
+ updateContent() {
69
+ const content = `\n{bold}CRITICAL ERRORS:{/bold}\n\n` +
70
+ this.currentErrors.map(e => `• ${e}`).join('\n\n') +
71
+ `\n\n{center}Retrying in ${this.timeLeft}s...{/center}\n`;
72
+
73
+ this.component.setContent(content);
74
+ this.screen.render();
75
+ }
76
+
77
+ /**
78
+ * Hide blocking error overlay
79
+ */
80
+ hide() {
81
+ if (this.timer) {
82
+ clearInterval(this.timer);
83
+ this.timer = null;
84
+ }
85
+
86
+ if (!this.component.hidden) {
87
+ this.component.hide();
88
+ this.screen.render();
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Check if overlay is visible
94
+ * @returns {boolean} True if visible
95
+ */
96
+ isVisible() {
97
+ return !this.component.hidden;
98
+ }
99
+ }
@@ -0,0 +1,63 @@
1
+ import blessed from 'blessed';
2
+ import { UI, COLORS, KEY_CODES } from '../../constants.js';
3
+
4
+ export class InputBox {
5
+ constructor(screen, onSend) {
6
+ this.screen = screen;
7
+ this.onSend = onSend;
8
+
9
+ this.component = blessed.textarea({
10
+ bottom: UI.STATUS_BAR_HEIGHT,
11
+ left: 0,
12
+ width: '100%',
13
+ height: UI.INPUT_BOX_HEIGHT,
14
+ inputOnFocus: true,
15
+ keys: true,
16
+ style: {
17
+ fg: COLORS.FG_WHITE,
18
+ bg: COLORS.BG_BLACK,
19
+ border: {
20
+ fg: COLORS.BORDER
21
+ }
22
+ },
23
+ border: {
24
+ type: 'line'
25
+ }
26
+ });
27
+
28
+ this.setupEvents();
29
+ this.screen.append(this.component);
30
+ }
31
+
32
+ setupEvents() {
33
+ this.component.key('enter', () => {
34
+ const message = this.component.getValue().trim();
35
+ if (message) {
36
+ this.onSend(message);
37
+ this.component.clearValue();
38
+ this.screen.render();
39
+ }
40
+ });
41
+ }
42
+
43
+ focus() {
44
+ if (!this.disabled) {
45
+ this.component.focus();
46
+ }
47
+ }
48
+
49
+ disable() {
50
+ this.disabled = true;
51
+ this.component.inputOnFocus = false;
52
+ // Optional: Change style to indicate disabled state
53
+ this.component.style.border.fg = 'gray';
54
+ this.screen.render();
55
+ }
56
+
57
+ enable() {
58
+ this.disabled = false;
59
+ this.component.inputOnFocus = true;
60
+ this.component.style.border.fg = COLORS.BORDER;
61
+ this.screen.render();
62
+ }
63
+ }
@@ -0,0 +1,51 @@
1
+ import blessed from 'blessed';
2
+ import { UI, COLORS } from '../../constants.js';
3
+
4
+ export class MessageBox {
5
+ constructor(screen) {
6
+ this.screen = screen;
7
+
8
+ this.component = blessed.box({
9
+ top: UI.TOP_BAR_HEIGHT,
10
+ left: 0,
11
+ width: '100%',
12
+ height: `100%-${UI.MESSAGE_BOX_OFFSET}`,
13
+ scrollable: true,
14
+ alwaysScroll: true,
15
+ keys: true,
16
+ vi: true,
17
+ mouse: true,
18
+ tags: true,
19
+ scrollbar: {
20
+ ch: UI.SCROLLBAR_CHAR,
21
+ style: {
22
+ bg: COLORS.BG_BLUE
23
+ }
24
+ },
25
+ style: {
26
+ fg: COLORS.FG_WHITE,
27
+ bg: COLORS.BG_BLACK
28
+ }
29
+ });
30
+
31
+ this.screen.append(this.component);
32
+ }
33
+
34
+ addMessage(formattedLine) {
35
+ const current = this.component.getContent();
36
+ const next = current && current.length > 0 ? `${current}\n${formattedLine}` : formattedLine;
37
+ this.component.setContent(next);
38
+ this.component.setScrollPerc(100);
39
+ this.screen.render();
40
+ }
41
+
42
+ scrollUp() {
43
+ this.component.scroll(-1);
44
+ this.screen.render();
45
+ }
46
+
47
+ scrollDown() {
48
+ this.component.scroll(1);
49
+ this.screen.render();
50
+ }
51
+ }
@@ -0,0 +1,32 @@
1
+ import blessed from 'blessed';
2
+ import { UI, COLORS } from '../../constants.js';
3
+
4
+ export class StatusBar {
5
+ constructor(screen) {
6
+ this.screen = screen;
7
+
8
+ this.component = blessed.box({
9
+ bottom: 0,
10
+ left: 0,
11
+ width: '100%',
12
+ height: UI.STATUS_BAR_HEIGHT,
13
+ content: ' Ready',
14
+ tags: true,
15
+ style: {
16
+ fg: COLORS.FG_WHITE,
17
+ bg: COLORS.BG_BLUE
18
+ }
19
+ });
20
+
21
+ this.screen.append(this.component);
22
+ }
23
+
24
+ update(message, type = 'info') {
25
+ let color = COLORS.INFO;
26
+ if (type === 'error') color = COLORS.ERROR;
27
+ if (type === 'success') color = COLORS.SUCCESS;
28
+
29
+ this.component.setContent(` {${color}}${message}{/${color}}`);
30
+ this.screen.render();
31
+ }
32
+ }