@neuraiproject/neurai-depin-msg 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/src/index.js ADDED
@@ -0,0 +1,597 @@
1
+ /**
2
+ * @neuraiproject/neurai-depin-msg
3
+ *
4
+ * Build encrypted DePIN messages for Neurai blockchain
5
+ * Self-contained library with bundled secp256k1
6
+ */
7
+
8
+ import * as secp256k1 from '@bitcoinerlab/secp256k1';
9
+
10
+ // ============================================
11
+ // SERIALIZATION UTILITIES
12
+ // ============================================
13
+
14
+ function writeCompactSize(value) {
15
+ if (value < 0) throw new Error('CompactSize cannot be negative');
16
+
17
+ if (value < 253) {
18
+ return new Uint8Array([value]);
19
+ } else if (value <= 0xffff) {
20
+ const buf = new Uint8Array(3);
21
+ buf[0] = 253;
22
+ buf[1] = value & 0xff;
23
+ buf[2] = (value >> 8) & 0xff;
24
+ return buf;
25
+ } else if (value <= 0xffffffff) {
26
+ const buf = new Uint8Array(5);
27
+ buf[0] = 254;
28
+ buf[1] = value & 0xff;
29
+ buf[2] = (value >> 8) & 0xff;
30
+ buf[3] = (value >> 16) & 0xff;
31
+ buf[4] = (value >> 24) & 0xff;
32
+ return buf;
33
+ } else {
34
+ const buf = new Uint8Array(9);
35
+ buf[0] = 255;
36
+ const low = value >>> 0;
37
+ const high = Math.floor(value / 0x100000000) >>> 0;
38
+ buf[1] = low & 0xff;
39
+ buf[2] = (low >> 8) & 0xff;
40
+ buf[3] = (low >> 16) & 0xff;
41
+ buf[4] = (low >> 24) & 0xff;
42
+ buf[5] = high & 0xff;
43
+ buf[6] = (high >> 8) & 0xff;
44
+ buf[7] = (high >> 16) & 0xff;
45
+ buf[8] = (high >> 24) & 0xff;
46
+ return buf;
47
+ }
48
+ }
49
+
50
+ function serializeString(str) {
51
+ const encoder = new TextEncoder();
52
+ const strBytes = encoder.encode(str);
53
+ return concatBytes(writeCompactSize(strBytes.length), strBytes);
54
+ }
55
+
56
+ function serializeVector(data) {
57
+ return concatBytes(writeCompactSize(data.length), data);
58
+ }
59
+
60
+ function serializeInt64(value) {
61
+ const buf = new Uint8Array(8);
62
+ const low = value >>> 0;
63
+ const high = Math.floor(value / 0x100000000) >>> 0;
64
+ buf[0] = low & 0xff;
65
+ buf[1] = (low >> 8) & 0xff;
66
+ buf[2] = (low >> 16) & 0xff;
67
+ buf[3] = (low >> 24) & 0xff;
68
+ buf[4] = high & 0xff;
69
+ buf[5] = (high >> 8) & 0xff;
70
+ buf[6] = (high >> 16) & 0xff;
71
+ buf[7] = (high >> 24) & 0xff;
72
+ return buf;
73
+ }
74
+
75
+ function concatBytes(...arrays) {
76
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
77
+ const result = new Uint8Array(totalLength);
78
+ let offset = 0;
79
+ for (const arr of arrays) {
80
+ result.set(arr, offset);
81
+ offset += arr.length;
82
+ }
83
+ return result;
84
+ }
85
+
86
+ function hexToBytes(hex) {
87
+ if (hex.length % 2 !== 0) throw new Error('Hex must have even length');
88
+ const bytes = new Uint8Array(hex.length / 2);
89
+ for (let i = 0; i < hex.length; i += 2) {
90
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
91
+ }
92
+ return bytes;
93
+ }
94
+
95
+ function bytesToHex(bytes) {
96
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
97
+ }
98
+
99
+ /**
100
+ * Encode ECDSA signature to DER format
101
+ * Takes 64-byte raw signature (32 bytes r + 32 bytes s) and converts to DER
102
+ */
103
+ function encodeDER(signature) {
104
+ if (signature.length !== 64) {
105
+ throw new Error('Raw signature must be 64 bytes');
106
+ }
107
+
108
+ const r = signature.slice(0, 32);
109
+ const s = signature.slice(32, 64);
110
+
111
+ // Helper to encode an integer in DER format
112
+ function encodeInteger(value) {
113
+ // Remove leading zeros (but keep one if needed for sign bit)
114
+ let i = 0;
115
+ while (i < value.length - 1 && value[i] === 0 && (value[i + 1] & 0x80) === 0) {
116
+ i++;
117
+ }
118
+ const trimmed = value.slice(i);
119
+
120
+ // If high bit is set, prepend 0x00 to indicate positive number
121
+ const needsPadding = (trimmed[0] & 0x80) !== 0;
122
+ const paddedValue = needsPadding
123
+ ? concatBytes(new Uint8Array([0x00]), trimmed)
124
+ : trimmed;
125
+
126
+ // DER integer: 0x02 (INTEGER tag) + length + value
127
+ return concatBytes(
128
+ new Uint8Array([0x02, paddedValue.length]),
129
+ paddedValue
130
+ );
131
+ }
132
+
133
+ const rDER = encodeInteger(r);
134
+ const sDER = encodeInteger(s);
135
+
136
+ // DER sequence: 0x30 (SEQUENCE tag) + length + rDER + sDER
137
+ const sequenceLength = rDER.length + sDER.length;
138
+ return concatBytes(
139
+ new Uint8Array([0x30, sequenceLength]),
140
+ rDER,
141
+ sDER
142
+ );
143
+ }
144
+
145
+ // ============================================
146
+ // BASE58 / WIF UTILITIES
147
+ // ============================================
148
+
149
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
150
+
151
+ function base58Decode(str) {
152
+ const bytes = [];
153
+ for (let i = 0; i < str.length; i++) {
154
+ const charIndex = BASE58_ALPHABET.indexOf(str[i]);
155
+ if (charIndex === -1) throw new Error('Invalid Base58 character: ' + str[i]);
156
+
157
+ let carry = charIndex;
158
+ for (let j = 0; j < bytes.length; j++) {
159
+ carry += bytes[j] * 58;
160
+ bytes[j] = carry & 0xff;
161
+ carry >>= 8;
162
+ }
163
+ while (carry > 0) {
164
+ bytes.push(carry & 0xff);
165
+ carry >>= 8;
166
+ }
167
+ }
168
+
169
+ for (let i = 0; i < str.length && str[i] === '1'; i++) {
170
+ bytes.push(0);
171
+ }
172
+
173
+ return new Uint8Array(bytes.reverse());
174
+ }
175
+
176
+ async function wifToHex(wif) {
177
+ const decoded = base58Decode(wif);
178
+
179
+ if (decoded.length < 37) {
180
+ throw new Error('Invalid WIF: too short');
181
+ }
182
+
183
+ const payload = decoded.slice(0, -4);
184
+ const checksum = decoded.slice(-4);
185
+ const hash = await doubleSha256(payload);
186
+
187
+ for (let i = 0; i < 4; i++) {
188
+ if (checksum[i] !== hash[i]) {
189
+ throw new Error('Invalid WIF: checksum mismatch');
190
+ }
191
+ }
192
+
193
+ let privateKeyBytes;
194
+ if (payload.length === 34) {
195
+ privateKeyBytes = payload.slice(1, 33);
196
+ } else if (payload.length === 33) {
197
+ privateKeyBytes = payload.slice(1, 33);
198
+ } else {
199
+ throw new Error('Invalid WIF: unexpected length ' + payload.length);
200
+ }
201
+
202
+ return bytesToHex(privateKeyBytes);
203
+ }
204
+
205
+ function isWIF(str) {
206
+ return /^[5KLcT][1-9A-HJ-NP-Za-km-z]{50,51}$/.test(str);
207
+ }
208
+
209
+ // ============================================
210
+ // CRYPTOGRAPHIC FUNCTIONS
211
+ // ============================================
212
+
213
+ async function sha256(data) {
214
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
215
+ return new Uint8Array(hashBuffer);
216
+ }
217
+
218
+ async function doubleSha256(data) {
219
+ const first = await sha256(data);
220
+ return sha256(first);
221
+ }
222
+
223
+ function ripemd160(data) {
224
+ let h0 = 0x67452301, h1 = 0xefcdab89, h2 = 0x98badcfe, h3 = 0x10325476, h4 = 0xc3d2e1f0;
225
+
226
+ const K1 = [0x00000000, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e];
227
+ const K2 = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0x00000000];
228
+
229
+ const R1 = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13];
230
+ const R2 = [5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11];
231
+ const S1 = [11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6];
232
+ const S2 = [8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11];
233
+
234
+ function rotl(x, n) { return ((x << n) | (x >>> (32 - n))) >>> 0; }
235
+
236
+ const bitLen = data.length * 8;
237
+ const padLen = (64 - ((data.length + 9) % 64)) % 64;
238
+ const padded = new Uint8Array(data.length + 1 + padLen + 8);
239
+ padded.set(data);
240
+ padded[data.length] = 0x80;
241
+ const view = new DataView(padded.buffer);
242
+ view.setUint32(padded.length - 8, bitLen, true);
243
+
244
+ const blocks = padded.length / 64;
245
+
246
+ for (let i = 0; i < blocks; i++) {
247
+ const X = new Uint32Array(16);
248
+ for (let j = 0; j < 16; j++) {
249
+ const offset = i * 64 + j * 4;
250
+ X[j] = padded[offset] | (padded[offset + 1] << 8) | (padded[offset + 2] << 16) | (padded[offset + 3] << 24);
251
+ }
252
+
253
+ let a1 = h0, b1 = h1, c1 = h2, d1 = h3, e1 = h4;
254
+ let a2 = h0, b2 = h1, c2 = h2, d2 = h3, e2 = h4;
255
+
256
+ for (let j = 0; j < 80; j++) {
257
+ const round = Math.floor(j / 16);
258
+ let f1, f2;
259
+
260
+ if (round === 0) { f1 = b1 ^ c1 ^ d1; f2 = b2 ^ (c2 | ~d2); }
261
+ else if (round === 1) { f1 = (b1 & c1) | (~b1 & d1); f2 = (b2 & d2) | (c2 & ~d2); }
262
+ else if (round === 2) { f1 = (b1 | ~c1) ^ d1; f2 = (b2 | ~c2) ^ d2; }
263
+ else if (round === 3) { f1 = (b1 & d1) | (c1 & ~d1); f2 = (b2 & c2) | (~b2 & d2); }
264
+ else { f1 = b1 ^ (c1 | ~d1); f2 = b2 ^ c2 ^ d2; }
265
+
266
+ const t1 = (rotl((a1 + f1 + X[R1[j]] + K1[round]) >>> 0, S1[j]) + e1) >>> 0;
267
+ a1 = e1; e1 = d1; d1 = rotl(c1, 10); c1 = b1; b1 = t1;
268
+
269
+ const t2 = (rotl((a2 + f2 + X[R2[j]] + K2[round]) >>> 0, S2[j]) + e2) >>> 0;
270
+ a2 = e2; e2 = d2; d2 = rotl(c2, 10); c2 = b2; b2 = t2;
271
+ }
272
+
273
+ const t = (h1 + c1 + d2) >>> 0;
274
+ h1 = (h2 + d1 + e2) >>> 0;
275
+ h2 = (h3 + e1 + a2) >>> 0;
276
+ h3 = (h4 + a1 + b2) >>> 0;
277
+ h4 = (h0 + b1 + c2) >>> 0;
278
+ h0 = t;
279
+ }
280
+
281
+ const result = new Uint8Array(20);
282
+ const rv = new DataView(result.buffer);
283
+ rv.setUint32(0, h0, true); rv.setUint32(4, h1, true);
284
+ rv.setUint32(8, h2, true); rv.setUint32(12, h3, true);
285
+ rv.setUint32(16, h4, true);
286
+ return result;
287
+ }
288
+
289
+ async function hash160(data) {
290
+ const sha = await sha256(data);
291
+ return ripemd160(sha);
292
+ }
293
+
294
+ async function kdfSha256(sharedSecret, outputLen) {
295
+ const output = new Uint8Array(outputLen);
296
+ let offset = 0;
297
+ let counter = 1;
298
+
299
+ while (offset < outputLen) {
300
+ const counterBytes = new Uint8Array(4);
301
+ counterBytes[0] = (counter >> 24) & 0xff;
302
+ counterBytes[1] = (counter >> 16) & 0xff;
303
+ counterBytes[2] = (counter >> 8) & 0xff;
304
+ counterBytes[3] = counter & 0xff;
305
+
306
+ const data = concatBytes(sharedSecret, counterBytes);
307
+ const hash = await sha256(data);
308
+
309
+ const remaining = outputLen - offset;
310
+ const toCopy = Math.min(remaining, 32);
311
+ output.set(hash.slice(0, toCopy), offset);
312
+ offset += toCopy;
313
+ counter++;
314
+ }
315
+
316
+ return output;
317
+ }
318
+
319
+ function randomBytes(length) {
320
+ const bytes = new Uint8Array(length);
321
+ crypto.getRandomValues(bytes);
322
+ return bytes;
323
+ }
324
+
325
+ async function aes256CbcEncrypt(plaintext, key, iv) {
326
+ const cryptoKey = await crypto.subtle.importKey(
327
+ 'raw', key, { name: 'AES-CBC' }, false, ['encrypt']
328
+ );
329
+ const ciphertext = await crypto.subtle.encrypt(
330
+ { name: 'AES-CBC', iv }, cryptoKey, plaintext
331
+ );
332
+ return new Uint8Array(ciphertext);
333
+ }
334
+
335
+ async function hmacSha256(key, data) {
336
+ const cryptoKey = await crypto.subtle.importKey(
337
+ 'raw',
338
+ key,
339
+ { name: 'HMAC', hash: { name: 'SHA-256' } },
340
+ false,
341
+ ['sign']
342
+ );
343
+ const mac = await crypto.subtle.sign('HMAC', cryptoKey, data);
344
+ return new Uint8Array(mac);
345
+ }
346
+
347
+ // ============================================
348
+ // ECIES ENCRYPTION (using @bitcoinerlab/secp256k1)
349
+ // ============================================
350
+
351
+ async function eciesEncrypt(plaintext, recipientPubKeys) {
352
+ // Neurai Core-compatible hybrid ECIES (see src/depinecies.cpp)
353
+ // - Ephemeral keypair per message
354
+ // - AES key derived from ephemeral privkey via KDF_SHA256
355
+ // - Payload: [IV(16) || ciphertext || HMAC_SHA256(aesKey, ciphertext)]
356
+ // - Per-recipient package: [recipientIV(16) || AES256_CBC(encKey, aesKey) || HMAC_SHA256(encKey, encryptedAESKey)]
357
+ // - encKey derived from ECDH secret (secp256k1_ecdh default), then KDF_SHA256
358
+
359
+ // Generate ephemeral key pair
360
+ const ephemeralPrivKey = randomBytes(32);
361
+ const ephemeralPubKey = secp256k1.pointFromScalar(ephemeralPrivKey, true);
362
+ if (!(ephemeralPubKey instanceof Uint8Array) || ephemeralPubKey.length !== 33) {
363
+ throw new Error('Failed to generate ephemeral public key');
364
+ }
365
+
366
+ // Derive AES key from ephemeral private key (matches KDF_SHA256 in C++)
367
+ const aesKey = await kdfSha256(ephemeralPrivKey, 32);
368
+
369
+ // Encrypt message with AES-256-CBC (PKCS7 padding)
370
+ const iv = randomBytes(16);
371
+ const ciphertext = await aes256CbcEncrypt(plaintext, aesKey, iv);
372
+
373
+ // HMAC over ciphertext only (matches C++)
374
+ const payloadHmac = await hmacSha256(aesKey, ciphertext);
375
+ const payload = concatBytes(iv, ciphertext, payloadHmac);
376
+
377
+ // For each recipient, encrypt the AES key
378
+ const recipientKeys = new Map();
379
+
380
+ for (const recipientPubKey of recipientPubKeys) {
381
+ if (!(recipientPubKey instanceof Uint8Array) || recipientPubKey.length !== 33) {
382
+ throw new Error('Recipient pubkey must be 33 bytes compressed');
383
+ }
384
+
385
+ // ECDH secret must match libsecp256k1's default: SHA256(compressed(shared_point))
386
+ // See src/secp256k1/src/modules/ecdh/main_impl.h
387
+ const sharedPointCompressed = secp256k1.pointMultiply(recipientPubKey, ephemeralPrivKey, true);
388
+ const sharedSecret = await sha256(sharedPointCompressed);
389
+
390
+ // Derive per-recipient encryption key
391
+ const encKey = await kdfSha256(sharedSecret, 32);
392
+
393
+ // Encrypt the AES key using AES-256-CBC with random per-recipient IV
394
+ const recipientIV = randomBytes(16);
395
+ const encryptedAESKey = await aes256CbcEncrypt(aesKey, encKey, recipientIV);
396
+
397
+ // HMAC over encrypted AES key
398
+ const recipientHmac = await hmacSha256(encKey, encryptedAESKey);
399
+
400
+ const recipientPackage = concatBytes(recipientIV, encryptedAESKey, recipientHmac);
401
+
402
+ // Map key is address hash160 (CKeyID): Hash160(serialized pubkey)
403
+ const keyHash = await hash160(recipientPubKey);
404
+ const keyHashHex = bytesToHex(keyHash);
405
+ recipientKeys.set(keyHashHex, recipientPackage);
406
+ }
407
+
408
+ return {
409
+ ephemeralPubKey,
410
+ encryptedPayload: payload,
411
+ recipientKeys
412
+ };
413
+ }
414
+
415
+ function serializeEciesMessage(msg) {
416
+ const parts = [];
417
+ parts.push(serializeVector(msg.ephemeralPubKey));
418
+
419
+ // CECIESEncryptedMessage serializes encryptedPayload as a vector (CompactSize + bytes)
420
+ parts.push(serializeVector(msg.encryptedPayload));
421
+
422
+ // recipientKeys is a std::map<uint160, vector<unsigned char>>
423
+ // Serialize as: CompactSize(count) + repeated (uint160 key raw 20 bytes) + (vector value)
424
+ const entries = Array.from(msg.recipientKeys.entries()).map(([hash160Hex, recipientPackage]) => {
425
+ const keyBytes = hexToBytes(hash160Hex);
426
+ if (keyBytes.length !== 20) throw new Error('recipient key hash160 must be 20 bytes');
427
+ return { keyBytes, recipientPackage };
428
+ });
429
+
430
+ // Deterministic order (helps reproducibility; C++ map is ordered)
431
+ entries.sort((a, b) => {
432
+ for (let i = 0; i < 20; i++) {
433
+ if (a.keyBytes[i] !== b.keyBytes[i]) return a.keyBytes[i] - b.keyBytes[i];
434
+ }
435
+ return 0;
436
+ });
437
+
438
+ parts.push(writeCompactSize(entries.length));
439
+ for (const { keyBytes, recipientPackage } of entries) {
440
+ parts.push(keyBytes);
441
+ parts.push(serializeVector(recipientPackage));
442
+ }
443
+
444
+ return concatBytes(...parts);
445
+ }
446
+
447
+ // ============================================
448
+ // MAIN API
449
+ // ============================================
450
+
451
+ async function buildDepinMessage(params) {
452
+ // Validate
453
+ if (!params.token) throw new Error('Token is required');
454
+ if (!params.senderAddress) throw new Error('Sender address is required');
455
+ if (!params.senderPubKey || params.senderPubKey.length !== 66) {
456
+ throw new Error('Sender public key must be 66 hex characters');
457
+ }
458
+
459
+ // Handle private key - can be WIF or hex
460
+ let privateKeyHex = params.privateKey;
461
+ if (!privateKeyHex) {
462
+ throw new Error('Private key is required');
463
+ }
464
+
465
+ // Auto-detect and convert WIF to hex
466
+ if (isWIF(privateKeyHex)) {
467
+ console.log('Detected WIF format, converting to hex...');
468
+ privateKeyHex = await wifToHex(privateKeyHex);
469
+ console.log('Private key converted successfully');
470
+ }
471
+
472
+ if (privateKeyHex.length !== 64) {
473
+ throw new Error('Private key must be 64 hex characters (or WIF format)');
474
+ }
475
+
476
+ if (!params.message) throw new Error('Message is required');
477
+ if (!params.recipientPubKeys || params.recipientPubKeys.length === 0) {
478
+ throw new Error('At least one recipient is required');
479
+ }
480
+ if (!params.timestamp || params.timestamp <= 0) {
481
+ throw new Error('Timestamp must be positive');
482
+ }
483
+
484
+ // Convert to bytes
485
+ const privateKey = hexToBytes(privateKeyHex);
486
+ const senderPubKey = hexToBytes(params.senderPubKey);
487
+
488
+ // Parse recipients
489
+ const recipientPubKeys = params.recipientPubKeys.map(pk => {
490
+ if (pk.length !== 66) throw new Error('Recipient pubkey must be 66 hex chars');
491
+ return hexToBytes(pk);
492
+ });
493
+
494
+ // Include sender so they can decrypt their own messages
495
+ const senderHex = params.senderPubKey.toLowerCase();
496
+ if (!params.recipientPubKeys.some(pk => pk.toLowerCase() === senderHex)) {
497
+ recipientPubKeys.push(senderPubKey);
498
+ }
499
+
500
+ // Encode message
501
+ const encoder = new TextEncoder();
502
+ const messageBytes = encoder.encode(params.message);
503
+
504
+ // ECIES encrypt
505
+ const eciesMsg = await eciesEncrypt(messageBytes, recipientPubKeys);
506
+ const encryptedPayload = serializeEciesMessage(eciesMsg);
507
+
508
+ // Build hash data for signing
509
+ const hashData = concatBytes(
510
+ serializeString(params.token),
511
+ serializeString(params.senderAddress),
512
+ serializeInt64(params.timestamp),
513
+ serializeVector(encryptedPayload)
514
+ );
515
+
516
+ // Neurai Core uses CHashWriter (SER_GETHASH), which is double-SHA256.
517
+ // This MUST match VerifyDepinMessageSignature in src/depinmsgpool.cpp.
518
+ const messageHashBytes = await doubleSha256(hashData);
519
+ // Neurai (like Bitcoin) typically displays uint256 hashes byte-reversed.
520
+ // This makes `messageHash` match what you'll see in debug.log.
521
+ const messageHash = bytesToHex(messageHashBytes.slice().reverse());
522
+
523
+ // Sign with secp256k1
524
+ const sigResult = secp256k1.sign(messageHashBytes, privateKey);
525
+
526
+ // Convert to DER format if needed
527
+ let signature;
528
+ if (sigResult instanceof Uint8Array) {
529
+ // Check if it's raw (64 bytes) or already DER (70-72 bytes typically)
530
+ if (sigResult.length === 64) {
531
+ // Raw signature - convert to DER
532
+ signature = encodeDER(sigResult);
533
+ } else {
534
+ // Assume already DER
535
+ signature = sigResult;
536
+ }
537
+ } else if (typeof sigResult === 'object' && sigResult.toDER) {
538
+ // Some libraries provide toDER method
539
+ signature = sigResult.toDER();
540
+ } else if (typeof sigResult === 'object' && sigResult.signature) {
541
+ // Some libraries return { signature, recovery }
542
+ if (sigResult.signature.length === 64) {
543
+ signature = encodeDER(sigResult.signature);
544
+ } else {
545
+ signature = sigResult.signature;
546
+ }
547
+ } else {
548
+ throw new Error('Unknown signature format from secp256k1.sign()');
549
+ }
550
+
551
+ // Serialize complete message
552
+ const serialized = concatBytes(
553
+ serializeString(params.token),
554
+ serializeString(params.senderAddress),
555
+ serializeInt64(params.timestamp),
556
+ serializeVector(signature),
557
+ serializeVector(encryptedPayload)
558
+ );
559
+
560
+ return {
561
+ hex: bytesToHex(serialized),
562
+ messageHash,
563
+ messageHashBytes: bytesToHex(messageHashBytes),
564
+ encryptedSize: encryptedPayload.length,
565
+ recipientCount: recipientPubKeys.length
566
+ };
567
+ }
568
+
569
+ // Export for browser (IIFE global)
570
+ export {
571
+ buildDepinMessage,
572
+ wifToHex,
573
+ isWIF,
574
+ hexToBytes,
575
+ bytesToHex,
576
+ sha256,
577
+ doubleSha256,
578
+ hash160,
579
+ base58Decode
580
+ };
581
+
582
+ // Expose on globalThis (browser + Node) for the IIFE bundle.
583
+ if (typeof globalThis !== 'undefined') {
584
+ globalThis.neuraiDepinMsg = {
585
+ buildDepinMessage,
586
+ wifToHex,
587
+ isWIF,
588
+ utils: {
589
+ hexToBytes,
590
+ bytesToHex,
591
+ sha256,
592
+ doubleSha256,
593
+ hash160,
594
+ base58Decode
595
+ }
596
+ };
597
+ }