@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
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
8
8
|
import { RPC_METHODS } from '../constants.js';
|
|
9
9
|
import { isEncryptedResponse } from '../utils.js';
|
|
10
|
+
import { MESSAGE_TYPES, normalizeMessageType } from '../domain/messageTypes.js';
|
|
11
|
+
import { RecipientDirectory } from './RecipientDirectory.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Polls for new DePIN messages at regular intervals
|
|
@@ -26,8 +28,9 @@ export class MessagePoller extends EventEmitter {
|
|
|
26
28
|
* @param {MessageStore} messageStore - Message store instance
|
|
27
29
|
* @param {Object} neuraiDepinMsg - DePIN message library
|
|
28
30
|
* @param {WalletManager} walletManager - Wallet manager instance (for decryption)
|
|
31
|
+
* @param {RecipientDirectory} [recipientDirectory] - Recipient directory (shared cache)
|
|
29
32
|
*/
|
|
30
|
-
constructor(config, rpcService, messageStore, neuraiDepinMsg, walletManager) {
|
|
33
|
+
constructor(config, rpcService, messageStore, neuraiDepinMsg, walletManager, recipientDirectory = null) {
|
|
31
34
|
super();
|
|
32
35
|
this.config = config;
|
|
33
36
|
this.rpcService = rpcService;
|
|
@@ -37,6 +40,8 @@ export class MessagePoller extends EventEmitter {
|
|
|
37
40
|
this.intervalId = null;
|
|
38
41
|
this.isPolling = false;
|
|
39
42
|
this.wasDisconnected = false; // Track if we were disconnected
|
|
43
|
+
this.recipientDirectory = recipientDirectory
|
|
44
|
+
|| new RecipientDirectory(config, rpcService, neuraiDepinMsg);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/**
|
|
@@ -263,13 +268,26 @@ export class MessagePoller extends EventEmitter {
|
|
|
263
268
|
return false; // Not for us or malformed
|
|
264
269
|
}
|
|
265
270
|
|
|
271
|
+
const messageType = normalizeMessageType(msg.message_type || msg.messageType);
|
|
272
|
+
let peerAddress = null;
|
|
273
|
+
|
|
274
|
+
if (messageType === MESSAGE_TYPES.PRIVATE) {
|
|
275
|
+
if (msg.sender === this.walletManager.getAddress()) {
|
|
276
|
+
peerAddress = await this.resolvePrivatePeerAddress(msg);
|
|
277
|
+
} else {
|
|
278
|
+
peerAddress = msg.sender;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
266
282
|
// Add to store with deduplication
|
|
267
283
|
const isNew = this.messageStore.addMessage({
|
|
268
284
|
sender: msg.sender,
|
|
269
285
|
message: plaintext,
|
|
270
286
|
timestamp: msg.timestamp,
|
|
271
287
|
hash: msg.hash,
|
|
272
|
-
signature: msg.signature_hex
|
|
288
|
+
signature: msg.signature_hex,
|
|
289
|
+
messageType: messageType,
|
|
290
|
+
peerAddress: peerAddress
|
|
273
291
|
});
|
|
274
292
|
|
|
275
293
|
if (isNew) {
|
|
@@ -286,7 +304,9 @@ export class MessagePoller extends EventEmitter {
|
|
|
286
304
|
sender: msg.sender,
|
|
287
305
|
message: plaintext,
|
|
288
306
|
timestamp: msg.timestamp,
|
|
289
|
-
hash: msg.hash
|
|
307
|
+
hash: msg.hash,
|
|
308
|
+
messageType: messageType,
|
|
309
|
+
peerAddress: peerAddress
|
|
290
310
|
});
|
|
291
311
|
return true;
|
|
292
312
|
}
|
|
@@ -297,4 +317,113 @@ export class MessagePoller extends EventEmitter {
|
|
|
297
317
|
return false;
|
|
298
318
|
}
|
|
299
319
|
}
|
|
320
|
+
|
|
321
|
+
async resolvePrivatePeerAddress(msg) {
|
|
322
|
+
const mapped = this.messageStore.getOutgoingPrivateRecipient(msg.hash);
|
|
323
|
+
if (mapped) {
|
|
324
|
+
return mapped;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const hashes = this.extractRecipientHashes(msg.encrypted_payload_hex);
|
|
328
|
+
if (!hashes.length) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const map = await this.recipientDirectory.getHashMap();
|
|
333
|
+
for (const hash of hashes) {
|
|
334
|
+
const address = map.get(hash);
|
|
335
|
+
if (address && address !== this.walletManager.getAddress()) {
|
|
336
|
+
return address;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
extractRecipientHashes(encryptedPayloadHex) {
|
|
344
|
+
const utils = this.neuraiDepinMsg?.utils;
|
|
345
|
+
if (!utils?.hexToBytes || !utils?.bytesToHex) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const hex = this.normalizeHex(encryptedPayloadHex);
|
|
351
|
+
if (!hex) {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
const serialized = utils.hexToBytes(hex);
|
|
355
|
+
let offset = 0;
|
|
356
|
+
|
|
357
|
+
const ephem = this.readVector(serialized, offset);
|
|
358
|
+
offset = ephem.offset;
|
|
359
|
+
const payload = this.readVector(serialized, offset);
|
|
360
|
+
offset = payload.offset;
|
|
361
|
+
const countRes = this.readCompactSize(serialized, offset);
|
|
362
|
+
const count = countRes.value;
|
|
363
|
+
offset = countRes.offset;
|
|
364
|
+
|
|
365
|
+
const hashes = [];
|
|
366
|
+
for (let i = 0; i < count; i += 1) {
|
|
367
|
+
if (offset + 20 > serialized.length) {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
const keyId = serialized.slice(offset, offset + 20);
|
|
371
|
+
offset += 20;
|
|
372
|
+
const v = this.readVector(serialized, offset);
|
|
373
|
+
offset = v.offset;
|
|
374
|
+
hashes.push(utils.bytesToHex(keyId).toLowerCase());
|
|
375
|
+
}
|
|
376
|
+
return hashes;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
normalizeHex(hex) {
|
|
383
|
+
if (typeof hex !== 'string') {
|
|
384
|
+
return '';
|
|
385
|
+
}
|
|
386
|
+
const trimmed = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
387
|
+
return trimmed.trim().toLowerCase();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
readCompactSize(buf, offset) {
|
|
391
|
+
if (offset >= buf.length) {
|
|
392
|
+
throw new Error('CompactSize: out of bounds');
|
|
393
|
+
}
|
|
394
|
+
const first = buf[offset];
|
|
395
|
+
if (first < 253) return { value: first, offset: offset + 1 };
|
|
396
|
+
if (first === 253) {
|
|
397
|
+
if (offset + 3 > buf.length) throw new Error('CompactSize: truncated uint16');
|
|
398
|
+
const value = buf[offset + 1] | (buf[offset + 2] << 8);
|
|
399
|
+
return { value, offset: offset + 3 };
|
|
400
|
+
}
|
|
401
|
+
if (first === 254) {
|
|
402
|
+
if (offset + 5 > buf.length) throw new Error('CompactSize: truncated uint32');
|
|
403
|
+
const value =
|
|
404
|
+
(buf[offset + 1]) |
|
|
405
|
+
(buf[offset + 2] << 8) |
|
|
406
|
+
(buf[offset + 3] << 16) |
|
|
407
|
+
(buf[offset + 4] << 24);
|
|
408
|
+
return { value: value >>> 0, offset: offset + 5 };
|
|
409
|
+
}
|
|
410
|
+
if (offset + 9 > buf.length) throw new Error('CompactSize: truncated uint64');
|
|
411
|
+
let value = 0n;
|
|
412
|
+
for (let i = 0; i < 8; i += 1) {
|
|
413
|
+
value |= BigInt(buf[offset + 1 + i]) << (8n * BigInt(i));
|
|
414
|
+
}
|
|
415
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
416
|
+
throw new Error('CompactSize: value too large');
|
|
417
|
+
}
|
|
418
|
+
return { value: Number(value), offset: offset + 9 };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
readVector(buf, offset) {
|
|
422
|
+
const { value: len, offset: afterLen } = this.readCompactSize(buf, offset);
|
|
423
|
+
if (afterLen + len > buf.length) {
|
|
424
|
+
throw new Error('Vector: truncated');
|
|
425
|
+
}
|
|
426
|
+
const data = buf.slice(afterLen, afterLen + len);
|
|
427
|
+
return { data, offset: afterLen + len };
|
|
428
|
+
}
|
|
300
429
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Message sender for Neurai DePIN Terminal
|
|
3
|
-
* Handles sending broadcast
|
|
3
|
+
* Handles sending broadcast and private messages
|
|
4
4
|
* @module MessageSender
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
ERROR_MESSAGES
|
|
10
10
|
} from '../constants.js';
|
|
11
11
|
import { MessageError, DepinError } from '../errors.js';
|
|
12
|
-
import {
|
|
12
|
+
import { hasPrivacyLayer } from '../utils.js';
|
|
13
|
+
import { RecipientDirectory } from './RecipientDirectory.js';
|
|
14
|
+
import { MESSAGE_TYPES } from '../domain/messageTypes.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* Sends DePIN messages to
|
|
17
|
+
* Sends DePIN messages to token holders or a specific recipient
|
|
16
18
|
*/
|
|
17
19
|
export class MessageSender {
|
|
18
20
|
/**
|
|
@@ -22,12 +24,15 @@ export class MessageSender {
|
|
|
22
24
|
* @param {WalletManager} walletManager - Wallet manager instance
|
|
23
25
|
* @param {RpcService} rpcService - RPC service instance
|
|
24
26
|
* @param {Object} neuraiDepinMsg - DePIN message library
|
|
27
|
+
* @param {RecipientDirectory} [recipientDirectory] - Recipient directory (shared cache)
|
|
25
28
|
*/
|
|
26
|
-
constructor(config, walletManager, rpcService, neuraiDepinMsg) {
|
|
29
|
+
constructor(config, walletManager, rpcService, neuraiDepinMsg, recipientDirectory = null) {
|
|
27
30
|
this.config = config;
|
|
28
31
|
this.walletManager = walletManager;
|
|
29
32
|
this.rpcService = rpcService;
|
|
30
33
|
this.neuraiDepinMsg = neuraiDepinMsg;
|
|
34
|
+
this.recipientDirectory = recipientDirectory
|
|
35
|
+
|| new RecipientDirectory(config, rpcService, neuraiDepinMsg);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/**
|
|
@@ -40,58 +45,95 @@ export class MessageSender {
|
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
47
|
* Get all addresses holding the token
|
|
43
|
-
* @returns {Promise<Array<string>>}
|
|
48
|
+
* @returns {Promise<Array<{address: string, pubkey: string}>>} Recipient entries
|
|
44
49
|
* @throws {MessageError} If no token holders found
|
|
45
50
|
*/
|
|
46
|
-
async
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const addresses = Object.keys(addressesData);
|
|
51
|
+
async fetchDepinRecipients() {
|
|
52
|
+
return this.recipientDirectory.fetchEntries();
|
|
53
|
+
}
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
async refreshRecipientCache(force = false) {
|
|
56
|
+
return this.recipientDirectory.refresh(force);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getCachedRecipientEntries() {
|
|
60
|
+
return this.recipientDirectory.getCachedEntries();
|
|
61
|
+
}
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
getCachedPrivateRecipientAddresses() {
|
|
64
|
+
return this.recipientDirectory.getCachedAddresses();
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
|
-
* Get revealed public keys
|
|
60
|
-
* @param {Array<string>} addresses - Array of addresses
|
|
68
|
+
* Get revealed public keys
|
|
61
69
|
* @returns {Promise<Array<string>>} Array of revealed public keys (normalized)
|
|
62
70
|
* @throws {MessageError} If no recipients with revealed public keys
|
|
63
71
|
*/
|
|
64
|
-
async getRecipientPubkeys(
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
async getRecipientPubkeys() {
|
|
73
|
+
return this.recipientDirectory.getPubkeys();
|
|
74
|
+
}
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Get revealed public keys with addresses
|
|
78
|
+
* @returns {Promise<Array<{address: string, pubkey: string}>>} Recipient entries
|
|
79
|
+
* @throws {MessageError} If no recipients with revealed public keys
|
|
80
|
+
*/
|
|
81
|
+
async getRecipientEntries() {
|
|
82
|
+
return this.recipientDirectory.getEntries();
|
|
83
|
+
}
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Get recipient addresses eligible for private messages
|
|
87
|
+
* @returns {Promise<Array<string>>} Array of addresses
|
|
88
|
+
* @throws {MessageError} If no recipients with revealed public keys
|
|
89
|
+
*/
|
|
90
|
+
async getPrivateRecipientAddresses() {
|
|
91
|
+
return this.recipientDirectory.getAddresses();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get revealed public key for a single address
|
|
96
|
+
* @param {string} address - Recipient address
|
|
97
|
+
* @returns {Promise<string>} Normalized recipient public key
|
|
98
|
+
* @throws {MessageError} If pubkey is not revealed or RPC fails
|
|
99
|
+
*/
|
|
100
|
+
async getRecipientPubkeyForAddress(address) {
|
|
101
|
+
return this.recipientDirectory.getPubkeyForAddress(address);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse message input to detect private messages
|
|
106
|
+
* @param {string} message - Raw message input
|
|
107
|
+
* @returns {{messageType: string, message: string, recipientAddress: (string|null)}}
|
|
108
|
+
* @throws {MessageError} If private format is invalid
|
|
109
|
+
*/
|
|
110
|
+
parseMessageInput(message) {
|
|
111
|
+
const trimmed = message.trim();
|
|
112
|
+
|
|
113
|
+
if (!trimmed.startsWith('@')) {
|
|
114
|
+
return { messageType: MESSAGE_TYPES.GROUP, message: trimmed, recipientAddress: null };
|
|
79
115
|
}
|
|
80
116
|
|
|
81
|
-
|
|
82
|
-
|
|
117
|
+
const match = trimmed.match(/^@(\S+)\s+(.+)$/);
|
|
118
|
+
|
|
119
|
+
if (!match) {
|
|
120
|
+
throw new MessageError(ERROR_MESSAGES.INVALID_PRIVATE_MESSAGE_FORMAT);
|
|
83
121
|
}
|
|
84
122
|
|
|
85
|
-
|
|
123
|
+
const recipientAddress = match[1];
|
|
124
|
+
const privateMessage = match[2].trim();
|
|
125
|
+
|
|
126
|
+
return { messageType: MESSAGE_TYPES.PRIVATE, message: privateMessage, recipientAddress };
|
|
86
127
|
}
|
|
87
128
|
|
|
88
129
|
/**
|
|
89
130
|
* Build encrypted DePIN message
|
|
90
131
|
* @param {string} message - Plaintext message
|
|
91
132
|
* @param {Array<string>} recipientPubKeys - Array of recipient public keys
|
|
133
|
+
* @param {"private"|"group"} messageType - Message type
|
|
92
134
|
* @returns {Promise<Object>} Build result with hex payload
|
|
93
135
|
*/
|
|
94
|
-
async buildEncryptedMessage(message, recipientPubKeys) {
|
|
136
|
+
async buildEncryptedMessage(message, recipientPubKeys, messageType) {
|
|
95
137
|
return await this.neuraiDepinMsg.buildDepinMessage({
|
|
96
138
|
token: this.config.token,
|
|
97
139
|
senderAddress: this.walletManager.getAddress(),
|
|
@@ -99,7 +141,8 @@ export class MessageSender {
|
|
|
99
141
|
privateKey: this.walletManager.getPrivateKeyHex(),
|
|
100
142
|
timestamp: Math.floor(Date.now() / 1000),
|
|
101
143
|
message: message,
|
|
102
|
-
recipientPubKeys: recipientPubKeys
|
|
144
|
+
recipientPubKeys: recipientPubKeys,
|
|
145
|
+
messageType: messageType
|
|
103
146
|
});
|
|
104
147
|
}
|
|
105
148
|
|
|
@@ -138,13 +181,16 @@ export class MessageSender {
|
|
|
138
181
|
}
|
|
139
182
|
|
|
140
183
|
/**
|
|
141
|
-
* Send a
|
|
184
|
+
* Send a group or private message
|
|
142
185
|
* @param {string} message - Plaintext message to send
|
|
143
186
|
* @returns {Promise<Object>} Result object with hash and recipient count
|
|
144
187
|
* @returns {Promise<Object>} result
|
|
145
188
|
* @returns {string} result.hash - Transaction hash
|
|
146
189
|
* @returns {number} result.recipients - Number of recipients
|
|
147
190
|
* @returns {number} result.timestamp - Send timestamp
|
|
191
|
+
* @returns {"private"|"group"} result.messageType - Message type
|
|
192
|
+
* @returns {string|null} result.recipientAddress - Target address for private messages
|
|
193
|
+
* @returns {string} result.messageHash - Message hash used for deduplication
|
|
148
194
|
* @throws {MessageError} If sending fails
|
|
149
195
|
*/
|
|
150
196
|
async send(message) {
|
|
@@ -158,14 +204,23 @@ export class MessageSender {
|
|
|
158
204
|
}
|
|
159
205
|
}
|
|
160
206
|
|
|
161
|
-
|
|
162
|
-
|
|
207
|
+
const parsed = this.parseMessageInput(message);
|
|
208
|
+
let recipientPubKeys = [];
|
|
163
209
|
|
|
164
|
-
|
|
165
|
-
|
|
210
|
+
if (parsed.messageType === 'private') {
|
|
211
|
+
const recipientPubkey = await this.getRecipientPubkeyForAddress(parsed.recipientAddress);
|
|
212
|
+
recipientPubKeys = [recipientPubkey];
|
|
213
|
+
} else {
|
|
214
|
+
// 1. Get pubkeys from all recipients (broadcast)
|
|
215
|
+
recipientPubKeys = await this.getRecipientPubkeys();
|
|
216
|
+
}
|
|
166
217
|
|
|
167
218
|
// 3. Build encrypted message
|
|
168
|
-
const buildResult = await this.buildEncryptedMessage(
|
|
219
|
+
const buildResult = await this.buildEncryptedMessage(
|
|
220
|
+
parsed.message,
|
|
221
|
+
recipientPubKeys,
|
|
222
|
+
parsed.messageType
|
|
223
|
+
);
|
|
169
224
|
|
|
170
225
|
// 4. Wrap with server privacy layer if enabled
|
|
171
226
|
const payload = await this.wrapWithPrivacyLayer(buildResult.hex);
|
|
@@ -178,8 +233,11 @@ export class MessageSender {
|
|
|
178
233
|
|
|
179
234
|
return {
|
|
180
235
|
hash: result.hash || result.txid,
|
|
181
|
-
recipients: recipientPubKeys.length,
|
|
182
|
-
timestamp: Math.floor(Date.now() / 1000)
|
|
236
|
+
recipients: parsed.messageType === 'private' ? 1 : recipientPubKeys.length,
|
|
237
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
238
|
+
messageType: parsed.messageType,
|
|
239
|
+
recipientAddress: parsed.recipientAddress,
|
|
240
|
+
messageHash: buildResult.messageHash
|
|
183
241
|
};
|
|
184
242
|
} catch (error) {
|
|
185
243
|
// Mark as disconnected on error
|
|
@@ -19,6 +19,9 @@ export class MessageStore {
|
|
|
19
19
|
|
|
20
20
|
/** @type {Set<string>} Set of seen message keys for deduplication */
|
|
21
21
|
this.seenHashes = new Set();
|
|
22
|
+
|
|
23
|
+
/** @type {Map<string, string>} Map of outgoing private message hash to recipient */
|
|
24
|
+
this.outgoingPrivateRecipients = new Map();
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
@@ -30,6 +33,8 @@ export class MessageStore {
|
|
|
30
33
|
* @param {number} msg.timestamp - Unix timestamp in seconds
|
|
31
34
|
* @param {string} msg.sender - Sender address
|
|
32
35
|
* @param {string} msg.message - Message content
|
|
36
|
+
* @param {string} [msg.messageType] - Message type ("group" or "private")
|
|
37
|
+
* @param {string} [msg.peerAddress] - Peer address for private messages
|
|
33
38
|
* @returns {boolean} True if message is new, false if duplicate
|
|
34
39
|
*/
|
|
35
40
|
addMessage(msg) {
|
|
@@ -48,6 +53,26 @@ export class MessageStore {
|
|
|
48
53
|
return true; // New message added
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Register a private message recipient by message hash
|
|
58
|
+
* @param {string} hash - Message hash
|
|
59
|
+
* @param {string} recipientAddress - Recipient address
|
|
60
|
+
*/
|
|
61
|
+
registerOutgoingPrivateMessage(hash, recipientAddress) {
|
|
62
|
+
if (hash && recipientAddress) {
|
|
63
|
+
this.outgoingPrivateRecipients.set(hash, recipientAddress);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get recipient address for a private message hash
|
|
69
|
+
* @param {string} hash - Message hash
|
|
70
|
+
* @returns {string|null} Recipient address or null
|
|
71
|
+
*/
|
|
72
|
+
getOutgoingPrivateRecipient(hash) {
|
|
73
|
+
return this.outgoingPrivateRecipients.get(hash) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
/**
|
|
52
77
|
* Get all stored messages
|
|
53
78
|
* @returns {Array<Object>} Copy of messages array
|
|
@@ -84,6 +109,7 @@ export class MessageStore {
|
|
|
84
109
|
clear() {
|
|
85
110
|
this.messages = [];
|
|
86
111
|
this.seenHashes.clear();
|
|
112
|
+
this.outgoingPrivateRecipients.clear();
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
/**
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipient directory for DePIN messaging
|
|
3
|
+
* Shared cache for addresses, pubkeys, and recipient hash lookups.
|
|
4
|
+
* @module RecipientDirectory
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
RPC_METHODS,
|
|
9
|
+
ERROR_MESSAGES,
|
|
10
|
+
RECIPIENT_CACHE
|
|
11
|
+
} from '../constants.js';
|
|
12
|
+
import { MessageError } from '../errors.js';
|
|
13
|
+
import { normalizePubkey } from '../utils.js';
|
|
14
|
+
|
|
15
|
+
export class RecipientDirectory {
|
|
16
|
+
constructor(config, rpcService, neuraiDepinMsg) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.rpcService = rpcService;
|
|
19
|
+
this.neuraiDepinMsg = neuraiDepinMsg;
|
|
20
|
+
this.cache = {
|
|
21
|
+
entries: [],
|
|
22
|
+
updatedAt: 0,
|
|
23
|
+
pending: null
|
|
24
|
+
};
|
|
25
|
+
this.hashCache = {
|
|
26
|
+
map: new Map(),
|
|
27
|
+
updatedAt: 0,
|
|
28
|
+
pending: null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getRpc() {
|
|
33
|
+
return this.rpcService.call.bind(this.rpcService);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async fetchEntries() {
|
|
37
|
+
const rpc = this.getRpc();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await rpc(RPC_METHODS.LIST_DEPIN_ADDRESSES, [this.config.token]);
|
|
41
|
+
|
|
42
|
+
if (!Array.isArray(result)) {
|
|
43
|
+
throw new MessageError('Invalid response from listdepinaddresses');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entries = result
|
|
47
|
+
.filter((entry) => entry && entry.address && entry.pubkey)
|
|
48
|
+
.map((entry) => ({
|
|
49
|
+
address: entry.address,
|
|
50
|
+
pubkey: normalizePubkey(entry.pubkey)
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
if (entries.length === 0) {
|
|
54
|
+
throw new MessageError(ERROR_MESSAGES.NO_RECIPIENTS);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return entries;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof MessageError) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
throw new MessageError(`Failed to fetch recipient list: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async refresh(force = false) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const hasCache = this.cache.entries.length > 0;
|
|
69
|
+
const isFresh = now - this.cache.updatedAt < RECIPIENT_CACHE.REFRESH_MS;
|
|
70
|
+
|
|
71
|
+
if (!force && hasCache && isFresh) {
|
|
72
|
+
return this.cache.entries;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (this.cache.pending) {
|
|
76
|
+
return this.cache.pending;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.cache.pending = (async () => {
|
|
80
|
+
const entries = await this.fetchEntries();
|
|
81
|
+
this.cache.entries = entries;
|
|
82
|
+
this.cache.updatedAt = Date.now();
|
|
83
|
+
return entries;
|
|
84
|
+
})();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
return await this.cache.pending;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (hasCache) {
|
|
90
|
+
return this.cache.entries;
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
} finally {
|
|
94
|
+
this.cache.pending = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getCachedEntries() {
|
|
99
|
+
return this.cache.entries;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getCachedAddresses() {
|
|
103
|
+
if (!this.cache.entries.length) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
return this.cache.entries.map((entry) => entry.address).sort();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getEntries() {
|
|
110
|
+
return this.refresh();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getAddresses() {
|
|
114
|
+
const entries = await this.refresh();
|
|
115
|
+
return entries.map((entry) => entry.address).sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getPubkeys() {
|
|
119
|
+
const entries = await this.refresh();
|
|
120
|
+
return entries.map((entry) => entry.pubkey);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getPubkeyForAddress(address) {
|
|
124
|
+
let recipients = await this.refresh();
|
|
125
|
+
let match = recipients.find((entry) => entry.address === address);
|
|
126
|
+
|
|
127
|
+
if (!match) {
|
|
128
|
+
recipients = await this.refresh(true);
|
|
129
|
+
match = recipients.find((entry) => entry.address === address);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!match) {
|
|
133
|
+
throw new MessageError(`${ERROR_MESSAGES.RECIPIENT_PUBKEY_NOT_REVEALED}: ${address}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return match.pubkey;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async getHashMap(force = false) {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const hasCache = this.hashCache.map.size > 0;
|
|
142
|
+
const isFresh = now - this.hashCache.updatedAt < RECIPIENT_CACHE.REFRESH_MS;
|
|
143
|
+
|
|
144
|
+
if (!force && hasCache && isFresh) {
|
|
145
|
+
return this.hashCache.map;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this.hashCache.pending) {
|
|
149
|
+
return this.hashCache.pending;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.hashCache.pending = (async () => {
|
|
153
|
+
const entries = await this.refresh(force);
|
|
154
|
+
const utils = this.neuraiDepinMsg?.utils;
|
|
155
|
+
if (!utils?.hexToBytes || !utils?.bytesToHex || !utils?.hash160) {
|
|
156
|
+
return this.hashCache.map;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const nextMap = new Map();
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const pubkeyBytes = utils.hexToBytes(entry.pubkey);
|
|
162
|
+
const hashBytes = await utils.hash160(pubkeyBytes);
|
|
163
|
+
const hashHex = utils.bytesToHex(hashBytes).toLowerCase();
|
|
164
|
+
nextMap.set(hashHex, entry.address);
|
|
165
|
+
|
|
166
|
+
const reversedHex = utils.bytesToHex(hashBytes.slice().reverse()).toLowerCase();
|
|
167
|
+
if (!nextMap.has(reversedHex)) {
|
|
168
|
+
nextMap.set(reversedHex, entry.address);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (nextMap.size > 0) {
|
|
173
|
+
this.hashCache.map = nextMap;
|
|
174
|
+
this.hashCache.updatedAt = Date.now();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return this.hashCache.map;
|
|
178
|
+
})();
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return await this.hashCache.pending;
|
|
182
|
+
} finally {
|
|
183
|
+
this.hashCache.pending = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|