shogun-core 3.2.3 → 3.3.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.
Files changed (32) hide show
  1. package/README.md +12 -0
  2. package/dist/browser/shogun-core.js +108102 -43579
  3. package/dist/browser/shogun-core.js.map +1 -1
  4. package/dist/ship/examples/messenger-cli.js +171 -55
  5. package/dist/ship/examples/wallet-cli.js +767 -0
  6. package/dist/ship/implementation/SHIP_00.js +478 -0
  7. package/dist/ship/implementation/SHIP_01.js +275 -492
  8. package/dist/ship/implementation/SHIP_02.js +1366 -0
  9. package/dist/ship/implementation/SHIP_03.js +855 -0
  10. package/dist/ship/interfaces/ISHIP_00.js +135 -0
  11. package/dist/ship/interfaces/ISHIP_01.js +81 -24
  12. package/dist/ship/interfaces/ISHIP_02.js +57 -0
  13. package/dist/ship/interfaces/ISHIP_03.js +61 -0
  14. package/dist/src/gundb/db.js +55 -11
  15. package/dist/src/index.js +10 -2
  16. package/dist/src/managers/CoreInitializer.js +41 -13
  17. package/dist/src/storage/storage.js +22 -9
  18. package/dist/types/ship/examples/messenger-cli.d.ts +7 -1
  19. package/dist/types/ship/examples/wallet-cli.d.ts +131 -0
  20. package/dist/types/ship/implementation/SHIP_00.d.ts +113 -0
  21. package/dist/types/ship/implementation/SHIP_01.d.ts +44 -77
  22. package/dist/types/ship/implementation/SHIP_02.d.ts +297 -0
  23. package/dist/types/ship/implementation/SHIP_03.d.ts +127 -0
  24. package/dist/types/ship/interfaces/ISHIP_00.d.ts +410 -0
  25. package/dist/types/ship/interfaces/ISHIP_01.d.ts +157 -119
  26. package/dist/types/ship/interfaces/ISHIP_02.d.ts +470 -0
  27. package/dist/types/ship/interfaces/ISHIP_03.d.ts +295 -0
  28. package/dist/types/src/gundb/db.d.ts +10 -3
  29. package/dist/types/src/index.d.ts +7 -0
  30. package/dist/types/src/interfaces/shogun.d.ts +2 -0
  31. package/dist/types/src/storage/storage.d.ts +2 -1
  32. package/package.json +22 -9
@@ -0,0 +1,855 @@
1
+ "use strict";
2
+ /**
3
+ * SHIP-03: Dual-Key Stealth Address Implementation
4
+ *
5
+ * Full port of shogun-stealth-address with Fluidkey integration.
6
+ * Extends SHIP-00 and SHIP-02 to provide ERC-5564 compatible stealth addresses.
7
+ *
8
+ * Based on:
9
+ * - SHIP-00 for identity foundation
10
+ * - SHIP-02 for Ethereum operations
11
+ * - ERC-5564 for stealth address standard
12
+ * - Fluidkey Stealth Account Kit
13
+ * - @scure/bip32 for HD key derivation
14
+ *
15
+ * Features:
16
+ * ✅ Dual-key stealth (viewing + spending keys)
17
+ * ✅ ERC-5564 / Fluidkey compatible
18
+ * ✅ Deterministic derivation from SHIP-00
19
+ * ✅ View tag optimization for scanning
20
+ * ✅ Announcement metadata
21
+ * ✅ Batch generation
22
+ * ✅ Fluidkey generateStealthAddresses
23
+ * ✅ Fluidkey generateStealthPrivateKey
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.SHIP_03 = void 0;
27
+ const ethers_1 = require("ethers");
28
+ // Fluidkey Stealth Account Kit (optional - graceful degradation if not available)
29
+ let generateKeysFromSignature;
30
+ let extractViewingPrivateKeyNode;
31
+ let generateEphemeralPrivateKey;
32
+ let generateStealthAddresses;
33
+ let generateStealthPrivateKey;
34
+ let HDKey;
35
+ try {
36
+ const fluidkey = require("@fluidkey/stealth-account-kit");
37
+ generateKeysFromSignature = fluidkey.generateKeysFromSignature;
38
+ extractViewingPrivateKeyNode = fluidkey.extractViewingPrivateKeyNode;
39
+ generateEphemeralPrivateKey = fluidkey.generateEphemeralPrivateKey;
40
+ generateStealthAddresses = fluidkey.generateStealthAddresses;
41
+ generateStealthPrivateKey = fluidkey.generateStealthPrivateKey;
42
+ const scure = require("@scure/bip32");
43
+ HDKey = scure.HDKey;
44
+ }
45
+ catch (error) {
46
+ console.warn("⚠️ Fluidkey/scure not available. Install with: yarn add @fluidkey/stealth-account-kit @scure/bip32");
47
+ }
48
+ // ============================================================================
49
+ // UTILITIES
50
+ // ============================================================================
51
+ /**
52
+ * Normalize hex string with optional length validation
53
+ */
54
+ function normalizeHex(str, length) {
55
+ if (!str)
56
+ return "";
57
+ let s = str.toLowerCase();
58
+ if (!s.startsWith("0x"))
59
+ s = "0x" + s;
60
+ if (length && s.length !== 2 + length * 2) {
61
+ s = "0x" + s.slice(2).padStart(length * 2, "0").slice(0, length * 2);
62
+ }
63
+ return s;
64
+ }
65
+ /**
66
+ * Normalize public key to compressed format (33 bytes)
67
+ */
68
+ function normalizePublicKey(publicKey) {
69
+ try {
70
+ let normalized = publicKey;
71
+ if (normalized.startsWith("0x")) {
72
+ normalized = normalized.slice(2);
73
+ }
74
+ // If uncompressed (130 hex chars = 65 bytes), compress it
75
+ if (normalized.length === 130) {
76
+ return ethers_1.SigningKey.computePublicKey("0x" + normalized, true);
77
+ }
78
+ // If already compressed (66 hex chars = 33 bytes), ensure 0x prefix
79
+ if (normalized.length === 66) {
80
+ return "0x" + normalized;
81
+ }
82
+ // If it's 64 hex chars (missing prefix byte), add 0x04 for uncompressed
83
+ if (normalized.length === 64) {
84
+ return ethers_1.SigningKey.computePublicKey("0x04" + normalized, true);
85
+ }
86
+ throw new Error(`Invalid public key length: ${normalized.length}`);
87
+ }
88
+ catch (error) {
89
+ console.error("Error normalizing public key:", error);
90
+ throw error;
91
+ }
92
+ }
93
+ // ============================================================================
94
+ // IMPLEMENTATION
95
+ // ============================================================================
96
+ /**
97
+ * SHIP-03 Reference Implementation
98
+ *
99
+ * Full Fluidkey-compatible stealth address system derived from SHIP-00.
100
+ * All keys are deterministically derived from the user's SHIP-00 identity.
101
+ */
102
+ class SHIP_03 {
103
+ constructor(identity, eth, config = {}) {
104
+ this.initialized = false;
105
+ // Stealth keys (derived from SHIP-00)
106
+ this.viewingKey = null;
107
+ this.spendingKey = null;
108
+ // Cache of owned stealth addresses
109
+ this.ownedStealthAddresses = new Map();
110
+ // Cache of announcements
111
+ this.announcementCache = new Map();
112
+ this.identity = identity;
113
+ this.eth = eth;
114
+ this.config = {
115
+ erc5564Compatible: config.erc5564Compatible ?? true,
116
+ defaultSchemeId: config.defaultSchemeId ?? 0,
117
+ enableViewTag: config.enableViewTag ?? true,
118
+ autoScan: config.autoScan ?? false,
119
+ };
120
+ }
121
+ // ========================================================================
122
+ // INITIALIZATION
123
+ // ========================================================================
124
+ async initialize() {
125
+ if (this.initialized) {
126
+ return;
127
+ }
128
+ try {
129
+ // Ensure SHIP-00 is authenticated
130
+ if (!this.identity.isLoggedIn()) {
131
+ throw new Error("SHIP-00 identity not authenticated");
132
+ }
133
+ // Ensure SHIP-02 is initialized
134
+ if (!this.eth.isInitialized()) {
135
+ throw new Error("SHIP-02 not initialized");
136
+ }
137
+ // Derive stealth keys from SHIP-00 identity
138
+ await this.deriveStealthKeysFromIdentity();
139
+ this.initialized = true;
140
+ console.log("✅ SHIP-03 initialized with Fluidkey stealth addresses");
141
+ console.log("📍 Viewing Public Key:", this.viewingKey?.publicKey);
142
+ console.log("📍 Spending Public Key:", this.spendingKey?.publicKey);
143
+ }
144
+ catch (error) {
145
+ throw new Error(`SHIP-03 initialization failed: ${error.message}`);
146
+ }
147
+ }
148
+ isInitialized() {
149
+ return this.initialized;
150
+ }
151
+ // ========================================================================
152
+ // KEY MANAGEMENT
153
+ // ========================================================================
154
+ async getStealthKeys() {
155
+ this.ensureInitialized();
156
+ if (!this.viewingKey || !this.spendingKey) {
157
+ throw new Error("Stealth keys not initialized");
158
+ }
159
+ return {
160
+ viewingKey: {
161
+ publicKey: this.viewingKey.publicKey,
162
+ privateKey: this.viewingKey.privateKey,
163
+ },
164
+ spendingKey: {
165
+ publicKey: this.spendingKey.publicKey,
166
+ privateKey: this.spendingKey.privateKey,
167
+ },
168
+ };
169
+ }
170
+ /**
171
+ * Get public stealth keys by username (alias)
172
+ * Resolves username → Gun pub → stealth keys
173
+ */
174
+ async getPublicStealthKeysByUsername(username) {
175
+ try {
176
+ console.log(`🔍 Looking up stealth keys for username: ${username}`);
177
+ // Use SHIP-00 to resolve username → Gun public key
178
+ const userData = await this.identity.getUserByAlias(username);
179
+ if (!userData || !userData.userPub) {
180
+ console.log(`❌ User not found: ${username}`);
181
+ return null;
182
+ }
183
+ console.log(`✅ Resolved ${username} → ${userData.userPub.slice(0, 20)}...`);
184
+ // Get stealth keys using Gun public key
185
+ return await this.getPublicStealthKeys(userData.userPub);
186
+ }
187
+ catch (error) {
188
+ console.error("Error getting stealth keys by username:", error);
189
+ return null;
190
+ }
191
+ }
192
+ /**
193
+ * Get public stealth keys by Gun public key
194
+ */
195
+ async getPublicStealthKeys(userPub) {
196
+ try {
197
+ // Access Gun through identity
198
+ const shogun = this.identity.getShogun();
199
+ const gun = shogun?.db?.gun;
200
+ if (!gun) {
201
+ console.warn("Gun not available");
202
+ return null;
203
+ }
204
+ console.log(`🔍 Loading stealth keys for pub: ${userPub.slice(0, 20)}...`);
205
+ // Get user's published stealth keys
206
+ const data = await new Promise((resolve) => {
207
+ let resolved = false;
208
+ const timeout = setTimeout(() => {
209
+ if (!resolved) {
210
+ resolved = true;
211
+ console.log("⏱️ Timeout waiting for stealth keys");
212
+ resolve(null);
213
+ }
214
+ }, 5000);
215
+ gun
216
+ .get(userPub)
217
+ .get(SHIP_03.NODES.STEALTH_KEYS_PUBLIC)
218
+ .once((data) => {
219
+ if (!resolved) {
220
+ resolved = true;
221
+ clearTimeout(timeout);
222
+ resolve(data || null);
223
+ }
224
+ });
225
+ });
226
+ if (!data || !data.viewingKey || !data.spendingKey) {
227
+ console.log(`❌ No stealth keys found for user: ${userPub.slice(0, 20)}...`);
228
+ console.log("💡 User may not have initialized SHIP-03 yet");
229
+ return null;
230
+ }
231
+ console.log("✅ Stealth keys found:");
232
+ console.log(` Viewing: ${data.viewingKey.slice(0, 20)}...`);
233
+ console.log(` Spending: ${data.spendingKey.slice(0, 20)}...`);
234
+ return {
235
+ viewingPublicKey: data.viewingKey,
236
+ spendingPublicKey: data.spendingKey,
237
+ };
238
+ }
239
+ catch (error) {
240
+ console.error("Error getting public stealth keys:", error);
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * Search stealth keys in directory (all published keys)
246
+ * Returns list of users who have published stealth keys
247
+ */
248
+ async searchStealthDirectory() {
249
+ try {
250
+ const shogun = this.identity.getShogun();
251
+ const gun = shogun?.db?.gun;
252
+ if (!gun) {
253
+ console.warn("Gun not available");
254
+ return [];
255
+ }
256
+ console.log("🔍 Searching stealth address directory...");
257
+ // In production, this would query a directory index
258
+ // For now, return empty array
259
+ console.warn("⚠️ Directory search not yet implemented");
260
+ console.log("💡 Users can share their username or Gun pub for stealth payments");
261
+ return [];
262
+ }
263
+ catch (error) {
264
+ console.error("Error searching directory:", error);
265
+ return [];
266
+ }
267
+ }
268
+ async exportStealthKeys() {
269
+ this.ensureInitialized();
270
+ const keys = await this.getStealthKeys();
271
+ // Encrypt keys using SHIP-00 SEA
272
+ const shogun = this.identity.getShogun();
273
+ const crypto = shogun?.db?.crypto;
274
+ const keyPair = this.identity.getKeyPair();
275
+ if (!crypto || !keyPair) {
276
+ // Fallback: return as JSON (should encrypt in production)
277
+ console.warn("Crypto not available, exporting unencrypted");
278
+ return JSON.stringify(keys);
279
+ }
280
+ // Encrypt with user's own keys
281
+ const encrypted = await crypto.encrypt(JSON.stringify(keys), keyPair);
282
+ return JSON.stringify(encrypted);
283
+ }
284
+ async importStealthKeys(encryptedKeys) {
285
+ this.ensureInitialized();
286
+ // Decrypt if needed
287
+ const shogun = this.identity.getShogun();
288
+ const crypto = shogun?.db?.crypto;
289
+ const keyPair = this.identity.getKeyPair();
290
+ let keys;
291
+ if (crypto && keyPair) {
292
+ try {
293
+ const encrypted = JSON.parse(encryptedKeys);
294
+ const decrypted = await crypto.decrypt(encrypted, keyPair);
295
+ keys = JSON.parse(decrypted);
296
+ }
297
+ catch {
298
+ // If decrypt fails, try parsing as plain JSON
299
+ keys = JSON.parse(encryptedKeys);
300
+ }
301
+ }
302
+ else {
303
+ keys = JSON.parse(encryptedKeys);
304
+ }
305
+ this.viewingKey = keys.viewingKey;
306
+ this.spendingKey = keys.spendingKey;
307
+ console.log("✅ Stealth keys imported");
308
+ }
309
+ // ========================================================================
310
+ // STEALTH ADDRESS GENERATION (FLUIDKEY)
311
+ // ========================================================================
312
+ async generateEphemeralKeyPair() {
313
+ // Use Fluidkey's method if we have viewing key
314
+ if (this.viewingKey) {
315
+ try {
316
+ const cleanPriv = this.viewingKey.privateKey.startsWith("0x")
317
+ ? this.viewingKey.privateKey.slice(2)
318
+ : this.viewingKey.privateKey;
319
+ const hdKey = HDKey.fromMasterSeed(Buffer.from(cleanPriv, "hex"));
320
+ const result = generateEphemeralPrivateKey({
321
+ viewingPrivateKeyNode: hdKey,
322
+ nonce: BigInt(Date.now()), // Use timestamp as nonce
323
+ chainId: 1,
324
+ coinType: 60,
325
+ });
326
+ const ephemeralWallet = new ethers_1.ethers.Wallet(result.ephemeralPrivateKey);
327
+ return {
328
+ publicKey: ephemeralWallet.signingKey.publicKey,
329
+ privateKey: result.ephemeralPrivateKey,
330
+ };
331
+ }
332
+ catch (error) {
333
+ console.warn("Fluidkey ephemeral key generation failed, using fallback:", error);
334
+ }
335
+ }
336
+ // Fallback: random ephemeral key
337
+ const ephemeralWallet = ethers_1.ethers.Wallet.createRandom();
338
+ return {
339
+ publicKey: ephemeralWallet.signingKey.publicKey,
340
+ privateKey: ephemeralWallet.privateKey,
341
+ };
342
+ }
343
+ async generateStealthAddress(recipientViewingKey, recipientSpendingKey, ephemeralPrivateKey) {
344
+ try {
345
+ this.ensureInitialized();
346
+ console.log("🔐 Generating stealth address using Fluidkey...");
347
+ // Normalize public keys
348
+ const normalizedViewingKey = normalizePublicKey(recipientViewingKey);
349
+ const normalizedSpendingKey = normalizePublicKey(recipientSpendingKey);
350
+ console.log("📍 Normalized viewing key:", normalizedViewingKey);
351
+ console.log("📍 Normalized spending key:", normalizedSpendingKey);
352
+ // Generate or use provided ephemeral key
353
+ let ephemeralKey = ephemeralPrivateKey;
354
+ if (!ephemeralKey) {
355
+ const ephemeralPair = await this.generateEphemeralKeyPair();
356
+ ephemeralKey = ephemeralPair.privateKey;
357
+ }
358
+ // Ensure 0x prefix
359
+ if (!ephemeralKey.startsWith("0x")) {
360
+ ephemeralKey = "0x" + ephemeralKey;
361
+ }
362
+ console.log("🔑 Using ephemeral key (first 10 chars):", ephemeralKey.slice(0, 10) + "...");
363
+ // Use Fluidkey's generateStealthAddresses
364
+ const result = generateStealthAddresses({
365
+ ephemeralPrivateKey: ephemeralKey,
366
+ spendingPublicKeys: [normalizedSpendingKey],
367
+ });
368
+ const stealthAddress = result.stealthAddresses[0];
369
+ const ephemeralWallet = new ethers_1.ethers.Wallet(ephemeralKey);
370
+ const ephemeralPublicKey = ephemeralWallet.signingKey.publicKey;
371
+ console.log("✅ Stealth address generated:", stealthAddress);
372
+ console.log("📍 Ephemeral public key:", ephemeralPublicKey);
373
+ // Generate view tag if enabled
374
+ let viewTag;
375
+ if (this.config.enableViewTag) {
376
+ // Compute shared secret for view tag
377
+ const sharedSecret = ephemeralWallet.signingKey.computeSharedSecret(normalizedViewingKey);
378
+ const hashedSecret = ethers_1.ethers.keccak256(sharedSecret);
379
+ viewTag = hashedSecret.slice(0, 6); // First byte as 0xNN
380
+ }
381
+ return {
382
+ success: true,
383
+ stealthAddress,
384
+ ephemeralPublicKey,
385
+ viewTag,
386
+ };
387
+ }
388
+ catch (error) {
389
+ console.error("❌ Error generating stealth address:", error);
390
+ return {
391
+ success: false,
392
+ error: error.message,
393
+ };
394
+ }
395
+ }
396
+ async generateMultipleStealthAddresses(recipients, ephemeralPrivateKey) {
397
+ try {
398
+ this.ensureInitialized();
399
+ // Generate ephemeral key if not provided
400
+ let ephemeralKey = ephemeralPrivateKey;
401
+ if (!ephemeralKey) {
402
+ const ephemeralPair = await this.generateEphemeralKeyPair();
403
+ ephemeralKey = ephemeralPair.privateKey;
404
+ }
405
+ if (!ephemeralKey.startsWith("0x")) {
406
+ ephemeralKey = "0x" + ephemeralKey;
407
+ }
408
+ // Normalize all spending keys
409
+ const normalizedSpendingKeys = recipients.map((r) => normalizePublicKey(r.spendingKey));
410
+ // Use Fluidkey batch generation
411
+ const result = generateStealthAddresses({
412
+ ephemeralPrivateKey: ephemeralKey,
413
+ spendingPublicKeys: normalizedSpendingKeys,
414
+ });
415
+ const ephemeralWallet = new ethers_1.ethers.Wallet(ephemeralKey);
416
+ const ephemeralPublicKey = ephemeralWallet.signingKey.publicKey;
417
+ // Map results
418
+ const results = result.stealthAddresses.map((addr, idx) => ({
419
+ success: true,
420
+ stealthAddress: addr,
421
+ ephemeralPublicKey,
422
+ viewTag: this.config.enableViewTag
423
+ ? this.computeViewTag(ephemeralKey, recipients[idx].viewingKey)
424
+ : undefined,
425
+ }));
426
+ console.log(`✅ Generated ${results.length} stealth addresses (batch)`);
427
+ return results;
428
+ }
429
+ catch (error) {
430
+ console.error("❌ Error generating multiple stealth addresses:", error);
431
+ return [
432
+ {
433
+ success: false,
434
+ error: error.message,
435
+ },
436
+ ];
437
+ }
438
+ }
439
+ // ========================================================================
440
+ // STEALTH ADDRESS OPENING (FLUIDKEY)
441
+ // ========================================================================
442
+ async openStealthAddress(stealthAddress, ephemeralPublicKey) {
443
+ this.ensureInitialized();
444
+ if (!this.viewingKey || !this.spendingKey) {
445
+ throw new Error("Stealth keys not available");
446
+ }
447
+ console.log("🔓 Opening stealth address using Fluidkey...");
448
+ console.log("📍 Stealth address:", stealthAddress);
449
+ console.log("📍 Ephemeral public key:", ephemeralPublicKey);
450
+ try {
451
+ // Normalize ephemeral public key
452
+ const normalizedEphemeralKey = normalizePublicKey(ephemeralPublicKey);
453
+ console.log("📍 Using keys:");
454
+ console.log(" Viewing private:", this.viewingKey.privateKey.slice(0, 10) + "...");
455
+ console.log(" Spending private:", this.spendingKey.privateKey.slice(0, 10) + "...");
456
+ console.log(" Ephemeral public:", normalizedEphemeralKey.slice(0, 20) + "...");
457
+ // Try using Fluidkey first
458
+ let stealthWallet;
459
+ if (generateStealthPrivateKey) {
460
+ try {
461
+ console.log("🔐 Trying Fluidkey generateStealthPrivateKey...");
462
+ // Fluidkey uses only ephemeralPublicKey and spendingPrivateKey
463
+ // The viewing key is used to compute shared secret separately
464
+ const result = generateStealthPrivateKey({
465
+ ephemeralPublicKey: normalizedEphemeralKey,
466
+ spendingPrivateKey: this.spendingKey.privateKey,
467
+ });
468
+ console.log("🔑 Fluidkey result:", result);
469
+ // Extract stealthPrivateKey from result
470
+ const stealthPrivateKey = result.stealthPrivateKey;
471
+ console.log("🔑 Stealth private key:", stealthPrivateKey.slice(0, 10) + "...");
472
+ stealthWallet = new ethers_1.ethers.Wallet(stealthPrivateKey);
473
+ console.log("✅ Wallet created from Fluidkey private key");
474
+ }
475
+ catch (fluidkeyError) {
476
+ console.warn("⚠️ Fluidkey failed, using fallback method:", fluidkeyError);
477
+ // Fallback: Manual computation
478
+ stealthWallet = await this.openStealthAddressFallback(stealthAddress, normalizedEphemeralKey);
479
+ }
480
+ }
481
+ else {
482
+ console.warn("⚠️ Fluidkey not available, using fallback method");
483
+ // Fallback: Manual computation
484
+ stealthWallet = await this.openStealthAddressFallback(stealthAddress, normalizedEphemeralKey);
485
+ }
486
+ // Verify the derived address matches
487
+ if (stealthWallet.address.toLowerCase() !== stealthAddress.toLowerCase()) {
488
+ throw new Error(`Derived address mismatch: ${stealthWallet.address} !== ${stealthAddress}`);
489
+ }
490
+ // Compute view tag for metadata
491
+ const viewingWallet = new ethers_1.ethers.Wallet(this.viewingKey.privateKey);
492
+ const sharedSecret = viewingWallet.signingKey.computeSharedSecret(normalizedEphemeralKey);
493
+ const hashedSecret = ethers_1.ethers.keccak256(sharedSecret);
494
+ const viewTag = hashedSecret.slice(0, 6);
495
+ // Cache the owned stealth address
496
+ const metadata = {
497
+ ephemeralPublicKey: normalizedEphemeralKey,
498
+ viewTag,
499
+ stealthAddress,
500
+ createdAt: Date.now(),
501
+ };
502
+ const ownedAddress = {
503
+ stealthAddress,
504
+ ephemeralPublicKey: normalizedEphemeralKey,
505
+ privateKey: stealthWallet.privateKey,
506
+ wallet: stealthWallet,
507
+ metadata,
508
+ };
509
+ this.ownedStealthAddresses.set(stealthAddress, ownedAddress);
510
+ console.log("✅ Stealth address opened successfully");
511
+ return stealthWallet;
512
+ }
513
+ catch (error) {
514
+ console.error("❌ Error opening stealth address:", error);
515
+ throw error;
516
+ }
517
+ }
518
+ async isStealthAddressMine(stealthAddress, ephemeralPublicKey) {
519
+ try {
520
+ const wallet = await this.openStealthAddress(stealthAddress, ephemeralPublicKey);
521
+ return wallet.address.toLowerCase() === stealthAddress.toLowerCase();
522
+ }
523
+ catch {
524
+ return false;
525
+ }
526
+ }
527
+ async getStealthPrivateKey(stealthAddress, ephemeralPublicKey) {
528
+ const wallet = await this.openStealthAddress(stealthAddress, ephemeralPublicKey);
529
+ return wallet.privateKey;
530
+ }
531
+ // ========================================================================
532
+ // SCANNING
533
+ // ========================================================================
534
+ async scanStealthAddresses(announcements) {
535
+ this.ensureInitialized();
536
+ console.log(`🔍 Scanning ${announcements.length} stealth announcements...`);
537
+ const owned = [];
538
+ for (const announcement of announcements) {
539
+ try {
540
+ const isMine = await this.isStealthAddressMine(announcement.stealthAddress, announcement.ephemeralPublicKey);
541
+ if (isMine) {
542
+ const ownedAddress = this.ownedStealthAddresses.get(announcement.stealthAddress);
543
+ if (ownedAddress) {
544
+ owned.push(ownedAddress);
545
+ }
546
+ }
547
+ }
548
+ catch (error) {
549
+ // Skip invalid announcements
550
+ console.warn("Invalid announcement:", announcement.stealthAddress, error);
551
+ }
552
+ }
553
+ console.log(`✅ Scanned ${announcements.length} announcements, found ${owned.length} owned`);
554
+ return owned;
555
+ }
556
+ async quickScanWithViewTags(announcements) {
557
+ this.ensureInitialized();
558
+ if (!this.config.enableViewTag) {
559
+ // If view tags disabled, return all for full scan
560
+ return announcements;
561
+ }
562
+ const potentiallyOwned = [];
563
+ for (const announcement of announcements) {
564
+ // Quick check using view tag (if available)
565
+ if (announcement.viewTag && this.viewingKey) {
566
+ try {
567
+ const normalizedEphemeralKey = normalizePublicKey(announcement.ephemeralPublicKey);
568
+ const viewingWallet = new ethers_1.ethers.Wallet(this.viewingKey.privateKey);
569
+ const sharedSecret = viewingWallet.signingKey.computeSharedSecret(normalizedEphemeralKey);
570
+ const hashedSecret = ethers_1.ethers.keccak256(sharedSecret);
571
+ const computedViewTag = hashedSecret.slice(0, 6);
572
+ // If view tags match, this might be ours
573
+ if (computedViewTag === announcement.viewTag) {
574
+ potentiallyOwned.push(announcement);
575
+ }
576
+ }
577
+ catch (error) {
578
+ // Include in full scan if view tag check fails
579
+ potentiallyOwned.push(announcement);
580
+ }
581
+ }
582
+ else {
583
+ // No view tag, include in full scan
584
+ potentiallyOwned.push(announcement);
585
+ }
586
+ }
587
+ console.log(`🔍 View tag quick scan: ${potentiallyOwned.length}/${announcements.length} potential matches`);
588
+ return potentiallyOwned;
589
+ }
590
+ // ========================================================================
591
+ // METADATA & ANNOUNCEMENTS
592
+ // ========================================================================
593
+ createAnnouncementMetadata(stealthAddress, ephemeralPublicKey) {
594
+ // Compute view tag
595
+ let viewTag = "0x00";
596
+ if (this.config.enableViewTag && this.viewingKey) {
597
+ try {
598
+ const normalizedEphemeralKey = normalizePublicKey(ephemeralPublicKey);
599
+ const viewingWallet = new ethers_1.ethers.Wallet(this.viewingKey.privateKey);
600
+ const sharedSecret = viewingWallet.signingKey.computeSharedSecret(normalizedEphemeralKey);
601
+ const hashedSecret = ethers_1.ethers.keccak256(sharedSecret);
602
+ viewTag = hashedSecret.slice(0, 6);
603
+ }
604
+ catch (error) {
605
+ console.warn("Error computing view tag:", error);
606
+ viewTag = "0x00";
607
+ }
608
+ }
609
+ return {
610
+ ephemeralPublicKey: normalizePublicKey(ephemeralPublicKey),
611
+ viewTag,
612
+ stealthAddress,
613
+ createdAt: Date.now(),
614
+ };
615
+ }
616
+ async parseAnnouncement(txData) {
617
+ try {
618
+ // Parse ERC-5564 Announcement event from transaction logs
619
+ // event Announcement(
620
+ // uint256 indexed schemeId,
621
+ // address indexed stealthAddress,
622
+ // address indexed caller,
623
+ // bytes ephemeralPubKey,
624
+ // bytes metadata
625
+ // )
626
+ if (!txData || !txData.logs) {
627
+ return null;
628
+ }
629
+ // ERC-5564 Announcement event signature
630
+ const announcementTopic = ethers_1.ethers.id("Announcement(uint256,address,address,bytes,bytes)");
631
+ for (const log of txData.logs) {
632
+ if (log.topics[0] === announcementTopic) {
633
+ // Parse event data
634
+ const schemeId = parseInt(log.topics[1], 16);
635
+ const stealthAddress = ethers_1.ethers.getAddress("0x" + log.topics[2].slice(26));
636
+ const announcer = ethers_1.ethers.getAddress("0x" + log.topics[3].slice(26));
637
+ // Decode ephemeral public key and metadata from data
638
+ const decoded = ethers_1.ethers.AbiCoder.defaultAbiCoder().decode(["bytes", "bytes"], log.data);
639
+ const ephemeralPublicKey = ethers_1.ethers.hexlify(decoded[0]);
640
+ const metadata = ethers_1.ethers.hexlify(decoded[1]);
641
+ // Extract view tag from metadata (first byte)
642
+ const viewTag = metadata.slice(0, 6);
643
+ return {
644
+ stealthAddress,
645
+ ephemeralPublicKey,
646
+ viewTag,
647
+ schemeId,
648
+ announcer,
649
+ txHash: txData.hash,
650
+ };
651
+ }
652
+ }
653
+ return null;
654
+ }
655
+ catch (error) {
656
+ console.error("Error parsing announcement:", error);
657
+ return null;
658
+ }
659
+ }
660
+ // ========================================================================
661
+ // UTILITIES
662
+ // ========================================================================
663
+ async getAllOwnedStealthAddresses() {
664
+ return Array.from(this.ownedStealthAddresses.values());
665
+ }
666
+ async clearCache() {
667
+ this.ownedStealthAddresses.clear();
668
+ this.announcementCache.clear();
669
+ console.log("✅ SHIP-03 cache cleared");
670
+ }
671
+ async verifyStealthAddress(stealthAddress, ephemeralPublicKey, spendingPublicKey) {
672
+ try {
673
+ if (!this.viewingKey || !this.spendingKey) {
674
+ return false;
675
+ }
676
+ const normalizedEphemeralKey = normalizePublicKey(ephemeralPublicKey);
677
+ const normalizedSpendingKey = normalizePublicKey(spendingPublicKey);
678
+ // Use Fluidkey to regenerate stealth private key
679
+ const stealthPrivateKey = generateStealthPrivateKey({
680
+ ephemeralPublicKey: normalizedEphemeralKey,
681
+ viewingPrivateKey: this.viewingKey.privateKey,
682
+ spendingPrivateKey: this.spendingKey.privateKey,
683
+ });
684
+ const derivedWallet = new ethers_1.ethers.Wallet(stealthPrivateKey);
685
+ return (derivedWallet.address.toLowerCase() === stealthAddress.toLowerCase());
686
+ }
687
+ catch {
688
+ return false;
689
+ }
690
+ }
691
+ // ========================================================================
692
+ // PRIVATE HELPERS
693
+ // ========================================================================
694
+ /**
695
+ * Ensure system is initialized
696
+ */
697
+ ensureInitialized() {
698
+ if (!this.initialized) {
699
+ throw new Error("SHIP-03 not initialized. Call initialize() first.");
700
+ }
701
+ }
702
+ /**
703
+ * Derive stealth keys deterministically from SHIP-00 identity
704
+ *
705
+ * Uses simple deterministic derivation instead of Fluidkey's generateKeysFromSignature
706
+ * because it requires specific EIP-712 signature format.
707
+ *
708
+ * We use Fluidkey only for:
709
+ * - generateStealthAddresses (address generation)
710
+ * - generateStealthPrivateKey (opening addresses)
711
+ */
712
+ async deriveStealthKeysFromIdentity() {
713
+ try {
714
+ // Get SHIP-00 keypair
715
+ const keyPair = this.identity.getKeyPair();
716
+ if (!keyPair || !keyPair.epriv || !keyPair.epub) {
717
+ throw new Error("SHIP-00 identity keypair not available");
718
+ }
719
+ console.log("🔐 Deriving stealth keys from SHIP-00 identity...");
720
+ // Derive viewing key deterministically from SHIP-00
721
+ // Path: keccak256("SHIP-03-VIEWING" + epriv)
722
+ const viewingSeed = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes("SHIP-03-VIEWING" + keyPair.epriv));
723
+ const viewingWallet = new ethers_1.ethers.Wallet(viewingSeed);
724
+ this.viewingKey = {
725
+ privateKey: viewingWallet.privateKey,
726
+ publicKey: viewingWallet.signingKey.publicKey,
727
+ };
728
+ // Derive spending key deterministically from SHIP-00
729
+ // Path: keccak256("SHIP-03-SPENDING" + epriv)
730
+ const spendingSeed = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes("SHIP-03-SPENDING" + keyPair.epriv));
731
+ const spendingWallet = new ethers_1.ethers.Wallet(spendingSeed);
732
+ this.spendingKey = {
733
+ privateKey: spendingWallet.privateKey,
734
+ publicKey: spendingWallet.signingKey.publicKey,
735
+ };
736
+ console.log("✅ Stealth keys derived from SHIP-00 (deterministic)");
737
+ console.log("📍 Keys are compatible with Fluidkey stealth address operations");
738
+ // Automatically publish public keys to Gun network
739
+ await this.publishStealthKeys();
740
+ }
741
+ catch (error) {
742
+ console.error("❌ Error deriving stealth keys:", error);
743
+ throw error;
744
+ }
745
+ }
746
+ /**
747
+ * Publish public stealth keys to Gun for others to use (PUBLIC METHOD)
748
+ */
749
+ async publishStealthKeys() {
750
+ try {
751
+ if (!this.viewingKey || !this.spendingKey) {
752
+ return;
753
+ }
754
+ // Access Gun through identity
755
+ const shogun = this.identity.getShogun();
756
+ const gun = shogun?.db?.gun;
757
+ if (!gun) {
758
+ console.warn("Gun not available, skipping key publication");
759
+ return;
760
+ }
761
+ const user = gun.user();
762
+ if (!user || !user.is) {
763
+ console.warn("User not authenticated on Gun");
764
+ return;
765
+ }
766
+ const userPub = user.is.pub;
767
+ // Publish public keys (NOT private keys!)
768
+ const publicKeys = {
769
+ viewingKey: this.viewingKey.publicKey,
770
+ spendingKey: this.spendingKey.publicKey,
771
+ timestamp: Date.now(),
772
+ };
773
+ // IMPORTANT: Save to PUBLIC path (not private user space)
774
+ // gun.get(~userPub) would be private, gun.get(userPub) is public
775
+ await new Promise((resolve, reject) => {
776
+ gun
777
+ .get(userPub)
778
+ .get(SHIP_03.NODES.STEALTH_KEYS_PUBLIC)
779
+ .put(publicKeys, (ack) => {
780
+ if (ack.err) {
781
+ console.error("Error publishing stealth keys:", ack.err);
782
+ reject(new Error(ack.err));
783
+ }
784
+ else {
785
+ console.log("✅ Public stealth keys published to Gun network");
786
+ console.log(`📍 Published at: ${userPub.slice(0, 20)}/${SHIP_03.NODES.STEALTH_KEYS_PUBLIC}`);
787
+ resolve();
788
+ }
789
+ });
790
+ });
791
+ }
792
+ catch (error) {
793
+ console.error("Error publishing stealth keys:", error);
794
+ // Don't throw - allow initialization to continue
795
+ }
796
+ }
797
+ /**
798
+ * Compute view tag for quick scanning
799
+ */
800
+ computeViewTag(ephemeralPrivateKey, viewingPublicKey) {
801
+ try {
802
+ const ephemeralWallet = new ethers_1.ethers.Wallet(ephemeralPrivateKey);
803
+ const normalizedViewingKey = normalizePublicKey(viewingPublicKey);
804
+ const sharedSecret = ephemeralWallet.signingKey.computeSharedSecret(normalizedViewingKey);
805
+ const hashedSecret = ethers_1.ethers.keccak256(sharedSecret);
806
+ return hashedSecret.slice(0, 6); // First byte as 0xNN
807
+ }
808
+ catch {
809
+ return "0x00";
810
+ }
811
+ }
812
+ /**
813
+ * Fallback method to open stealth address (manual ECDH computation)
814
+ * Used when Fluidkey is not available or fails
815
+ */
816
+ async openStealthAddressFallback(stealthAddress, normalizedEphemeralKey) {
817
+ if (!this.viewingKey || !this.spendingKey) {
818
+ throw new Error("Stealth keys not available");
819
+ }
820
+ console.log("🔧 Using fallback (manual ECDH)...");
821
+ // Step 1: Compute shared secret (viewingPrivateKey * ephemeralPublicKey)
822
+ const viewingWallet = new ethers_1.ethers.Wallet(this.viewingKey.privateKey);
823
+ const sharedSecret = viewingWallet.signingKey.computeSharedSecret(normalizedEphemeralKey);
824
+ console.log("🔑 Shared secret:", sharedSecret.slice(0, 20) + "...");
825
+ // Step 2: Hash shared secret
826
+ const hashedSecret = ethers_1.ethers.keccak256(sharedSecret);
827
+ console.log("🔑 Hashed secret:", hashedSecret.slice(0, 20) + "...");
828
+ // Step 3: Add to spending private key (mod secp256k1 order)
829
+ const stealthPrivateKey = this.addPrivateKeys(hashedSecret, this.spendingKey.privateKey);
830
+ console.log("🔑 Stealth private key:", stealthPrivateKey.slice(0, 10) + "...");
831
+ // Step 4: Create wallet
832
+ const stealthWallet = new ethers_1.ethers.Wallet(stealthPrivateKey);
833
+ console.log("✅ Fallback wallet created:", stealthWallet.address);
834
+ return stealthWallet;
835
+ }
836
+ /**
837
+ * Add two private keys using modular arithmetic (secp256k1)
838
+ */
839
+ addPrivateKeys(key1, key2) {
840
+ // secp256k1 curve order
841
+ const n = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141");
842
+ const k1 = BigInt(key1);
843
+ const k2 = BigInt(key2);
844
+ // Add and mod by curve order
845
+ const sum = (k1 + k2) % n;
846
+ // Convert back to hex with 0x prefix
847
+ return "0x" + sum.toString(16).padStart(64, "0");
848
+ }
849
+ }
850
+ exports.SHIP_03 = SHIP_03;
851
+ // GunDB Node Names for SHIP-03 storage
852
+ SHIP_03.NODES = {
853
+ STEALTH_KEYS_PUBLIC: "stealth_keys_public",
854
+ STEALTH_ANNOUNCEMENTS: "stealth_announcements",
855
+ };