gun-eth 1.4.26 → 1.4.28

Sign up to get free protection for your applications and to get access to all the features.
package/src/index.js ADDED
@@ -0,0 +1,962 @@
1
+ // =============================================
2
+ // IMPORTS AND GLOBAL VARIABLES
3
+ // =============================================
4
+ import Gun from "gun";
5
+ import SEA from "gun/sea.js";
6
+ import {
7
+ ethers,
8
+ JsonRpcProvider,
9
+ BrowserProvider,
10
+ Contract,
11
+ Wallet,
12
+ getBytes,
13
+ concat,
14
+ toUtf8Bytes,
15
+ keccak256,
16
+ verifyMessage,
17
+ randomBytes,
18
+ hexlify
19
+ } from "ethers/lib/ethers.js";
20
+ import {
21
+ PROOF_OF_INTEGRITY_ABI,
22
+ STEALTH_ANNOUNCER_ABI,
23
+ getAddressesForChain,
24
+ isLocalEnvironment,
25
+ CHAIN_CONFIG
26
+ } from "./abis/abis.js";
27
+ import { LOCAL_CONFIG } from "./config/local.js";
28
+
29
+ let PROOF_CONTRACT_ADDRESS;
30
+ let rpcUrl = "";
31
+ let privateKey = "";
32
+
33
+ export const MESSAGE_TO_SIGN = "Access GunDB with Ethereum";
34
+
35
+ let contractAddresses = {
36
+ PROOF_OF_INTEGRITY_ADDRESS: CHAIN_CONFIG.optimismSepolia.PROOF_OF_INTEGRITY_ADDRESS,
37
+ STEALTH_ANNOUNCER_ADDRESS: CHAIN_CONFIG.optimismSepolia.STEALTH_ANNOUNCER_ADDRESS
38
+ };
39
+
40
+ // Solo per Node.js
41
+ if (typeof window === 'undefined') {
42
+ const { fileURLToPath } = require('url');
43
+ const { dirname } = require('path');
44
+ const { readFileSync } = require('fs');
45
+ const path = require('path');
46
+
47
+ const __filename = fileURLToPath(import.meta.url);
48
+ const __dirname = dirname(__filename);
49
+
50
+ try {
51
+ const rawdata = readFileSync(path.join(__dirname, 'contract-address.json'), 'utf8');
52
+ contractAddresses = JSON.parse(rawdata);
53
+ console.log('Loaded contract addresses:', contractAddresses);
54
+ } catch (err) {
55
+ console.warn('Warning: contract-address.json not found or invalid');
56
+ }
57
+ }
58
+
59
+ // =============================================
60
+ // UTILITY FUNCTIONS
61
+ // =============================================
62
+ /**
63
+ * Generates a random node ID for GunDB
64
+ * @returns {string} A random hexadecimal string
65
+ */
66
+ export function generateRandomId() {
67
+ const bytes = randomBytes(32);
68
+ return hexlify(bytes).slice(2);
69
+ }
70
+
71
+ /**
72
+ * Generates a password from a signature.
73
+ * @param {string} signature - The signature to derive the password from.
74
+ * @returns {string|null} The generated password or null if generation fails.
75
+ */
76
+ export function generatePassword(signature) {
77
+ try {
78
+ const signatureBytes = getBytes(signature);
79
+ const hash = keccak256(signatureBytes);
80
+ console.log("Generated password:", hash);
81
+ return hash;
82
+ } catch (error) {
83
+ console.error("Error generating password:", error);
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Converts a Gun private key to an Ethereum account.
90
+ * @param {string} gunPrivateKey - The Gun private key in base64url format.
91
+ * @returns {Object} An object containing the Ethereum account and public key.
92
+ */
93
+ export function gunToEthAccount(gunPrivateKey) {
94
+ // Function to convert base64url to hex
95
+ const base64UrlToHex = (base64url) => {
96
+ const padding = "=".repeat((4 - (base64url.length % 4)) % 4);
97
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/") + padding;
98
+ const binary = atob(base64);
99
+ return Array.from(binary, (char) =>
100
+ char.charCodeAt(0).toString(16).padStart(2, "0")
101
+ ).join("");
102
+ };
103
+
104
+ // Convert Gun private key to hex format
105
+ const hexPrivateKey = "0x" + base64UrlToHex(gunPrivateKey);
106
+
107
+ // Create an Ethereum wallet from the private key
108
+ const wallet = new ethers.Wallet(hexPrivateKey);
109
+
110
+ // Get the public address (public key)
111
+ const publicKey = wallet.address;
112
+
113
+ return {
114
+ account: wallet,
115
+ publicKey: publicKey,
116
+ privateKey: hexPrivateKey,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Gets an Ethereum signer based on current configuration
122
+ * @returns {Promise<ethers.Signer>} The configured signer
123
+ * @throws {Error} If no valid provider is found
124
+ */
125
+ export const getSigner = async () => {
126
+ if (rpcUrl && privateKey) {
127
+ const provider = new JsonRpcProvider(rpcUrl, {
128
+ chainId: LOCAL_CONFIG.CHAIN_ID,
129
+ name: "localhost"
130
+ });
131
+ return new Wallet(privateKey, provider);
132
+ } else if (
133
+ typeof window !== "undefined" &&
134
+ typeof window.ethereum !== "undefined"
135
+ ) {
136
+ await window.ethereum.request({ method: "eth_requestAccounts" });
137
+ const provider = new BrowserProvider(window.ethereum);
138
+ return provider.getSigner();
139
+ } else {
140
+ throw new Error("No valid Ethereum provider found");
141
+ }
142
+ };
143
+
144
+ /**
145
+ * Utility function to generate stealth address
146
+ * @param {string} sharedSecret - The shared secret
147
+ * @param {string} spendingPublicKey - The spending public key
148
+ * @returns {Object} The stealth address and private key
149
+ */
150
+ export function deriveStealthAddress(sharedSecret, spendingPublicKey) {
151
+ try {
152
+ const sharedSecretBytes = getBytes(sharedSecret);
153
+ const spendingPublicKeyBytes = getBytes(spendingPublicKey);
154
+
155
+ const stealthPrivateKey = keccak256(
156
+ concat([
157
+ sharedSecretBytes,
158
+ spendingPublicKeyBytes
159
+ ])
160
+ );
161
+
162
+ const stealthWallet = new Wallet(stealthPrivateKey);
163
+
164
+ console.log("Debug deriveStealthAddress:", {
165
+ sharedSecretHex: stealthPrivateKey,
166
+ spendingPublicKey,
167
+ stealthPrivateKey,
168
+ stealthAddress: stealthWallet.address
169
+ });
170
+
171
+ return {
172
+ stealthPrivateKey,
173
+ stealthAddress: stealthWallet.address,
174
+ wallet: stealthWallet
175
+ };
176
+ } catch (error) {
177
+ console.error("Error in deriveStealthAddress:", error);
178
+ throw error;
179
+ }
180
+ }
181
+
182
+ // =============================================
183
+ // BASIC GUN-ETH CHAIN METHODS
184
+ // =============================================
185
+
186
+ // Set the message to sign
187
+ Gun.chain.MESSAGE_TO_SIGN = MESSAGE_TO_SIGN;
188
+
189
+ /**
190
+ * Sets standalone configuration for Gun.
191
+ * @param {string} newRpcUrl - The new RPC URL.
192
+ * @param {string} newPrivateKey - The new private key.
193
+ * @returns {Gun} The Gun instance for chaining.
194
+ */
195
+ Gun.chain.setSigner = function (newRpcUrl, newPrivateKey) {
196
+ rpcUrl = newRpcUrl;
197
+ privateKey = newPrivateKey;
198
+ console.log("Standalone configuration set");
199
+ return this;
200
+ };
201
+
202
+ Gun.chain.getSigner = getSigner();
203
+
204
+ /**
205
+ * Verifies an Ethereum signature.
206
+ * @param {string} message - The original message that was signed.
207
+ * @param {string} signature - The signature to verify.
208
+ * @returns {Promise<string|null>} The recovered address or null if verification fails.
209
+ */
210
+ Gun.chain.verifySignature = async function (message, signature) {
211
+ try {
212
+ const recoveredAddress = verifyMessage(message, signature);
213
+ return recoveredAddress;
214
+ } catch (error) {
215
+ console.error("Error verifying signature:", error);
216
+ return null;
217
+ }
218
+ };
219
+
220
+ /**
221
+ * Generates a password from a signature.
222
+ * @param {string} signature - The signature to derive the password from.
223
+ * @returns {string|null} The generated password or null if generation fails.
224
+ */
225
+ Gun.chain.generatePassword = function (signature) {
226
+ return generatePassword(signature);
227
+ };
228
+
229
+ /**
230
+ * Creates an Ethereum signature for a given message.
231
+ * @param {string} message - The message to sign.
232
+ * @returns {Promise<string|null>} The signature or null if signing fails.
233
+ */
234
+ Gun.chain.createSignature = async function (message) {
235
+ try {
236
+ // Check if message matches MESSAGE_TO_SIGN
237
+ if (message !== MESSAGE_TO_SIGN) {
238
+ throw new Error("Invalid message, valid message is: " + MESSAGE_TO_SIGN);
239
+ }
240
+ const signer = await getSigner();
241
+ const signature = await signer.signMessage(message);
242
+ console.log("Signature created:", signature);
243
+ return signature;
244
+ } catch (error) {
245
+ console.error("Error creating signature:", error);
246
+ return null;
247
+ }
248
+ };
249
+
250
+ // =============================================
251
+ // KEY PAIR MANAGEMENT
252
+ // =============================================
253
+ /**
254
+ * Creates and stores an encrypted key pair for a given address.
255
+ * @param {string} address - The Ethereum address to associate with the key pair.
256
+ * @param {string} signature - The signature to use for encryption.
257
+ * @returns {Promise<void>}
258
+ */
259
+ Gun.chain.createAndStoreEncryptedPair = async function (address, signature) {
260
+ try {
261
+ const gun = this;
262
+ const pair = await SEA.pair();
263
+ const v_pair = await SEA.pair();
264
+ const s_pair = await SEA.pair();
265
+ const password = generatePassword(signature);
266
+
267
+ // Save original SEA pairs
268
+ const encryptedPair = await SEA.encrypt(JSON.stringify(pair), password);
269
+ const encryptedV_pair = await SEA.encrypt(JSON.stringify(v_pair), password);
270
+ const encryptedS_pair = await SEA.encrypt(JSON.stringify(s_pair), password);
271
+
272
+ // Convert only to get Ethereum addresses
273
+ const viewingAccount = gunToEthAccount(v_pair.priv);
274
+ const spendingAccount = gunToEthAccount(s_pair.priv);
275
+
276
+ gun.get("gun-eth").get("users").get(address).put({
277
+ pair: encryptedPair,
278
+ v_pair: encryptedV_pair,
279
+ s_pair: encryptedS_pair,
280
+ publicKeys: {
281
+ viewingPublicKey: v_pair.epub, // Use SEA encryption public key
282
+ viewingPublicKey: v_pair.epub, // Use SEA encryption public key
283
+ spendingPublicKey: spendingAccount.publicKey, // Use Ethereum address
284
+ ethViewingAddress: viewingAccount.publicKey // Also save Ethereum address
285
+ }
286
+ });
287
+
288
+ console.log("Encrypted pairs and public keys stored for:", address);
289
+ } catch (error) {
290
+ console.error("Error creating and storing encrypted pair:", error);
291
+ throw error;
292
+ }
293
+ };
294
+
295
+ /**
296
+ * Retrieves and decrypts a stored key pair for a given address.
297
+ * @param {string} address - The Ethereum address associated with the key pair.
298
+ * @param {string} signature - The signature to use for decryption.
299
+ * @returns {Promise<Object|null>} The decrypted key pair or null if retrieval fails.
300
+ */
301
+ Gun.chain.getAndDecryptPair = async function (address, signature) {
302
+ try {
303
+ const gun = this;
304
+ const encryptedData = await gun
305
+ .get("gun-eth")
306
+ .get("users")
307
+ .get(address)
308
+ .get("pair")
309
+ .then();
310
+ if (!encryptedData) {
311
+ throw new Error("No encrypted data found for this address");
312
+ }
313
+ const password = generatePassword(signature);
314
+ const decryptedPair = await SEA.decrypt(encryptedData, password);
315
+ console.log(decryptedPair);
316
+ return decryptedPair;
317
+ } catch (error) {
318
+ console.error("Error retrieving and decrypting pair:", error);
319
+ return null;
320
+ }
321
+ };
322
+
323
+ // =============================================
324
+ // PROOF OF INTEGRITY
325
+ // =============================================
326
+ /**
327
+ * Proof of Integrity
328
+ * @param {string} chain - The blockchain to use (e.g., "optimismSepolia").
329
+ * @param {string} nodeId - The ID of the node to verify or write.
330
+ * @param {Object} data - The data to write (if writing).
331
+ * @param {Function} callback - Callback function to handle the result.
332
+ * @returns {Gun} The Gun instance for chaining.
333
+ */
334
+ Gun.chain.proof = function (chain, nodeId, data, callback) {
335
+ console.log("Proof plugin called with:", { chain, nodeId, data });
336
+
337
+ if (typeof callback !== "function") {
338
+ console.error("Callback must be a function");
339
+ return this;
340
+ }
341
+
342
+ try {
343
+ // Se siamo in localhost e in development, usa automaticamente la chain locale
344
+ const targetChain = isLocalEnvironment() ? 'localhost' : chain;
345
+ const chainConfig = getAddressesForChain(targetChain);
346
+
347
+ console.log(`Using ${targetChain} configuration:`, chainConfig);
348
+
349
+ // Usa gli indirizzi dalla configurazione
350
+ const contract = new Contract(
351
+ chainConfig.PROOF_OF_INTEGRITY_ADDRESS,
352
+ PROOF_OF_INTEGRITY_ABI,
353
+ signer
354
+ );
355
+
356
+ // Funzione per verificare on-chain
357
+ const verifyOnChain = async (nodeId, contentHash) => {
358
+ console.log("Verifying on chain:", { nodeId, contentHash });
359
+ const signer = await getSigner();
360
+ const contract = new Contract(
361
+ PROOF_CONTRACT_ADDRESS,
362
+ PROOF_OF_INTEGRITY_ABI,
363
+ signer
364
+ );
365
+ const [isValid, timestamp, updater] = await contract.verifyData(
366
+ toUtf8Bytes(nodeId),
367
+ contentHash
368
+ );
369
+ console.log("Verification result:", { isValid, timestamp, updater });
370
+ return { isValid, timestamp, updater };
371
+ };
372
+
373
+ // Funzione per scrivere on-chain
374
+ const writeOnChain = async (nodeId, contentHash) => {
375
+ console.log("Writing on chain:", { nodeId, contentHash });
376
+ const signer = await getSigner();
377
+ const contract = new Contract(
378
+ PROOF_CONTRACT_ADDRESS,
379
+ PROOF_OF_INTEGRITY_ABI,
380
+ signer
381
+ );
382
+ const tx = await contract.updateData(
383
+ toUtf8Bytes(nodeId),
384
+ contentHash
385
+ );
386
+ console.log("Transaction sent:", tx.hash);
387
+ const receipt = await tx.wait();
388
+ console.log("Transaction confirmed:", receipt);
389
+ return tx;
390
+ };
391
+
392
+ // Funzione per ottenere l'ultimo record
393
+ const getLatestRecord = async (nodeId) => {
394
+ const signer = await getSigner();
395
+ const contract = new Contract(
396
+ PROOF_CONTRACT_ADDRESS,
397
+ PROOF_OF_INTEGRITY_ABI,
398
+ signer
399
+ );
400
+ const [contentHash, timestamp, updater] = await contract.getLatestRecord(
401
+ toUtf8Bytes(nodeId)
402
+ );
403
+ console.log("Latest record from blockchain:", {
404
+ nodeId,
405
+ contentHash,
406
+ timestamp,
407
+ updater,
408
+ });
409
+ return { contentHash, timestamp, updater };
410
+ };
411
+
412
+
413
+ if (nodeId && !data) {
414
+ // Case 1: User passes only node
415
+ gun.get(nodeId).once(async (existingData) => {
416
+ if (!existingData) {
417
+ if (callback) callback({ err: "Node not found in GunDB" });
418
+ return;
419
+ }
420
+
421
+ console.log("existingData", existingData);
422
+
423
+ // Use stored contentHash instead of recalculating
424
+ const contentHash = existingData._contentHash;
425
+ console.log("contentHash", contentHash);
426
+
427
+ if (!contentHash) {
428
+ if (callback) callback({ err: "No content hash found for this node" });
429
+ return;
430
+ }
431
+
432
+ try {
433
+ const { isValid, timestamp, updater } = await verifyOnChain(
434
+ nodeId,
435
+ contentHash
436
+ );
437
+ const latestRecord = await getLatestRecord(nodeId);
438
+
439
+ if (isValid) {
440
+ if (callback)
441
+ callback({
442
+ ok: true,
443
+ message: "Data verified on blockchain",
444
+ timestamp,
445
+ updater,
446
+ latestRecord,
447
+ });
448
+ } else {
449
+ if (callback)
450
+ callback({
451
+ ok: false,
452
+ message: "Data not verified on blockchain",
453
+ latestRecord,
454
+ });
455
+ }
456
+ } catch (error) {
457
+ if (callback) callback({ err: error.message });
458
+ }
459
+ });
460
+ } else if (data && !nodeId) {
461
+ // Case 2: User passes only text (data)
462
+ const newNodeId = generateRandomId();
463
+ const dataString = JSON.stringify(data);
464
+ const contentHash = keccak256(toUtf8Bytes(dataString));
465
+
466
+ gun
467
+ .get(newNodeId)
468
+ .put({ ...data, _contentHash: contentHash }, async (ack) => {
469
+ console.log("ack", ack);
470
+ if (ack.err) {
471
+ if (callback) callback({ err: "Error saving data to GunDB" });
472
+ return;
473
+ }
474
+
475
+ try {
476
+ const tx = await writeOnChain(newNodeId, contentHash);
477
+ if (callback)
478
+ callback({
479
+ ok: true,
480
+ message: "Data written to GunDB and blockchain",
481
+ nodeId: newNodeId,
482
+ txHash: tx.hash,
483
+ });
484
+ } catch (error) {
485
+ if (callback) callback({ err: error.message });
486
+ }
487
+ });
488
+ } else {
489
+ if (callback)
490
+ callback({
491
+ err: "Invalid input. Provide either nodeId or data, not both.",
492
+ });
493
+ }
494
+
495
+ return gun;
496
+ } catch (error) {
497
+ callback({ err: error.message });
498
+ return this;
499
+ }
500
+ };
501
+
502
+ // =============================================
503
+ // STEALTH ADDRESS CORE FUNCTIONS
504
+ // =============================================
505
+ /**
506
+ * Converts a Gun private key to an Ethereum account.
507
+ * @param {string} gunPrivateKey - The Gun private key in base64url format.
508
+ * @returns {Object} An object containing the Ethereum account and public key.
509
+ */
510
+ Gun.chain.gunToEthAccount = function (gunPrivateKey) {
511
+ return gunToEthAccount(gunPrivateKey);
512
+ };
513
+
514
+ /**
515
+ * Generate a stealth key and related key pairs
516
+ * @param {string} recipientAddress - The recipient's Ethereum address
517
+ * @param {string} signature - The sender's signature to access their keys
518
+ * @returns {Promise<Object>} Object containing stealth addresses and keys
519
+ */
520
+ Gun.chain.generateStealthAddress = async function (recipientAddress, signature) {
521
+ try {
522
+ const gun = this;
523
+
524
+ // Get recipient's public keys
525
+ const recipientData = await gun
526
+ .get("gun-eth")
527
+ .get("users")
528
+ .get(recipientAddress)
529
+ .get("publicKeys")
530
+ .then();
531
+
532
+ if (!recipientData || !recipientData.viewingPublicKey || !recipientData.spendingPublicKey) {
533
+ throw new Error("Recipient's public keys not found");
534
+ }
535
+
536
+ // Get sender's keys
537
+ const senderAddress = await this.verifySignature(MESSAGE_TO_SIGN, signature);
538
+ const password = generatePassword(signature);
539
+
540
+ const senderData = await gun
541
+ .get("gun-eth")
542
+ .get("users")
543
+ .get(senderAddress)
544
+ .then();
545
+
546
+ if (!senderData || !senderData.s_pair) {
547
+ throw new Error("Sender's keys not found");
548
+ }
549
+
550
+ // Decrypt sender's spending pair
551
+ let spendingKeyPair;
552
+ try {
553
+ const decryptedData = await SEA.decrypt(senderData.s_pair, password);
554
+ spendingKeyPair = typeof decryptedData === 'string' ?
555
+ JSON.parse(decryptedData) :
556
+ decryptedData;
557
+ } catch (error) {
558
+ console.error("Error decrypting spending pair:", error);
559
+ throw new Error("Unable to decrypt spending pair");
560
+ }
561
+
562
+ // Generate shared secret using SEA ECDH with encryption public key
563
+ const sharedSecret = await SEA.secret(recipientData.viewingPublicKey, spendingKeyPair);
564
+
565
+ if (!sharedSecret) {
566
+ throw new Error("Unable to generate shared secret");
567
+ }
568
+
569
+ console.log("Generate shared secret:", sharedSecret);
570
+
571
+ const { stealthAddress } = deriveStealthAddress(
572
+ sharedSecret,
573
+ recipientData.spendingPublicKey
574
+ );
575
+
576
+ return {
577
+ stealthAddress,
578
+ senderPublicKey: spendingKeyPair.epub, // Use encryption public key
579
+ spendingPublicKey: recipientData.spendingPublicKey
580
+ };
581
+
582
+ } catch (error) {
583
+ console.error("Error generating stealth address:", error);
584
+ throw error;
585
+ }
586
+ };
587
+
588
+ /**
589
+ * Publish public keys needed to receive stealth payments
590
+ * @param {string} signature - The signature to authenticate the user
591
+ * @returns {Promise<void>}
592
+ */
593
+ Gun.chain.publishStealthKeys = async function (signature) {
594
+ try {
595
+ const gun = this;
596
+ const address = await this.verifySignature(MESSAGE_TO_SIGN, signature);
597
+ const password = generatePassword(signature);
598
+
599
+ // Get encrypted key pairs
600
+ const encryptedData = await gun
601
+ .get("gun-eth")
602
+ .get("users")
603
+ .get(address)
604
+ .then();
605
+
606
+ if (!encryptedData || !encryptedData.v_pair || !encryptedData.s_pair) {
607
+ throw new Error("Keys not found");
608
+ }
609
+
610
+ // Decrypt viewing and spending pairs
611
+ const viewingKeyPair = JSON.parse(
612
+ await SEA.decrypt(encryptedData.v_pair, password)
613
+ );
614
+ const spendingKeyPair = JSON.parse(
615
+ await SEA.decrypt(encryptedData.s_pair, password)
616
+ );
617
+
618
+ const viewingAccount = gunToEthAccount(viewingKeyPair.priv);
619
+ const spendingAccount = gunToEthAccount(spendingKeyPair.priv);
620
+
621
+ // Publish only public keys
622
+ gun.get("gun-eth").get("users").get(address).get("publicKeys").put({
623
+ viewingPublicKey: viewingAccount.publicKey,
624
+ spendingPublicKey: spendingAccount.publicKey,
625
+ });
626
+
627
+ console.log("Stealth public keys published successfully");
628
+ } catch (error) {
629
+ console.error("Error publishing stealth keys:", error);
630
+ throw error;
631
+ }
632
+ };
633
+
634
+ // =============================================
635
+ // STEALTH PAYMENT FUNCTIONS
636
+ // =============================================
637
+ /**
638
+ * Recover funds from a stealth address
639
+ * @param {string} stealthAddress - The stealth address to recover funds from
640
+ * @param {string} senderPublicKey - The sender's public key used to generate the address
641
+ * @param {string} signature - The signature to decrypt private keys
642
+ * @returns {Promise<Object>} Object containing wallet to access funds
643
+ */
644
+ Gun.chain.recoverStealthFunds = async function (
645
+ stealthAddress,
646
+ senderPublicKey,
647
+ signature,
648
+ spendingPublicKey
649
+ ) {
650
+ try {
651
+ const gun = this;
652
+ const password = generatePassword(signature);
653
+
654
+ // Get own key pairs
655
+ const myAddress = await this.verifySignature(MESSAGE_TO_SIGN, signature);
656
+ const encryptedData = await gun
657
+ .get("gun-eth")
658
+ .get("users")
659
+ .get(myAddress)
660
+ .then();
661
+
662
+ if (!encryptedData || !encryptedData.v_pair || !encryptedData.s_pair) {
663
+ throw new Error("Keys not found");
664
+ }
665
+
666
+ // Decrypt viewing and spending pairs
667
+ let viewingKeyPair;
668
+ try {
669
+ const decryptedViewingData = await SEA.decrypt(encryptedData.v_pair, password);
670
+ viewingKeyPair = typeof decryptedViewingData === 'string' ?
671
+ JSON.parse(decryptedViewingData) :
672
+ decryptedViewingData;
673
+ } catch (error) {
674
+ console.error("Error decrypting keys:", error);
675
+ throw new Error("Unable to decrypt keys");
676
+ }
677
+
678
+ // Generate shared secret using SEA ECDH
679
+ const sharedSecret = await SEA.secret(senderPublicKey, viewingKeyPair);
680
+
681
+ if (!sharedSecret) {
682
+ throw new Error("Unable to generate shared secret");
683
+ }
684
+
685
+ console.log("Recover shared secret:", sharedSecret);
686
+
687
+ const { wallet, stealthAddress: recoveredAddress } = deriveStealthAddress(
688
+ sharedSecret,
689
+ spendingPublicKey
690
+ );
691
+
692
+ // Verify address matches
693
+ if (recoveredAddress.toLowerCase() !== stealthAddress.toLowerCase()) {
694
+ console.error("Mismatch:", {
695
+ recovered: recoveredAddress,
696
+ expected: stealthAddress,
697
+ sharedSecret
698
+ });
699
+ throw new Error("Recovered stealth address does not match");
700
+ }
701
+
702
+ return {
703
+ wallet,
704
+ address: recoveredAddress,
705
+ };
706
+ } catch (error) {
707
+ console.error("Error recovering stealth funds:", error);
708
+ throw error;
709
+ }
710
+ };
711
+
712
+ /**
713
+ * Announce a stealth payment
714
+ * @param {string} stealthAddress - The generated stealth address
715
+ * @param {string} senderPublicKey - The sender's public key
716
+ * @param {string} spendingPublicKey - The spending public key
717
+ * @param {string} signature - The sender's signature
718
+ * @returns {Promise<void>}
719
+ */
720
+ Gun.chain.announceStealthPayment = async function (
721
+ stealthAddress,
722
+ senderPublicKey,
723
+ spendingPublicKey,
724
+ signature,
725
+ options = { onChain: false, chain: 'optimismSepolia' }
726
+ ) {
727
+ try {
728
+ const gun = this;
729
+ const senderAddress = await this.verifySignature(MESSAGE_TO_SIGN, signature);
730
+
731
+ if (options.onChain) {
732
+ // On-chain announcement
733
+ const signer = await getSigner();
734
+ const chainConfig = getAddressesForChain(options.chain);
735
+ const contractAddress = chainConfig.STEALTH_ANNOUNCER_ADDRESS;
736
+
737
+ console.log("Using contract address:", contractAddress);
738
+
739
+ const contract = new Contract(
740
+ contractAddress,
741
+ STEALTH_ANNOUNCER_ABI,
742
+ signer
743
+ );
744
+
745
+ // Get dev fee from contract
746
+ const devFee = await contract.devFee();
747
+ console.log("Dev fee:", devFee.toString());
748
+
749
+ // Call contract
750
+ const tx = await contract.announcePayment(
751
+ senderPublicKey,
752
+ spendingPublicKey,
753
+ stealthAddress,
754
+ { value: devFee }
755
+ );
756
+
757
+ console.log("Transaction sent:", tx.hash);
758
+ const receipt = await tx.wait();
759
+ console.log("Transaction confirmed:", receipt.hash);
760
+
761
+ console.log("Stealth payment announced on-chain (dev fee paid)");
762
+ } else {
763
+ // Off-chain announcement (GunDB)
764
+ gun
765
+ .get("gun-eth")
766
+ .get("stealth-payments")
767
+ .set({
768
+ stealthAddress,
769
+ senderAddress,
770
+ senderPublicKey,
771
+ spendingPublicKey,
772
+ timestamp: Date.now(),
773
+ });
774
+ console.log("Stealth payment announced off-chain");
775
+ }
776
+ } catch (error) {
777
+ console.error("Error announcing stealth payment:", error);
778
+ console.error("Error details:", error.stack);
779
+ throw error;
780
+ }
781
+ };
782
+
783
+ /**
784
+ * Get all stealth payments for an address
785
+ * @param {string} signature - The signature to authenticate the user
786
+ * @returns {Promise<Array>} List of stealth payments
787
+ */
788
+ Gun.chain.getStealthPayments = async function (signature, options = { source: 'both' }) {
789
+ try {
790
+ const payments = [];
791
+
792
+ if (options.source === 'onChain' || options.source === 'both') {
793
+ const signer = await getSigner();
794
+ const chainConfig = getAddressesForChain(options.chain || 'optimismSepolia');
795
+ const contractAddress = chainConfig.STEALTH_ANNOUNCER_ADDRESS;
796
+
797
+ const contract = new Contract(
798
+ contractAddress,
799
+ STEALTH_ANNOUNCER_ABI,
800
+ signer
801
+ );
802
+
803
+ try {
804
+ // Get total number of announcements
805
+ const totalAnnouncements = await contract.getAnnouncementsCount();
806
+ const totalCount = Number(totalAnnouncements.toString());
807
+ console.log("Total on-chain announcements:", totalCount);
808
+
809
+ if (totalCount > 0) {
810
+ // Get announcements in batches of 100
811
+ const batchSize = 100;
812
+ const lastIndex = totalCount - 1;
813
+
814
+ for(let i = 0; i <= lastIndex; i += batchSize) {
815
+ const toIndex = Math.min(i + batchSize - 1, lastIndex);
816
+ const batch = await contract.getAnnouncementsInRange(i, toIndex);
817
+
818
+ // For each announcement, try to decrypt
819
+ for(const announcement of batch) {
820
+ try {
821
+ // Verify announcement is valid
822
+ if (!announcement || !announcement.stealthAddress ||
823
+ !announcement.senderPublicKey || !announcement.spendingPublicKey) {
824
+ console.log("Invalid announcement:", announcement);
825
+ continue;
826
+ }
827
+
828
+ // Try to recover funds to verify if announcement is for us
829
+ const recoveredWallet = await this.recoverStealthFunds(
830
+ announcement.stealthAddress,
831
+ announcement.senderPublicKey,
832
+ signature,
833
+ announcement.spendingPublicKey
834
+ );
835
+
836
+ // If no errors thrown, announcement is for us
837
+ payments.push({
838
+ stealthAddress: announcement.stealthAddress,
839
+ senderPublicKey: announcement.senderPublicKey,
840
+ spendingPublicKey: announcement.spendingPublicKey,
841
+ timestamp: Number(announcement.timestamp),
842
+ source: 'onChain',
843
+ wallet: recoveredWallet
844
+ });
845
+
846
+ } catch (e) {
847
+ // Not for us, continue
848
+ console.log(`Announcement not for us: ${announcement.stealthAddress}`);
849
+ continue;
850
+ }
851
+ }
852
+ }
853
+ }
854
+ } catch (error) {
855
+ console.error("Error retrieving on-chain announcements:", error);
856
+ }
857
+ }
858
+
859
+ if (options.source === 'offChain' || options.source === 'both') {
860
+ // Get off-chain payments
861
+ const gun = this;
862
+ const offChainPayments = await new Promise((resolve) => {
863
+ const p = [];
864
+ gun
865
+ .get("gun-eth")
866
+ .get("stealth-payments")
867
+ .get(recipientAddress)
868
+ .map()
869
+ .once((payment, id) => {
870
+ if (payment?.stealthAddress) {
871
+ p.push({ ...payment, id, source: 'offChain' });
872
+ }
873
+ });
874
+ setTimeout(() => resolve(p), 2000);
875
+ });
876
+
877
+ payments.push(...offChainPayments);
878
+ }
879
+
880
+ console.log(`Found ${payments.length} stealth payments`);
881
+ return payments;
882
+ } catch (error) {
883
+ console.error("Error retrieving stealth payments:", error);
884
+ throw error;
885
+ }
886
+ };
887
+
888
+ /**
889
+ * Clean up old stealth payments
890
+ * @param {string} recipientAddress - The recipient's address
891
+ * @returns {Promise<void>}
892
+ */
893
+ Gun.chain.cleanStealthPayments = async function(recipientAddress) {
894
+ try {
895
+ const gun = this;
896
+ const payments = await gun
897
+ .get("gun-eth")
898
+ .get("stealth-payments")
899
+ .get(recipientAddress)
900
+ .map()
901
+ .once()
902
+ .then();
903
+
904
+ // Remove empty or invalid nodes
905
+ if (payments) {
906
+ Object.keys(payments).forEach(async (key) => {
907
+ const payment = payments[key];
908
+ if (!payment || !payment.stealthAddress || !payment.senderPublicKey || !payment.spendingPublicKey) {
909
+ await gun
910
+ .get("gun-eth")
911
+ .get("stealth-payments")
912
+ .get(recipientAddress)
913
+ .get(key)
914
+ .put(null);
915
+ }
916
+ });
917
+ }
918
+ } catch (error) {
919
+ console.error("Error cleaning stealth payments:", error);
920
+ }
921
+ };
922
+
923
+ // =============================================
924
+ // EXPORTS
925
+ // =============================================
926
+
927
+ // Crea una classe GunEth che contiene tutti i metodi e le utility
928
+ export class GunEth {
929
+ // Static utility methods
930
+ static generateRandomId = generateRandomId;
931
+ static generatePassword = generatePassword;
932
+ static gunToEthAccount = gunToEthAccount;
933
+ static getSigner = getSigner;
934
+ static deriveStealthAddress = deriveStealthAddress;
935
+
936
+ // Chain methods
937
+ static chainMethods = {
938
+ setSigner: Gun.chain.setSigner,
939
+ getSigner: Gun.chain.getSigner,
940
+ verifySignature: Gun.chain.verifySignature,
941
+ generatePassword: Gun.chain.generatePassword,
942
+ createSignature: Gun.chain.createSignature,
943
+ createAndStoreEncryptedPair: Gun.chain.createAndStoreEncryptedPair,
944
+ getAndDecryptPair: Gun.chain.getAndDecryptPair,
945
+ proof: Gun.chain.proof,
946
+ gunToEthAccount: Gun.chain.gunToEthAccount,
947
+ generateStealthAddress: Gun.chain.generateStealthAddress,
948
+ publishStealthKeys: Gun.chain.publishStealthKeys,
949
+ recoverStealthFunds: Gun.chain.recoverStealthFunds,
950
+ announceStealthPayment: Gun.chain.announceStealthPayment,
951
+ getStealthPayments: Gun.chain.getStealthPayments,
952
+ cleanStealthPayments: Gun.chain.cleanStealthPayments
953
+ };
954
+
955
+ // Constants
956
+ static MESSAGE_TO_SIGN = MESSAGE_TO_SIGN;
957
+ static PROOF_CONTRACT_ADDRESS = PROOF_CONTRACT_ADDRESS;
958
+ static LOCAL_CONFIG = LOCAL_CONFIG;
959
+ }
960
+
961
+ // Esporta Gun come default
962
+ export default Gun;