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.
- package/README.md +12 -0
- package/dist/browser/shogun-core.js +108102 -43579
- package/dist/browser/shogun-core.js.map +1 -1
- package/dist/ship/examples/messenger-cli.js +171 -55
- package/dist/ship/examples/wallet-cli.js +767 -0
- package/dist/ship/implementation/SHIP_00.js +478 -0
- package/dist/ship/implementation/SHIP_01.js +275 -492
- package/dist/ship/implementation/SHIP_02.js +1366 -0
- package/dist/ship/implementation/SHIP_03.js +855 -0
- package/dist/ship/interfaces/ISHIP_00.js +135 -0
- package/dist/ship/interfaces/ISHIP_01.js +81 -24
- package/dist/ship/interfaces/ISHIP_02.js +57 -0
- package/dist/ship/interfaces/ISHIP_03.js +61 -0
- package/dist/src/gundb/db.js +55 -11
- package/dist/src/index.js +10 -2
- package/dist/src/managers/CoreInitializer.js +41 -13
- package/dist/src/storage/storage.js +22 -9
- package/dist/types/ship/examples/messenger-cli.d.ts +7 -1
- package/dist/types/ship/examples/wallet-cli.d.ts +131 -0
- package/dist/types/ship/implementation/SHIP_00.d.ts +113 -0
- package/dist/types/ship/implementation/SHIP_01.d.ts +44 -77
- package/dist/types/ship/implementation/SHIP_02.d.ts +297 -0
- package/dist/types/ship/implementation/SHIP_03.d.ts +127 -0
- package/dist/types/ship/interfaces/ISHIP_00.d.ts +410 -0
- package/dist/types/ship/interfaces/ISHIP_01.d.ts +157 -119
- package/dist/types/ship/interfaces/ISHIP_02.d.ts +470 -0
- package/dist/types/ship/interfaces/ISHIP_03.d.ts +295 -0
- package/dist/types/src/gundb/db.d.ts +10 -3
- package/dist/types/src/index.d.ts +7 -0
- package/dist/types/src/interfaces/shogun.d.ts +2 -0
- package/dist/types/src/storage/storage.d.ts +2 -1
- 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
|
+
};
|