@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.
@@ -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 messages to all token holders
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 { isPubkeyRevealed, normalizePubkey, hasPrivacyLayer } from '../utils.js';
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 all token holders
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>>} Array of addresses
48
+ * @returns {Promise<Array<{address: string, pubkey: string}>>} Recipient entries
44
49
  * @throws {MessageError} If no token holders found
45
50
  */
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);
51
+ async fetchDepinRecipients() {
52
+ return this.recipientDirectory.fetchEntries();
53
+ }
50
54
 
51
- if (addresses.length === 0) {
52
- throw new MessageError(ERROR_MESSAGES.NO_TOKEN_HOLDERS);
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
- return addresses;
63
+ getCachedPrivateRecipientAddresses() {
64
+ return this.recipientDirectory.getCachedAddresses();
56
65
  }
57
66
 
58
67
  /**
59
- * Get revealed public keys from addresses
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(addresses) {
65
- const recipientPubKeys = [];
66
- const rpc = this.getRpc();
72
+ async getRecipientPubkeys() {
73
+ return this.recipientDirectory.getPubkeys();
74
+ }
67
75
 
68
- for (const addr of addresses) {
69
- try {
70
- const res = await rpc(RPC_METHODS.GET_PUBKEY, [addr]);
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
- if (isPubkeyRevealed(res)) {
73
- recipientPubKeys.push(normalizePubkey(res.pubkey));
74
- }
75
- } catch (error) {
76
- // Skip addresses without revealed pubkey
77
- continue;
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
- if (recipientPubKeys.length === 0) {
82
- throw new MessageError(ERROR_MESSAGES.NO_RECIPIENTS);
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
- return recipientPubKeys;
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 broadcast message to all token holders
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
- // 1. Get all token holders (broadcast)
162
- const addresses = await this.getTokenHolders();
207
+ const parsed = this.parseMessageInput(message);
208
+ let recipientPubKeys = [];
163
209
 
164
- // 2. Get pubkeys from all recipients
165
- const recipientPubKeys = await this.getRecipientPubkeys(addresses);
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(message, recipientPubKeys);
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
+ }