@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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DePIN message library loader
|
|
3
|
+
* Loads the IIFE bundle from @neuraiproject/neurai-depin-msg
|
|
4
|
+
* @module depinMsgLoader
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { LibraryError } from '../errors.js';
|
|
11
|
+
import { ERROR_MESSAGES } from '../constants.js';
|
|
12
|
+
|
|
13
|
+
// Handle both ESM and CJS (for bundling)
|
|
14
|
+
// We use a check that doesn't trigger esbuild warnings for CJS targets
|
|
15
|
+
const getDirname = () => {
|
|
16
|
+
try {
|
|
17
|
+
if (typeof __dirname !== 'undefined') return __dirname;
|
|
18
|
+
return path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return process.cwd();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const _dirname = getDirname();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Path to the neurai-depin-msg bundle
|
|
28
|
+
* Checks multiple locations to support both development and bundled environments
|
|
29
|
+
*/
|
|
30
|
+
const possiblePaths = [
|
|
31
|
+
path.join(_dirname, '../../node_modules/@neuraiproject/neurai-depin-msg/dist/neurai-depin-msg.js'),
|
|
32
|
+
path.join(_dirname, '../node_modules/@neuraiproject/neurai-depin-msg/dist/neurai-depin-msg.js'),
|
|
33
|
+
path.join(process.cwd(), 'node_modules/@neuraiproject/neurai-depin-msg/dist/neurai-depin-msg.js')
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const BUNDLE_PATH = possiblePaths.find(p => fs.existsSync(p)) || possiblePaths[0];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load the DePIN message library IIFE bundle into globalThis
|
|
40
|
+
* The library provides functions for building, encrypting, and decrypting DePIN messages
|
|
41
|
+
* @returns {Promise<Object>} The neuraiDepinMsg library object
|
|
42
|
+
* @throws {LibraryError} If bundle not found or fails to load
|
|
43
|
+
*/
|
|
44
|
+
export async function loadDepinMsgLibrary() {
|
|
45
|
+
// Check if bundle exists
|
|
46
|
+
if (!fs.existsSync(BUNDLE_PATH)) {
|
|
47
|
+
throw new LibraryError(
|
|
48
|
+
'neurai-depin-msg bundle not found. Please run: npm install'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Read bundle code
|
|
54
|
+
const bundleCode = fs.readFileSync(BUNDLE_PATH, 'utf-8');
|
|
55
|
+
|
|
56
|
+
// Execute IIFE in global context
|
|
57
|
+
// The bundle assigns to globalThis.neuraiDepinMsg
|
|
58
|
+
const scriptFunction = new Function(bundleCode);
|
|
59
|
+
scriptFunction();
|
|
60
|
+
|
|
61
|
+
// Verify library loaded successfully
|
|
62
|
+
if (!globalThis.neuraiDepinMsg) {
|
|
63
|
+
throw new LibraryError(ERROR_MESSAGES.LIBRARY_LOAD_FAILED);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return globalThis.neuraiDepinMsg;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof LibraryError) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
throw new LibraryError(`${ERROR_MESSAGES.LIBRARY_LOAD_FAILED}: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/lib/empty.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message poller for Neurai DePIN Terminal
|
|
3
|
+
* Handles automatic polling for new messages
|
|
4
|
+
* @module MessagePoller
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import { RPC_METHODS } from '../constants.js';
|
|
9
|
+
import { isEncryptedResponse } from '../utils.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Polls for new DePIN messages at regular intervals
|
|
13
|
+
* @extends EventEmitter
|
|
14
|
+
* @fires MessagePoller#message
|
|
15
|
+
* @fires MessagePoller#poll-complete
|
|
16
|
+
* @fires MessagePoller#error
|
|
17
|
+
* @fires MessagePoller#reconnected
|
|
18
|
+
*/
|
|
19
|
+
export class MessagePoller extends EventEmitter {
|
|
20
|
+
/**
|
|
21
|
+
* Create a new MessagePoller instance
|
|
22
|
+
* @param {Object} config - Configuration object
|
|
23
|
+
* @param {string} config.token - DePIN token name
|
|
24
|
+
* @param {number} config.pollInterval - Polling interval in milliseconds
|
|
25
|
+
* @param {RpcService} rpcService - RPC service instance
|
|
26
|
+
* @param {MessageStore} messageStore - Message store instance
|
|
27
|
+
* @param {Object} neuraiDepinMsg - DePIN message library
|
|
28
|
+
* @param {WalletManager} walletManager - Wallet manager instance (for decryption)
|
|
29
|
+
*/
|
|
30
|
+
constructor(config, rpcService, messageStore, neuraiDepinMsg, walletManager) {
|
|
31
|
+
super();
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.rpcService = rpcService;
|
|
34
|
+
this.messageStore = messageStore;
|
|
35
|
+
this.neuraiDepinMsg = neuraiDepinMsg;
|
|
36
|
+
this.walletManager = walletManager;
|
|
37
|
+
this.intervalId = null;
|
|
38
|
+
this.isPolling = false;
|
|
39
|
+
this.wasDisconnected = false; // Track if we were disconnected
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get current RPC client (dynamically to handle reconnections)
|
|
44
|
+
* @returns {Function} RPC function
|
|
45
|
+
*/
|
|
46
|
+
getRpc() {
|
|
47
|
+
return this.rpcService.call.bind(this.rpcService);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Start polling at configured interval
|
|
52
|
+
*/
|
|
53
|
+
start() {
|
|
54
|
+
if (this.intervalId) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.poll(); // Initial poll
|
|
58
|
+
this.intervalId = setInterval(() => this.poll(), this.config.pollInterval);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stop polling
|
|
63
|
+
*/
|
|
64
|
+
stop() {
|
|
65
|
+
if (this.intervalId) {
|
|
66
|
+
clearInterval(this.intervalId);
|
|
67
|
+
this.intervalId = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build RPC parameters for depinreceivemsg
|
|
73
|
+
* Includes incremental timestamp for efficiency
|
|
74
|
+
* @param {boolean} [forceFullPoll=false] - If true, fetch all messages without timestamp filter
|
|
75
|
+
* @returns {Array} RPC parameters
|
|
76
|
+
*/
|
|
77
|
+
buildRpcParams(forceFullPoll = false) {
|
|
78
|
+
const params = [this.config.token, this.walletManager.getAddress()];
|
|
79
|
+
|
|
80
|
+
// If forcing full poll (after reconnection), don't include timestamp
|
|
81
|
+
if (forceFullPoll) {
|
|
82
|
+
return params;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lastTimestamp = this.messageStore.getLastTimestamp();
|
|
86
|
+
|
|
87
|
+
if (lastTimestamp > 0) {
|
|
88
|
+
params.push(lastTimestamp);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return params;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Unwrap server privacy layer if present
|
|
96
|
+
* @param {Object} result - RPC result
|
|
97
|
+
* @returns {Promise<Object>} Unwrapped result
|
|
98
|
+
*/
|
|
99
|
+
async unwrapPrivacyLayer(result) {
|
|
100
|
+
if (!isEncryptedResponse(result)) {
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const decryptedJson = await this.neuraiDepinMsg.unwrapMessageFromServer(
|
|
105
|
+
result.encrypted,
|
|
106
|
+
this.walletManager.getPrivateKeyHex()
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return JSON.parse(decryptedJson);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fetch pool information from RPC
|
|
114
|
+
* @returns {Promise<Object|null>} Pool info or null if failed
|
|
115
|
+
*/
|
|
116
|
+
async fetchPoolInfo() {
|
|
117
|
+
try {
|
|
118
|
+
return await this.rpcService.call(RPC_METHODS.DEPIN_GET_MSG_INFO, []);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return null; // Non-fatal, continue without pool info
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Poll for new messages
|
|
126
|
+
* Emits 'poll-complete' on success, 'error' on failure
|
|
127
|
+
* Automatically attempts reconnection if disconnected
|
|
128
|
+
* @returns {Promise<void>}
|
|
129
|
+
*/
|
|
130
|
+
async poll() {
|
|
131
|
+
if (this.isPolling) {
|
|
132
|
+
return; // Avoid concurrent polling
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.isPolling = true;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Do not attempt reconnection here.
|
|
139
|
+
// Reconnection is handled by the verification loop (aligned with the UI countdown).
|
|
140
|
+
if (!this.rpcService.isConnected()) {
|
|
141
|
+
throw new Error('RPC server not available');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Call depinreceivemsg
|
|
145
|
+
// If recovering from disconnect, do full poll to get all messages
|
|
146
|
+
const forceFull = this.wasDisconnected;
|
|
147
|
+
const params = this.buildRpcParams(forceFull);
|
|
148
|
+
const rpc = this.getRpc();
|
|
149
|
+
let result = await rpc(RPC_METHODS.DEPIN_RECEIVE_MSG, params);
|
|
150
|
+
|
|
151
|
+
// Mark as connected on successful call
|
|
152
|
+
this.walletManager.connected = true;
|
|
153
|
+
this.rpcService.connected = true;
|
|
154
|
+
|
|
155
|
+
// Unwrap privacy layer if present
|
|
156
|
+
result = await this.unwrapPrivacyLayer(result);
|
|
157
|
+
|
|
158
|
+
// Process messages
|
|
159
|
+
const messages = Array.isArray(result) ? result : [];
|
|
160
|
+
let newMessagesCount = 0;
|
|
161
|
+
|
|
162
|
+
for (const msg of messages) {
|
|
163
|
+
const processed = await this.processMessage(msg);
|
|
164
|
+
if (processed) {
|
|
165
|
+
newMessagesCount++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Get pool info
|
|
170
|
+
const poolInfo = await this.fetchPoolInfo();
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Poll complete event
|
|
174
|
+
* @event MessagePoller#poll-complete
|
|
175
|
+
* @type {Object}
|
|
176
|
+
* @property {Date} date - Poll completion time
|
|
177
|
+
* @property {number} newMessages - Number of new messages
|
|
178
|
+
* @property {number} totalMessages - Total messages in store
|
|
179
|
+
* @property {Object|null} poolInfo - Pool information from RPC
|
|
180
|
+
*/
|
|
181
|
+
this.emit('poll-complete', {
|
|
182
|
+
date: new Date(),
|
|
183
|
+
newMessages: newMessagesCount,
|
|
184
|
+
totalMessages: this.messageStore.getCount(),
|
|
185
|
+
poolInfo: poolInfo
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Emit reconnection event only once after successful poll
|
|
189
|
+
if (this.wasDisconnected) {
|
|
190
|
+
this.wasDisconnected = false;
|
|
191
|
+
this.emit('reconnected');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Mark as disconnected on error
|
|
196
|
+
this.walletManager.connected = false;
|
|
197
|
+
this.rpcService.connected = false;
|
|
198
|
+
this.wasDisconnected = true;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Error event
|
|
202
|
+
* @event MessagePoller#error
|
|
203
|
+
* @type {Error}
|
|
204
|
+
*/
|
|
205
|
+
this.emit('error', error);
|
|
206
|
+
} finally {
|
|
207
|
+
this.isPolling = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reconnected event - emitted when RPC connection is restored
|
|
213
|
+
* @event MessagePoller#reconnected
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Validate message has required fields
|
|
218
|
+
* @param {Object} msg - Message to validate
|
|
219
|
+
* @returns {boolean} True if valid
|
|
220
|
+
*/
|
|
221
|
+
isValidMessage(msg) {
|
|
222
|
+
return Boolean(
|
|
223
|
+
msg &&
|
|
224
|
+
msg.hash &&
|
|
225
|
+
msg.signature_hex &&
|
|
226
|
+
msg.encrypted_payload_hex
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Decrypt message payload
|
|
232
|
+
* @param {string} encryptedPayloadHex - Encrypted payload in hex format
|
|
233
|
+
* @returns {Promise<string|null>} Decrypted plaintext or null if failed
|
|
234
|
+
*/
|
|
235
|
+
async decryptPayload(encryptedPayloadHex) {
|
|
236
|
+
try {
|
|
237
|
+
return await this.neuraiDepinMsg.decryptDepinReceiveEncryptedPayload(
|
|
238
|
+
encryptedPayloadHex,
|
|
239
|
+
this.walletManager.getPrivateKeyHex()
|
|
240
|
+
);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return null; // Not for us or malformed
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Process a single message
|
|
248
|
+
* Validates, decrypts, deduplicates, and emits if new
|
|
249
|
+
* @param {Object} msg - Raw message from RPC
|
|
250
|
+
* @returns {Promise<boolean>} True if message was new and processed
|
|
251
|
+
*/
|
|
252
|
+
async processMessage(msg) {
|
|
253
|
+
try {
|
|
254
|
+
// Validate required fields
|
|
255
|
+
if (!this.isValidMessage(msg)) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Decrypt payload
|
|
260
|
+
const plaintext = await this.decryptPayload(msg.encrypted_payload_hex);
|
|
261
|
+
|
|
262
|
+
if (!plaintext) {
|
|
263
|
+
return false; // Not for us or malformed
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Add to store with deduplication
|
|
267
|
+
const isNew = this.messageStore.addMessage({
|
|
268
|
+
sender: msg.sender,
|
|
269
|
+
message: plaintext,
|
|
270
|
+
timestamp: msg.timestamp,
|
|
271
|
+
hash: msg.hash,
|
|
272
|
+
signature: msg.signature_hex
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (isNew) {
|
|
276
|
+
/**
|
|
277
|
+
* Message event
|
|
278
|
+
* @event MessagePoller#message
|
|
279
|
+
* @type {Object}
|
|
280
|
+
* @property {string} sender - Sender address
|
|
281
|
+
* @property {string} message - Decrypted message content
|
|
282
|
+
* @property {number} timestamp - Unix timestamp in seconds
|
|
283
|
+
* @property {string} hash - Message hash
|
|
284
|
+
*/
|
|
285
|
+
this.emit('message', {
|
|
286
|
+
sender: msg.sender,
|
|
287
|
+
message: plaintext,
|
|
288
|
+
timestamp: msg.timestamp,
|
|
289
|
+
hash: msg.hash
|
|
290
|
+
});
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return false;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
// Error decrypting individual message, skip
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message sender for Neurai DePIN Terminal
|
|
3
|
+
* Handles sending broadcast messages to all token holders
|
|
4
|
+
* @module MessageSender
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
RPC_METHODS,
|
|
9
|
+
ERROR_MESSAGES
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
import { MessageError, DepinError } from '../errors.js';
|
|
12
|
+
import { isPubkeyRevealed, normalizePubkey, hasPrivacyLayer } from '../utils.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sends DePIN messages to all token holders
|
|
16
|
+
*/
|
|
17
|
+
export class MessageSender {
|
|
18
|
+
/**
|
|
19
|
+
* Create a new MessageSender instance
|
|
20
|
+
* @param {Object} config - Configuration object
|
|
21
|
+
* @param {string} config.token - DePIN token name
|
|
22
|
+
* @param {WalletManager} walletManager - Wallet manager instance
|
|
23
|
+
* @param {RpcService} rpcService - RPC service instance
|
|
24
|
+
* @param {Object} neuraiDepinMsg - DePIN message library
|
|
25
|
+
*/
|
|
26
|
+
constructor(config, walletManager, rpcService, neuraiDepinMsg) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.walletManager = walletManager;
|
|
29
|
+
this.rpcService = rpcService;
|
|
30
|
+
this.neuraiDepinMsg = neuraiDepinMsg;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get current RPC client (dynamically to handle reconnections)
|
|
35
|
+
* @returns {Function} RPC function
|
|
36
|
+
*/
|
|
37
|
+
getRpc() {
|
|
38
|
+
return this.rpcService.call.bind(this.rpcService);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all addresses holding the token
|
|
43
|
+
* @returns {Promise<Array<string>>} Array of addresses
|
|
44
|
+
* @throws {MessageError} If no token holders found
|
|
45
|
+
*/
|
|
46
|
+
async getTokenHolders() {
|
|
47
|
+
const rpc = this.getRpc();
|
|
48
|
+
const addressesData = await rpc(RPC_METHODS.LIST_ADDRESSES_BY_ASSET, [this.config.token]);
|
|
49
|
+
const addresses = Object.keys(addressesData);
|
|
50
|
+
|
|
51
|
+
if (addresses.length === 0) {
|
|
52
|
+
throw new MessageError(ERROR_MESSAGES.NO_TOKEN_HOLDERS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return addresses;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get revealed public keys from addresses
|
|
60
|
+
* @param {Array<string>} addresses - Array of addresses
|
|
61
|
+
* @returns {Promise<Array<string>>} Array of revealed public keys (normalized)
|
|
62
|
+
* @throws {MessageError} If no recipients with revealed public keys
|
|
63
|
+
*/
|
|
64
|
+
async getRecipientPubkeys(addresses) {
|
|
65
|
+
const recipientPubKeys = [];
|
|
66
|
+
const rpc = this.getRpc();
|
|
67
|
+
|
|
68
|
+
for (const addr of addresses) {
|
|
69
|
+
try {
|
|
70
|
+
const res = await rpc(RPC_METHODS.GET_PUBKEY, [addr]);
|
|
71
|
+
|
|
72
|
+
if (isPubkeyRevealed(res)) {
|
|
73
|
+
recipientPubKeys.push(normalizePubkey(res.pubkey));
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Skip addresses without revealed pubkey
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (recipientPubKeys.length === 0) {
|
|
82
|
+
throw new MessageError(ERROR_MESSAGES.NO_RECIPIENTS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return recipientPubKeys;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build encrypted DePIN message
|
|
90
|
+
* @param {string} message - Plaintext message
|
|
91
|
+
* @param {Array<string>} recipientPubKeys - Array of recipient public keys
|
|
92
|
+
* @returns {Promise<Object>} Build result with hex payload
|
|
93
|
+
*/
|
|
94
|
+
async buildEncryptedMessage(message, recipientPubKeys) {
|
|
95
|
+
return await this.neuraiDepinMsg.buildDepinMessage({
|
|
96
|
+
token: this.config.token,
|
|
97
|
+
senderAddress: this.walletManager.getAddress(),
|
|
98
|
+
senderPubKey: this.walletManager.getPublicKey(),
|
|
99
|
+
privateKey: this.walletManager.getPrivateKeyHex(),
|
|
100
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
101
|
+
message: message,
|
|
102
|
+
recipientPubKeys: recipientPubKeys
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wrap message with server privacy layer if enabled
|
|
108
|
+
* @param {string} payloadHex - Message payload in hex format
|
|
109
|
+
* @returns {Promise<string>} Wrapped payload or original if no privacy
|
|
110
|
+
*/
|
|
111
|
+
async wrapWithPrivacyLayer(payloadHex) {
|
|
112
|
+
try {
|
|
113
|
+
const rpc = this.getRpc();
|
|
114
|
+
const msgInfo = await rpc(RPC_METHODS.DEPIN_GET_MSG_INFO, []);
|
|
115
|
+
|
|
116
|
+
if (hasPrivacyLayer(msgInfo)) {
|
|
117
|
+
return await this.neuraiDepinMsg.wrapMessageForServer(
|
|
118
|
+
payloadHex,
|
|
119
|
+
msgInfo.depinpoolpkey,
|
|
120
|
+
this.walletManager.getAddress()
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// If depingetmsginfo fails, continue without privacy layer
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return payloadHex;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Submit message to DePIN pool
|
|
132
|
+
* @param {string} payload - Message payload (potentially wrapped)
|
|
133
|
+
* @returns {Promise<Object>} Submission result with hash/txid
|
|
134
|
+
*/
|
|
135
|
+
async submitToPool(payload) {
|
|
136
|
+
const rpc = this.getRpc();
|
|
137
|
+
return await rpc(RPC_METHODS.DEPIN_SUBMIT_MSG, [payload]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Send a broadcast message to all token holders
|
|
142
|
+
* @param {string} message - Plaintext message to send
|
|
143
|
+
* @returns {Promise<Object>} Result object with hash and recipient count
|
|
144
|
+
* @returns {Promise<Object>} result
|
|
145
|
+
* @returns {string} result.hash - Transaction hash
|
|
146
|
+
* @returns {number} result.recipients - Number of recipients
|
|
147
|
+
* @returns {number} result.timestamp - Send timestamp
|
|
148
|
+
* @throws {MessageError} If sending fails
|
|
149
|
+
*/
|
|
150
|
+
async send(message) {
|
|
151
|
+
try {
|
|
152
|
+
// Attempt reconnection if not connected
|
|
153
|
+
if (!this.rpcService.isConnected()) {
|
|
154
|
+
const reconnected = await this.rpcService.attemptReconnect(true);
|
|
155
|
+
|
|
156
|
+
if (!reconnected) {
|
|
157
|
+
throw new MessageError('RPC server not available. Cannot send message.');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 1. Get all token holders (broadcast)
|
|
162
|
+
const addresses = await this.getTokenHolders();
|
|
163
|
+
|
|
164
|
+
// 2. Get pubkeys from all recipients
|
|
165
|
+
const recipientPubKeys = await this.getRecipientPubkeys(addresses);
|
|
166
|
+
|
|
167
|
+
// 3. Build encrypted message
|
|
168
|
+
const buildResult = await this.buildEncryptedMessage(message, recipientPubKeys);
|
|
169
|
+
|
|
170
|
+
// 4. Wrap with server privacy layer if enabled
|
|
171
|
+
const payload = await this.wrapWithPrivacyLayer(buildResult.hex);
|
|
172
|
+
|
|
173
|
+
// 5. Send to pool
|
|
174
|
+
const result = await this.submitToPool(payload);
|
|
175
|
+
|
|
176
|
+
// Mark as connected on successful send
|
|
177
|
+
this.rpcService.connected = true;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
hash: result.hash || result.txid,
|
|
181
|
+
recipients: recipientPubKeys.length,
|
|
182
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// Mark as disconnected on error
|
|
186
|
+
this.rpcService.connected = false;
|
|
187
|
+
|
|
188
|
+
if (error instanceof DepinError) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
throw new MessageError(`Failed to send message: ${error.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message store for Neurai DePIN Terminal
|
|
3
|
+
* Handles message storage and deduplication
|
|
4
|
+
* @module MessageStore
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createMessageKey } from '../utils.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stores and deduplicates DePIN messages
|
|
11
|
+
*/
|
|
12
|
+
export class MessageStore {
|
|
13
|
+
/**
|
|
14
|
+
* Create a new MessageStore instance
|
|
15
|
+
*/
|
|
16
|
+
constructor() {
|
|
17
|
+
/** @type {Array<Object>} Stored messages sorted by timestamp */
|
|
18
|
+
this.messages = [];
|
|
19
|
+
|
|
20
|
+
/** @type {Set<string>} Set of seen message keys for deduplication */
|
|
21
|
+
this.seenHashes = new Set();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add a message to the store with deduplication
|
|
26
|
+
* Deduplication key format: hash|signature
|
|
27
|
+
* @param {Object} msg - Message object
|
|
28
|
+
* @param {string} msg.hash - Message hash
|
|
29
|
+
* @param {string} msg.signature - Message signature
|
|
30
|
+
* @param {number} msg.timestamp - Unix timestamp in seconds
|
|
31
|
+
* @param {string} msg.sender - Sender address
|
|
32
|
+
* @param {string} msg.message - Message content
|
|
33
|
+
* @returns {boolean} True if message is new, false if duplicate
|
|
34
|
+
*/
|
|
35
|
+
addMessage(msg) {
|
|
36
|
+
const key = createMessageKey(msg.hash, msg.signature);
|
|
37
|
+
|
|
38
|
+
if (this.seenHashes.has(key)) {
|
|
39
|
+
return false; // Duplicate message
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.seenHashes.add(key);
|
|
43
|
+
this.messages.push(msg);
|
|
44
|
+
|
|
45
|
+
// Keep messages sorted by timestamp (oldest first)
|
|
46
|
+
this.messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
47
|
+
|
|
48
|
+
return true; // New message added
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get all stored messages
|
|
53
|
+
* @returns {Array<Object>} Copy of messages array
|
|
54
|
+
*/
|
|
55
|
+
getMessages() {
|
|
56
|
+
return [...this.messages];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the latest timestamp from stored messages
|
|
61
|
+
* Used for incremental polling
|
|
62
|
+
* @returns {number} Latest timestamp or 0 if no messages
|
|
63
|
+
*/
|
|
64
|
+
getLastTimestamp() {
|
|
65
|
+
if (this.messages.length === 0) {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Math.max(...this.messages.map(m => m.timestamp));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get total count of stored messages
|
|
74
|
+
* @returns {number} Number of messages
|
|
75
|
+
*/
|
|
76
|
+
getCount() {
|
|
77
|
+
return this.messages.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear all stored messages
|
|
82
|
+
* Useful for testing or resetting state
|
|
83
|
+
*/
|
|
84
|
+
clear() {
|
|
85
|
+
this.messages = [];
|
|
86
|
+
this.seenHashes.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if a message exists by hash and signature
|
|
91
|
+
* @param {string} hash - Message hash
|
|
92
|
+
* @param {string} signature - Message signature
|
|
93
|
+
* @returns {boolean} True if message exists
|
|
94
|
+
*/
|
|
95
|
+
hasMessage(hash, signature) {
|
|
96
|
+
const key = createMessageKey(hash, signature);
|
|
97
|
+
return this.seenHashes.has(key);
|
|
98
|
+
}
|
|
99
|
+
}
|