gun-eth 1.4.17 → 1.4.18

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