@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,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
+ }
@@ -0,0 +1,2 @@
1
+ // Empty file to stub optional dependencies
2
+ export default {};
@@ -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
+ }