shogun-core 3.2.2 → 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 +108133 -43791
- package/dist/browser/shogun-core.js.map +1 -1
- package/dist/ship/examples/messenger-cli.js +173 -60
- 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 +300 -695
- 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 +47 -76
- 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 +23 -9
|
@@ -0,0 +1,1366 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SHIP-02: Ethereum HD Wallet Implementation
|
|
4
|
+
*
|
|
5
|
+
* Full port of shogun-BIP44 with SHIP-00 derivation.
|
|
6
|
+
* Extends SHIP-00 to provide deterministic Ethereum address derivation.
|
|
7
|
+
*
|
|
8
|
+
* Based on:
|
|
9
|
+
* - SHIP-00 for identity foundation (replaces mnemonic dependency)
|
|
10
|
+
* - BIP-32 for hierarchical deterministic wallets
|
|
11
|
+
* - BIP-44 for multi-account hierarchy
|
|
12
|
+
* - Ethers.js for Ethereum operations
|
|
13
|
+
* - Gun/SEA for encrypted storage
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* ✅ Deterministic address derivation from SHIP-00 identity (no mnemonics needed)
|
|
17
|
+
* ✅ BIP-44 compliant HD wallet support
|
|
18
|
+
* ✅ Multiple address management
|
|
19
|
+
* ✅ Transaction signing
|
|
20
|
+
* ✅ Message signing and verification
|
|
21
|
+
* ✅ Gun persistence with encryption
|
|
22
|
+
* ✅ Export/import functionality
|
|
23
|
+
* ✅ Address book management
|
|
24
|
+
*
|
|
25
|
+
* Note: Stealth addresses moved to SHIP-03
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.SHIP_02 = void 0;
|
|
29
|
+
const ethers_1 = require("ethers");
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// IMPLEMENTATION
|
|
32
|
+
// ============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* SHIP-02 Reference Implementation
|
|
35
|
+
*
|
|
36
|
+
* Provides Ethereum address derivation on top of SHIP-00 identity.
|
|
37
|
+
* All addresses are deterministically derived from the user's SHIP-00 keypair.
|
|
38
|
+
*/
|
|
39
|
+
class SHIP_02 {
|
|
40
|
+
constructor(identity, config = {}) {
|
|
41
|
+
this.initialized = false;
|
|
42
|
+
// Master seed derived from SHIP-00 identity OR from mnemonic
|
|
43
|
+
this.masterSeed = null;
|
|
44
|
+
// Optional BIP-39 mnemonic (for MetaMask compatibility)
|
|
45
|
+
this.mnemonic = null;
|
|
46
|
+
// HD Wallet for derivation
|
|
47
|
+
this.hdWallet = null;
|
|
48
|
+
// Cache of derived addresses
|
|
49
|
+
this.addressCache = new Map();
|
|
50
|
+
// Cache of wallets by address
|
|
51
|
+
this.walletCache = new Map();
|
|
52
|
+
// Address index counter
|
|
53
|
+
this.nextIndex = 0;
|
|
54
|
+
// Persistence flag
|
|
55
|
+
this.persistToGun = false;
|
|
56
|
+
// RPC Provider for network operations
|
|
57
|
+
this.provider = null;
|
|
58
|
+
// Main wallet (derived from Gun user keys, not BIP-44)
|
|
59
|
+
this.mainWallet = null;
|
|
60
|
+
// Wallet paths storage (for createWallet/loadWallets API)
|
|
61
|
+
this.walletPaths = {};
|
|
62
|
+
this.identity = identity;
|
|
63
|
+
this.config = {
|
|
64
|
+
defaultCoinType: config.defaultCoinType ?? 60, // Ethereum
|
|
65
|
+
defaultAccount: config.defaultAccount ?? 0,
|
|
66
|
+
enableStealth: config.enableStealth ?? true,
|
|
67
|
+
customPathPrefix: config.customPathPrefix,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// ========================================================================
|
|
71
|
+
// INITIALIZATION
|
|
72
|
+
// ========================================================================
|
|
73
|
+
async initialize(useMnemonic = false) {
|
|
74
|
+
if (this.initialized) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
// Get SHIP-00 keypair
|
|
79
|
+
const keyPair = this.identity.getKeyPair();
|
|
80
|
+
if (!keyPair || !keyPair.epriv || !keyPair.epub) {
|
|
81
|
+
throw new Error("SHIP-00 identity not authenticated");
|
|
82
|
+
}
|
|
83
|
+
if (useMnemonic) {
|
|
84
|
+
// Option 1: Use BIP-39 mnemonic (MetaMask compatible)
|
|
85
|
+
console.log("🔐 Initializing with BIP-39 mnemonic...");
|
|
86
|
+
// Try to load existing mnemonic
|
|
87
|
+
this.mnemonic = await this.loadMnemonicFromGun();
|
|
88
|
+
if (!this.mnemonic) {
|
|
89
|
+
// Generate new mnemonic
|
|
90
|
+
this.mnemonic = this.generateNewMnemonic();
|
|
91
|
+
await this.saveMnemonicToGun(this.mnemonic);
|
|
92
|
+
console.log("✅ New BIP-39 mnemonic generated and saved");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log("✅ Existing mnemonic loaded");
|
|
96
|
+
}
|
|
97
|
+
// Create HD wallet from mnemonic
|
|
98
|
+
this.hdWallet = ethers_1.ethers.HDNodeWallet.fromPhrase(this.mnemonic);
|
|
99
|
+
console.log("✅ HD wallet initialized from mnemonic (MetaMask compatible)");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Option 2: Derive from SHIP-00 identity (default, no mnemonic needed)
|
|
103
|
+
console.log("🔐 Deriving wallet from SHIP-00 identity...");
|
|
104
|
+
// Derive master seed from SHIP-00 keypair
|
|
105
|
+
this.masterSeed = await this.deriveMasterSeed(keyPair);
|
|
106
|
+
// Create HD wallet from seed
|
|
107
|
+
this.hdWallet = ethers_1.ethers.HDNodeWallet.fromSeed(ethers_1.ethers.getBytes("0x" + this.masterSeed));
|
|
108
|
+
console.log("✅ HD wallet derived from SHIP-00 (no mnemonic needed)");
|
|
109
|
+
}
|
|
110
|
+
this.initialized = true;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw new Error(`SHIP-02 initialization failed: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
isInitialized() {
|
|
117
|
+
return this.initialized;
|
|
118
|
+
}
|
|
119
|
+
// ========================================================================
|
|
120
|
+
// BASIC DERIVATION
|
|
121
|
+
// ========================================================================
|
|
122
|
+
async deriveEthereumAddress(path = "m/44'/60'/0'/0/0") {
|
|
123
|
+
try {
|
|
124
|
+
this.ensureInitialized();
|
|
125
|
+
// Verify HD wallet is available
|
|
126
|
+
if (!this.hdWallet) {
|
|
127
|
+
throw new Error("HD wallet not initialized. Call initialize() first.");
|
|
128
|
+
}
|
|
129
|
+
// Derive child wallet from HD node
|
|
130
|
+
const childWallet = this.hdWallet.derivePath(path);
|
|
131
|
+
const address = childWallet.address;
|
|
132
|
+
const publicKey = childWallet.publicKey;
|
|
133
|
+
// Cache the wallet and address
|
|
134
|
+
this.walletCache.set(address, childWallet);
|
|
135
|
+
const entry = {
|
|
136
|
+
address,
|
|
137
|
+
path,
|
|
138
|
+
publicKey,
|
|
139
|
+
index: this.nextIndex++,
|
|
140
|
+
createdAt: Date.now(),
|
|
141
|
+
};
|
|
142
|
+
this.addressCache.set(address, entry);
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
address,
|
|
146
|
+
path,
|
|
147
|
+
publicKey,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: error.message,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async deriveMultipleAddresses(count, startIndex = 0) {
|
|
158
|
+
const results = [];
|
|
159
|
+
for (let i = 0; i < count; i++) {
|
|
160
|
+
const index = startIndex + i;
|
|
161
|
+
const path = this.buildBIP44Path(this.config.defaultCoinType, this.config.defaultAccount, 0, index);
|
|
162
|
+
const result = await this.deriveEthereumAddress(path);
|
|
163
|
+
results.push(result);
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
async getPrimaryAddress() {
|
|
168
|
+
this.ensureInitialized();
|
|
169
|
+
// Verify HD wallet is available
|
|
170
|
+
if (!this.hdWallet) {
|
|
171
|
+
throw new Error("HD wallet not initialized. Please call initialize() before deriving addresses.");
|
|
172
|
+
}
|
|
173
|
+
// Check if primary address already exists in cache
|
|
174
|
+
const primary = Array.from(this.addressCache.values()).find((entry) => entry.index === 0);
|
|
175
|
+
if (primary) {
|
|
176
|
+
return primary.address;
|
|
177
|
+
}
|
|
178
|
+
// Derive primary address (index 0)
|
|
179
|
+
const result = await this.deriveEthereumAddress();
|
|
180
|
+
if (!result.success || !result.address) {
|
|
181
|
+
const errorDetail = result.error || "Unknown error";
|
|
182
|
+
throw new Error(`Failed to derive primary address: ${errorDetail}`);
|
|
183
|
+
}
|
|
184
|
+
return result.address;
|
|
185
|
+
}
|
|
186
|
+
// ========================================================================
|
|
187
|
+
// BIP-44 STANDARD DERIVATION
|
|
188
|
+
// ========================================================================
|
|
189
|
+
async deriveBIP44Address(coinType = 60, account = 0, change = 0, index = 0) {
|
|
190
|
+
const path = this.buildBIP44Path(coinType, account, change, index);
|
|
191
|
+
return this.deriveEthereumAddress(path);
|
|
192
|
+
}
|
|
193
|
+
async deriveMultipleAccounts(accountCount) {
|
|
194
|
+
const results = [];
|
|
195
|
+
for (let i = 0; i < accountCount; i++) {
|
|
196
|
+
const result = await this.deriveBIP44Address(this.config.defaultCoinType, i, 0, 0);
|
|
197
|
+
results.push(result);
|
|
198
|
+
}
|
|
199
|
+
return results;
|
|
200
|
+
}
|
|
201
|
+
// ========================================================================
|
|
202
|
+
// STEALTH ADDRESSES - DEPRECATED
|
|
203
|
+
// ========================================================================
|
|
204
|
+
// Note: Basic stealth functionality moved to SHIP-03
|
|
205
|
+
// These methods are kept for backward compatibility
|
|
206
|
+
/**
|
|
207
|
+
* @deprecated Use SHIP-03 for dual-key stealth addresses
|
|
208
|
+
*/
|
|
209
|
+
async generateStealthAddress(recipientPublicKey) {
|
|
210
|
+
console.warn("⚠️ DEPRECATED: Use SHIP-03 for stealth addresses");
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
error: "Stealth addresses moved to SHIP-03. Please use SHIP-03 for dual-key stealth functionality.",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* @deprecated Use SHIP-03 for stealth operations
|
|
218
|
+
*/
|
|
219
|
+
async deriveSharedSecret(publicKey) {
|
|
220
|
+
console.warn("⚠️ DEPRECATED: Use SHIP-03 for stealth operations");
|
|
221
|
+
throw new Error("Use SHIP-03 for stealth operations");
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* @deprecated Use SHIP-03 for stealth operations
|
|
225
|
+
*/
|
|
226
|
+
async isStealthAddress(address) {
|
|
227
|
+
console.warn("⚠️ DEPRECATED: Use SHIP-03 for stealth operations");
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
// ========================================================================
|
|
231
|
+
// KEY MANAGEMENT
|
|
232
|
+
// ========================================================================
|
|
233
|
+
async getPrivateKeyForAddress(address) {
|
|
234
|
+
this.ensureInitialized();
|
|
235
|
+
const wallet = this.walletCache.get(address);
|
|
236
|
+
if (!wallet) {
|
|
237
|
+
throw new Error(`No wallet found for address: ${address}`);
|
|
238
|
+
}
|
|
239
|
+
return wallet.privateKey;
|
|
240
|
+
}
|
|
241
|
+
async getPublicKeyForAddress(address) {
|
|
242
|
+
this.ensureInitialized();
|
|
243
|
+
const entry = this.addressCache.get(address);
|
|
244
|
+
if (!entry) {
|
|
245
|
+
throw new Error(`No address found: ${address}`);
|
|
246
|
+
}
|
|
247
|
+
return entry.publicKey;
|
|
248
|
+
}
|
|
249
|
+
async getPathForAddress(address) {
|
|
250
|
+
const entry = this.addressCache.get(address);
|
|
251
|
+
return entry?.path;
|
|
252
|
+
}
|
|
253
|
+
// ========================================================================
|
|
254
|
+
// TRANSACTION SIGNING
|
|
255
|
+
// ========================================================================
|
|
256
|
+
async signTransaction(tx, address) {
|
|
257
|
+
try {
|
|
258
|
+
this.ensureInitialized();
|
|
259
|
+
const wallet = this.walletCache.get(address);
|
|
260
|
+
if (!wallet) {
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
error: `No wallet found for address: ${address}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// Sign the transaction
|
|
267
|
+
const signedTx = await wallet.signTransaction(tx);
|
|
268
|
+
const parsedTx = ethers_1.ethers.Transaction.from(signedTx);
|
|
269
|
+
const result = {
|
|
270
|
+
raw: signedTx,
|
|
271
|
+
hash: parsedTx.hash,
|
|
272
|
+
from: wallet.address,
|
|
273
|
+
to: tx.to,
|
|
274
|
+
value: tx.value?.toString() || "0",
|
|
275
|
+
data: tx.data || "0x",
|
|
276
|
+
chainId: tx.chainId || 1,
|
|
277
|
+
nonce: tx.nonce || 0,
|
|
278
|
+
gasLimit: tx.gasLimit?.toString() || "21000",
|
|
279
|
+
signature: {
|
|
280
|
+
r: parsedTx.signature.r,
|
|
281
|
+
s: parsedTx.signature.s,
|
|
282
|
+
v: parsedTx.signature.v,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
success: true,
|
|
287
|
+
signature: parsedTx.signature.serialized,
|
|
288
|
+
signedTransaction: signedTx,
|
|
289
|
+
txHash: parsedTx.hash,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return {
|
|
294
|
+
success: false,
|
|
295
|
+
error: error.message,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Send transaction to Ethereum network
|
|
301
|
+
* Combines signing + broadcasting in one step
|
|
302
|
+
*/
|
|
303
|
+
async sendTransaction(tx, address, waitForConfirmation = false) {
|
|
304
|
+
try {
|
|
305
|
+
this.ensureInitialized();
|
|
306
|
+
// Verify RPC provider is configured
|
|
307
|
+
if (!this.provider) {
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
error: "RPC provider not configured. Call setRpcUrl() first.",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// Get wallet for address
|
|
314
|
+
const wallet = this.walletCache.get(address);
|
|
315
|
+
if (!wallet) {
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
error: `No wallet found for address: ${address}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
console.log(`📤 Sending transaction from ${address}...`);
|
|
322
|
+
console.log(` To: ${tx.to}`);
|
|
323
|
+
console.log(` Value: ${tx.value ? ethers_1.ethers.formatEther(tx.value) : "0"} ETH`);
|
|
324
|
+
// Connect wallet to provider
|
|
325
|
+
const connectedWallet = wallet.connect(this.provider);
|
|
326
|
+
// Send transaction (ethers handles signing + broadcasting)
|
|
327
|
+
const txResponse = await connectedWallet.sendTransaction(tx);
|
|
328
|
+
console.log(`✅ Transaction sent: ${txResponse.hash}`);
|
|
329
|
+
console.log(`📍 View on Etherscan: https://etherscan.io/tx/${txResponse.hash}`);
|
|
330
|
+
// Wait for confirmation if requested
|
|
331
|
+
if (waitForConfirmation) {
|
|
332
|
+
console.log(`⏳ Waiting for confirmation...`);
|
|
333
|
+
const receipt = await txResponse.wait();
|
|
334
|
+
console.log(`✅ Transaction confirmed in block ${receipt.blockNumber}`);
|
|
335
|
+
console.log(` Gas used: ${receipt.gasUsed.toString()}`);
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
txHash: txResponse.hash,
|
|
339
|
+
receipt: receipt,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
success: true,
|
|
344
|
+
txHash: txResponse.hash,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
console.error("❌ Transaction failed:", error);
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
error: error.message,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async signMessage(message, address) {
|
|
356
|
+
this.ensureInitialized();
|
|
357
|
+
const wallet = this.walletCache.get(address);
|
|
358
|
+
if (!wallet) {
|
|
359
|
+
throw new Error(`No wallet found for address: ${address}`);
|
|
360
|
+
}
|
|
361
|
+
if (typeof message === "string") {
|
|
362
|
+
return wallet.signMessage(message);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
return wallet.signMessage(message);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async verifySignature(message, signature, address) {
|
|
369
|
+
try {
|
|
370
|
+
let recoveredAddress;
|
|
371
|
+
if (typeof message === "string") {
|
|
372
|
+
recoveredAddress = ethers_1.ethers.verifyMessage(message, signature);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
recoveredAddress = ethers_1.ethers.verifyMessage(message, signature);
|
|
376
|
+
}
|
|
377
|
+
return recoveredAddress.toLowerCase() === address.toLowerCase();
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ========================================================================
|
|
384
|
+
// ADDRESS MANAGEMENT
|
|
385
|
+
// ========================================================================
|
|
386
|
+
async getAllAddresses() {
|
|
387
|
+
return Array.from(this.addressCache.values()).sort((a, b) => a.index - b.index);
|
|
388
|
+
}
|
|
389
|
+
async getAddressByIndex(index) {
|
|
390
|
+
return Array.from(this.addressCache.values()).find((entry) => entry.index === index);
|
|
391
|
+
}
|
|
392
|
+
async setAddressLabel(address, label) {
|
|
393
|
+
const entry = this.addressCache.get(address);
|
|
394
|
+
if (entry) {
|
|
395
|
+
entry.label = label;
|
|
396
|
+
this.addressCache.set(address, entry);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async exportAddressBook() {
|
|
400
|
+
this.ensureInitialized();
|
|
401
|
+
const addressBook = {
|
|
402
|
+
addresses: await this.getAllAddresses(),
|
|
403
|
+
masterPublicKey: this.hdWallet.publicKey,
|
|
404
|
+
derivationMethod: "bip44",
|
|
405
|
+
};
|
|
406
|
+
// Optionally save to Gun for backup
|
|
407
|
+
if (this.persistToGun) {
|
|
408
|
+
await this.saveAddressBookToGun(addressBook);
|
|
409
|
+
}
|
|
410
|
+
return addressBook;
|
|
411
|
+
}
|
|
412
|
+
async importAddressBook(addressBook) {
|
|
413
|
+
this.ensureInitialized();
|
|
414
|
+
// Verify master public key matches
|
|
415
|
+
if (addressBook.masterPublicKey !== this.hdWallet.publicKey) {
|
|
416
|
+
throw new Error("Address book master public key mismatch");
|
|
417
|
+
}
|
|
418
|
+
// Re-derive all addresses from the book
|
|
419
|
+
for (const entry of addressBook.addresses) {
|
|
420
|
+
if (entry.path.startsWith("stealth/")) {
|
|
421
|
+
// Skip stealth addresses (moved to SHIP-03)
|
|
422
|
+
console.warn(`Skipping stealth address: ${entry.address}`);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
await this.deriveEthereumAddress(entry.path);
|
|
426
|
+
if (entry.label) {
|
|
427
|
+
await this.setAddressLabel(entry.address, entry.label);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
console.log(`✅ Imported ${addressBook.addresses.length} addresses`);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Enable persistence to Gun database
|
|
434
|
+
*/
|
|
435
|
+
enableGunPersistence() {
|
|
436
|
+
this.persistToGun = true;
|
|
437
|
+
console.log("✅ Gun persistence enabled for SHIP-02");
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Disable persistence to Gun database
|
|
441
|
+
*/
|
|
442
|
+
disableGunPersistence() {
|
|
443
|
+
this.persistToGun = false;
|
|
444
|
+
console.log("✅ Gun persistence disabled for SHIP-02");
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Save address book to Gun (private storage)
|
|
448
|
+
*/
|
|
449
|
+
async saveAddressBookToGun(addressBook) {
|
|
450
|
+
try {
|
|
451
|
+
// Access Gun through identity
|
|
452
|
+
const shogun = this.identity.getShogun();
|
|
453
|
+
const gun = shogun?.db?.gun;
|
|
454
|
+
if (!gun) {
|
|
455
|
+
console.warn("Gun not available, skipping persistence");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const user = gun.user();
|
|
459
|
+
if (!user || !user.is) {
|
|
460
|
+
console.warn("User not authenticated on Gun");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
// Save encrypted addressbook
|
|
464
|
+
await user.get(SHIP_02.NODES.ADDRESS_BOOK).put(JSON.stringify(addressBook));
|
|
465
|
+
console.log("✅ Address book saved to Gun");
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
console.error("Error saving address book to Gun:", error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Load address book from Gun
|
|
473
|
+
*/
|
|
474
|
+
async loadAddressBookFromGun() {
|
|
475
|
+
try {
|
|
476
|
+
this.ensureInitialized();
|
|
477
|
+
const shogun = this.identity.getShogun();
|
|
478
|
+
const gun = shogun?.db?.gun;
|
|
479
|
+
if (!gun) {
|
|
480
|
+
console.warn("Gun not available");
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
const user = gun.user();
|
|
484
|
+
if (!user || !user.is) {
|
|
485
|
+
console.warn("User not authenticated on Gun");
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
// Load addressbook
|
|
489
|
+
const data = await new Promise((resolve) => {
|
|
490
|
+
let resolved = false;
|
|
491
|
+
const timeout = setTimeout(() => {
|
|
492
|
+
if (!resolved) {
|
|
493
|
+
resolved = true;
|
|
494
|
+
resolve(null);
|
|
495
|
+
}
|
|
496
|
+
}, 5000);
|
|
497
|
+
user.get(SHIP_02.NODES.ADDRESS_BOOK).once((data) => {
|
|
498
|
+
if (!resolved) {
|
|
499
|
+
resolved = true;
|
|
500
|
+
clearTimeout(timeout);
|
|
501
|
+
resolve(data || null);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
if (!data) {
|
|
506
|
+
console.log("No address book found on Gun");
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const addressBook = JSON.parse(data);
|
|
510
|
+
console.log("✅ Address book loaded from Gun");
|
|
511
|
+
return addressBook;
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
console.error("Error loading address book from Gun:", error);
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Sync local cache with Gun storage
|
|
520
|
+
*/
|
|
521
|
+
async syncWithGun() {
|
|
522
|
+
this.ensureInitialized();
|
|
523
|
+
// Try to load from Gun first
|
|
524
|
+
const remoteBook = await this.loadAddressBookFromGun();
|
|
525
|
+
if (remoteBook) {
|
|
526
|
+
// Merge remote addresses with local
|
|
527
|
+
await this.importAddressBook(remoteBook);
|
|
528
|
+
}
|
|
529
|
+
// Save current state back to Gun
|
|
530
|
+
if (this.persistToGun) {
|
|
531
|
+
await this.exportAddressBook();
|
|
532
|
+
}
|
|
533
|
+
console.log("✅ Synced with Gun");
|
|
534
|
+
}
|
|
535
|
+
// ========================================================================
|
|
536
|
+
// UTILITIES
|
|
537
|
+
// ========================================================================
|
|
538
|
+
async ownsAddress(address) {
|
|
539
|
+
return this.addressCache.has(address);
|
|
540
|
+
}
|
|
541
|
+
async getMasterPublicKey() {
|
|
542
|
+
this.ensureInitialized();
|
|
543
|
+
return this.hdWallet.publicKey;
|
|
544
|
+
}
|
|
545
|
+
async clearCache() {
|
|
546
|
+
this.addressCache.clear();
|
|
547
|
+
this.walletCache.clear();
|
|
548
|
+
this.walletPaths = {};
|
|
549
|
+
this.nextIndex = 0;
|
|
550
|
+
this.initialized = false;
|
|
551
|
+
this.masterSeed = null;
|
|
552
|
+
this.hdWallet = null;
|
|
553
|
+
this.mainWallet = null;
|
|
554
|
+
this.provider = null;
|
|
555
|
+
console.log("✅ SHIP-02 cache cleared");
|
|
556
|
+
}
|
|
557
|
+
// ========================================================================
|
|
558
|
+
// ADVANCED FEATURES (from shogun-BIP44)
|
|
559
|
+
// ========================================================================
|
|
560
|
+
// ========================================================================
|
|
561
|
+
// MNEMONIC MANAGEMENT (BIP-39)
|
|
562
|
+
// ========================================================================
|
|
563
|
+
/**
|
|
564
|
+
* Generate new BIP-39 mnemonic (12 words)
|
|
565
|
+
* Compatible with MetaMask and other wallets
|
|
566
|
+
*/
|
|
567
|
+
generateNewMnemonic() {
|
|
568
|
+
const wallet = ethers_1.ethers.Wallet.createRandom();
|
|
569
|
+
return wallet.mnemonic?.phrase || "";
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Get addresses that would be derived from a mnemonic (standard BIP-44)
|
|
573
|
+
* Useful to verify compatibility with MetaMask
|
|
574
|
+
*/
|
|
575
|
+
getStandardBIP44Addresses(mnemonic, count = 5) {
|
|
576
|
+
const addresses = [];
|
|
577
|
+
for (let i = 0; i < count; i++) {
|
|
578
|
+
const path = `m/44'/60'/0'/0/${i}`;
|
|
579
|
+
const wallet = ethers_1.ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, path);
|
|
580
|
+
addresses.push(wallet.address);
|
|
581
|
+
}
|
|
582
|
+
return addresses;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Get current mnemonic (if using mnemonic mode)
|
|
586
|
+
*/
|
|
587
|
+
async getMnemonic() {
|
|
588
|
+
return this.mnemonic;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Get user's master mnemonic from Gun or localStorage
|
|
592
|
+
* Used by createWallet/loadWallets for frontend compatibility
|
|
593
|
+
*/
|
|
594
|
+
async getUserMasterMnemonic() {
|
|
595
|
+
try {
|
|
596
|
+
// First check if already in memory
|
|
597
|
+
if (this.mnemonic) {
|
|
598
|
+
return this.mnemonic;
|
|
599
|
+
}
|
|
600
|
+
// Try to load from Gun
|
|
601
|
+
const gunMnemonic = await this.loadMnemonicFromGun();
|
|
602
|
+
if (gunMnemonic) {
|
|
603
|
+
this.mnemonic = gunMnemonic;
|
|
604
|
+
return gunMnemonic;
|
|
605
|
+
}
|
|
606
|
+
// Try localStorage as fallback
|
|
607
|
+
if (typeof localStorage !== "undefined") {
|
|
608
|
+
const storageKey = `shogun_master_mnemonic_${this.getStorageUserIdentifier()}`;
|
|
609
|
+
const encryptedMnemonic = localStorage.getItem(storageKey);
|
|
610
|
+
if (encryptedMnemonic) {
|
|
611
|
+
const decrypted = await this.decryptSensitiveData(encryptedMnemonic);
|
|
612
|
+
if (decrypted) {
|
|
613
|
+
this.mnemonic = decrypted;
|
|
614
|
+
// Sync back to Gun
|
|
615
|
+
await this.saveMnemonicToGun(decrypted);
|
|
616
|
+
return decrypted;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
console.error("Error retrieving master mnemonic:", error);
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Export mnemonic (encrypted)
|
|
629
|
+
*/
|
|
630
|
+
async exportMnemonic() {
|
|
631
|
+
if (!this.mnemonic) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
return await this.encryptSensitiveData(this.mnemonic);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Import mnemonic and re-initialize wallet
|
|
638
|
+
*/
|
|
639
|
+
async importMnemonic(encryptedMnemonic) {
|
|
640
|
+
const decrypted = await this.decryptSensitiveData(encryptedMnemonic);
|
|
641
|
+
if (!decrypted) {
|
|
642
|
+
throw new Error("Failed to decrypt mnemonic");
|
|
643
|
+
}
|
|
644
|
+
// Validate mnemonic
|
|
645
|
+
const words = decrypted.trim().split(/\s+/);
|
|
646
|
+
if (words.length !== 12 && words.length !== 24) {
|
|
647
|
+
throw new Error("Invalid mnemonic (must be 12 or 24 words)");
|
|
648
|
+
}
|
|
649
|
+
this.mnemonic = decrypted;
|
|
650
|
+
// Re-initialize with mnemonic
|
|
651
|
+
this.initialized = false;
|
|
652
|
+
await this.initialize(true);
|
|
653
|
+
console.log("✅ Mnemonic imported and wallet re-initialized");
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Save mnemonic to Gun (encrypted)
|
|
657
|
+
*/
|
|
658
|
+
async saveMnemonicToGun(mnemonic) {
|
|
659
|
+
try {
|
|
660
|
+
const shogun = this.identity.getShogun();
|
|
661
|
+
const gun = shogun?.db?.gun;
|
|
662
|
+
if (!gun)
|
|
663
|
+
return;
|
|
664
|
+
const user = gun.user();
|
|
665
|
+
if (!user || !user.is)
|
|
666
|
+
return;
|
|
667
|
+
const encrypted = await this.encryptSensitiveData(mnemonic);
|
|
668
|
+
await user.get(SHIP_02.NODES.MNEMONIC).put(encrypted);
|
|
669
|
+
console.log("✅ Mnemonic saved to Gun (encrypted)");
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
console.error("Error saving mnemonic:", error);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Load mnemonic from Gun (encrypted)
|
|
677
|
+
*/
|
|
678
|
+
async loadMnemonicFromGun() {
|
|
679
|
+
try {
|
|
680
|
+
const shogun = this.identity.getShogun();
|
|
681
|
+
const gun = shogun?.db?.gun;
|
|
682
|
+
if (!gun)
|
|
683
|
+
return null;
|
|
684
|
+
const user = gun.user();
|
|
685
|
+
if (!user || !user.is)
|
|
686
|
+
return null;
|
|
687
|
+
const encrypted = await new Promise((resolve) => {
|
|
688
|
+
let resolved = false;
|
|
689
|
+
const timeout = setTimeout(() => {
|
|
690
|
+
if (!resolved) {
|
|
691
|
+
resolved = true;
|
|
692
|
+
resolve(null);
|
|
693
|
+
}
|
|
694
|
+
}, 5000);
|
|
695
|
+
user.get(SHIP_02.NODES.MNEMONIC).once((data) => {
|
|
696
|
+
if (!resolved) {
|
|
697
|
+
resolved = true;
|
|
698
|
+
clearTimeout(timeout);
|
|
699
|
+
resolve(data || null);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
if (!encrypted)
|
|
704
|
+
return null;
|
|
705
|
+
return await this.decryptSensitiveData(encrypted);
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
console.error("Error loading mnemonic:", error);
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Get storage identifier for current user
|
|
714
|
+
*/
|
|
715
|
+
getStorageUserIdentifier() {
|
|
716
|
+
const currentUser = this.identity.getCurrentUser();
|
|
717
|
+
const pub = currentUser?.pub;
|
|
718
|
+
if (pub) {
|
|
719
|
+
return pub.substring(0, 12); // Use part of the public key
|
|
720
|
+
}
|
|
721
|
+
return "guest"; // Identifier for unauthenticated users
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Encrypt sensitive data using SEA
|
|
725
|
+
*/
|
|
726
|
+
async encryptSensitiveData(text) {
|
|
727
|
+
try {
|
|
728
|
+
const shogun = this.identity.getShogun();
|
|
729
|
+
const crypto = shogun?.db?.crypto;
|
|
730
|
+
const keyPair = this.identity.getKeyPair();
|
|
731
|
+
if (!crypto || !keyPair) {
|
|
732
|
+
throw new Error("Crypto or keypair not available");
|
|
733
|
+
}
|
|
734
|
+
// Encrypt with user's keys
|
|
735
|
+
const encrypted = await crypto.encrypt(text, keyPair);
|
|
736
|
+
if (!encrypted) {
|
|
737
|
+
throw new Error("Encryption failed");
|
|
738
|
+
}
|
|
739
|
+
return JSON.stringify(encrypted);
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
console.error("Error encrypting data:", error);
|
|
743
|
+
throw new Error(`Encryption failed: ${error.message}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Decrypt sensitive data using SEA
|
|
748
|
+
*/
|
|
749
|
+
async decryptSensitiveData(encryptedText) {
|
|
750
|
+
try {
|
|
751
|
+
const shogun = this.identity.getShogun();
|
|
752
|
+
const crypto = shogun?.db?.crypto;
|
|
753
|
+
const keyPair = this.identity.getKeyPair();
|
|
754
|
+
if (!crypto || !keyPair) {
|
|
755
|
+
throw new Error("Crypto or keypair not available");
|
|
756
|
+
}
|
|
757
|
+
const encrypted = JSON.parse(encryptedText);
|
|
758
|
+
const decrypted = await crypto.decrypt(encrypted, keyPair);
|
|
759
|
+
if (!decrypted) {
|
|
760
|
+
throw new Error("Decryption failed");
|
|
761
|
+
}
|
|
762
|
+
return decrypted;
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
console.error("Error decrypting data:", error);
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Export master seed (encrypted)
|
|
771
|
+
* SECURITY: Handle with extreme care!
|
|
772
|
+
*/
|
|
773
|
+
async exportMasterSeed() {
|
|
774
|
+
this.ensureInitialized();
|
|
775
|
+
if (!this.masterSeed) {
|
|
776
|
+
throw new Error("Master seed not available");
|
|
777
|
+
}
|
|
778
|
+
// Encrypt the seed
|
|
779
|
+
return await this.encryptSensitiveData(this.masterSeed);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Export all wallet data (encrypted)
|
|
783
|
+
*/
|
|
784
|
+
async exportWalletData() {
|
|
785
|
+
this.ensureInitialized();
|
|
786
|
+
const data = {
|
|
787
|
+
addressBook: await this.exportAddressBook(),
|
|
788
|
+
masterPublicKey: this.hdWallet.publicKey,
|
|
789
|
+
timestamp: Date.now(),
|
|
790
|
+
};
|
|
791
|
+
return await this.encryptSensitiveData(JSON.stringify(data));
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Import wallet data (encrypted)
|
|
795
|
+
*/
|
|
796
|
+
async importWalletData(encryptedData) {
|
|
797
|
+
this.ensureInitialized();
|
|
798
|
+
const decrypted = await this.decryptSensitiveData(encryptedData);
|
|
799
|
+
if (!decrypted) {
|
|
800
|
+
throw new Error("Failed to decrypt wallet data");
|
|
801
|
+
}
|
|
802
|
+
const data = JSON.parse(decrypted);
|
|
803
|
+
// Verify master public key matches
|
|
804
|
+
if (data.masterPublicKey !== this.hdWallet.publicKey) {
|
|
805
|
+
throw new Error("Master public key mismatch - wrong identity");
|
|
806
|
+
}
|
|
807
|
+
// Import address book
|
|
808
|
+
await this.importAddressBook(data.addressBook);
|
|
809
|
+
console.log("✅ Wallet data imported successfully");
|
|
810
|
+
}
|
|
811
|
+
// ========================================================================
|
|
812
|
+
// PRIVATE HELPERS
|
|
813
|
+
// ========================================================================
|
|
814
|
+
/**
|
|
815
|
+
* Ensure system is initialized
|
|
816
|
+
*/
|
|
817
|
+
ensureInitialized() {
|
|
818
|
+
if (!this.initialized) {
|
|
819
|
+
throw new Error("SHIP-02 not initialized. Call initialize() first.");
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Derive master seed from SHIP-00 keypair
|
|
824
|
+
*/
|
|
825
|
+
async deriveMasterSeed(keyPair) {
|
|
826
|
+
// Create deterministic seed from SHIP-00 keypair
|
|
827
|
+
// Combine public and private encryption keys
|
|
828
|
+
const seedMaterial = keyPair.epub + keyPair.epriv;
|
|
829
|
+
// Hash to create 32-byte seed
|
|
830
|
+
const seed = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(seedMaterial));
|
|
831
|
+
// Remove 0x prefix
|
|
832
|
+
return seed.slice(2);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Build BIP-44 derivation path
|
|
836
|
+
*/
|
|
837
|
+
buildBIP44Path(coinType, account, change, index) {
|
|
838
|
+
if (this.config.customPathPrefix) {
|
|
839
|
+
return `${this.config.customPathPrefix}/${account}'/${change}/${index}`;
|
|
840
|
+
}
|
|
841
|
+
return `m/44'/${coinType}'/${account}'/${change}/${index}`;
|
|
842
|
+
}
|
|
843
|
+
// ========================================================================
|
|
844
|
+
// RPC PROVIDER MANAGEMENT
|
|
845
|
+
// ========================================================================
|
|
846
|
+
/**
|
|
847
|
+
* Set RPC provider URL
|
|
848
|
+
*/
|
|
849
|
+
async setRpcUrl(rpcUrl) {
|
|
850
|
+
try {
|
|
851
|
+
this.provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
|
|
852
|
+
console.log(`✅ RPC Provider configured: ${rpcUrl}`);
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
console.error("Error setting RPC URL:", error);
|
|
856
|
+
throw new Error(`Failed to set RPC URL: ${error.message}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Get current RPC provider
|
|
861
|
+
*/
|
|
862
|
+
getProvider() {
|
|
863
|
+
return this.provider;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Get signer (main wallet connected to provider)
|
|
867
|
+
*/
|
|
868
|
+
getSigner() {
|
|
869
|
+
if (!this.provider) {
|
|
870
|
+
throw new Error("Provider not configured. Call setRpcUrl() first.");
|
|
871
|
+
}
|
|
872
|
+
const mainWallet = this.getMainWallet();
|
|
873
|
+
return mainWallet.connect(this.provider);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Set custom signer
|
|
877
|
+
*/
|
|
878
|
+
async setSigner(signer) {
|
|
879
|
+
if (!this.provider) {
|
|
880
|
+
throw new Error("Provider not configured. Call setRpcUrl() first.");
|
|
881
|
+
}
|
|
882
|
+
// Note: The signer will use the configured provider
|
|
883
|
+
console.log(`✅ Custom signer set: ${signer.address}`);
|
|
884
|
+
}
|
|
885
|
+
// ========================================================================
|
|
886
|
+
// FRONTEND-FRIENDLY WALLET MANAGEMENT
|
|
887
|
+
// ========================================================================
|
|
888
|
+
/**
|
|
889
|
+
* Get main wallet (derived from Gun user keys, not BIP-44)
|
|
890
|
+
* This provides a consistent "main" wallet independent of HD derivation
|
|
891
|
+
*/
|
|
892
|
+
getMainWallet() {
|
|
893
|
+
if (!this.mainWallet) {
|
|
894
|
+
const shogun = this.identity.getShogun();
|
|
895
|
+
const gun = shogun?.db?.gun;
|
|
896
|
+
if (!gun) {
|
|
897
|
+
throw new Error("Gun not available");
|
|
898
|
+
}
|
|
899
|
+
const user = gun.user();
|
|
900
|
+
if (!user || !user.is) {
|
|
901
|
+
throw new Error("User not authenticated");
|
|
902
|
+
}
|
|
903
|
+
// Check SEA keys availability
|
|
904
|
+
if (!user._ || !user._.sea || !user._.sea.priv || !user._.sea.pub) {
|
|
905
|
+
throw new Error("Insufficient user data to generate main wallet");
|
|
906
|
+
}
|
|
907
|
+
// Create deterministic seed from Gun user keys
|
|
908
|
+
const userSeed = user._.sea.priv;
|
|
909
|
+
const userPub = user._.sea.pub;
|
|
910
|
+
const userAlias = user.is.alias;
|
|
911
|
+
const seed = `${userSeed}|${userPub}|${userAlias}`;
|
|
912
|
+
// Generate private key from seed
|
|
913
|
+
const privateKey = this.generatePrivateKeyFromString(seed);
|
|
914
|
+
this.mainWallet = new ethers_1.ethers.Wallet(privateKey);
|
|
915
|
+
}
|
|
916
|
+
return this.mainWallet;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Get main wallet credentials
|
|
920
|
+
*/
|
|
921
|
+
getMainWalletCredentials() {
|
|
922
|
+
const wallet = this.getMainWallet();
|
|
923
|
+
return {
|
|
924
|
+
address: wallet.address,
|
|
925
|
+
priv: wallet.privateKey,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Create new wallet with auto-incremented index
|
|
930
|
+
* Frontend-friendly API that returns ready-to-use wallet object
|
|
931
|
+
*/
|
|
932
|
+
async createWallet() {
|
|
933
|
+
this.ensureInitialized();
|
|
934
|
+
const shogun = this.identity.getShogun();
|
|
935
|
+
const gun = shogun?.db?.gun;
|
|
936
|
+
const user = gun?.user();
|
|
937
|
+
if (!user || !user.is) {
|
|
938
|
+
throw new Error("User not authenticated");
|
|
939
|
+
}
|
|
940
|
+
// Get next index
|
|
941
|
+
const nextIndex = Object.keys(this.walletPaths).length;
|
|
942
|
+
const path = `m/44'/60'/0'/0/${nextIndex}`;
|
|
943
|
+
// Get or generate mnemonic
|
|
944
|
+
let mnemonic = await this.getMnemonic();
|
|
945
|
+
if (!mnemonic) {
|
|
946
|
+
// If no mnemonic, use SHIP-00 derived wallet
|
|
947
|
+
await this.initialize(false);
|
|
948
|
+
mnemonic = await this.getMnemonic();
|
|
949
|
+
}
|
|
950
|
+
// Derive wallet
|
|
951
|
+
let wallet;
|
|
952
|
+
if (mnemonic) {
|
|
953
|
+
wallet = ethers_1.ethers.HDNodeWallet.fromPhrase(mnemonic, path);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
// Fallback: derive from HD wallet
|
|
957
|
+
wallet = this.hdWallet.derivePath(path);
|
|
958
|
+
}
|
|
959
|
+
// Store path
|
|
960
|
+
this.walletPaths[wallet.address] = {
|
|
961
|
+
path,
|
|
962
|
+
created: Date.now(),
|
|
963
|
+
};
|
|
964
|
+
// Cache wallet
|
|
965
|
+
this.walletCache.set(wallet.address, wallet);
|
|
966
|
+
this.addressCache.set(wallet.address, {
|
|
967
|
+
address: wallet.address,
|
|
968
|
+
path,
|
|
969
|
+
publicKey: wallet.publicKey,
|
|
970
|
+
index: nextIndex,
|
|
971
|
+
createdAt: Date.now(),
|
|
972
|
+
});
|
|
973
|
+
// Save to Gun
|
|
974
|
+
if (this.persistToGun) {
|
|
975
|
+
await this.saveWalletPathsToGun();
|
|
976
|
+
}
|
|
977
|
+
console.log(`✅ Created wallet #${nextIndex}: ${wallet.address}`);
|
|
978
|
+
return {
|
|
979
|
+
wallet,
|
|
980
|
+
path,
|
|
981
|
+
address: wallet.address,
|
|
982
|
+
publicKey: wallet.publicKey,
|
|
983
|
+
index: nextIndex,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Load all wallets from stored paths
|
|
988
|
+
* Reconstructs wallet objects from mnemonic/seed and paths
|
|
989
|
+
*/
|
|
990
|
+
async loadWallets() {
|
|
991
|
+
this.ensureInitialized();
|
|
992
|
+
const wallets = [];
|
|
993
|
+
const mnemonic = await this.getMnemonic();
|
|
994
|
+
for (const [address, pathData] of Object.entries(this.walletPaths)) {
|
|
995
|
+
let wallet;
|
|
996
|
+
if (mnemonic) {
|
|
997
|
+
// Derive from mnemonic
|
|
998
|
+
wallet = ethers_1.ethers.HDNodeWallet.fromPhrase(mnemonic, pathData.path);
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
// Derive from HD wallet (SHIP-00 based)
|
|
1002
|
+
wallet = this.hdWallet.derivePath(pathData.path);
|
|
1003
|
+
}
|
|
1004
|
+
wallets.push({
|
|
1005
|
+
wallet,
|
|
1006
|
+
path: pathData.path,
|
|
1007
|
+
address: wallet.address,
|
|
1008
|
+
publicKey: wallet.publicKey,
|
|
1009
|
+
});
|
|
1010
|
+
// Update caches
|
|
1011
|
+
this.walletCache.set(wallet.address, wallet);
|
|
1012
|
+
}
|
|
1013
|
+
console.log(`✅ Loaded ${wallets.length} wallets`);
|
|
1014
|
+
return wallets;
|
|
1015
|
+
}
|
|
1016
|
+
// ========================================================================
|
|
1017
|
+
// ADVANCED EXPORT/IMPORT
|
|
1018
|
+
// ========================================================================
|
|
1019
|
+
/**
|
|
1020
|
+
* Export wallet keys for all derived addresses
|
|
1021
|
+
*/
|
|
1022
|
+
async exportWalletKeys() {
|
|
1023
|
+
this.ensureInitialized();
|
|
1024
|
+
const walletKeys = await this.loadWallets();
|
|
1025
|
+
const exportData = walletKeys.map((w) => ({
|
|
1026
|
+
address: w.address,
|
|
1027
|
+
privateKey: w.wallet.privateKey,
|
|
1028
|
+
path: w.path,
|
|
1029
|
+
publicKey: w.publicKey,
|
|
1030
|
+
}));
|
|
1031
|
+
return JSON.stringify(exportData);
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Export Gun SEA keypair
|
|
1035
|
+
*/
|
|
1036
|
+
async exportGunPair() {
|
|
1037
|
+
const keyPair = this.identity.getKeyPair();
|
|
1038
|
+
if (!keyPair) {
|
|
1039
|
+
throw new Error("No keypair available");
|
|
1040
|
+
}
|
|
1041
|
+
return JSON.stringify({
|
|
1042
|
+
pub: keyPair.pub,
|
|
1043
|
+
priv: keyPair.priv,
|
|
1044
|
+
epub: keyPair.epub,
|
|
1045
|
+
epriv: keyPair.epriv,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Export all user data (mnemonic, wallets, Gun pair)
|
|
1050
|
+
*/
|
|
1051
|
+
async exportAllUserData() {
|
|
1052
|
+
this.ensureInitialized();
|
|
1053
|
+
const data = {
|
|
1054
|
+
mnemonic: await this.exportMnemonic(),
|
|
1055
|
+
walletKeys: await this.exportWalletKeys(),
|
|
1056
|
+
gunPair: await this.exportGunPair(),
|
|
1057
|
+
addressBook: await this.exportAddressBook(),
|
|
1058
|
+
masterPublicKey: this.hdWallet.publicKey,
|
|
1059
|
+
timestamp: Date.now(),
|
|
1060
|
+
};
|
|
1061
|
+
// Encrypt the entire backup
|
|
1062
|
+
return await this.encryptSensitiveData(JSON.stringify(data));
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Import wallet keys and restore wallets
|
|
1066
|
+
*/
|
|
1067
|
+
async importWalletKeys(walletsData) {
|
|
1068
|
+
this.ensureInitialized();
|
|
1069
|
+
const wallets = JSON.parse(walletsData);
|
|
1070
|
+
let count = 0;
|
|
1071
|
+
for (const walletData of wallets) {
|
|
1072
|
+
// Store path
|
|
1073
|
+
this.walletPaths[walletData.address] = {
|
|
1074
|
+
path: walletData.path,
|
|
1075
|
+
created: Date.now(),
|
|
1076
|
+
};
|
|
1077
|
+
// Re-derive wallet
|
|
1078
|
+
await this.deriveEthereumAddress(walletData.path);
|
|
1079
|
+
count++;
|
|
1080
|
+
}
|
|
1081
|
+
// Save to Gun
|
|
1082
|
+
if (this.persistToGun) {
|
|
1083
|
+
await this.saveWalletPathsToGun();
|
|
1084
|
+
}
|
|
1085
|
+
console.log(`✅ Imported ${count} wallet keys`);
|
|
1086
|
+
return count;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Import Gun SEA keypair
|
|
1090
|
+
* Note: This is a placeholder for compatibility
|
|
1091
|
+
* SHIP-02 doesn't manage Gun keys directly (use SHIP-00 for that)
|
|
1092
|
+
*/
|
|
1093
|
+
async importGunPair(pairData) {
|
|
1094
|
+
try {
|
|
1095
|
+
const pair = JSON.parse(pairData);
|
|
1096
|
+
// Validate Gun pair structure
|
|
1097
|
+
if (!pair.pub || !pair.priv || !pair.epub || !pair.epriv) {
|
|
1098
|
+
throw new Error("Invalid Gun pair structure");
|
|
1099
|
+
}
|
|
1100
|
+
console.log("⚠️ Gun pair import detected");
|
|
1101
|
+
console.log("💡 Gun keypair management is handled by SHIP-00");
|
|
1102
|
+
console.log("💡 Use identity.importKeyPair() instead for Gun key restoration");
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
console.error("Error importing Gun pair:", error);
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Import all user data from backup
|
|
1112
|
+
*/
|
|
1113
|
+
async importAllUserData(backupData, options = { importMnemonic: true, importWallets: true, importGunPair: true }) {
|
|
1114
|
+
try {
|
|
1115
|
+
// Decrypt backup
|
|
1116
|
+
const decrypted = await this.decryptSensitiveData(backupData);
|
|
1117
|
+
if (!decrypted) {
|
|
1118
|
+
throw new Error("Failed to decrypt backup data");
|
|
1119
|
+
}
|
|
1120
|
+
const data = JSON.parse(decrypted);
|
|
1121
|
+
const result = {
|
|
1122
|
+
success: true,
|
|
1123
|
+
mnemonicImported: false,
|
|
1124
|
+
walletsImported: 0,
|
|
1125
|
+
gunPairImported: false,
|
|
1126
|
+
};
|
|
1127
|
+
// Import mnemonic
|
|
1128
|
+
if (options.importMnemonic && data.mnemonic) {
|
|
1129
|
+
try {
|
|
1130
|
+
const mnemonicDecrypted = await this.decryptSensitiveData(data.mnemonic);
|
|
1131
|
+
if (mnemonicDecrypted) {
|
|
1132
|
+
await this.importMnemonic(mnemonicDecrypted);
|
|
1133
|
+
result.mnemonicImported = true;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
catch (error) {
|
|
1137
|
+
console.error("Error importing mnemonic:", error);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// Import wallet keys
|
|
1141
|
+
if (options.importWallets && data.walletKeys) {
|
|
1142
|
+
try {
|
|
1143
|
+
result.walletsImported = await this.importWalletKeys(data.walletKeys);
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
console.error("Error importing wallet keys:", error);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
// Import address book
|
|
1150
|
+
if (data.addressBook) {
|
|
1151
|
+
try {
|
|
1152
|
+
await this.importAddressBook(data.addressBook);
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
console.error("Error importing address book:", error);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
console.log(`✅ Import completed:`, result);
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
console.error("Error importing all user data:", error);
|
|
1163
|
+
return {
|
|
1164
|
+
success: false,
|
|
1165
|
+
mnemonicImported: false,
|
|
1166
|
+
walletsImported: 0,
|
|
1167
|
+
gunPairImported: false,
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
// ========================================================================
|
|
1172
|
+
// WALLET PATH MANAGEMENT
|
|
1173
|
+
// ========================================================================
|
|
1174
|
+
/**
|
|
1175
|
+
* Initialize wallet paths from Gun storage
|
|
1176
|
+
*/
|
|
1177
|
+
async initializeWalletPaths() {
|
|
1178
|
+
try {
|
|
1179
|
+
this.walletPaths = {};
|
|
1180
|
+
await this.loadWalletPathsFromGun();
|
|
1181
|
+
await this.loadWalletPathsFromLocalStorage();
|
|
1182
|
+
const count = Object.keys(this.walletPaths).length;
|
|
1183
|
+
if (count === 0) {
|
|
1184
|
+
console.log("No wallet paths found, new wallets will be created when needed");
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
console.log(`✅ Initialized ${count} wallet paths`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
console.error("Error initializing wallet paths:", error);
|
|
1192
|
+
throw new Error(`Failed to initialize wallet paths: ${error.message}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Save wallet paths to localStorage
|
|
1197
|
+
*/
|
|
1198
|
+
async saveWalletPathsToLocalStorage() {
|
|
1199
|
+
try {
|
|
1200
|
+
const storageKey = `shogun_wallet_paths_${this.getStorageUserIdentifier()}`;
|
|
1201
|
+
const pathsToSave = JSON.stringify(this.walletPaths);
|
|
1202
|
+
if (typeof localStorage !== "undefined") {
|
|
1203
|
+
localStorage.setItem(storageKey, pathsToSave);
|
|
1204
|
+
console.log(`✅ Saved ${Object.keys(this.walletPaths).length} wallet paths to localStorage`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
catch (error) {
|
|
1208
|
+
console.error("Error saving wallet paths to localStorage:", error);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Load wallet paths from localStorage
|
|
1213
|
+
*/
|
|
1214
|
+
async loadWalletPathsFromLocalStorage() {
|
|
1215
|
+
try {
|
|
1216
|
+
if (typeof localStorage === "undefined") {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const storageKey = `shogun_wallet_paths_${this.getStorageUserIdentifier()}`;
|
|
1220
|
+
const storedPaths = localStorage.getItem(storageKey);
|
|
1221
|
+
if (storedPaths) {
|
|
1222
|
+
const parsedPaths = JSON.parse(storedPaths);
|
|
1223
|
+
Object.entries(parsedPaths).forEach(([address, pathData]) => {
|
|
1224
|
+
if (!this.walletPaths[address]) {
|
|
1225
|
+
this.walletPaths[address] = pathData;
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
console.log(`✅ Loaded wallet paths from localStorage`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
console.error("Error loading wallet paths from localStorage:", error);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Save wallet paths to Gun
|
|
1237
|
+
*/
|
|
1238
|
+
async saveWalletPathsToGun() {
|
|
1239
|
+
try {
|
|
1240
|
+
const shogun = this.identity.getShogun();
|
|
1241
|
+
const gun = shogun?.db?.gun;
|
|
1242
|
+
if (!gun)
|
|
1243
|
+
return;
|
|
1244
|
+
const user = gun.user();
|
|
1245
|
+
if (!user || !user.is)
|
|
1246
|
+
return;
|
|
1247
|
+
await user.get(SHIP_02.NODES.WALLET_PATHS).put(JSON.stringify(this.walletPaths));
|
|
1248
|
+
console.log("✅ Wallet paths saved to Gun");
|
|
1249
|
+
}
|
|
1250
|
+
catch (error) {
|
|
1251
|
+
console.error("Error saving wallet paths to Gun:", error);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Load wallet paths from Gun
|
|
1256
|
+
*/
|
|
1257
|
+
async loadWalletPathsFromGun() {
|
|
1258
|
+
try {
|
|
1259
|
+
const shogun = this.identity.getShogun();
|
|
1260
|
+
const gun = shogun?.db?.gun;
|
|
1261
|
+
if (!gun)
|
|
1262
|
+
return;
|
|
1263
|
+
const user = gun.user();
|
|
1264
|
+
if (!user || !user.is)
|
|
1265
|
+
return;
|
|
1266
|
+
const data = await new Promise((resolve) => {
|
|
1267
|
+
let resolved = false;
|
|
1268
|
+
const timeout = setTimeout(() => {
|
|
1269
|
+
if (!resolved) {
|
|
1270
|
+
resolved = true;
|
|
1271
|
+
resolve(null);
|
|
1272
|
+
}
|
|
1273
|
+
}, 5000);
|
|
1274
|
+
user.get(SHIP_02.NODES.WALLET_PATHS).once((data) => {
|
|
1275
|
+
if (!resolved) {
|
|
1276
|
+
resolved = true;
|
|
1277
|
+
clearTimeout(timeout);
|
|
1278
|
+
resolve(data || null);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
if (data) {
|
|
1283
|
+
const paths = JSON.parse(data);
|
|
1284
|
+
Object.entries(paths).forEach(([address, pathData]) => {
|
|
1285
|
+
this.walletPaths[address] = pathData;
|
|
1286
|
+
});
|
|
1287
|
+
console.log("✅ Wallet paths loaded from Gun");
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
catch (error) {
|
|
1291
|
+
console.error("Error loading wallet paths from Gun:", error);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
// ========================================================================
|
|
1295
|
+
// PRIVATE KEY GENERATION (from shogun-BIP44)
|
|
1296
|
+
// ========================================================================
|
|
1297
|
+
/**
|
|
1298
|
+
* Generate deterministic private key from string seed
|
|
1299
|
+
* Uses same algorithm as shogun-BIP44 for compatibility
|
|
1300
|
+
*/
|
|
1301
|
+
generatePrivateKeyFromString(input) {
|
|
1302
|
+
try {
|
|
1303
|
+
const encoder = new TextEncoder();
|
|
1304
|
+
const data = encoder.encode(input);
|
|
1305
|
+
// MurmurHash3-style digest
|
|
1306
|
+
const digestSync = (data) => {
|
|
1307
|
+
let h1 = 0xdeadbeef;
|
|
1308
|
+
let h2 = 0x41c6ce57;
|
|
1309
|
+
for (let i = 0; i < data.length; i++) {
|
|
1310
|
+
h1 = Math.imul(h1 ^ data[i], 2654435761);
|
|
1311
|
+
h2 = Math.imul(h2 ^ data[i], 1597334677);
|
|
1312
|
+
}
|
|
1313
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
1314
|
+
h1 = Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
1315
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
1316
|
+
h2 = Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
1317
|
+
const out = new Uint8Array(32);
|
|
1318
|
+
for (let i = 0; i < 4; i++) {
|
|
1319
|
+
out[i] = (h1 >> (8 * i)) & 0xff;
|
|
1320
|
+
}
|
|
1321
|
+
for (let i = 0; i < 4; i++) {
|
|
1322
|
+
out[i + 4] = (h2 >> (8 * i)) & 0xff;
|
|
1323
|
+
}
|
|
1324
|
+
for (let i = 8; i < 32; i++) {
|
|
1325
|
+
out[i] = (out[i % 8] ^ out[(i - 1) % 8]) & 0xff;
|
|
1326
|
+
}
|
|
1327
|
+
return out;
|
|
1328
|
+
};
|
|
1329
|
+
const hashArray = digestSync(data);
|
|
1330
|
+
const privateKey = "0x" +
|
|
1331
|
+
Array.from(hashArray)
|
|
1332
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
1333
|
+
.join("");
|
|
1334
|
+
return privateKey;
|
|
1335
|
+
}
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
console.error("Error generating private key:", error);
|
|
1338
|
+
throw new Error("Failed to generate private key from seed");
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
// ========================================================================
|
|
1342
|
+
// UTILITIES
|
|
1343
|
+
// ========================================================================
|
|
1344
|
+
/**
|
|
1345
|
+
* Initialize wallet paths and test encryption system
|
|
1346
|
+
*/
|
|
1347
|
+
async initializeWalletPathsAndTestEncryption() {
|
|
1348
|
+
await this.initializeWalletPaths();
|
|
1349
|
+
// Note: testEncryptionSystem is UI-specific, skipped
|
|
1350
|
+
console.log("✅ Wallet paths initialized");
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Cleanup resources
|
|
1354
|
+
*/
|
|
1355
|
+
async cleanup() {
|
|
1356
|
+
await this.clearCache();
|
|
1357
|
+
console.log("✅ SHIP-02 cleanup completed");
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
exports.SHIP_02 = SHIP_02;
|
|
1361
|
+
// GunDB Node Names for SHIP-02 storage
|
|
1362
|
+
SHIP_02.NODES = {
|
|
1363
|
+
ADDRESS_BOOK: "addressbook",
|
|
1364
|
+
MNEMONIC: "mnemonic",
|
|
1365
|
+
WALLET_PATHS: "wallet_paths",
|
|
1366
|
+
};
|