hd-wallet-ui 1.0.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/src/app.js ADDED
@@ -0,0 +1,3302 @@
1
+ /**
2
+ * HD Wallet UI - Main Application
3
+ *
4
+ * Standalone wallet interface with HD key derivation, multi-chain address
5
+ * generation, balance fetching, vCard export, and PIN/passkey storage.
6
+ */
7
+
8
+ // =============================================================================
9
+ // External Imports
10
+ // =============================================================================
11
+
12
+ import initHDWallet, { Curve } from 'hd-wallet-wasm';
13
+ import { x25519, ed25519 } from '@noble/curves/ed25519';
14
+ import { secp256k1 } from '@noble/curves/secp256k1';
15
+ import { p256 } from '@noble/curves/p256';
16
+ import { sha256 as sha256Noble } from '@noble/hashes/sha256';
17
+ import { keccak_256 } from '@noble/hashes/sha3';
18
+ import QRCode from 'qrcode';
19
+ import { Buffer } from 'buffer';
20
+ import { createV3 } from 'vcard-cryptoperson';
21
+
22
+ // Make Buffer available globally for various crypto libraries
23
+ window.Buffer = Buffer;
24
+
25
+ // =============================================================================
26
+ // Local Module Imports
27
+ // =============================================================================
28
+
29
+ import { getModalHTML } from './template.js';
30
+ import WalletStorage, { StorageMethod } from './wallet-storage.js';
31
+
32
+ import {
33
+ cryptoConfig,
34
+ coinTypeToConfig,
35
+ buildSigningPath,
36
+ buildEncryptionPath,
37
+ } from './constants.js';
38
+
39
+ import {
40
+ toHexCompact,
41
+ toHex,
42
+ hexToBytes,
43
+ ensureUint8Array,
44
+ generateBtcAddress,
45
+ generateEthAddress,
46
+ generateSolAddress,
47
+ deriveEthAddress,
48
+ // deriveSuiAddress, // Commented out — BTC/ETH/SOL only
49
+ // deriveMonadAddress,
50
+ // deriveCardanoAddress,
51
+ generateAddresses,
52
+ generateAddressForCoin,
53
+ truncateAddress,
54
+ fetchBtcBalance,
55
+ fetchEthBalance,
56
+ fetchSolBalance,
57
+ // fetchSuiBalance, // Commented out — BTC/ETH/SOL only
58
+ // fetchMonadBalance,
59
+ // fetchAdaBalance,
60
+ // generateXrpAddress,
61
+ // fetchXrpBalance,
62
+ apiUrl,
63
+ } from './address-derivation.js';
64
+
65
+ // =============================================================================
66
+ // DOM Helper
67
+ // =============================================================================
68
+
69
+ let _root = document;
70
+ const $ = (id) => {
71
+ const el = _root.getElementById ? _root.getElementById(id) : _root.querySelector(`#${id}`);
72
+ if (el) return el;
73
+ // Fallback to document for elements in the light DOM (e.g. widget mode)
74
+ if (_root !== document) return document.getElementById(id);
75
+ return null;
76
+ };
77
+ const $q = (sel) => _root.querySelector(sel) || (_root !== document ? document.querySelector(sel) : null);
78
+ const $qa = (sel) => {
79
+ const list = _root.querySelectorAll(sel);
80
+ if (list.length > 0 || _root === document) return list;
81
+ return document.querySelectorAll(sel);
82
+ };
83
+
84
+ // =============================================================================
85
+ // Wallet Info Box (dismissible notice)
86
+ // =============================================================================
87
+
88
+ function dismissWalletInfo() {
89
+ localStorage.setItem('walletInfoDismissed', '1');
90
+ $('wallet-info-expanded').style.display = 'none';
91
+ $('wallet-info-collapsed').style.display = 'flex';
92
+ }
93
+
94
+ function showWalletInfo() {
95
+ localStorage.removeItem('walletInfoDismissed');
96
+ $('wallet-info-expanded').style.display = 'flex';
97
+ $('wallet-info-collapsed').style.display = 'none';
98
+ }
99
+
100
+ function initWalletInfoBox() {
101
+ if (localStorage.getItem('walletInfoDismissed') === '1') {
102
+ $('wallet-info-expanded').style.display = 'none';
103
+ $('wallet-info-collapsed').style.display = 'flex';
104
+ }
105
+ }
106
+
107
+ function toggleXpubInfo() {
108
+ const box = $('xpub-info-box');
109
+ if (box) box.style.display = box.style.display === 'none' ? 'flex' : 'none';
110
+ }
111
+
112
+ function toggleMemoryInfo() {
113
+ const box = $('memory-info-box');
114
+ if (box) box.style.display = box.style.display === 'none' ? 'flex' : 'none';
115
+ }
116
+
117
+ function bindInfoHandlers() {
118
+ $('wallet-info-dismiss')?.addEventListener('click', dismissWalletInfo);
119
+ $('wallet-info-collapsed')?.addEventListener('click', showWalletInfo);
120
+ $('xpub-info-toggle')?.addEventListener('click', toggleXpubInfo);
121
+ $('xpub-info-close')?.addEventListener('click', toggleXpubInfo);
122
+ $('memory-info-toggle')?.addEventListener('click', toggleMemoryInfo);
123
+ $('memory-info-close')?.addEventListener('click', toggleMemoryInfo);
124
+ }
125
+
126
+ // =============================================================================
127
+ // Utilities
128
+ // =============================================================================
129
+
130
+ function setTruncatedValue(el, value) {
131
+ if (!el) return;
132
+ el.dataset.fullValue = value;
133
+ el.textContent = middleTruncate(value, 17, 17);
134
+ }
135
+
136
+ function middleTruncate(str, startChars, endChars) {
137
+ if (!str || str.length <= startChars + endChars + 3) return str;
138
+ return str.slice(0, startChars) + '…' + str.slice(-endChars);
139
+ }
140
+
141
+ function toBase64(arr) {
142
+ return btoa(String.fromCharCode(...arr));
143
+ }
144
+
145
+ // =============================================================================
146
+ // SHA-256 and HKDF (WebCrypto-based)
147
+ // =============================================================================
148
+
149
+ async function sha256(data) {
150
+ const hash = await crypto.subtle.digest('SHA-256', data);
151
+ return new Uint8Array(hash);
152
+ }
153
+
154
+ async function hkdf(ikm, salt, info, length) {
155
+ const key = await crypto.subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
156
+ const derived = await crypto.subtle.deriveBits(
157
+ { name: 'HKDF', hash: 'SHA-256', salt, info },
158
+ key,
159
+ length * 8
160
+ );
161
+ return new Uint8Array(derived);
162
+ }
163
+
164
+ // =============================================================================
165
+ // Key Generation
166
+ // =============================================================================
167
+
168
+ function generateKeyPair(curveType) {
169
+ if (curveType === Curve.SECP256K1) {
170
+ const privateKey = secp256k1.utils.randomPrivateKey();
171
+ const publicKey = secp256k1.getPublicKey(privateKey, true);
172
+ return { privateKey, publicKey };
173
+ }
174
+ if (curveType === Curve.X25519) {
175
+ const privateKey = x25519.utils.randomPrivateKey();
176
+ const publicKey = x25519.getPublicKey(privateKey);
177
+ return { privateKey, publicKey };
178
+ }
179
+ throw new Error(`Unsupported curve type: ${curveType}`);
180
+ }
181
+
182
+ async function p256GenerateKeyPairAsync() {
183
+ const keyPair = await crypto.subtle.generateKey(
184
+ { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']
185
+ );
186
+ const rawPublic = await crypto.subtle.exportKey('raw', keyPair.publicKey);
187
+ const pkcs8Private = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
188
+ return { publicKey: new Uint8Array(rawPublic), privateKey: new Uint8Array(pkcs8Private) };
189
+ }
190
+
191
+ async function p384GenerateKeyPairAsync() {
192
+ const keyPair = await crypto.subtle.generateKey(
193
+ { name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign', 'verify']
194
+ );
195
+ const rawPublic = await crypto.subtle.exportKey('raw', keyPair.publicKey);
196
+ const pkcs8Private = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
197
+ return { publicKey: new Uint8Array(rawPublic), privateKey: new Uint8Array(pkcs8Private) };
198
+ }
199
+
200
+ // =============================================================================
201
+ // State
202
+ // =============================================================================
203
+
204
+ const state = {
205
+ initialized: false,
206
+ loggedIn: false,
207
+ selectedCrypto: 'btc',
208
+ addresses: {
209
+ btc: null,
210
+ eth: null,
211
+ sol: null,
212
+ },
213
+ wallet: {
214
+ x25519: null,
215
+ ed25519: null,
216
+ secp256k1: null,
217
+ p256: null,
218
+ },
219
+ // HD wallet state
220
+ hdWalletModule: null,
221
+ masterSeed: null,
222
+ hdRoot: null,
223
+ mnemonic: null,
224
+ // Encryption keys (derived from password/seed)
225
+ encryptionKey: null,
226
+ encryptionIV: null,
227
+ // vCard photo (base64 data URI)
228
+ vcardPhoto: null,
229
+ // PKI Demo state
230
+ pki: {
231
+ alice: null,
232
+ bob: null,
233
+ algorithm: 'x25519',
234
+ },
235
+ };
236
+
237
+ // =============================================================================
238
+ // Entropy Calculation & Password Strength
239
+ // =============================================================================
240
+
241
+ function calculateEntropy(password) {
242
+ if (!password) return 0;
243
+
244
+ let charsetSize = 0;
245
+ if (/[a-z]/.test(password)) charsetSize += 26;
246
+ if (/[A-Z]/.test(password)) charsetSize += 26;
247
+ if (/[0-9]/.test(password)) charsetSize += 10;
248
+ if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]/.test(password)) charsetSize += 32;
249
+ if (/\s/.test(password)) charsetSize += 1;
250
+ if (/[^\x00-\x7F]/.test(password)) charsetSize += 100;
251
+
252
+ if (charsetSize === 0) return 0;
253
+ return Math.round(password.length * Math.log2(charsetSize));
254
+ }
255
+
256
+ function updatePasswordStrength(password) {
257
+ const entropy = calculateEntropy(password);
258
+ const fill = $('strength-fill');
259
+ const bits = $('entropy-bits');
260
+ const btn = $('derive-from-password');
261
+
262
+ if (bits) bits.textContent = `${entropy}`;
263
+
264
+ const MIN_SAFE_ENTROPY = 112;
265
+
266
+ let strength, percentage;
267
+
268
+ if (entropy < 40) {
269
+ strength = 'weak';
270
+ percentage = Math.min(25, (entropy / 40) * 25);
271
+ } else if (entropy < 80) {
272
+ strength = 'fair';
273
+ percentage = 25 + ((entropy - 40) / 40) * 25;
274
+ } else if (entropy < MIN_SAFE_ENTROPY) {
275
+ strength = 'good';
276
+ percentage = 50 + ((entropy - 80) / (MIN_SAFE_ENTROPY - 80)) * 25;
277
+ } else {
278
+ strength = 'strong';
279
+ percentage = 75 + Math.min(25, ((entropy - MIN_SAFE_ENTROPY) / 50) * 25);
280
+ }
281
+
282
+ if (fill) {
283
+ fill.style.width = `${percentage}%`;
284
+ // Gradient: deep red (0) → orange (50%) → green (100%)
285
+ const ratio = percentage / 100;
286
+ let r, g;
287
+ if (ratio < 0.5) {
288
+ r = 180 + Math.round(75 * (ratio / 0.5)); // 180→255
289
+ g = Math.round(140 * (ratio / 0.5)); // 0→140
290
+ } else {
291
+ r = 255 - Math.round(200 * ((ratio - 0.5) / 0.5)); // 255→55
292
+ g = 140 + Math.round(115 * ((ratio - 0.5) / 0.5)); // 140→255
293
+ }
294
+ fill.style.background = `rgb(${r}, ${g}, 30)`;
295
+ }
296
+
297
+ const username = $('wallet-username')?.value;
298
+ if (btn) btn.disabled = !username || password.length < 24;
299
+ }
300
+
301
+ // =============================================================================
302
+ // Key Derivation
303
+ // =============================================================================
304
+
305
+ async function deriveKeysFromPassword(username, password) {
306
+ const encoder = new TextEncoder();
307
+ const usernameSalt = encoder.encode(username);
308
+ const passwordBytes = encoder.encode(password);
309
+
310
+ const initialHash = await sha256(new Uint8Array([...usernameSalt, ...passwordBytes]));
311
+ const masterKey = await hkdf(initialHash, usernameSalt, encoder.encode('master-key'), 32);
312
+
313
+ state.encryptionKey = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
314
+ state.encryptionIV = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
315
+
316
+ // Create 64-byte seed for HD wallet (password-based, not BIP39)
317
+ const hdSeed = await hkdf(masterKey, new Uint8Array(0), encoder.encode('hd-wallet-seed'), 64);
318
+ state.masterSeed = hdSeed;
319
+ state.hdRoot = state.hdWalletModule.hdkey.fromSeed(hdSeed);
320
+ state.mnemonic = null; // Not available for password-derived wallets
321
+ console.log('HD wallet initialized from password, hdRoot:', !!state.hdRoot);
322
+
323
+ const keys = deriveKeysFromHDRoot(state.hdRoot);
324
+ // Also derive auxiliary keys for encryption / key agreement
325
+ keys.x25519 = generateKeyPair(Curve.X25519);
326
+ keys.p256 = await p256GenerateKeyPairAsync();
327
+ keys.p384 = await p384GenerateKeyPairAsync();
328
+
329
+ return keys;
330
+ }
331
+
332
+ async function deriveKeysFromSeed(seedPhrase) {
333
+ const seed = state.hdWalletModule.mnemonic.toSeed(seedPhrase);
334
+ const encoder = new TextEncoder();
335
+
336
+ const masterKey = await hkdf(
337
+ new Uint8Array(seed.slice(0, 32)),
338
+ new Uint8Array(0),
339
+ encoder.encode('wallet-master'),
340
+ 32
341
+ );
342
+
343
+ state.encryptionKey = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
344
+ state.encryptionIV = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
345
+
346
+ state.masterSeed = new Uint8Array(seed);
347
+ state.hdRoot = state.hdWalletModule.hdkey.fromSeed(new Uint8Array(seed));
348
+ state.mnemonic = seedPhrase;
349
+ console.log('HD wallet initialized from seed phrase, hdRoot:', !!state.hdRoot);
350
+
351
+ const keys = deriveKeysFromHDRoot(state.hdRoot);
352
+ keys.x25519 = generateKeyPair(Curve.X25519);
353
+ keys.p256 = await p256GenerateKeyPairAsync();
354
+ keys.p384 = await p384GenerateKeyPairAsync();
355
+
356
+ return keys;
357
+ }
358
+
359
+ /**
360
+ * Derive secp256k1 and ed25519 signing keys from the HD root using BIP44 signing paths.
361
+ * This ensures addresses match what the HD derivation grid produces.
362
+ */
363
+ function deriveKeysFromHDRoot(hdRoot) {
364
+ // BTC signing path m/44'/0'/0'/0/0 — secp256k1
365
+ const btcKey = hdRoot.derivePath(buildSigningPath(0, 0, 0));
366
+ const secp256k1PrivKey = btcKey.privateKey();
367
+ const secp256k1PubKey = btcKey.publicKey();
368
+
369
+ // SOL signing path m/44'/501'/0'/0/0 — ed25519
370
+ const solKey = hdRoot.derivePath(buildSigningPath(501, 0, 0));
371
+ const ed25519PrivKey = solKey.privateKey();
372
+ const ed25519PubKey = ed25519.getPublicKey(ed25519PrivKey);
373
+
374
+ return {
375
+ secp256k1: { privateKey: secp256k1PrivKey, publicKey: secp256k1PubKey },
376
+ ed25519: { privateKey: ed25519PrivKey, publicKey: ed25519PubKey },
377
+ };
378
+ }
379
+
380
+ /**
381
+ * Derive all blockchain addresses from the HD root using signing paths.
382
+ * Each coin uses its own BIP44 signing path: m/44'/{coinType}'/0'/0/0
383
+ */
384
+ function deriveAllAddressesFromHD() {
385
+ if (!state.hdRoot) return {};
386
+
387
+ const deriveAddress = (coinType) => {
388
+ try {
389
+ const path = buildSigningPath(coinType, 0, 0);
390
+ const derived = state.hdRoot.derivePath(path);
391
+ const pubKey = derived.publicKey();
392
+ return generateAddressForCoin(pubKey, coinType);
393
+ } catch (e) {
394
+ console.error(`Failed to derive address for coinType ${coinType}:`, e);
395
+ return null;
396
+ }
397
+ };
398
+
399
+ // For SOL, derive ed25519 key and use it directly
400
+ let solAddress = null;
401
+ try {
402
+ const solPath = buildSigningPath(501, 0, 0);
403
+ const solDerived = state.hdRoot.derivePath(solPath);
404
+ const solPrivKey = solDerived.privateKey();
405
+ solAddress = generateSolAddress(ed25519.getPublicKey(solPrivKey));
406
+ } catch (e) {
407
+ console.error('Failed to derive SOL address:', e);
408
+ }
409
+
410
+ return {
411
+ btc: deriveAddress(0),
412
+ eth: deriveAddress(60),
413
+ sol: solAddress,
414
+ // xrp: deriveAddress(144), // Commented out — BTC/ETH/SOL only
415
+ };
416
+ }
417
+
418
+ function generateSeedPhrase() {
419
+ return state.hdWalletModule.mnemonic.generate(24);
420
+ }
421
+
422
+ function validateSeedPhrase(phrase) {
423
+ return state.hdWalletModule.mnemonic.validate(phrase.trim().toLowerCase());
424
+ }
425
+
426
+ // =============================================================================
427
+ // HD Wallet Derivation
428
+ // =============================================================================
429
+
430
+ function deriveHDKey(path) {
431
+ if (!state.hdRoot) {
432
+ throw new Error('HD wallet not initialized');
433
+ }
434
+ try {
435
+ return state.hdRoot.derivePath(path);
436
+ } catch (e) {
437
+ console.error('HD derivation error:', e, 'path:', path);
438
+ throw e;
439
+ }
440
+ }
441
+
442
+ function updatePathDisplay() {
443
+ const coin = $('hd-coin')?.value;
444
+ const account = $('hd-account')?.value || '0';
445
+ const index = $('hd-index')?.value || '0';
446
+
447
+ const signingPath = buildSigningPath(coin, account, index);
448
+ const encryptionPath = buildEncryptionPath(coin, account, index);
449
+
450
+ const signingPathEl = $('signing-path');
451
+ const encryptionPathEl = $('encryption-path');
452
+
453
+ if (signingPathEl) signingPathEl.textContent = signingPath;
454
+ if (encryptionPathEl) encryptionPathEl.textContent = encryptionPath;
455
+ }
456
+
457
+ async function deriveAndDisplayAddress() {
458
+ console.log('deriveAndDisplayAddress called, hdRoot:', !!state.hdRoot);
459
+
460
+ const hdNotInitialized = $('hd-not-initialized');
461
+ const derivedResult = $('derived-result');
462
+
463
+ if (!state.hdRoot) {
464
+ console.log('HD not initialized, showing warning.');
465
+ if (hdNotInitialized) hdNotInitialized.style.display = 'block';
466
+ if (derivedResult) derivedResult.style.display = 'none';
467
+ return;
468
+ }
469
+
470
+ if (hdNotInitialized) hdNotInitialized.style.display = 'none';
471
+
472
+ const coin = $('hd-coin')?.value;
473
+ const account = $('hd-account')?.value || '0';
474
+ const index = $('hd-index')?.value || '0';
475
+ const coinType = parseInt(coin);
476
+ const coinOption = $('hd-coin')?.selectedOptions[0];
477
+ const cryptoName = coinOption?.dataset.name || 'Unknown';
478
+ const cryptoSymbol = coinOption?.dataset.symbol || '???';
479
+
480
+ const signingPath = buildSigningPath(coin, account, index);
481
+ const encryptionPath = buildEncryptionPath(coin, account, index);
482
+
483
+ console.log('Deriving signing path:', signingPath);
484
+ console.log('Deriving encryption path:', encryptionPath);
485
+
486
+ try {
487
+ const signingKey = deriveHDKey(signingPath);
488
+ const signingPubKey = signingKey.publicKey();
489
+
490
+ const encryptionKey = deriveHDKey(encryptionPath);
491
+ const encryptionPubKey = encryptionKey.publicKey();
492
+
493
+ const address = generateAddressForCoin(signingPubKey, coinType);
494
+ console.log('Generated address:', address);
495
+
496
+ const config = coinTypeToConfig[coinType];
497
+ const explorerUrl = config ? config.explorer + address : null;
498
+
499
+ if (derivedResult) derivedResult.style.display = 'block';
500
+
501
+ const signingPathEl = $('signing-path');
502
+ const encryptionPathEl = $('encryption-path');
503
+ if (signingPathEl) signingPathEl.textContent = signingPath;
504
+ if (encryptionPathEl) encryptionPathEl.textContent = encryptionPath;
505
+
506
+ const signingPubkeyEl = $('signing-pubkey');
507
+ const encryptionPubkeyEl = $('encryption-pubkey');
508
+ if (signingPubkeyEl) signingPubkeyEl.textContent = toHexCompact(signingPubKey);
509
+ if (encryptionPubkeyEl) encryptionPubkeyEl.textContent = toHexCompact(encryptionPubKey);
510
+
511
+ const derivedCryptoName = $('derived-crypto-name');
512
+ const derivedIcon = $('derived-icon');
513
+ const derivedAddress = $('derived-address');
514
+ if (derivedCryptoName) derivedCryptoName.textContent = cryptoName;
515
+ if (derivedIcon) derivedIcon.textContent = cryptoSymbol.substring(0, 2);
516
+ if (derivedAddress) derivedAddress.textContent = address;
517
+
518
+ const explorerLink = $('derived-explorer-link');
519
+ if (explorerLink) {
520
+ if (explorerUrl) {
521
+ explorerLink.href = explorerUrl;
522
+ explorerLink.style.display = 'inline-flex';
523
+ } else {
524
+ explorerLink.style.display = 'none';
525
+ }
526
+ }
527
+
528
+ // Generate QR code
529
+ try {
530
+ const qrCanvas = $('address-qr');
531
+ if (qrCanvas) {
532
+ await QRCode.toCanvas(qrCanvas, address, {
533
+ width: 64,
534
+ margin: 1,
535
+ color: { dark: '#1e293b', light: '#ffffff' },
536
+ });
537
+ }
538
+ } catch (qrErr) {
539
+ console.warn('QR generation failed:', qrErr);
540
+ }
541
+
542
+ } catch (err) {
543
+ console.error('Derivation failed:', err);
544
+ }
545
+ }
546
+
547
+ // =============================================================================
548
+ // PKI Key Derivation from HD Wallet
549
+ // =============================================================================
550
+
551
+ function deriveKeyFromPath(path) {
552
+ if (!state.hdRoot) {
553
+ throw new Error('HD wallet not initialized');
554
+ }
555
+ const derived = state.hdRoot.derivePath(path);
556
+ return derived.privateKey();
557
+ }
558
+
559
+ function deriveX25519FromSeed(seed) {
560
+ const privateKey = new Uint8Array(seed);
561
+ const publicKey = x25519.getPublicKey(privateKey);
562
+ return {
563
+ privateKey,
564
+ publicKey: new Uint8Array(publicKey),
565
+ };
566
+ }
567
+
568
+ function deriveSecp256k1FromSeed(seed) {
569
+ const privateKey = new Uint8Array(seed);
570
+ const publicKey = secp256k1.getPublicKey(privateKey, true);
571
+ return {
572
+ privateKey,
573
+ publicKey: new Uint8Array(publicKey),
574
+ };
575
+ }
576
+
577
+ function deriveP256FromSeed(seed) {
578
+ const privateKey = new Uint8Array(seed);
579
+ const publicKey = p256.getPublicKey(privateKey, true);
580
+ return {
581
+ privateKey,
582
+ publicKey: new Uint8Array(publicKey),
583
+ };
584
+ }
585
+
586
+ function derivePKIKeysFromHD() {
587
+ if (!state.hdRoot) {
588
+ console.warn('HD wallet not initialized, cannot derive PKI keys');
589
+ return false;
590
+ }
591
+
592
+ const algorithm = $('pki-algorithm')?.value || 'x25519';
593
+ state.pki.algorithm = algorithm;
594
+
595
+ try {
596
+ const alicePath = "m/44'/0'/0'/0/0";
597
+ const bobPath = "m/44'/0'/0'/0/1";
598
+
599
+ const aliceSeed = deriveKeyFromPath(alicePath);
600
+ const bobSeed = deriveKeyFromPath(bobPath);
601
+
602
+ switch (algorithm) {
603
+ case 'x25519':
604
+ state.pki.alice = deriveX25519FromSeed(aliceSeed);
605
+ state.pki.bob = deriveX25519FromSeed(bobSeed);
606
+ break;
607
+ case 'secp256k1':
608
+ state.pki.alice = deriveSecp256k1FromSeed(aliceSeed);
609
+ state.pki.bob = deriveSecp256k1FromSeed(bobSeed);
610
+ break;
611
+ case 'p256':
612
+ state.pki.alice = deriveP256FromSeed(aliceSeed);
613
+ state.pki.bob = deriveP256FromSeed(bobSeed);
614
+ break;
615
+ default:
616
+ state.pki.alice = deriveX25519FromSeed(aliceSeed);
617
+ state.pki.bob = deriveX25519FromSeed(bobSeed);
618
+ }
619
+
620
+ return true;
621
+ } catch (e) {
622
+ console.error('Failed to derive PKI keys from HD:', e);
623
+ return false;
624
+ }
625
+ }
626
+
627
+ function savePKIKeys() {
628
+ if (!state.pki.alice || !state.pki.bob) {
629
+ console.warn('Cannot save PKI keys: alice or bob is null');
630
+ return;
631
+ }
632
+
633
+ const data = {
634
+ algorithm: state.pki.algorithm,
635
+ alice: {
636
+ publicKey: toHexCompact(state.pki.alice.publicKey),
637
+ privateKey: toHexCompact(state.pki.alice.privateKey),
638
+ },
639
+ bob: {
640
+ publicKey: toHexCompact(state.pki.bob.publicKey),
641
+ privateKey: toHexCompact(state.pki.bob.privateKey),
642
+ },
643
+ savedAt: new Date().toISOString(),
644
+ };
645
+
646
+ if (state.encryptionKey && state.encryptionIV) {
647
+ data.encryptionKey = toHexCompact(state.encryptionKey);
648
+ data.encryptionIV = toHexCompact(state.encryptionIV);
649
+ }
650
+
651
+ try {
652
+ localStorage.setItem(PKI_STORAGE_KEY, JSON.stringify(data));
653
+ } catch (e) {
654
+ console.warn('Failed to save PKI keys to localStorage:', e);
655
+ }
656
+ }
657
+
658
+ function loadPKIKeys() {
659
+ try {
660
+ const stored = localStorage.getItem(PKI_STORAGE_KEY);
661
+ if (!stored) return false;
662
+
663
+ const data = JSON.parse(stored);
664
+ if (!data.alice || !data.bob || !data.algorithm) {
665
+ console.warn('Invalid PKI data in localStorage');
666
+ return false;
667
+ }
668
+
669
+ state.pki.algorithm = data.algorithm;
670
+ state.pki.alice = {
671
+ publicKey: hexToBytes(data.alice.publicKey),
672
+ privateKey: hexToBytes(data.alice.privateKey),
673
+ };
674
+ state.pki.bob = {
675
+ publicKey: hexToBytes(data.bob.publicKey),
676
+ privateKey: hexToBytes(data.bob.privateKey),
677
+ };
678
+
679
+ if (data.encryptionKey && data.encryptionIV) {
680
+ state.encryptionKey = hexToBytes(data.encryptionKey);
681
+ state.encryptionIV = hexToBytes(data.encryptionIV);
682
+ }
683
+
684
+ // Update UI
685
+ const alicePublicKey = $('alice-public-key');
686
+ const alicePrivateKey = $('alice-private-key');
687
+ const bobPublicKey = $('bob-public-key');
688
+ const bobPrivateKey = $('bob-private-key');
689
+ const pkiParties = $('pki-parties');
690
+ const pkiDemo = $('pki-demo');
691
+ const pkiSecurity = $('pki-security');
692
+ const pkiClearKeys = $('pki-clear-keys');
693
+
694
+ const pkiAlgorithm = $('pki-algorithm');
695
+ if (pkiAlgorithm) pkiAlgorithm.value = data.algorithm;
696
+ if (alicePublicKey) alicePublicKey.textContent = data.alice.publicKey;
697
+ if (alicePrivateKey) alicePrivateKey.textContent = data.alice.privateKey;
698
+ if (bobPublicKey) bobPublicKey.textContent = data.bob.publicKey;
699
+ if (bobPrivateKey) bobPrivateKey.textContent = data.bob.privateKey;
700
+ if (pkiParties) pkiParties.style.display = 'grid';
701
+ if (pkiDemo) pkiDemo.style.display = 'block';
702
+ if (pkiSecurity) pkiSecurity.style.display = 'block';
703
+ if (pkiClearKeys) pkiClearKeys.style.display = 'inline-flex';
704
+
705
+ return true;
706
+ } catch (e) {
707
+ console.warn('Failed to load PKI keys from localStorage:', e);
708
+ return false;
709
+ }
710
+ }
711
+
712
+ function clearPKIKeys() {
713
+ try {
714
+ localStorage.removeItem(PKI_STORAGE_KEY);
715
+ } catch (e) {
716
+ console.warn('Failed to clear PKI keys:', e);
717
+ }
718
+
719
+ state.pki.alice = null;
720
+ state.pki.bob = null;
721
+ state.pki.algorithm = 'x25519';
722
+
723
+ const els = ['alice-public-key', 'alice-private-key', 'bob-public-key', 'bob-private-key'];
724
+ els.forEach(id => {
725
+ const el = $(id);
726
+ if (el) el.textContent = '--';
727
+ });
728
+
729
+ const loginPrompt = $('pki-login-prompt');
730
+ if (loginPrompt) loginPrompt.style.display = 'block';
731
+ const pkiControls = $('pki-controls');
732
+ if (pkiControls) pkiControls.style.display = 'none';
733
+ const pkiParties = $('pki-parties');
734
+ if (pkiParties) pkiParties.style.display = 'none';
735
+ const pkiDemo = $('pki-demo');
736
+ if (pkiDemo) pkiDemo.style.display = 'none';
737
+ const pkiSecurity = $('pki-security');
738
+ if (pkiSecurity) pkiSecurity.style.display = 'none';
739
+ const pkiClearKeys = $('pki-clear-keys');
740
+ if (pkiClearKeys) pkiClearKeys.style.display = 'none';
741
+ }
742
+
743
+ async function generatePKIKeyPairs() {
744
+ // First try to derive from HD wallet
745
+ if (state.hdRoot && derivePKIKeysFromHD()) {
746
+ // PKI keys derived from HD wallet
747
+ } else {
748
+ // Fallback to random generation
749
+ const algorithm = $('pki-algorithm')?.value || 'x25519';
750
+ state.pki.algorithm = algorithm;
751
+
752
+ try {
753
+ if (algorithm === 'p256') {
754
+ state.pki.alice = await p256GenerateKeyPairAsync();
755
+ state.pki.bob = await p256GenerateKeyPairAsync();
756
+ } else if (algorithm === 'p384') {
757
+ state.pki.alice = await p384GenerateKeyPairAsync();
758
+ state.pki.bob = await p384GenerateKeyPairAsync();
759
+ } else {
760
+ const curveType = algorithm === 'secp256k1' ? Curve.SECP256K1 : Curve.X25519;
761
+ state.pki.alice = generateKeyPair(curveType);
762
+ state.pki.bob = generateKeyPair(curveType);
763
+ }
764
+ } catch (e) {
765
+ console.error('Failed to generate PKI keys:', e);
766
+ alert('Failed to generate keys: ' + e.message);
767
+ return;
768
+ }
769
+ }
770
+
771
+ savePKIKeys();
772
+
773
+ // Display keys
774
+ const alicePub = $('alice-public-key');
775
+ const alicePriv = $('alice-private-key');
776
+ const bobPub = $('bob-public-key');
777
+ const bobPriv = $('bob-private-key');
778
+ if (alicePub) alicePub.textContent = toHexCompact(state.pki.alice.publicKey);
779
+ if (alicePriv) alicePriv.textContent = toHexCompact(state.pki.alice.privateKey);
780
+ if (bobPub) bobPub.textContent = toHexCompact(state.pki.bob.publicKey);
781
+ if (bobPriv) bobPriv.textContent = toHexCompact(state.pki.bob.privateKey);
782
+
783
+ const algorithmNames = {
784
+ x25519: 'X25519 (Curve25519)',
785
+ secp256k1: 'secp256k1 (Bitcoin/Ethereum)',
786
+ p256: 'P-256 / secp256r1 (NIST)',
787
+ p384: 'P-384 / secp384r1 (NIST)',
788
+ };
789
+ const algDisplay = $('pki-algorithm-display');
790
+ if (algDisplay) algDisplay.textContent = algorithmNames[state.pki.algorithm] || state.pki.algorithm;
791
+
792
+ const selector = $('pki-algorithm');
793
+ if (selector) selector.value = state.pki.algorithm;
794
+
795
+ // Show UI sections
796
+ const loginPrompt = $('pki-login-prompt');
797
+ if (loginPrompt) loginPrompt.style.display = 'none';
798
+ const pkiControls = $('pki-controls');
799
+ if (pkiControls) pkiControls.style.display = 'flex';
800
+ const pkiParties = $('pki-parties');
801
+ if (pkiParties) pkiParties.style.display = 'grid';
802
+ const pkiDemo = $('pki-demo');
803
+ if (pkiDemo) pkiDemo.style.display = 'block';
804
+ const pkiSecurity = $('pki-security');
805
+ if (pkiSecurity) pkiSecurity.style.display = 'block';
806
+ const pkiClearKeys = $('pki-clear-keys');
807
+ if (pkiClearKeys) pkiClearKeys.style.display = 'inline-flex';
808
+ }
809
+
810
+ // =============================================================================
811
+ // Login / Logout
812
+ // =============================================================================
813
+
814
+ function login(keys) {
815
+ state.loggedIn = true;
816
+ state.wallet = keys;
817
+ state.addresses = deriveAllAddressesFromHD();
818
+ state.selectedCrypto = 'btc';
819
+
820
+ // Close login modal if open
821
+ $('login-modal')?.classList.remove('active');
822
+
823
+ // Update hero stats display
824
+ const heroWalletType = $('hero-wallet-type');
825
+ const heroAddress = $('hero-address');
826
+ const heroStats = $('hero-stats');
827
+ if (heroWalletType) heroWalletType.textContent = cryptoConfig[state.selectedCrypto].name;
828
+ if (heroAddress) heroAddress.textContent = truncateAddress(state.addresses[state.selectedCrypto]);
829
+ if (heroStats) heroStats.classList.remove('hidden');
830
+
831
+ // Show nav action buttons, hide login button
832
+ const navLogin = $('nav-login');
833
+ const navKeys = $('nav-keys');
834
+ const navLogout = $('nav-logout');
835
+ if (navLogin) navLogin.style.display = 'none';
836
+ if (navKeys) navKeys.style.display = 'flex';
837
+ if (navLogout) navLogout.style.display = 'flex';
838
+
839
+ // Update mobile menu buttons
840
+ const mobileLogin = $('mobile-login');
841
+ const mobileLogout = $('mobile-logout');
842
+ if (mobileLogin) mobileLogin.style.display = 'none';
843
+ if (mobileLogout) mobileLogout.style.display = 'block';
844
+
845
+ // Update HD wallet root keys display
846
+ if (state.hdRoot) {
847
+ const xpubEl = $('wallet-xpub');
848
+ const xprvEl = $('wallet-xprv');
849
+ const seedEl = $('wallet-seed-phrase');
850
+
851
+ if (xpubEl) {
852
+ setTruncatedValue(xpubEl, state.hdRoot.toXpub() || 'N/A');
853
+ }
854
+ const keysXpubEl = $('keys-xpub');
855
+ if (keysXpubEl) {
856
+ setTruncatedValue(keysXpubEl, state.hdRoot.toXpub() || 'N/A');
857
+ }
858
+ populateAccountAddressDropdown();
859
+ if (xprvEl) {
860
+ setTruncatedValue(xprvEl, state.hdRoot.toXprv() || 'N/A');
861
+ xprvEl.dataset.revealed = 'false';
862
+ }
863
+ if (seedEl && state.mnemonic) {
864
+ seedEl.textContent = state.mnemonic;
865
+ seedEl.dataset.revealed = 'false';
866
+ } else if (seedEl) {
867
+ seedEl.textContent = 'Not available (derived from password)';
868
+ }
869
+ }
870
+
871
+ // Derive PKI keys from HD wallet if available
872
+ if (state.hdRoot) {
873
+ generatePKIKeyPairs();
874
+ } else if (state.pki.alice && state.pki.bob) {
875
+ const alicePub = $('alice-public-key');
876
+ const alicePriv = $('alice-private-key');
877
+ const bobPub = $('bob-public-key');
878
+ const bobPriv = $('bob-private-key');
879
+ if (alicePub) alicePub.textContent = toHexCompact(state.pki.alice.publicKey);
880
+ if (alicePriv) alicePriv.textContent = toHexCompact(state.pki.alice.privateKey);
881
+ if (bobPub) bobPub.textContent = toHexCompact(state.pki.bob.publicKey);
882
+ if (bobPriv) bobPriv.textContent = toHexCompact(state.pki.bob.privateKey);
883
+
884
+ const algorithmNames = {
885
+ x25519: 'X25519 (Curve25519)',
886
+ secp256k1: 'secp256k1 (Bitcoin)',
887
+ p256: 'P-256 (NIST)',
888
+ p384: 'P-384 (NIST)',
889
+ };
890
+ const algDisplay = $('pki-algorithm-display');
891
+ if (algDisplay) algDisplay.textContent = algorithmNames[state.pki.algorithm] || state.pki.algorithm;
892
+ const loginPrompt = $('pki-login-prompt');
893
+ if (loginPrompt) loginPrompt.style.display = 'none';
894
+ const pkiControls = $('pki-controls');
895
+ if (pkiControls) pkiControls.style.display = 'flex';
896
+ const pkiParties = $('pki-parties');
897
+ if (pkiParties) pkiParties.style.display = 'grid';
898
+ const pkiDemo = $('pki-demo');
899
+ if (pkiDemo) pkiDemo.style.display = 'block';
900
+ const pkiSecurity = $('pki-security');
901
+ if (pkiSecurity) pkiSecurity.style.display = 'block';
902
+ const pkiClearKeys = $('pki-clear-keys');
903
+ if (pkiClearKeys) pkiClearKeys.style.display = 'inline-flex';
904
+ } else if (!loadPKIKeys()) {
905
+ generatePKIKeyPairs();
906
+ }
907
+
908
+ // Update wallet addresses and balances
909
+ updateAdversarialSecurity();
910
+
911
+ // Populate vCard keys display
912
+ populateVCardKeysDisplay();
913
+
914
+ // Open Account modal so user can see the wallet they just loaded
915
+ $('keys-modal')?.classList.add('active');
916
+ deriveAndDisplayAddress();
917
+
918
+ // Resolve names and update title
919
+ clearNameCache();
920
+ resolveNames().then(names => updateAccountTitle(names));
921
+
922
+ // Start trust auto-scanning
923
+ if (state._startTrustScanning) state._startTrustScanning();
924
+ }
925
+
926
+ function logout() {
927
+ // Stop trust auto-scanning
928
+ if (state._stopTrustScanning) state._stopTrustScanning();
929
+
930
+ clearNameCache();
931
+ const titleEl = $('account-title');
932
+ if (titleEl) titleEl.textContent = 'Account';
933
+ state.loggedIn = false;
934
+ state.wallet = { x25519: null, ed25519: null, secp256k1: null, p256: null };
935
+ state.encryptionKey = null;
936
+ state.encryptionIV = null;
937
+ state.masterSeed = null;
938
+ state.hdRoot = null;
939
+ state.mnemonic = null;
940
+
941
+ localStorage.removeItem(PKI_STORAGE_KEY);
942
+
943
+ // Update hero stats
944
+ const heroWalletType = $('hero-wallet-type');
945
+ const heroAddress = $('hero-address');
946
+ const heroStats = $('hero-stats');
947
+ if (heroWalletType) heroWalletType.textContent = '--';
948
+ if (heroAddress) heroAddress.textContent = '--';
949
+ if (heroStats) heroStats.classList.add('hidden');
950
+
951
+ // Show login button, hide other nav action buttons
952
+ const navLogin = $('nav-login');
953
+ const navKeys = $('nav-keys');
954
+ const navLogout = $('nav-logout');
955
+ if (navLogin) navLogin.style.display = 'flex';
956
+ if (navKeys) navKeys.style.display = 'none';
957
+ if (navLogout) navLogout.style.display = 'none';
958
+
959
+ // Update mobile menu buttons
960
+ const mobileLogin = $('mobile-login');
961
+ const mobileLogout = $('mobile-logout');
962
+ if (mobileLogin) mobileLogin.style.display = 'block';
963
+ if (mobileLogout) mobileLogout.style.display = 'none';
964
+
965
+ // Clear form inputs
966
+ const usernameEl = $('wallet-username');
967
+ const passwordEl = $('wallet-password');
968
+ const seedEl = $('seed-phrase');
969
+ if (usernameEl) usernameEl.value = '';
970
+ if (passwordEl) passwordEl.value = '';
971
+ if (seedEl) seedEl.value = '';
972
+ updatePasswordStrength('');
973
+
974
+ // Clear HD wallet UI
975
+ const derivedResult = $('derived-result');
976
+ if (derivedResult) derivedResult.style.display = 'none';
977
+ }
978
+
979
+ // =============================================================================
980
+ // Export Wallet
981
+ // =============================================================================
982
+
983
+ async function exportWallet(format) {
984
+ if (!state.loggedIn) {
985
+ alert('Please log in first to export wallet data.');
986
+ return;
987
+ }
988
+
989
+ let data, filename, mimeType;
990
+
991
+ switch (format) {
992
+ case 'mnemonic':
993
+ if (!state.mnemonic) {
994
+ alert('Seed phrase not available. This wallet was derived from a password.');
995
+ return;
996
+ }
997
+ data = state.mnemonic;
998
+ filename = 'wallet-seed-phrase.txt';
999
+ mimeType = 'text/plain';
1000
+ break;
1001
+
1002
+ case 'xpub':
1003
+ if (!state.hdRoot?.publicExtendedKey) {
1004
+ alert('Extended public key not available.');
1005
+ return;
1006
+ }
1007
+ data = state.hdRoot.toXpub();
1008
+ filename = 'wallet-xpub.txt';
1009
+ mimeType = 'text/plain';
1010
+ break;
1011
+
1012
+ case 'xprv':
1013
+ if (!state.hdRoot?.privateExtendedKey) {
1014
+ alert('Extended private key not available.');
1015
+ return;
1016
+ }
1017
+ if (!confirm('Warning: You are about to export your master private key. Anyone with this key can access all your funds. Continue?')) {
1018
+ return;
1019
+ }
1020
+ data = state.hdRoot.toXprv();
1021
+ filename = 'wallet-xprv.txt';
1022
+ mimeType = 'text/plain';
1023
+ break;
1024
+
1025
+ case 'hex':
1026
+ if (!state.masterSeed) {
1027
+ alert('Master seed not available.');
1028
+ return;
1029
+ }
1030
+ if (!confirm('Warning: You are about to export your raw master seed in hex format. This is extremely sensitive data. Continue?')) {
1031
+ return;
1032
+ }
1033
+ data = toHexCompact(state.masterSeed);
1034
+ filename = 'wallet-seed-hex.txt';
1035
+ mimeType = 'text/plain';
1036
+ break;
1037
+
1038
+ default:
1039
+ alert('Unknown export format: ' + format);
1040
+ return;
1041
+ }
1042
+
1043
+ // Download the file
1044
+ downloadData(data, filename, mimeType);
1045
+ }
1046
+
1047
+ function downloadData(data, filename, mimeType) {
1048
+ const blob = new Blob([data], { type: mimeType });
1049
+ const url = URL.createObjectURL(blob);
1050
+ const a = document.createElement('a');
1051
+ a.href = url;
1052
+ a.download = filename;
1053
+ document.body.appendChild(a);
1054
+ a.click();
1055
+ document.body.removeChild(a);
1056
+ URL.revokeObjectURL(url);
1057
+ }
1058
+
1059
+ // =============================================================================
1060
+ // Wallet Address Population & Balance Fetching
1061
+ // =============================================================================
1062
+
1063
+ // Account address dropdown — populated once after login, updated when balances arrive
1064
+ let _accountAddressData = {}; // { xpub: { addr, value }, btc: { addr, value }, ... }
1065
+
1066
+ function populateAccountAddressDropdown() {
1067
+ const sel = $('account-address-select');
1068
+ if (!sel) return;
1069
+
1070
+ const xpubStr = state.hdRoot ? state.hdRoot.toXpub() : '';
1071
+ const addrs = state.addresses || {};
1072
+
1073
+ const networks = [
1074
+ { key: 'xpub', label: 'xpub', addr: xpubStr },
1075
+ { key: 'btc', label: 'Bitcoin', addr: addrs.btc || '' },
1076
+ { key: 'eth', label: 'Ethereum', addr: addrs.eth || '' },
1077
+ { key: 'sol', label: 'Solana', addr: addrs.sol || '' },
1078
+ // { key: 'xrp', label: 'Ripple', addr: addrs.xrp || '' },
1079
+ ];
1080
+
1081
+ // // Add SUI/Monad/ADA if we can derive them
1082
+ // if (state.hdRoot) {
1083
+ // try {
1084
+ // const suiPath = buildSigningPath(784, 0, 0);
1085
+ // const suiDerived = state.hdRoot.derivePath(suiPath);
1086
+ // const suiPubKey = ed25519.getPublicKey(suiDerived.privateKey());
1087
+ // networks.push({ key: 'sui', label: 'SUI', addr: deriveSuiAddress(suiPubKey, 'ed25519') });
1088
+ // } catch (_) {}
1089
+ // networks.push({ key: 'monad', label: 'Monad', addr: addrs.eth || '' });
1090
+ // try {
1091
+ // const adaPath = buildSigningPath(1815, 0, 0);
1092
+ // const adaDerived = state.hdRoot.derivePath(adaPath);
1093
+ // const adaPubKey = ed25519.getPublicKey(adaDerived.privateKey());
1094
+ // networks.push({ key: 'ada', label: 'Cardano', addr: deriveCardanoAddress(adaPubKey) });
1095
+ // } catch (_) {}
1096
+ // }
1097
+
1098
+ _accountAddressData = {};
1099
+ sel.innerHTML = '';
1100
+ for (const n of networks) {
1101
+ if (!n.addr) continue;
1102
+ _accountAddressData[n.key] = { addr: n.addr, value: '' };
1103
+ const opt = document.createElement('option');
1104
+ opt.value = n.key;
1105
+ opt.textContent = n.label;
1106
+ sel.appendChild(opt);
1107
+ }
1108
+
1109
+ sel.removeEventListener('change', updateAccountAddressDisplay);
1110
+ sel.addEventListener('change', updateAccountAddressDisplay);
1111
+
1112
+ const copyBtn = $('account-address-copy');
1113
+ if (copyBtn) {
1114
+ copyBtn.onclick = () => {
1115
+ const key = sel.value;
1116
+ const data = _accountAddressData[key];
1117
+ if (data?.addr) {
1118
+ navigator.clipboard.writeText(data.addr).then(() => {
1119
+ copyBtn.title = 'Copied!';
1120
+ setTimeout(() => { copyBtn.title = 'Copy address'; }, 1500);
1121
+ });
1122
+ }
1123
+ };
1124
+ }
1125
+
1126
+ updateAccountAddressDisplay();
1127
+ }
1128
+
1129
+ function updateAccountAddressDisplay() {
1130
+ const sel = $('account-address-select');
1131
+ const addrEl = $('account-address-display');
1132
+ const valEl = $('account-address-value');
1133
+ if (!sel || !addrEl) return;
1134
+
1135
+ const key = sel.value;
1136
+ const data = _accountAddressData[key];
1137
+ if (!data) return;
1138
+
1139
+ const addr = data.addr;
1140
+ addrEl.textContent = addr;
1141
+ addrEl.title = addr;
1142
+ if (valEl) valEl.textContent = data.value || (key !== 'xpub' ? '$0.00' : '');
1143
+ }
1144
+
1145
+ function updateAccountAddressValues(bondBalances, prices, currency) {
1146
+ const symbol = CURRENCY_SYMBOLS[currency] || currency;
1147
+ const keyToSymbol = { btc: 'BTC', eth: 'ETH', sol: 'SOL' };
1148
+
1149
+ for (const [key, data] of Object.entries(_accountAddressData)) {
1150
+ if (key === 'xpub') {
1151
+ data.value = '';
1152
+ continue;
1153
+ }
1154
+ const sym = keyToSymbol[key];
1155
+ const bal = parseFloat(bondBalances[key]) || 0;
1156
+ const price = (prices && sym) ? (prices[sym] || 0) : 0;
1157
+ const converted = bal * price;
1158
+ data.value = converted > 0 ? symbol + converted.toFixed(2) : bal > 0 ? bal.toFixed(6) + ' ' + (sym || '') : '';
1159
+ }
1160
+ updateAccountAddressDisplay();
1161
+ }
1162
+
1163
+ function populateWalletAddresses() {
1164
+ if (!state.wallet) return;
1165
+
1166
+ const btcAddress = state.addresses?.btc || '--';
1167
+ const ethAddress = state.addresses?.eth || '--';
1168
+ const solAddress = state.addresses?.sol || '--';
1169
+
1170
+ // let suiAddress = '--';
1171
+ // let monadAddress = ethAddress; // Monad uses same address as ETH (same coin type 60)
1172
+ // let adaAddress = '--';
1173
+
1174
+ // // Derive SUI and ADA from HD root using their signing paths
1175
+ // if (state.hdRoot) {
1176
+ // try {
1177
+ // // SUI: coin type 784, uses ed25519
1178
+ // const suiPath = buildSigningPath(784, 0, 0);
1179
+ // const suiDerived = state.hdRoot.derivePath(suiPath);
1180
+ // const suiPrivKey = suiDerived.privateKey();
1181
+ // const suiPubKey = ed25519.getPublicKey(suiPrivKey);
1182
+ // suiAddress = deriveSuiAddress(suiPubKey, 'ed25519');
1183
+ // } catch (e) {
1184
+ // console.error('Failed to derive SUI address:', e);
1185
+ // }
1186
+
1187
+ // try {
1188
+ // // ADA: coin type 1815, uses ed25519
1189
+ // const adaPath = buildSigningPath(1815, 0, 0);
1190
+ // const adaDerived = state.hdRoot.derivePath(adaPath);
1191
+ // const adaPrivKey = adaDerived.privateKey();
1192
+ // const adaPubKey = ed25519.getPublicKey(adaPrivKey);
1193
+ // adaAddress = deriveCardanoAddress(adaPubKey);
1194
+ // } catch (e) {
1195
+ // console.error('Failed to derive ADA address:', e);
1196
+ // }
1197
+ // }
1198
+
1199
+ const updateAddressCard = (network, address, explorerBase) => {
1200
+ const addrEl = $(`wallet-${network}-address`);
1201
+ const linkEl = $(`wallet-${network}-explorer`);
1202
+
1203
+ if (addrEl && address !== '--') {
1204
+ addrEl.textContent = address.length > 20
1205
+ ? address.slice(0, 10) + '...' + address.slice(-8)
1206
+ : address;
1207
+ addrEl.title = address;
1208
+ }
1209
+
1210
+ if (linkEl && address !== '--') {
1211
+ linkEl.href = explorerBase + address;
1212
+ }
1213
+ };
1214
+
1215
+ updateAddressCard('btc', btcAddress, 'https://blockstream.info/address/');
1216
+ updateAddressCard('eth', ethAddress, 'https://etherscan.io/address/');
1217
+ updateAddressCard('sol', solAddress, 'https://solscan.io/account/');
1218
+ // updateAddressCard('sui', suiAddress, 'https://suiscan.xyz/mainnet/account/');
1219
+ // updateAddressCard('monad', monadAddress, 'https://monadscan.com/address/');
1220
+ // updateAddressCard('ada', adaAddress, 'https://cardanoscan.io/address/');
1221
+
1222
+ // const xrpAddress = state.addresses?.xrp || '--';
1223
+
1224
+ // Also populate bond tab addresses
1225
+ const bondAddresses = {
1226
+ btc: { addr: btcAddress, explorer: 'https://blockstream.info/address/' },
1227
+ eth: { addr: ethAddress, explorer: 'https://etherscan.io/address/' },
1228
+ sol: { addr: solAddress, explorer: 'https://solscan.io/account/' },
1229
+ // sui: { addr: suiAddress, explorer: 'https://suiscan.xyz/mainnet/account/' },
1230
+ // monad: { addr: monadAddress, explorer: 'https://monadscan.com/address/' },
1231
+ // ada: { addr: adaAddress, explorer: 'https://cardanoscan.io/address/' },
1232
+ // xrp: { addr: xrpAddress, explorer: 'https://xrpscan.com/account/' },
1233
+ };
1234
+
1235
+ Object.entries(bondAddresses).forEach(([net, { addr, explorer }]) => {
1236
+ const addrEl = $(`bond-${net}-address`);
1237
+ const linkEl = $(`bond-${net}-explorer`);
1238
+ if (addrEl && addr !== '--') {
1239
+ addrEl.textContent = addr.length > 20
1240
+ ? addr.slice(0, 10) + '...' + addr.slice(-8)
1241
+ : addr;
1242
+ addrEl.title = addr;
1243
+ }
1244
+ if (linkEl && addr !== '--') {
1245
+ linkEl.href = explorer + addr;
1246
+ }
1247
+ });
1248
+ }
1249
+
1250
+ // =============================================================================
1251
+ // Currency Conversion (Coinbase API)
1252
+ // =============================================================================
1253
+
1254
+ const CURRENCY_SYMBOLS = {
1255
+ USD: '$', EUR: '€', GBP: '£', JPY: '¥', CAD: 'C$', AUD: 'A$',
1256
+ CHF: 'CHF', CNY: '¥', BTC: '₿',
1257
+ };
1258
+
1259
+ const CURRENCY_OPTIONS = Object.keys(CURRENCY_SYMBOLS);
1260
+
1261
+ let priceCache = { data: null, currency: null, timestamp: 0 };
1262
+
1263
+ function getSelectedCurrency() {
1264
+ return localStorage.getItem('bond-currency') || 'USD';
1265
+ }
1266
+
1267
+ function setSelectedCurrency(currency) {
1268
+ localStorage.setItem('bond-currency', currency);
1269
+ }
1270
+
1271
+ async function fetchCryptoPrices(currency) {
1272
+ const now = Date.now();
1273
+ if (priceCache.data && priceCache.currency === currency && now - priceCache.timestamp < 60000) {
1274
+ return priceCache.data;
1275
+ }
1276
+
1277
+ const cryptos = ['BTC', 'ETH', 'SOL'];
1278
+ const prices = {};
1279
+
1280
+ if (currency === 'BTC') {
1281
+ // For BTC denomination, fetch each crypto's price in BTC
1282
+ prices.BTC = 1;
1283
+ const others = ['ETH', 'SOL'];
1284
+ const results = await Promise.allSettled(
1285
+ others.map(async (crypto) => {
1286
+ const url = apiUrl(`https://api.coinbase.com/v2/exchange-rates?currency=${crypto}`);
1287
+ const res = await fetch(url);
1288
+ const json = await res.json();
1289
+ return { crypto, rate: parseFloat(json.data?.rates?.BTC) || 0 };
1290
+ })
1291
+ );
1292
+ results.forEach(r => {
1293
+ if (r.status === 'fulfilled') prices[r.value.crypto] = r.value.rate;
1294
+ });
1295
+ // prices.MONAD = 0; // Testnet token, no market price
1296
+ } else {
1297
+ // Fetch exchange rates with USD as base, then convert
1298
+ const results = await Promise.allSettled(
1299
+ cryptos.map(async (crypto) => {
1300
+ const url = apiUrl(`https://api.coinbase.com/v2/prices/${crypto}-${currency}/spot`);
1301
+ const res = await fetch(url);
1302
+ const json = await res.json();
1303
+ return { crypto, price: parseFloat(json.data?.amount) || 0 };
1304
+ })
1305
+ );
1306
+ results.forEach(r => {
1307
+ if (r.status === 'fulfilled') prices[r.value.crypto] = r.value.price;
1308
+ });
1309
+ // prices.MONAD = 0;
1310
+ }
1311
+
1312
+ priceCache = { data: prices, currency, timestamp: now };
1313
+ return prices;
1314
+ }
1315
+
1316
+ function formatCurrencyValue(value, currency) {
1317
+ const symbol = CURRENCY_SYMBOLS[currency] || currency;
1318
+ if (currency === 'BTC') {
1319
+ return `${symbol}${value.toFixed(8)}`;
1320
+ }
1321
+ if (currency === 'JPY' || currency === 'CNY') {
1322
+ return `${symbol}${Math.round(value).toLocaleString()}`;
1323
+ }
1324
+ return `${symbol}${value.toFixed(2)}`;
1325
+ }
1326
+
1327
+ // =============================================================================
1328
+ // Name Resolution (ENS, BNS, Solana Names)
1329
+ // =============================================================================
1330
+
1331
+ let nameCache = null;
1332
+
1333
+ async function resolveENSName(ethAddress) {
1334
+ if (!ethAddress) return null;
1335
+ try {
1336
+ // ENS reverse resolution: call addr.reverse resolver
1337
+ const addr = ethAddress.toLowerCase().slice(2);
1338
+ // namehash of <addr>.addr.reverse
1339
+ const node = await ensReverseNode(addr);
1340
+ // Call the ENS universal resolver
1341
+ const data = '0x691f3431' + node.slice(2); // name(bytes32)
1342
+ const response = await fetch(apiUrl('https://cloudflare-eth.com'), {
1343
+ method: 'POST',
1344
+ headers: { 'Content-Type': 'application/json' },
1345
+ body: JSON.stringify({
1346
+ jsonrpc: '2.0', id: 1,
1347
+ method: 'eth_call',
1348
+ params: [{ to: '0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb', data }, 'latest'],
1349
+ }),
1350
+ });
1351
+ const json = await response.json();
1352
+ if (json.result && json.result !== '0x' && json.result.length > 130) {
1353
+ const name = decodeENSName(json.result);
1354
+ if (name && name.endsWith('.eth')) return name;
1355
+ }
1356
+ } catch (e) { console.warn('ENS resolution failed:', e); }
1357
+ return null;
1358
+ }
1359
+
1360
+ async function ensReverseNode(addrHex) {
1361
+ // namehash for <addr>.addr.reverse
1362
+ // Start with namehash('') = 0x0...0
1363
+ let node = new Uint8Array(32);
1364
+ node = keccak_256(new Uint8Array([...node, ...keccak_256(new TextEncoder().encode('reverse'))]));
1365
+ node = keccak_256(new Uint8Array([...node, ...keccak_256(new TextEncoder().encode('addr'))]));
1366
+ node = keccak_256(new Uint8Array([...node, ...keccak_256(new TextEncoder().encode(addrHex))]));
1367
+ return '0x' + Array.from(node).map(b => b.toString(16).padStart(2, '0')).join('');
1368
+ }
1369
+
1370
+ function decodeENSName(hexResult) {
1371
+ try {
1372
+ // ABI-decode the string result
1373
+ const bytes = hexResult.slice(2);
1374
+ const offset = parseInt(bytes.slice(0, 64), 16) * 2;
1375
+ const length = parseInt(bytes.slice(offset, offset + 64), 16);
1376
+ const nameHex = bytes.slice(offset + 64, offset + 64 + length * 2);
1377
+ let name = '';
1378
+ for (let i = 0; i < nameHex.length; i += 2) {
1379
+ name += String.fromCharCode(parseInt(nameHex.slice(i, i + 2), 16));
1380
+ }
1381
+ return name;
1382
+ } catch { return null; }
1383
+ }
1384
+
1385
+ async function resolveBNSName(btcAddress) {
1386
+ if (!btcAddress) return null;
1387
+ try {
1388
+ const response = await fetch(apiUrl(`https://api.hiro.so/v1/addresses/stacks/${btcAddress}`));
1389
+ if (!response.ok) return null;
1390
+ const json = await response.json();
1391
+ const names = json.names || [];
1392
+ if (names.length > 0) return names[0]; // Returns e.g. "alice.btc"
1393
+ } catch (e) { console.warn('BNS resolution failed:', e); }
1394
+ return null;
1395
+ }
1396
+
1397
+ async function resolveSolanaName(solAddress) {
1398
+ if (!solAddress) return null;
1399
+ try {
1400
+ // Use Solana Name Service reverse lookup via public API
1401
+ const response = await fetch(`https://sns-sdk-proxy.bonfida.workers.dev/v2/domain/${solAddress}`);
1402
+ if (!response.ok) return null;
1403
+ const json = await response.json();
1404
+ if (json.result && json.result.length > 0) {
1405
+ return json.result[0] + '.sol';
1406
+ }
1407
+ } catch (e) { console.warn('Solana name resolution failed:', e); }
1408
+ return null;
1409
+ }
1410
+
1411
+ async function resolveNames() {
1412
+ if (nameCache) return nameCache;
1413
+
1414
+ const btcAddress = state.addresses?.btc;
1415
+ const ethAddress = state.addresses?.eth;
1416
+ const solAddress = state.addresses?.sol;
1417
+
1418
+ const [bns, ens, sol] = await Promise.allSettled([
1419
+ resolveBNSName(btcAddress),
1420
+ resolveENSName(ethAddress),
1421
+ resolveSolanaName(solAddress),
1422
+ ]);
1423
+
1424
+ nameCache = {
1425
+ bns: bns.status === 'fulfilled' ? bns.value : null,
1426
+ ens: ens.status === 'fulfilled' ? ens.value : null,
1427
+ sol: sol.status === 'fulfilled' ? sol.value : null,
1428
+ };
1429
+ return nameCache;
1430
+ }
1431
+
1432
+ function clearNameCache() {
1433
+ nameCache = null;
1434
+ }
1435
+
1436
+ function updateAccountTitle(names) {
1437
+ const titleEl = $('account-title');
1438
+ if (!titleEl) return;
1439
+
1440
+ const resolved = [];
1441
+ if (names.bns) resolved.push({ name: names.bns, service: 'BNS' });
1442
+ if (names.ens) resolved.push({ name: names.ens, service: 'ENS' });
1443
+ if (names.sol) resolved.push({ name: names.sol, service: 'SOL' });
1444
+
1445
+ if (resolved.length === 0) {
1446
+ // Fallback to truncated xpub
1447
+ const xpub = state.hdRoot?.toXpub?.() || '';
1448
+ titleEl.innerHTML = xpub ? middleTruncate(xpub, 12, 8) : 'Account';
1449
+ return;
1450
+ }
1451
+
1452
+ titleEl.innerHTML = resolved.map(({ name, service }) =>
1453
+ `${name}<sub class="name-service-label">${service}</sub>`
1454
+ ).join(' · ');
1455
+ }
1456
+
1457
+ // =============================================================================
1458
+ // Currency Selector UI
1459
+ // =============================================================================
1460
+
1461
+ function initCurrencySelector() {
1462
+ const gearBtn = $('bond-currency-gear');
1463
+ const popover = $('bond-currency-popover');
1464
+ if (!gearBtn || !popover) return;
1465
+
1466
+ // Populate options
1467
+ const current = getSelectedCurrency();
1468
+ popover.innerHTML = CURRENCY_OPTIONS.map(c =>
1469
+ `<button class="currency-option${c === current ? ' active' : ''}" data-currency="${c}">${CURRENCY_SYMBOLS[c]} ${c}</button>`
1470
+ ).join('');
1471
+
1472
+ gearBtn.addEventListener('click', (e) => {
1473
+ e.stopPropagation();
1474
+ popover.classList.toggle('visible');
1475
+ });
1476
+
1477
+ popover.addEventListener('click', async (e) => {
1478
+ const btn = e.target.closest('[data-currency]');
1479
+ if (!btn) return;
1480
+ const currency = btn.dataset.currency;
1481
+ setSelectedCurrency(currency);
1482
+ popover.querySelectorAll('.currency-option').forEach(b => b.classList.remove('active'));
1483
+ btn.classList.add('active');
1484
+ popover.classList.remove('visible');
1485
+ priceCache = { data: null, currency: null, timestamp: 0 }; // invalidate
1486
+ await updateAdversarialSecurity();
1487
+ });
1488
+
1489
+ // Close popover on outside click
1490
+ document.addEventListener('click', () => popover.classList.remove('visible'));
1491
+ }
1492
+
1493
+ // =============================================================================
1494
+ // Adversarial Security / Bond Balances
1495
+ // =============================================================================
1496
+
1497
+ async function updateAdversarialSecurity() {
1498
+ const loginRequired = $('adversarial-login-required');
1499
+ const balancesSection = $('adversarial-balances');
1500
+
1501
+ const hasWallet = state.wallet && (state.wallet.secp256k1 || state.wallet.ed25519);
1502
+
1503
+ if (!hasWallet) {
1504
+ if (loginRequired) loginRequired.style.display = 'block';
1505
+ if (balancesSection) balancesSection.style.display = 'none';
1506
+ const trustNote = $('trust-note');
1507
+ if (trustNote) trustNote.textContent = 'Login to derive addresses and check balances.';
1508
+ return;
1509
+ }
1510
+
1511
+ if (loginRequired) loginRequired.style.display = 'none';
1512
+ if (balancesSection) balancesSection.style.display = 'block';
1513
+
1514
+ populateWalletAddresses();
1515
+
1516
+ const btcAddress = state.addresses?.btc;
1517
+ const ethAddress = state.addresses?.eth;
1518
+ const solAddress = state.addresses?.sol;
1519
+
1520
+ // let suiAddress = null;
1521
+ // let adaAddress = null;
1522
+ // if (state.hdRoot) {
1523
+ // try {
1524
+ // const suiPath = buildSigningPath(784, 0, 0);
1525
+ // const suiDerived = state.hdRoot.derivePath(suiPath);
1526
+ // const suiPubKey = ed25519.getPublicKey(suiDerived.privateKey());
1527
+ // suiAddress = deriveSuiAddress(suiPubKey, 'ed25519');
1528
+ // } catch (e) { console.error('SUI derivation error:', e); }
1529
+
1530
+ // try {
1531
+ // const adaPath = buildSigningPath(1815, 0, 0);
1532
+ // const adaDerived = state.hdRoot.derivePath(adaPath);
1533
+ // const adaPubKey = ed25519.getPublicKey(adaDerived.privateKey());
1534
+ // adaAddress = deriveCardanoAddress(adaPubKey);
1535
+ // } catch (e) { console.error('ADA derivation error:', e); }
1536
+ // }
1537
+
1538
+ // const monadAddress = ethAddress;
1539
+ // const xrpAddress = state.addresses?.xrp;
1540
+
1541
+ // Set loading state
1542
+ const networks = ['btc', 'eth', 'sol'];
1543
+ networks.forEach(net => {
1544
+ const balEl = $(`wallet-${net}-balance`);
1545
+ if (balEl) balEl.textContent = '...';
1546
+ });
1547
+ const trustNote = $('trust-note');
1548
+ if (trustNote) trustNote.textContent = 'Fetching balances from blockchain...';
1549
+
1550
+ const fetchResults = await Promise.allSettled([
1551
+ btcAddress ? fetchBtcBalance(btcAddress) : Promise.resolve({ balance: '0' }),
1552
+ ethAddress ? fetchEthBalance(ethAddress) : Promise.resolve({ balance: '0' }),
1553
+ solAddress ? fetchSolBalance(solAddress) : Promise.resolve({ balance: '0' }),
1554
+ // suiAddress ? fetchSuiBalance(suiAddress) : Promise.resolve({ balance: '0' }),
1555
+ // monadAddress ? fetchMonadBalance(monadAddress) : Promise.resolve({ balance: '0' }),
1556
+ // adaAddress ? fetchAdaBalance(adaAddress) : Promise.resolve({ balance: '0' }),
1557
+ // xrpAddress ? fetchXrpBalance(xrpAddress) : Promise.resolve({ balance: '0' }),
1558
+ ]);
1559
+
1560
+ const [btcResult, ethResult, solResult] = fetchResults.map(
1561
+ r => r.status === 'fulfilled' ? r.value : { balance: '0' }
1562
+ );
1563
+
1564
+ const updateBalance = (network, balance, decimals = 4) => {
1565
+ const balEl = $(`wallet-${network}-balance`);
1566
+ if (balEl) {
1567
+ const val = parseFloat(balance) || 0;
1568
+ balEl.textContent = val > 0 ? val.toFixed(val < 0.0001 ? 8 : decimals) : '0';
1569
+ }
1570
+
1571
+ const card = $(`wallet-${network}-card`);
1572
+ if (card) {
1573
+ const hasBalance = parseFloat(balance) > 0;
1574
+ card.classList.toggle('has-balance', hasBalance);
1575
+ card.classList.toggle('secure', hasBalance);
1576
+ }
1577
+ };
1578
+
1579
+ updateBalance('btc', btcResult.balance, 8);
1580
+ updateBalance('eth', ethResult.balance, 6);
1581
+ updateBalance('sol', solResult.balance, 6);
1582
+ // updateBalance('sui', suiResult.balance, 4);
1583
+ // updateBalance('monad', monadResult.balance, 4);
1584
+ // updateBalance('ada', adaResult.balance, 6);
1585
+ // updateBalance('xrp', xrpResult.balance, 6);
1586
+
1587
+ // Update bond tab per-network balances
1588
+ const bondBalances = {
1589
+ btc: btcResult.balance, eth: ethResult.balance, sol: solResult.balance,
1590
+ // sui: suiResult.balance, monad: monadResult.balance, ada: adaResult.balance,
1591
+ // xrp: xrpResult.balance,
1592
+ };
1593
+ Object.entries(bondBalances).forEach(([net, bal]) => {
1594
+ const el = $(`bond-${net}-balance`);
1595
+ const card = $(`bond-${net}-card`);
1596
+ const val = parseFloat(bal) || 0;
1597
+ if (el) el.textContent = val > 0 ? val.toFixed(val < 0.0001 ? 8 : 4) : '0';
1598
+ if (card) card.classList.toggle('has-balance', val > 0);
1599
+ });
1600
+
1601
+ // Convert to selected currency
1602
+ const currency = getSelectedCurrency();
1603
+ let totalConverted = 0;
1604
+ let cryptoPrices = null;
1605
+
1606
+ try {
1607
+ cryptoPrices = await fetchCryptoPrices(currency);
1608
+ const prices = cryptoPrices;
1609
+ const balances = {
1610
+ BTC: parseFloat(btcResult.balance) || 0,
1611
+ ETH: parseFloat(ethResult.balance) || 0,
1612
+ SOL: parseFloat(solResult.balance) || 0,
1613
+ // SUI: parseFloat(suiResult.balance) || 0,
1614
+ // MONAD: parseFloat(monadResult.balance) || 0,
1615
+ // ADA: parseFloat(adaResult.balance) || 0,
1616
+ // XRP: parseFloat(xrpResult.balance) || 0,
1617
+ };
1618
+
1619
+ for (const [crypto, bal] of Object.entries(balances)) {
1620
+ totalConverted += bal * (prices[crypto] || 0);
1621
+ }
1622
+ } catch (e) {
1623
+ console.warn('Price conversion failed:', e);
1624
+ }
1625
+
1626
+ // Update account header total value
1627
+ const accountTotalEl = $('account-total-value');
1628
+ if (accountTotalEl) {
1629
+ accountTotalEl.textContent = 'Security Level: ' + formatCurrencyValue(totalConverted, currency);
1630
+ }
1631
+
1632
+ // Update account address dropdown values
1633
+ updateAccountAddressValues(bondBalances, cryptoPrices, currency);
1634
+ }
1635
+
1636
+ // =============================================================================
1637
+ // vCard Generation
1638
+ // =============================================================================
1639
+
1640
+ function generateVCard(info, { skipPhoto = false } = {}) {
1641
+ const person = {};
1642
+
1643
+ if (info.firstName || info.lastName) {
1644
+ if (info.lastName) person.FAMILY_NAME = info.lastName;
1645
+ if (info.firstName) person.GIVEN_NAME = info.firstName;
1646
+ if (info.middleName) person.ADDITIONAL_NAME = info.middleName;
1647
+ if (info.prefix) person.HONORIFIC_PREFIX = info.prefix;
1648
+ if (info.suffix) person.HONORIFIC_SUFFIX = info.suffix;
1649
+ }
1650
+
1651
+ if (info.email) {
1652
+ person.CONTACT_POINT = [{ EMAIL: info.email }];
1653
+ }
1654
+
1655
+ if (info.org) {
1656
+ person.AFFILIATION = { LEGAL_NAME: info.org };
1657
+ }
1658
+
1659
+ if (info.title) {
1660
+ person.HAS_OCCUPATION = { NAME: info.title };
1661
+ }
1662
+
1663
+ if (!skipPhoto && state.vcardPhoto) {
1664
+ person.IMAGE = state.vcardPhoto;
1665
+ }
1666
+
1667
+ if (info.includeKeys && state.wallet.x25519) {
1668
+ person.KEY = [
1669
+ ...(state.hdRoot?.publicExtendedKey ? [{
1670
+ KEY_TYPE: 'xpub',
1671
+ PUBLIC_KEY: state.hdRoot.toXpub(),
1672
+ }] : []),
1673
+ {
1674
+ KEY_TYPE: 'X25519',
1675
+ PUBLIC_KEY: toBase64(state.wallet.x25519.publicKey),
1676
+ },
1677
+ {
1678
+ KEY_TYPE: 'Ed25519',
1679
+ PUBLIC_KEY: toBase64(state.wallet.ed25519.publicKey),
1680
+ },
1681
+ {
1682
+ KEY_TYPE: 'secp256k1',
1683
+ PUBLIC_KEY: toBase64(state.wallet.secp256k1.publicKey),
1684
+ CRYPTO_ADDRESS: state.addresses.btc || undefined,
1685
+ },
1686
+ ];
1687
+ }
1688
+
1689
+ const note = info.includeKeys
1690
+ ? 'Generated by HD Wallet UI'
1691
+ : undefined;
1692
+
1693
+ let vcard = createV3(person, note);
1694
+
1695
+ // Convert PHOTO from data URI format to iOS-compatible inline base64 format
1696
+ vcard = vcard.replace(
1697
+ /PHOTO;VALUE=URI:data:image\/(\w+);base64,([^\n]+)\n/,
1698
+ (_, type, b64) => {
1699
+ const vcardType = type.toUpperCase();
1700
+ let folded = `PHOTO;ENCODING=b;TYPE=${vcardType}:`;
1701
+ for (let i = 0; i < b64.length; i += 74) {
1702
+ folded += '\n ' + b64.slice(i, i + 74);
1703
+ }
1704
+ return folded + '\n';
1705
+ }
1706
+ );
1707
+
1708
+ return vcard;
1709
+ }
1710
+
1711
+ // =============================================================================
1712
+ // vCard Keys Display
1713
+ // =============================================================================
1714
+
1715
+ function populateVCardKeysDisplay() {
1716
+ const keysDisplay = $('vcard-keys-display');
1717
+ if (!keysDisplay) return;
1718
+
1719
+ const keys = [];
1720
+
1721
+ // Bitcoin signing key
1722
+ if (state.addresses.btc) {
1723
+ keys.push({
1724
+ label: 'Bitcoin Signing',
1725
+ curve: 'secp256k1',
1726
+ address: state.addresses.btc,
1727
+ pubkey: state.wallet.secp256k1 ? toHex(state.wallet.secp256k1.publicKey) : '—',
1728
+ path: buildSigningPath(0, 0, 0), // m/44'/0'/0'/0'/0'
1729
+ role: 'signing',
1730
+ explorer: `https://blockstream.info/address/${state.addresses.btc}`,
1731
+ });
1732
+ }
1733
+
1734
+ // Ethereum signing key
1735
+ if (state.addresses.eth) {
1736
+ keys.push({
1737
+ label: 'Ethereum Signing',
1738
+ curve: 'secp256k1',
1739
+ address: state.addresses.eth,
1740
+ pubkey: state.wallet.secp256k1 ? toHex(state.wallet.secp256k1.publicKey) : '—',
1741
+ path: buildSigningPath(60, 0, 0), // m/44'/60'/0'/0'/0'
1742
+ role: 'signing',
1743
+ explorer: `https://etherscan.io/address/${state.addresses.eth}`,
1744
+ });
1745
+ }
1746
+
1747
+ // Solana signing key
1748
+ if (state.addresses.sol) {
1749
+ keys.push({
1750
+ label: 'Solana Signing',
1751
+ curve: 'Ed25519',
1752
+ address: state.addresses.sol,
1753
+ pubkey: state.wallet.ed25519 ? toHex(state.wallet.ed25519.publicKey) : '—',
1754
+ path: buildSigningPath(501, 0, 0), // m/44'/501'/0'/0'
1755
+ role: 'signing',
1756
+ explorer: `https://explorer.solana.com/address/${state.addresses.sol}`,
1757
+ });
1758
+ }
1759
+
1760
+ // P-256 encryption key
1761
+ if (state.wallet.p256) {
1762
+ keys.push({
1763
+ label: 'Encryption Key',
1764
+ curve: 'P-256 (NIST)',
1765
+ address: '—',
1766
+ pubkey: toHex(state.wallet.p256.publicKey),
1767
+ path: buildEncryptionPath(0, 0, 0), // m/44'/0'/0'/1'/0'
1768
+ role: 'encryption',
1769
+ explorer: null,
1770
+ });
1771
+ }
1772
+
1773
+ // Clear and populate
1774
+ keysDisplay.innerHTML = '';
1775
+
1776
+ keys.forEach(key => {
1777
+ const keyCard = document.createElement('div');
1778
+ keyCard.className = 'key-display-card';
1779
+ keyCard.innerHTML = `
1780
+ <div class="key-display-header">
1781
+ <span class="key-display-label">${key.label}</span>
1782
+ <span class="key-display-badge ${key.role}">${key.role}</span>
1783
+ </div>
1784
+ <div class="key-display-row">
1785
+ <span class="key-display-field">Curve</span>
1786
+ <code class="key-display-value">${key.curve}</code>
1787
+ </div>
1788
+ <div class="key-display-row">
1789
+ <span class="key-display-field">Public Key</span>
1790
+ <code class="key-display-value truncate" title="${key.pubkey}">${truncateAddress(key.pubkey, 16)}</code>
1791
+ <button class="copy-btn-small" data-copy-text="${key.pubkey}" title="Copy">
1792
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1793
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
1794
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
1795
+ </svg>
1796
+ </button>
1797
+ </div>
1798
+ ${key.address !== '—' ? `
1799
+ <div class="key-display-row">
1800
+ <span class="key-display-field">Address</span>
1801
+ <code class="key-display-value truncate" title="${key.address}">${truncateAddress(key.address, 16)}</code>
1802
+ ${key.explorer ? `<a href="${key.explorer}" target="_blank" rel="noopener" class="explorer-link-small" title="View on Explorer">
1803
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1804
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
1805
+ <polyline points="15 3 21 3 21 9"/>
1806
+ <line x1="10" y1="14" x2="21" y2="3"/>
1807
+ </svg>
1808
+ </a>` : ''}
1809
+ </div>
1810
+ ` : ''}
1811
+ <div class="key-display-row">
1812
+ <span class="key-display-field">Derivation Path</span>
1813
+ <code class="key-display-value">${key.path}</code>
1814
+ </div>
1815
+ `;
1816
+ keysDisplay.appendChild(keyCard);
1817
+ });
1818
+
1819
+ // Add copy button event listeners
1820
+ keysDisplay.querySelectorAll('.copy-btn-small').forEach(btn => {
1821
+ btn.addEventListener('click', async () => {
1822
+ const text = btn.getAttribute('data-copy-text');
1823
+ try {
1824
+ await navigator.clipboard.writeText(text);
1825
+ const originalHTML = btn.innerHTML;
1826
+ btn.innerHTML = '✓';
1827
+ setTimeout(() => { btn.innerHTML = originalHTML; }, 1000);
1828
+ } catch (err) {
1829
+ console.error('Copy failed:', err);
1830
+ }
1831
+ });
1832
+ });
1833
+ }
1834
+
1835
+ function parseAndDisplayVCF(vcfText) {
1836
+ const lines = vcfText.replace(/\r?\n /g, '').split(/\r?\n/);
1837
+ const fields = {};
1838
+ const keys = [];
1839
+ let photo = null;
1840
+
1841
+ for (const line of lines) {
1842
+ const colonIdx = line.indexOf(':');
1843
+ if (colonIdx === -1) continue;
1844
+ const prop = line.substring(0, colonIdx).toUpperCase();
1845
+ const value = line.substring(colonIdx + 1);
1846
+
1847
+ if (prop === 'FN') {
1848
+ fields.name = value;
1849
+ } else if (prop.startsWith('N')) {
1850
+ if (!fields.name) {
1851
+ const parts = value.split(';');
1852
+ fields.name = [parts[3], parts[1], parts[2], parts[0], parts[4]].filter(Boolean).join(' ');
1853
+ }
1854
+ } else if (prop.startsWith('EMAIL')) {
1855
+ fields.email = value;
1856
+ } else if (prop.startsWith('ORG')) {
1857
+ fields.org = value.replace(/;/g, ', ');
1858
+ } else if (prop.startsWith('TITLE')) {
1859
+ fields.title = value;
1860
+ } else if (prop.startsWith('TEL')) {
1861
+ fields.tel = value;
1862
+ } else if (prop.startsWith('PHOTO')) {
1863
+ if (prop.includes('VALUE=URI') || value.startsWith('data:') || value.startsWith('http')) {
1864
+ photo = value;
1865
+ } else if (prop.includes('ENCODING=B') || prop.includes('ENCODING=b')) {
1866
+ const typeMatch = prop.match(/TYPE=(\w+)/i);
1867
+ const imgType = typeMatch ? typeMatch[1].toLowerCase() : 'jpeg';
1868
+ photo = `data:image/${imgType};base64,${value}`;
1869
+ }
1870
+ } else if (prop.startsWith('KEY')) {
1871
+ const typeMatch = prop.match(/TYPE=(\w+)/i);
1872
+ keys.push({ type: typeMatch ? typeMatch[1] : 'Unknown', value });
1873
+ } else if (prop.startsWith('X-CRYPTO-KEY') || prop.startsWith('X-KEY')) {
1874
+ keys.push({ type: prop.split(';')[0], value });
1875
+ }
1876
+ }
1877
+
1878
+ const resultEl = $('vcf-import-result');
1879
+ const photoEl = $('vcf-import-photo');
1880
+ const fieldsEl = $('vcf-import-fields');
1881
+ if (!resultEl || !fieldsEl) return;
1882
+
1883
+ if (photoEl) {
1884
+ photoEl.innerHTML = photo
1885
+ ? `<img src="${photo}" alt="Contact photo">`
1886
+ : `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:32px;height:32px;opacity:0.3">
1887
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
1888
+ </svg>`;
1889
+ }
1890
+
1891
+ let html = '';
1892
+ const fieldMap = [
1893
+ ['Name', fields.name],
1894
+ ['Email', fields.email],
1895
+ ['Org', fields.org],
1896
+ ['Title', fields.title],
1897
+ ['Phone', fields.tel],
1898
+ ];
1899
+ for (const [label, val] of fieldMap) {
1900
+ if (val) {
1901
+ html += `<div class="vcf-import-field">
1902
+ <span class="vcf-import-field-label">${label}</span>
1903
+ <span class="vcf-import-field-value">${val}</span>
1904
+ </div>`;
1905
+ }
1906
+ }
1907
+
1908
+ if (keys.length > 0) {
1909
+ html += '<div class="vcf-import-keys">';
1910
+ for (const k of keys) {
1911
+ html += `<div class="vcf-import-key"><strong>${k.type}:</strong> <code>${k.value}</code></div>`;
1912
+ }
1913
+ html += '</div>';
1914
+ }
1915
+
1916
+ fieldsEl.innerHTML = html;
1917
+ resultEl.style.display = 'block';
1918
+ }
1919
+
1920
+ // =============================================================================
1921
+ // Grid Canvas Animation
1922
+ // =============================================================================
1923
+
1924
+ function initGridAnimation() {
1925
+ const canvas = $('grid-canvas') || document.getElementById('grid-canvas');
1926
+ if (!canvas) return;
1927
+
1928
+ const ctx = canvas.getContext('2d');
1929
+ const gridSize = 40;
1930
+ const dotRadius = 1.5;
1931
+
1932
+ const travelers = [];
1933
+ const maxTravelers = 30;
1934
+
1935
+ function resize() {
1936
+ canvas.width = window.innerWidth;
1937
+ canvas.height = window.innerHeight;
1938
+ }
1939
+ resize();
1940
+ window.addEventListener('resize', resize);
1941
+
1942
+ function createTraveler() {
1943
+ const horizontal = Math.random() > 0.5;
1944
+ const value = Math.floor(Math.random() * 256).toString(16).padStart(2, '0').toUpperCase();
1945
+
1946
+ if (horizontal) {
1947
+ const row = Math.floor(Math.random() * (canvas.height / gridSize)) * gridSize;
1948
+ return {
1949
+ x: Math.random() > 0.5 ? -20 : canvas.width + 20,
1950
+ y: row,
1951
+ dx: (Math.random() > 0.5 ? 1 : -1) * (0.3 + Math.random() * 0.4),
1952
+ dy: 0,
1953
+ value,
1954
+ opacity: 0.3 + Math.random() * 0.4
1955
+ };
1956
+ } else {
1957
+ const col = Math.floor(Math.random() * (canvas.width / gridSize)) * gridSize;
1958
+ return {
1959
+ x: col,
1960
+ y: Math.random() > 0.5 ? -20 : canvas.height + 20,
1961
+ dx: 0,
1962
+ dy: (Math.random() > 0.5 ? 1 : -1) * (0.3 + Math.random() * 0.4),
1963
+ value,
1964
+ opacity: 0.3 + Math.random() * 0.4
1965
+ };
1966
+ }
1967
+ }
1968
+
1969
+ for (let i = 0; i < maxTravelers; i++) {
1970
+ travelers.push(createTraveler());
1971
+ }
1972
+
1973
+ function draw() {
1974
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1975
+
1976
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
1977
+ for (let x = 0; x <= canvas.width; x += gridSize) {
1978
+ for (let y = 0; y <= canvas.height; y += gridSize) {
1979
+ ctx.beginPath();
1980
+ ctx.arc(x, y, dotRadius, 0, Math.PI * 2);
1981
+ ctx.fill();
1982
+ }
1983
+ }
1984
+
1985
+ ctx.font = '10px monospace';
1986
+ for (let i = 0; i < travelers.length; i++) {
1987
+ const t = travelers[i];
1988
+
1989
+ t.x += t.dx;
1990
+ t.y += t.dy;
1991
+
1992
+ if (t.x < -30 || t.x > canvas.width + 30 || t.y < -30 || t.y > canvas.height + 30) {
1993
+ travelers[i] = createTraveler();
1994
+ continue;
1995
+ }
1996
+
1997
+ ctx.fillStyle = `rgba(100, 200, 255, ${t.opacity})`;
1998
+ ctx.fillText(t.value, t.x - 6, t.y + 3);
1999
+ }
2000
+
2001
+ requestAnimationFrame(draw);
2002
+ }
2003
+
2004
+ draw();
2005
+ }
2006
+
2007
+ // =============================================================================
2008
+ // WebAuthn / Passkey Helpers
2009
+ // =============================================================================
2010
+
2011
+ function isPasskeySupported() {
2012
+ return window.PublicKeyCredential !== undefined &&
2013
+ typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function';
2014
+ }
2015
+
2016
+ // =============================================================================
2017
+ // Login Handler Setup
2018
+ // =============================================================================
2019
+
2020
+ // Track selected remember method (pin or passkey) for each login type
2021
+ const rememberMethod = {
2022
+ password: 'passkey',
2023
+ seed: 'passkey'
2024
+ };
2025
+
2026
+ function setupLoginHandlers() {
2027
+ // Migrate from old storage format if needed
2028
+ WalletStorage.migrateStorage();
2029
+
2030
+ // Check for stored wallet using module
2031
+ const storageMetadata = WalletStorage.getStorageMetadata();
2032
+ const storageMethod = storageMetadata?.method || StorageMethod.NONE;
2033
+
2034
+ if (storageMethod !== StorageMethod.NONE) {
2035
+ const storedTab = $('stored-tab');
2036
+ if (storedTab) storedTab.style.display = '';
2037
+
2038
+ const dateEl = $('stored-wallet-date');
2039
+ if (dateEl && storageMetadata?.date) {
2040
+ dateEl.textContent = `Saved on ${storageMetadata.date}`;
2041
+ }
2042
+
2043
+ const pinSection = $('stored-pin-section');
2044
+ const passkeySection = $('stored-passkey-section');
2045
+ const divider = $('stored-divider');
2046
+
2047
+ if (divider) divider.style.display = 'none';
2048
+
2049
+ if (storageMethod === StorageMethod.PIN) {
2050
+ if (pinSection) pinSection.style.display = 'block';
2051
+ if (passkeySection) passkeySection.style.display = 'none';
2052
+ } else if (storageMethod === StorageMethod.PASSKEY) {
2053
+ if (pinSection) pinSection.style.display = 'none';
2054
+ if (passkeySection) passkeySection.style.display = 'block';
2055
+ }
2056
+
2057
+ // Pre-select stored tab (but don't open modal automatically)
2058
+ $qa('.method-tab').forEach(t => t.classList.remove('active'));
2059
+ $qa('.method-content').forEach(c => c.classList.remove('active'));
2060
+ if (storedTab) storedTab.classList.add('active');
2061
+ const storedMethod = $('stored-method');
2062
+ if (storedMethod) storedMethod.classList.add('active');
2063
+ }
2064
+
2065
+ // Hide passkey buttons if not supported
2066
+ if (!isPasskeySupported()) {
2067
+ const ppBtn = $('passkey-btn-password');
2068
+ if (ppBtn) ppBtn.style.display = 'none';
2069
+ const psBtn = $('passkey-btn-seed');
2070
+ if (psBtn) psBtn.style.display = 'none';
2071
+ }
2072
+
2073
+ // Method tab switching
2074
+ $qa('.method-tab').forEach(tab => {
2075
+ tab.addEventListener('click', () => {
2076
+ $qa('.method-tab').forEach(t => t.classList.remove('active'));
2077
+ $qa('.method-content').forEach(c => c.classList.remove('active'));
2078
+ tab.classList.add('active');
2079
+ const methodEl = $(`${tab.dataset.method}-method`);
2080
+ if (methodEl) methodEl.classList.add('active');
2081
+ });
2082
+ });
2083
+
2084
+ // Remember method selector (PIN vs Passkey)
2085
+ $qa('.remember-method-btn').forEach(btn => {
2086
+ btn.addEventListener('click', () => {
2087
+ const target = btn.dataset.target;
2088
+ const method = btn.dataset.method;
2089
+ rememberMethod[target] = method;
2090
+
2091
+ $qa(`.remember-method-btn[data-target="${target}"]`).forEach(b => b.classList.remove('active'));
2092
+ btn.classList.add('active');
2093
+
2094
+ const pinGroup = $(`pin-group-${target}`);
2095
+ const passkeyInfo = $(`passkey-info-${target}`);
2096
+ if (pinGroup) pinGroup.style.display = method === 'pin' ? 'block' : 'none';
2097
+ if (passkeyInfo) passkeyInfo.style.display = method === 'passkey' ? 'flex' : 'none';
2098
+ });
2099
+ });
2100
+
2101
+ // Remember wallet checkbox handlers
2102
+ $('remember-wallet-password')?.addEventListener('change', (e) => {
2103
+ const opts = $('remember-options-password');
2104
+ if (opts) opts.style.display = e.target.checked ? 'block' : 'none';
2105
+ if (e.target.checked && rememberMethod.password === 'pin') {
2106
+ $('pin-input-password')?.focus();
2107
+ }
2108
+ });
2109
+
2110
+ $('remember-wallet-seed')?.addEventListener('change', (e) => {
2111
+ const opts = $('remember-options-seed');
2112
+ if (opts) opts.style.display = e.target.checked ? 'block' : 'none';
2113
+ if (e.target.checked && rememberMethod.seed === 'pin') {
2114
+ $('pin-input-seed')?.focus();
2115
+ }
2116
+ });
2117
+
2118
+ // PIN input validation
2119
+ ['pin-input-password', 'pin-input-seed', 'pin-input-unlock'].forEach(id => {
2120
+ $(id)?.addEventListener('input', (e) => {
2121
+ e.target.value = e.target.value.replace(/\D/g, '').slice(0, 6);
2122
+ if (id === 'pin-input-unlock') {
2123
+ const unlockBtn = $('unlock-stored-wallet');
2124
+ if (unlockBtn) unlockBtn.disabled = e.target.value.length !== 6;
2125
+ }
2126
+ });
2127
+ });
2128
+
2129
+ // Password input handler
2130
+ $('wallet-password')?.addEventListener('input', (e) => {
2131
+ updatePasswordStrength(e.target.value);
2132
+ });
2133
+
2134
+ $('wallet-username')?.addEventListener('input', () => {
2135
+ const pw = $('wallet-password');
2136
+ if (pw) updatePasswordStrength(pw.value);
2137
+ });
2138
+
2139
+ // Derive from password button
2140
+ $('derive-from-password')?.addEventListener('click', async () => {
2141
+ const username = $('wallet-username')?.value;
2142
+ const password = $('wallet-password')?.value;
2143
+ const rememberWallet = $('remember-wallet-password')?.checked;
2144
+ const usePasskey = rememberMethod.password === 'passkey';
2145
+ const pin = $('pin-input-password')?.value;
2146
+
2147
+ console.log('Login clicked, username:', username, 'password length:', password?.length);
2148
+ if (!username || !password || password.length < 24) {
2149
+ console.log('Login validation failed');
2150
+ return;
2151
+ }
2152
+
2153
+ if (rememberWallet && !usePasskey && (!pin || pin.length !== 6)) {
2154
+ alert('Please enter a 6-digit PIN to store your wallet');
2155
+ return;
2156
+ }
2157
+
2158
+ const btn = $('derive-from-password');
2159
+ btn.disabled = true;
2160
+ btn.textContent = 'Logging in...';
2161
+
2162
+ try {
2163
+ console.log('Calling deriveKeysFromPassword...');
2164
+ const keys = await deriveKeysFromPassword(username, password);
2165
+ console.log('Keys derived, hdRoot after derivation:', !!state.hdRoot);
2166
+
2167
+ if (rememberWallet) {
2168
+ const walletData = {
2169
+ type: 'password',
2170
+ username,
2171
+ password,
2172
+ masterSeed: Array.from(state.masterSeed)
2173
+ };
2174
+
2175
+ if (usePasskey) {
2176
+ await WalletStorage.storeWithPasskey(walletData, {
2177
+ rpName: 'HD Wallet',
2178
+ userName: username,
2179
+ userDisplayName: username
2180
+ });
2181
+ const pinSect = $('stored-pin-section');
2182
+ if (pinSect) pinSect.style.display = 'none';
2183
+ const psSect = $('stored-passkey-section');
2184
+ if (psSect) psSect.style.display = 'block';
2185
+ } else {
2186
+ await WalletStorage.storeWithPIN(pin, walletData);
2187
+ const pinSect = $('stored-pin-section');
2188
+ if (pinSect) pinSect.style.display = 'block';
2189
+ const psSect = $('stored-passkey-section');
2190
+ if (psSect) psSect.style.display = 'none';
2191
+ }
2192
+ const storedTab = $('stored-tab');
2193
+ if (storedTab) storedTab.style.display = '';
2194
+ const divider = $('stored-divider');
2195
+ if (divider) divider.style.display = 'none';
2196
+ const dateEl = $('stored-wallet-date');
2197
+ if (dateEl) dateEl.textContent = `Saved on ${new Date().toLocaleDateString()}`;
2198
+ }
2199
+
2200
+ login(keys);
2201
+ console.log('Login complete, hdRoot:', !!state.hdRoot);
2202
+ } catch (err) {
2203
+ console.error('Login error:', err);
2204
+ alert('Error: ' + err.message);
2205
+ } finally {
2206
+ btn.disabled = false;
2207
+ btn.textContent = 'Login';
2208
+ }
2209
+ });
2210
+
2211
+ // Generate seed phrase button
2212
+ $('generate-seed')?.addEventListener('click', () => {
2213
+ const seedEl = $('seed-phrase');
2214
+ if (seedEl) seedEl.value = generateSeedPhrase();
2215
+ const deriveBtn = $('derive-from-seed');
2216
+ if (deriveBtn) deriveBtn.disabled = false;
2217
+ });
2218
+
2219
+ // Validate seed phrase button
2220
+ $('validate-seed')?.addEventListener('click', () => {
2221
+ const seedEl = $('seed-phrase');
2222
+ const valid = validateSeedPhrase(seedEl?.value || '');
2223
+ if (valid) {
2224
+ alert('Valid BIP39 seed phrase!');
2225
+ const deriveBtn = $('derive-from-seed');
2226
+ if (deriveBtn) deriveBtn.disabled = false;
2227
+ } else {
2228
+ alert('Invalid seed phrase');
2229
+ const deriveBtn = $('derive-from-seed');
2230
+ if (deriveBtn) deriveBtn.disabled = true;
2231
+ }
2232
+ });
2233
+
2234
+ // Seed phrase input validation
2235
+ $('seed-phrase')?.addEventListener('input', () => {
2236
+ const phrase = $('seed-phrase')?.value.trim();
2237
+ const deriveBtn = $('derive-from-seed');
2238
+ if (phrase && phrase.split(/\s+/).length >= 12) {
2239
+ if (deriveBtn) deriveBtn.disabled = !validateSeedPhrase(phrase);
2240
+ } else {
2241
+ if (deriveBtn) deriveBtn.disabled = true;
2242
+ }
2243
+ });
2244
+
2245
+ // Derive from seed button
2246
+ $('derive-from-seed')?.addEventListener('click', async () => {
2247
+ const phrase = $('seed-phrase')?.value;
2248
+ if (!phrase || !validateSeedPhrase(phrase)) return;
2249
+
2250
+ const rememberWallet = $('remember-wallet-seed')?.checked;
2251
+ const usePasskey = rememberMethod.seed === 'passkey';
2252
+ const pin = $('pin-input-seed')?.value;
2253
+
2254
+ if (rememberWallet && !usePasskey && (!pin || pin.length !== 6)) {
2255
+ alert('Please enter a 6-digit PIN to store your wallet');
2256
+ return;
2257
+ }
2258
+
2259
+ const btn = $('derive-from-seed');
2260
+ btn.disabled = true;
2261
+ btn.textContent = 'Logging in...';
2262
+
2263
+ try {
2264
+ const keys = await deriveKeysFromSeed(phrase);
2265
+
2266
+ if (rememberWallet) {
2267
+ const walletData = {
2268
+ type: 'seed',
2269
+ seedPhrase: phrase,
2270
+ masterSeed: Array.from(state.masterSeed)
2271
+ };
2272
+
2273
+ if (usePasskey) {
2274
+ await WalletStorage.storeWithPasskey(walletData, {
2275
+ rpName: 'HD Wallet',
2276
+ userName: 'seed-wallet',
2277
+ userDisplayName: 'Seed Phrase Wallet'
2278
+ });
2279
+ const pinSect = $('stored-pin-section');
2280
+ if (pinSect) pinSect.style.display = 'none';
2281
+ const psSect = $('stored-passkey-section');
2282
+ if (psSect) psSect.style.display = 'block';
2283
+ } else {
2284
+ await WalletStorage.storeWithPIN(pin, walletData);
2285
+ const pinSect = $('stored-pin-section');
2286
+ if (pinSect) pinSect.style.display = 'block';
2287
+ const psSect = $('stored-passkey-section');
2288
+ if (psSect) psSect.style.display = 'none';
2289
+ }
2290
+ const storedTab = $('stored-tab');
2291
+ if (storedTab) storedTab.style.display = '';
2292
+ const divider = $('stored-divider');
2293
+ if (divider) divider.style.display = 'none';
2294
+ const dateEl = $('stored-wallet-date');
2295
+ if (dateEl) dateEl.textContent = `Saved on ${new Date().toLocaleDateString()}`;
2296
+ }
2297
+
2298
+ login(keys);
2299
+ } catch (err) {
2300
+ alert('Error: ' + err.message);
2301
+ } finally {
2302
+ btn.disabled = false;
2303
+ btn.textContent = 'Login';
2304
+ }
2305
+ });
2306
+
2307
+ // Unlock stored wallet with PIN
2308
+ $('unlock-stored-wallet')?.addEventListener('click', async () => {
2309
+ const pin = $('pin-input-unlock')?.value;
2310
+ if (!pin || pin.length !== 6) {
2311
+ alert('Please enter a 6-digit PIN');
2312
+ return;
2313
+ }
2314
+
2315
+ const btn = $('unlock-stored-wallet');
2316
+ btn.disabled = true;
2317
+ btn.textContent = 'Unlocking...';
2318
+
2319
+ try {
2320
+ const walletData = await WalletStorage.retrieveWithPIN(pin);
2321
+
2322
+ let keys;
2323
+ if (walletData.type === 'password') {
2324
+ keys = await deriveKeysFromPassword(walletData.username, walletData.password);
2325
+ } else if (walletData.type === 'seed') {
2326
+ keys = await deriveKeysFromSeed(walletData.seedPhrase);
2327
+ } else {
2328
+ throw new Error('Unknown wallet type');
2329
+ }
2330
+
2331
+ login(keys);
2332
+ } catch (err) {
2333
+ alert('Error: ' + err.message);
2334
+ const pinInput = $('pin-input-unlock');
2335
+ if (pinInput) pinInput.value = '';
2336
+ } finally {
2337
+ btn.disabled = false;
2338
+ btn.textContent = 'Unlock with PIN';
2339
+ }
2340
+ });
2341
+
2342
+ // Unlock stored wallet with Passkey
2343
+ $('unlock-with-passkey')?.addEventListener('click', async () => {
2344
+ const btn = $('unlock-with-passkey');
2345
+ btn.disabled = true;
2346
+ btn.innerHTML = 'Authenticating...';
2347
+
2348
+ try {
2349
+ const walletData = await WalletStorage.retrieveWithPasskey();
2350
+
2351
+ let keys;
2352
+ if (walletData.type === 'password') {
2353
+ keys = await deriveKeysFromPassword(walletData.username, walletData.password);
2354
+ } else if (walletData.type === 'seed') {
2355
+ keys = await deriveKeysFromSeed(walletData.seedPhrase);
2356
+ } else {
2357
+ throw new Error('Unknown wallet type');
2358
+ }
2359
+
2360
+ login(keys);
2361
+ } catch (err) {
2362
+ alert('Error: ' + err.message);
2363
+ } finally {
2364
+ btn.disabled = false;
2365
+ btn.innerHTML = 'Unlock with Passkey';
2366
+ }
2367
+ });
2368
+
2369
+ // Forget stored wallet
2370
+ $('forget-stored-wallet')?.addEventListener('click', () => {
2371
+ if (confirm('Are you sure you want to forget your stored wallet? You will need to enter your password or seed phrase again.')) {
2372
+ WalletStorage.clearStorage();
2373
+ const storedTab = $('stored-tab');
2374
+ if (storedTab) storedTab.style.display = 'none';
2375
+ const pinSect = $('stored-pin-section');
2376
+ if (pinSect) pinSect.style.display = 'block';
2377
+ const psSect = $('stored-passkey-section');
2378
+ if (psSect) psSect.style.display = 'none';
2379
+ const divider = $('stored-divider');
2380
+ if (divider) divider.style.display = 'none';
2381
+ // Switch to password tab
2382
+ $qa('.method-tab').forEach(t => t.classList.remove('active'));
2383
+ $qa('.method-content').forEach(c => c.classList.remove('active'));
2384
+ const pwMethod = $('password-method');
2385
+ if (pwMethod) pwMethod.classList.add('active');
2386
+ const pwTab = $q('.method-tab[data-method="password"]');
2387
+ if (pwTab) pwTab.classList.add('active');
2388
+ }
2389
+ });
2390
+ }
2391
+
2392
+ // =============================================================================
2393
+ // Main App UI Handlers
2394
+ // =============================================================================
2395
+
2396
+ function setupMainAppHandlers() {
2397
+ // Nav actions
2398
+ $('nav-login')?.addEventListener('click', () => {
2399
+ $('login-modal')?.classList.add('active');
2400
+ });
2401
+ $('hero-login')?.addEventListener('click', () => {
2402
+ $('login-modal')?.classList.add('active');
2403
+ });
2404
+ $('nav-logout')?.addEventListener('click', logout);
2405
+ $('nav-keys')?.addEventListener('click', async () => {
2406
+ $('keys-modal')?.classList.add('active');
2407
+ deriveAndDisplayAddress();
2408
+ if (state.loggedIn) {
2409
+ const names = await resolveNames();
2410
+ updateAccountTitle(names);
2411
+ }
2412
+ });
2413
+
2414
+ // Modal close handlers
2415
+ $qa('.modal').forEach(modal => {
2416
+ modal.addEventListener('click', (e) => {
2417
+ if (e.target === modal || e.target.classList.contains('modal-close')) {
2418
+ modal.classList.remove('active');
2419
+ }
2420
+ });
2421
+ });
2422
+
2423
+ // Account modal tab switching
2424
+ $qa('.modal-tab[data-modal-tab]').forEach(tab => {
2425
+ tab.addEventListener('click', () => {
2426
+ $qa('.modal-tab[data-modal-tab]').forEach(t => t.classList.remove('active'));
2427
+ $qa('.modal-tab-content').forEach(c => c.classList.remove('active'));
2428
+ tab.classList.add('active');
2429
+ const target = $(tab.dataset.modalTab);
2430
+ if (target) target.classList.add('active');
2431
+ });
2432
+ });
2433
+
2434
+ // vCard identity auto-save
2435
+ const VCARD_STORAGE_KEY = 'hd-wallet-vcard-identity';
2436
+ const vcardFieldIds = [
2437
+ 'vcard-prefix', 'vcard-firstname', 'vcard-middlename', 'vcard-lastname',
2438
+ 'vcard-suffix', 'vcard-email', 'vcard-phone', 'vcard-org', 'vcard-title',
2439
+ 'vcard-street', 'vcard-city', 'vcard-region', 'vcard-postal', 'vcard-country'
2440
+ ];
2441
+
2442
+ function saveVcardIdentity() {
2443
+ const data = {};
2444
+ for (const id of vcardFieldIds) {
2445
+ const el = $(id);
2446
+ if (el) data[id] = el.value;
2447
+ }
2448
+ if (state.vcardPhoto) data._photo = state.vcardPhoto;
2449
+ try { localStorage.setItem(VCARD_STORAGE_KEY, JSON.stringify(data)); } catch (e) { /* ignore */ }
2450
+ }
2451
+
2452
+ function restoreVcardIdentity() {
2453
+ try {
2454
+ const raw = localStorage.getItem(VCARD_STORAGE_KEY);
2455
+ if (!raw) return;
2456
+ const data = JSON.parse(raw);
2457
+ for (const id of vcardFieldIds) {
2458
+ const el = $(id);
2459
+ if (el && data[id]) el.value = data[id];
2460
+ }
2461
+ if (data._photo) {
2462
+ state.vcardPhoto = data._photo;
2463
+ showPhotoPreview(data._photo);
2464
+ }
2465
+ } catch (e) { /* ignore */ }
2466
+ }
2467
+
2468
+ restoreVcardIdentity();
2469
+
2470
+ let vcardSaveTimer = null;
2471
+ function debouncedVcardSave() {
2472
+ clearTimeout(vcardSaveTimer);
2473
+ vcardSaveTimer = setTimeout(saveVcardIdentity, 500);
2474
+ }
2475
+
2476
+ for (const id of vcardFieldIds) {
2477
+ $(id)?.addEventListener('input', debouncedVcardSave);
2478
+ }
2479
+
2480
+ // Photo upload handler
2481
+ $('vcard-photo-input')?.addEventListener('change', (e) => {
2482
+ const file = e.target.files[0];
2483
+ if (!file) return;
2484
+ const reader = new FileReader();
2485
+ reader.onload = (ev) => {
2486
+ const img = new Image();
2487
+ img.onload = () => {
2488
+ const canvas = document.createElement('canvas');
2489
+ const size = 128;
2490
+ canvas.width = size;
2491
+ canvas.height = size;
2492
+ const ctx = canvas.getContext('2d');
2493
+ const min = Math.min(img.width, img.height);
2494
+ const sx = (img.width - min) / 2;
2495
+ const sy = (img.height - min) / 2;
2496
+ ctx.drawImage(img, sx, sy, min, min, 0, 0, size, size);
2497
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.7);
2498
+ state.vcardPhoto = dataUrl;
2499
+ stopCamera();
2500
+ showPhotoPreview(dataUrl);
2501
+ saveVcardIdentity();
2502
+ };
2503
+ img.src = ev.target.result;
2504
+ };
2505
+ reader.readAsDataURL(file);
2506
+ });
2507
+
2508
+ // Photo remove handler with confirmation modal
2509
+ $('vcard-photo-remove')?.addEventListener('click', () => {
2510
+ const modal = $('photo-remove-confirm-modal');
2511
+ if (modal) modal.classList.add('active');
2512
+ });
2513
+
2514
+ $('photo-remove-yes')?.addEventListener('click', () => {
2515
+ state.vcardPhoto = null;
2516
+ resetPhotoPreview();
2517
+ saveVcardIdentity();
2518
+ const removeBtn = $('vcard-photo-remove');
2519
+ if (removeBtn) removeBtn.style.display = 'none';
2520
+ const input = $('vcard-photo-input');
2521
+ if (input) input.value = '';
2522
+ // Show upload/camera buttons again
2523
+ const uploadLabel = document.querySelector('label[for="vcard-photo-input"]');
2524
+ if (uploadLabel) uploadLabel.style.display = '';
2525
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
2526
+ const cameraBtn = $('vcard-camera-btn');
2527
+ if (cameraBtn) cameraBtn.style.display = '';
2528
+ }
2529
+ const modal = $('photo-remove-confirm-modal');
2530
+ if (modal) modal.classList.remove('active');
2531
+ });
2532
+
2533
+ $('photo-remove-no')?.addEventListener('click', () => {
2534
+ const modal = $('photo-remove-confirm-modal');
2535
+ if (modal) modal.classList.remove('active');
2536
+ });
2537
+
2538
+ function resetPhotoPreview() {
2539
+ const preview = $('vcard-photo-preview');
2540
+ if (!preview) return;
2541
+ preview.querySelectorAll('img').forEach(el => el.remove());
2542
+ const placeholder = preview.querySelector('.photo-placeholder-icon');
2543
+ if (placeholder) placeholder.style.display = '';
2544
+ const video = $('vcard-camera-video');
2545
+ if (video) video.style.display = 'none';
2546
+ }
2547
+
2548
+ function showPhotoPreview(dataUrl) {
2549
+ const preview = $('vcard-photo-preview');
2550
+ if (!preview) return;
2551
+ const placeholder = preview.querySelector('.photo-placeholder-icon');
2552
+ if (placeholder) placeholder.style.display = 'none';
2553
+ const video = $('vcard-camera-video');
2554
+ if (video) video.style.display = 'none';
2555
+ preview.querySelectorAll('img').forEach(el => el.remove());
2556
+ const img = document.createElement('img');
2557
+ img.src = dataUrl;
2558
+ img.alt = 'Photo';
2559
+ preview.appendChild(img);
2560
+ const removeBtn = $('vcard-photo-remove');
2561
+ if (removeBtn) removeBtn.style.display = '';
2562
+ // Hide upload/camera buttons when photo is present
2563
+ const uploadLabel = document.querySelector('label[for="vcard-photo-input"]');
2564
+ if (uploadLabel) uploadLabel.style.display = 'none';
2565
+ const cameraBtn = $('vcard-camera-btn');
2566
+ if (cameraBtn) cameraBtn.style.display = 'none';
2567
+ }
2568
+
2569
+ // Camera support
2570
+ let cameraStream = null;
2571
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
2572
+ const cameraBtn = $('vcard-camera-btn');
2573
+ if (cameraBtn && !state.vcardPhoto) cameraBtn.style.display = '';
2574
+
2575
+ cameraBtn?.addEventListener('click', async () => {
2576
+ try {
2577
+ cameraStream = await navigator.mediaDevices.getUserMedia({
2578
+ video: { facingMode: 'user', width: { ideal: 512 }, height: { ideal: 512 } }
2579
+ });
2580
+ const video = $('vcard-camera-video');
2581
+ if (video) {
2582
+ video.srcObject = cameraStream;
2583
+ video.style.display = '';
2584
+ }
2585
+ const preview = $('vcard-photo-preview');
2586
+ if (preview) {
2587
+ const placeholder = preview.querySelector('.photo-placeholder-icon');
2588
+ if (placeholder) placeholder.style.display = 'none';
2589
+ preview.querySelectorAll('img').forEach(el => el.style.display = 'none');
2590
+ }
2591
+ cameraBtn.style.display = 'none';
2592
+ const captureBtn = $('vcard-camera-capture');
2593
+ const cancelBtn = $('vcard-camera-cancel');
2594
+ if (captureBtn) captureBtn.style.display = '';
2595
+ if (cancelBtn) cancelBtn.style.display = '';
2596
+ } catch (err) {
2597
+ console.error('Camera access denied:', err);
2598
+ alert('Could not access camera. Please check your browser permissions.');
2599
+ }
2600
+ });
2601
+
2602
+ $('vcard-camera-capture')?.addEventListener('click', () => {
2603
+ const video = $('vcard-camera-video');
2604
+ if (!video) return;
2605
+ const canvas = document.createElement('canvas');
2606
+ const size = 128;
2607
+ canvas.width = size;
2608
+ canvas.height = size;
2609
+ const ctx = canvas.getContext('2d');
2610
+ const vw = video.videoWidth;
2611
+ const vh = video.videoHeight;
2612
+ const min = Math.min(vw, vh);
2613
+ const sx = (vw - min) / 2;
2614
+ const sy = (vh - min) / 2;
2615
+ ctx.drawImage(video, sx, sy, min, min, 0, 0, size, size);
2616
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.7);
2617
+ state.vcardPhoto = dataUrl;
2618
+ stopCamera();
2619
+ showPhotoPreview(dataUrl);
2620
+ saveVcardIdentity();
2621
+ });
2622
+
2623
+ $('vcard-camera-cancel')?.addEventListener('click', () => {
2624
+ stopCamera();
2625
+ if (state.vcardPhoto) {
2626
+ showPhotoPreview(state.vcardPhoto);
2627
+ } else {
2628
+ resetPhotoPreview();
2629
+ }
2630
+ });
2631
+ }
2632
+
2633
+ function stopCamera() {
2634
+ if (cameraStream) {
2635
+ cameraStream.getTracks().forEach(t => t.stop());
2636
+ cameraStream = null;
2637
+ }
2638
+ const video = $('vcard-camera-video');
2639
+ if (video) {
2640
+ video.srcObject = null;
2641
+ video.style.display = 'none';
2642
+ }
2643
+ const cameraBtn = $('vcard-camera-btn');
2644
+ const captureBtn = $('vcard-camera-capture');
2645
+ const cancelBtn = $('vcard-camera-cancel');
2646
+ if (cameraBtn) cameraBtn.style.display = state.vcardPhoto ? 'none' : '';
2647
+ if (captureBtn) captureBtn.style.display = 'none';
2648
+ if (cancelBtn) cancelBtn.style.display = 'none';
2649
+ }
2650
+
2651
+ // VCF import handler
2652
+ $('vcf-import-input')?.addEventListener('change', (e) => {
2653
+ const file = e.target.files[0];
2654
+ if (!file) return;
2655
+ const reader = new FileReader();
2656
+ reader.onload = (ev) => {
2657
+ const vcfText = ev.target.result;
2658
+ parseAndDisplayVCF(vcfText);
2659
+ };
2660
+ reader.readAsText(file);
2661
+ e.target.value = '';
2662
+ });
2663
+
2664
+ // Reveal sensitive key buttons
2665
+ $qa('.reveal-key-btn').forEach(btn => {
2666
+ btn.addEventListener('click', () => {
2667
+ const targetId = btn.dataset.target;
2668
+ const targetEl = $(targetId);
2669
+ if (targetEl) {
2670
+ const isRevealed = targetEl.dataset.revealed === 'true';
2671
+ targetEl.dataset.revealed = isRevealed ? 'false' : 'true';
2672
+ btn.innerHTML = isRevealed
2673
+ ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
2674
+ : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
2675
+ }
2676
+ });
2677
+ });
2678
+
2679
+ // Copy key buttons
2680
+ $qa('.copy-key-btn').forEach(btn => {
2681
+ btn.addEventListener('click', async () => {
2682
+ const targetId = btn.dataset.copy;
2683
+ const targetEl = $(targetId);
2684
+ if (targetEl) {
2685
+ try {
2686
+ await navigator.clipboard.writeText(targetEl.dataset.fullValue || targetEl.textContent);
2687
+ btn.classList.add('copied');
2688
+ setTimeout(() => btn.classList.remove('copied'), 1500);
2689
+ } catch (err) {
2690
+ console.error('Copy failed:', err);
2691
+ }
2692
+ }
2693
+ });
2694
+ });
2695
+
2696
+ // Export wallet dropdown
2697
+ const exportBtn = $('export-wallet-btn');
2698
+ const exportMenu = $('export-menu');
2699
+ if (exportBtn && exportMenu) {
2700
+ exportBtn.addEventListener('click', () => {
2701
+ exportMenu.classList.toggle('active');
2702
+ });
2703
+
2704
+ _root.addEventListener('click', (e) => {
2705
+ if (!exportBtn.contains(e.target) && !exportMenu.contains(e.target)) {
2706
+ exportMenu.classList.remove('active');
2707
+ }
2708
+ });
2709
+
2710
+ $qa('.export-option').forEach(option => {
2711
+ option.addEventListener('click', async () => {
2712
+ const format = option.dataset.format;
2713
+ await exportWallet(format);
2714
+ exportMenu.classList.remove('active');
2715
+ });
2716
+ });
2717
+ }
2718
+
2719
+ // Mobile menu toggle
2720
+ const mobileMenuBtn = $('nav-menu-btn');
2721
+ const mobileMenu = $('nav-mobile-menu');
2722
+
2723
+ if (mobileMenuBtn && mobileMenu) {
2724
+ mobileMenuBtn.addEventListener('click', () => {
2725
+ mobileMenu.classList.toggle('open');
2726
+ const isOpen = mobileMenu.classList.contains('open');
2727
+ mobileMenuBtn.innerHTML = isOpen
2728
+ ? '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
2729
+ : '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>';
2730
+ });
2731
+
2732
+ const mobileLogin = $('mobile-login');
2733
+ const mobileLogout = $('mobile-logout');
2734
+
2735
+ if (mobileLogin) {
2736
+ mobileLogin.addEventListener('click', () => {
2737
+ $('login-modal')?.classList.add('active');
2738
+ mobileMenu.classList.remove('open');
2739
+ mobileMenuBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>';
2740
+ });
2741
+ }
2742
+
2743
+ if (mobileLogout) {
2744
+ mobileLogout.addEventListener('click', () => {
2745
+ logout();
2746
+ mobileMenu.classList.remove('open');
2747
+ mobileMenuBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>';
2748
+ });
2749
+ }
2750
+ }
2751
+
2752
+ // Navigation links - scroll to sections
2753
+ $qa('.nav-link[data-tab]').forEach(link => {
2754
+ link.addEventListener('click', (e) => {
2755
+ e.preventDefault();
2756
+ $qa('.nav-link[data-tab]').forEach(l => l.classList.remove('active'));
2757
+ link.classList.add('active');
2758
+ const tabEl = $(`${link.dataset.tab}-tab`);
2759
+ if (tabEl) {
2760
+ tabEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
2761
+ }
2762
+ if (mobileMenu) {
2763
+ mobileMenu.classList.remove('open');
2764
+ if (mobileMenuBtn) {
2765
+ mobileMenuBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>';
2766
+ }
2767
+ }
2768
+ });
2769
+ });
2770
+
2771
+ // HD wallet controls
2772
+ $('hd-coin')?.addEventListener('change', () => {
2773
+ updatePathDisplay();
2774
+ deriveAndDisplayAddress();
2775
+ });
2776
+ $('hd-account')?.addEventListener('input', () => {
2777
+ updatePathDisplay();
2778
+ deriveAndDisplayAddress();
2779
+ });
2780
+ $('hd-index')?.addEventListener('input', () => {
2781
+ updatePathDisplay();
2782
+ deriveAndDisplayAddress();
2783
+ });
2784
+
2785
+ // PKI clear keys
2786
+ $('pki-clear-keys')?.addEventListener('click', clearPKIKeys);
2787
+
2788
+ // PKI algorithm change
2789
+ $('pki-algorithm')?.addEventListener('change', async () => {
2790
+ const newAlgorithm = $('pki-algorithm').value;
2791
+ state.pki.algorithm = newAlgorithm;
2792
+
2793
+ if (state.hdRoot) {
2794
+ derivePKIKeysFromHD();
2795
+ savePKIKeys();
2796
+ } else {
2797
+ try {
2798
+ if (newAlgorithm === 'p256') {
2799
+ state.pki.alice = await p256GenerateKeyPairAsync();
2800
+ state.pki.bob = await p256GenerateKeyPairAsync();
2801
+ } else if (newAlgorithm === 'p384') {
2802
+ state.pki.alice = await p384GenerateKeyPairAsync();
2803
+ state.pki.bob = await p384GenerateKeyPairAsync();
2804
+ } else {
2805
+ const curveType = newAlgorithm === 'secp256k1' ? Curve.SECP256K1 : Curve.X25519;
2806
+ state.pki.alice = generateKeyPair(curveType);
2807
+ state.pki.bob = generateKeyPair(curveType);
2808
+ }
2809
+ savePKIKeys();
2810
+ } catch (err) {
2811
+ console.error('Failed to generate keys for', newAlgorithm, err);
2812
+ return;
2813
+ }
2814
+ }
2815
+
2816
+ // Update display
2817
+ const alicePub = $('alice-public-key');
2818
+ const alicePriv = $('alice-private-key');
2819
+ const bobPub = $('bob-public-key');
2820
+ const bobPriv = $('bob-private-key');
2821
+ if (alicePub) alicePub.textContent = toHexCompact(state.pki.alice.publicKey);
2822
+ if (alicePriv) alicePriv.textContent = toHexCompact(state.pki.alice.privateKey);
2823
+ if (bobPub) bobPub.textContent = toHexCompact(state.pki.bob.publicKey);
2824
+ if (bobPriv) bobPriv.textContent = toHexCompact(state.pki.bob.privateKey);
2825
+
2826
+ const algorithmNames = {
2827
+ x25519: 'X25519 (Curve25519)',
2828
+ secp256k1: 'secp256k1 (Bitcoin/Ethereum)',
2829
+ p256: 'P-256 / secp256r1 (NIST)',
2830
+ p384: 'P-384 / secp384r1 (NIST)',
2831
+ };
2832
+ const algDisplay = $('pki-algorithm-display');
2833
+ if (algDisplay) algDisplay.textContent = algorithmNames[newAlgorithm] || newAlgorithm;
2834
+ });
2835
+
2836
+ // vCard generation
2837
+ $('generate-vcard')?.addEventListener('click', async () => {
2838
+ const info = {
2839
+ prefix: $('vcard-prefix')?.value || '',
2840
+ firstName: $('vcard-firstname')?.value || '',
2841
+ middleName: $('vcard-middlename')?.value || '',
2842
+ lastName: $('vcard-lastname')?.value || '',
2843
+ suffix: $('vcard-suffix')?.value || '',
2844
+ email: $('vcard-email')?.value || '',
2845
+ org: $('vcard-org')?.value || '',
2846
+ title: $('vcard-title')?.value || '',
2847
+ includeKeys: true,
2848
+ };
2849
+
2850
+ if (!info.firstName && !info.lastName) {
2851
+ alert('Please enter at least a first or last name');
2852
+ return;
2853
+ }
2854
+
2855
+ const vcard = generateVCard(info);
2856
+ const vcardForQR = generateVCard(info, { skipPhoto: true });
2857
+ const vcardPreview = $('vcard-preview');
2858
+ if (vcardPreview) vcardPreview.textContent = vcard;
2859
+
2860
+ try {
2861
+ const qrCanvas = $('qr-code');
2862
+ if (qrCanvas) {
2863
+ await QRCode.toCanvas(qrCanvas, vcardForQR, {
2864
+ width: 256,
2865
+ margin: 2,
2866
+ color: { dark: '#1e293b', light: '#ffffff' },
2867
+ });
2868
+ }
2869
+ const formView = $('vcard-form-view');
2870
+ const resultView = $('vcard-result-view');
2871
+ if (formView) formView.style.display = 'none';
2872
+ if (resultView) resultView.style.display = '';
2873
+ } catch (err) {
2874
+ alert('Error generating QR code: ' + err.message);
2875
+ }
2876
+ });
2877
+
2878
+ // Back to editor from result view
2879
+ $('vcard-back-btn')?.addEventListener('click', () => {
2880
+ const resultView = $('vcard-result-view');
2881
+ const formView = $('vcard-form-view');
2882
+ if (resultView) resultView.style.display = 'none';
2883
+ if (formView) formView.style.display = '';
2884
+ });
2885
+
2886
+ // Download vCard
2887
+ $('download-vcard')?.addEventListener('click', () => {
2888
+ const vcard = $('vcard-preview')?.textContent || '';
2889
+ const blob = new Blob([vcard], { type: 'text/vcard' });
2890
+ const url = URL.createObjectURL(blob);
2891
+ const a = document.createElement('a');
2892
+ a.href = url;
2893
+ a.download = 'contact.vcf';
2894
+ a.click();
2895
+ URL.revokeObjectURL(url);
2896
+ });
2897
+
2898
+ // Copy vCard
2899
+ $('copy-vcard')?.addEventListener('click', async () => {
2900
+ const vcard = $('vcard-preview')?.textContent || '';
2901
+ try {
2902
+ await navigator.clipboard.writeText(vcard);
2903
+ const btn = $('copy-vcard');
2904
+ if (btn) {
2905
+ btn.textContent = 'Copied!';
2906
+ setTimeout(() => { btn.textContent = 'Copy vCard'; }, 2000);
2907
+ }
2908
+ } catch (err) {
2909
+ alert('Failed to copy: ' + err.message);
2910
+ }
2911
+ });
2912
+
2913
+ // Refresh balances button
2914
+ $('refresh-balances')?.addEventListener('click', () => {
2915
+ updateAdversarialSecurity();
2916
+ });
2917
+
2918
+ // Escape key closes modals
2919
+ document.addEventListener('keydown', (e) => {
2920
+ if (e.key === 'Escape') {
2921
+ $qa('.modal.active').forEach(m => m.classList.remove('active'));
2922
+ }
2923
+ });
2924
+
2925
+ // Trust system handlers
2926
+ setupTrustHandlers();
2927
+ }
2928
+
2929
+ // =============================================================================
2930
+ // Trust System Handlers
2931
+ // =============================================================================
2932
+
2933
+ function setupTrustHandlers() {
2934
+ let trustScanInterval = null;
2935
+ const TRUST_SCAN_INTERVAL_MS = 60000; // 60 seconds
2936
+ const TRUST_RULES_KEY = 'trust-rules';
2937
+ const TRUST_IMPORTED_KEY = 'trust-imported-txs';
2938
+
2939
+ // Auto-scan trust transactions
2940
+ async function runTrustScan() {
2941
+ if (!state.loggedIn || !state.addresses) return;
2942
+
2943
+ const statusEl = $('trust-scan-status');
2944
+ const labelEl = $('trust-scan-label');
2945
+ const countEl = $('trust-scan-count');
2946
+ if (statusEl) statusEl.classList.add('active');
2947
+ if (labelEl) labelEl.textContent = 'Scanning...';
2948
+
2949
+ try {
2950
+ const { scanAllTrustTransactions, renderTrustList } = await import('./trust-ui.js');
2951
+ const { buildTrustGraph, analyzeTrustRelationships } = await import('./blockchain-trust.js');
2952
+
2953
+ // Scan on-chain transactions
2954
+ const onChainTxs = await scanAllTrustTransactions(state.addresses);
2955
+
2956
+ // Merge with imported transactions
2957
+ let importedTxs = [];
2958
+ try {
2959
+ const raw = localStorage.getItem(TRUST_IMPORTED_KEY);
2960
+ if (raw) importedTxs = JSON.parse(raw);
2961
+ } catch (e) { /* ignore */ }
2962
+
2963
+ const allTxs = [...onChainTxs, ...importedTxs];
2964
+
2965
+ // Deduplicate by txHash
2966
+ const seen = new Set();
2967
+ const dedupedTxs = allTxs.filter(tx => {
2968
+ if (seen.has(tx.txHash)) return false;
2969
+ seen.add(tx.txHash);
2970
+ return true;
2971
+ });
2972
+
2973
+ // Build graph and analyze relationships
2974
+ const graph = buildTrustGraph(dedupedTxs);
2975
+ const relationships = analyzeTrustRelationships(state.addresses, dedupedTxs);
2976
+
2977
+ // Apply trust rules
2978
+ const rules = loadTrustRules();
2979
+ if (rules.length > 0) {
2980
+ applyTrustRules(relationships, rules);
2981
+ }
2982
+
2983
+ // Store in state
2984
+ state.trustGraph = graph;
2985
+ state.trustTransactions = dedupedTxs;
2986
+ state.trustRelationships = relationships;
2987
+
2988
+ // Update UI
2989
+ const listEl = $('trust-list');
2990
+ if (listEl) {
2991
+ renderTrustList(listEl, relationships, state.addresses);
2992
+ }
2993
+
2994
+ if (labelEl) labelEl.textContent = 'Last scan: just now';
2995
+ if (countEl) countEl.textContent = `${relationships.length} relationships`;
2996
+
2997
+ console.log(`Trust scan: ${dedupedTxs.length} txs, ${relationships.length} relationships`);
2998
+ } catch (err) {
2999
+ console.error('Trust scan failed:', err);
3000
+ if (labelEl) labelEl.textContent = 'Scan failed';
3001
+ }
3002
+ }
3003
+
3004
+ // Start auto-scanning
3005
+ function startTrustScanning() {
3006
+ runTrustScan();
3007
+ trustScanInterval = setInterval(runTrustScan, TRUST_SCAN_INTERVAL_MS);
3008
+ }
3009
+
3010
+ // Stop auto-scanning
3011
+ function stopTrustScanning() {
3012
+ if (trustScanInterval) {
3013
+ clearInterval(trustScanInterval);
3014
+ trustScanInterval = null;
3015
+ }
3016
+ const statusEl = $('trust-scan-status');
3017
+ if (statusEl) statusEl.classList.remove('active');
3018
+ }
3019
+
3020
+ // Load trust rules from localStorage
3021
+ function loadTrustRules() {
3022
+ try {
3023
+ const raw = localStorage.getItem(TRUST_RULES_KEY);
3024
+ return raw ? JSON.parse(raw) : [];
3025
+ } catch (e) { return []; }
3026
+ }
3027
+
3028
+ // Apply trust rules to relationships
3029
+ function applyTrustRules(relationships, rules) {
3030
+ for (const rel of relationships) {
3031
+ for (const rule of rules) {
3032
+ switch (rule.type) {
3033
+ case 'mutual_tx_count':
3034
+ if (rel.direction === 'mutual' && rel.txCount >= rule.params.threshold) {
3035
+ rel.ruleLevel = Math.max(rel.ruleLevel || 0, rule.resultLevel);
3036
+ }
3037
+ break;
3038
+ case 'last_interaction_days': {
3039
+ const daysSince = (Date.now() - rel.lastSeen) / (1000 * 60 * 60 * 24);
3040
+ if (daysSince <= rule.params.threshold) {
3041
+ rel.ruleLevel = Math.max(rel.ruleLevel || 0, rule.resultLevel);
3042
+ }
3043
+ break;
3044
+ }
3045
+ case 'bidirectional_trust':
3046
+ if (rel.direction === 'mutual') {
3047
+ rel.ruleLevel = Math.min((rel.level || 2) + 1, 5);
3048
+ }
3049
+ break;
3050
+ case 'address_blocklist':
3051
+ // Handled by NEVER trust level on-chain
3052
+ break;
3053
+ }
3054
+ }
3055
+ }
3056
+ }
3057
+
3058
+ // Establish trust button
3059
+ $('establish-trust-btn')?.addEventListener('click', async () => {
3060
+ if (!state.loggedIn) { alert('Please login first'); return; }
3061
+ const { showEstablishTrustModal } = await import('./trust-ui.js');
3062
+ showEstablishTrustModal(({ level, network, recipientAddress }) => {
3063
+ console.log('Establish trust:', { level, network, recipientAddress });
3064
+ // TODO: Build, sign, and broadcast trust transaction
3065
+ alert(`Trust transaction would be published on ${network.toUpperCase()} for level ${level}.\nTransaction signing/broadcasting is not yet implemented.`);
3066
+ });
3067
+ });
3068
+
3069
+ // Rules button
3070
+ $('trust-rules-btn')?.addEventListener('click', async () => {
3071
+ const { showRulesModal } = await import('./trust-ui.js');
3072
+ const currentRules = loadTrustRules();
3073
+ showRulesModal(currentRules, (updatedRules) => {
3074
+ localStorage.setItem(TRUST_RULES_KEY, JSON.stringify(updatedRules));
3075
+ // Re-apply rules
3076
+ if (state.trustRelationships) {
3077
+ applyTrustRules(state.trustRelationships, updatedRules);
3078
+ runTrustScan();
3079
+ }
3080
+ });
3081
+ });
3082
+
3083
+ // Export trust data
3084
+ $('trust-export-btn')?.addEventListener('click', async () => {
3085
+ if (!state.trustTransactions || state.trustTransactions.length === 0) {
3086
+ alert('No trust data to export. Wait for a scan to complete.');
3087
+ return;
3088
+ }
3089
+ const { exportTrustData } = await import('./trust-ui.js');
3090
+ const xpub = state.hdRoot ? state.hdRoot.publicExtendedKey() : '';
3091
+ exportTrustData(state.trustTransactions, xpub);
3092
+ });
3093
+
3094
+ // Import trust data
3095
+ $('trust-import-input')?.addEventListener('change', async (e) => {
3096
+ const file = e.target.files[0];
3097
+ if (!file) return;
3098
+ try {
3099
+ const { importTrustData } = await import('./trust-ui.js');
3100
+ const importedTxs = await importTrustData(file);
3101
+
3102
+ // Merge with existing imported txs
3103
+ let existing = [];
3104
+ try {
3105
+ const raw = localStorage.getItem(TRUST_IMPORTED_KEY);
3106
+ if (raw) existing = JSON.parse(raw);
3107
+ } catch (err) { /* ignore */ }
3108
+
3109
+ const merged = [...existing, ...importedTxs];
3110
+ const seen = new Set();
3111
+ const deduped = merged.filter(tx => {
3112
+ if (seen.has(tx.txHash)) return false;
3113
+ seen.add(tx.txHash);
3114
+ return true;
3115
+ });
3116
+
3117
+ localStorage.setItem(TRUST_IMPORTED_KEY, JSON.stringify(deduped));
3118
+ alert(`Imported ${importedTxs.length} trust transactions.`);
3119
+
3120
+ // Re-scan to incorporate
3121
+ runTrustScan();
3122
+ } catch (err) {
3123
+ console.error('Trust import failed:', err);
3124
+ alert('Failed to import trust data: ' + err.message);
3125
+ }
3126
+ e.target.value = '';
3127
+ });
3128
+
3129
+ // Expose start/stop for login/logout
3130
+ state._startTrustScanning = startTrustScanning;
3131
+ state._stopTrustScanning = stopTrustScanning;
3132
+ }
3133
+
3134
+ // =============================================================================
3135
+ // Homepage Handlers
3136
+ // =============================================================================
3137
+
3138
+ function setupHomepageHandlers() {
3139
+ // Version tag
3140
+ const versionTag = $('version-tag');
3141
+ if (versionTag) {
3142
+ try {
3143
+ const pkg = __APP_VERSION__;
3144
+ versionTag.textContent = pkg ? `v${pkg}` : '';
3145
+ } catch { /* ignore */ }
3146
+ }
3147
+
3148
+ // Code copy buttons
3149
+ document.querySelectorAll('.code-copy-btn').forEach(btn => {
3150
+ btn.addEventListener('click', () => {
3151
+ const code = btn.closest('.code-block')?.querySelector('code');
3152
+ if (code) {
3153
+ navigator.clipboard.writeText(code.textContent).then(() => {
3154
+ btn.title = 'Copied!';
3155
+ setTimeout(() => { btn.title = 'Copy code'; }, 2000);
3156
+ });
3157
+ }
3158
+ });
3159
+ });
3160
+ }
3161
+
3162
+ // =============================================================================
3163
+ // Initialization
3164
+ // =============================================================================
3165
+
3166
+ export async function init(rootElement, options = {}) {
3167
+ const { autoOpenWallet = false } = typeof rootElement === 'object' && !(rootElement instanceof Node)
3168
+ ? (options = rootElement, {}) : options;
3169
+ if (rootElement && rootElement instanceof Node) _root = rootElement;
3170
+
3171
+ // Inject modal HTML if not already present in the DOM
3172
+ if (!document.getElementById('keys-modal')) {
3173
+ const container = document.createElement('div');
3174
+ container.id = 'hd-wallet-ui-container';
3175
+ container.innerHTML = getModalHTML();
3176
+ document.body.appendChild(container);
3177
+ }
3178
+
3179
+ const status = $('status');
3180
+ const loadingOverlay = $('loading-overlay');
3181
+
3182
+ // Initialize grid animation
3183
+ initGridAnimation();
3184
+
3185
+ // Initialize wallet info box state
3186
+ initWalletInfoBox();
3187
+ bindInfoHandlers();
3188
+
3189
+ try {
3190
+ // Load HD wallet WASM
3191
+ if (status) status.textContent = 'Loading HD wallet module...';
3192
+ state.hdWalletModule = await initHDWallet();
3193
+
3194
+ // Load saved PKI keys if available
3195
+ const hasSavedKeys = loadPKIKeys();
3196
+
3197
+ state.initialized = true;
3198
+
3199
+ // Update nav status
3200
+ const navStatus = $('nav-status');
3201
+ if (navStatus) {
3202
+ navStatus.className = 'nav-status ready';
3203
+ }
3204
+
3205
+ // Hide loading overlay with fade
3206
+ if (loadingOverlay) {
3207
+ loadingOverlay.classList.add('hidden');
3208
+ setTimeout(() => {
3209
+ loadingOverlay.style.display = 'none';
3210
+ }, 500);
3211
+ }
3212
+
3213
+ setupLoginHandlers();
3214
+ setupMainAppHandlers();
3215
+ initCurrencySelector();
3216
+ setupHomepageHandlers();
3217
+
3218
+ // Handle initial hash navigation
3219
+ const initialHash = window.location.hash.slice(1);
3220
+ if (initialHash) {
3221
+ const tabEl = $(`${initialHash}-tab`);
3222
+ if (tabEl) {
3223
+ setTimeout(() => {
3224
+ tabEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
3225
+ $qa('.nav-link[data-tab]').forEach(link => {
3226
+ link.classList.remove('active');
3227
+ if (link.dataset.tab === initialHash) {
3228
+ link.classList.add('active');
3229
+ }
3230
+ });
3231
+ }, 100);
3232
+ }
3233
+ }
3234
+
3235
+ // Check if there's a stored wallet
3236
+ const storageMetadata = WalletStorage.getStorageMetadata();
3237
+ const hasStoredWallet = storageMetadata?.method && storageMetadata.method !== StorageMethod.NONE;
3238
+
3239
+ // Auto-open login modal if stored wallet found (opt-in for integrators)
3240
+ if (hasStoredWallet && autoOpenWallet) {
3241
+ const loginModal = $('login-modal');
3242
+ if (loginModal) {
3243
+ loginModal.classList.add('active');
3244
+ // Switch to stored wallet tab
3245
+ const storedTab = loginModal.querySelector('[data-tab="stored"]');
3246
+ if (storedTab) storedTab.click();
3247
+ }
3248
+ }
3249
+
3250
+ // Auto-login with saved PKI keys if no stored wallet
3251
+ if (hasSavedKeys && !hasStoredWallet) {
3252
+ const tempEd25519Seed = new Uint8Array(32);
3253
+ crypto.getRandomValues(tempEd25519Seed);
3254
+ const tempKeys = {
3255
+ x25519: generateKeyPair(Curve.X25519),
3256
+ ed25519: {
3257
+ privateKey: tempEd25519Seed,
3258
+ publicKey: ed25519.getPublicKey(tempEd25519Seed),
3259
+ },
3260
+ secp256k1: generateKeyPair(Curve.SECP256K1),
3261
+ p256: await p256GenerateKeyPairAsync(),
3262
+ p384: await p384GenerateKeyPairAsync(),
3263
+ };
3264
+
3265
+ login(tempKeys);
3266
+ }
3267
+
3268
+ } catch (err) {
3269
+ console.error('Initialization failed:', err);
3270
+ if (status) status.textContent = `Error: ${err.message}`;
3271
+ }
3272
+ }
3273
+
3274
+ /**
3275
+ * Create a wallet UI instance that can be controlled programmatically.
3276
+ * Consumers attach openLogin / openAccount to their own buttons.
3277
+ *
3278
+ * @param {Node} [rootElement] - Optional root element for DOM queries
3279
+ * @param {Object} [options] - Options passed to init()
3280
+ * @returns {Promise<{openLogin: Function, openAccount: Function, destroy: Function}>}
3281
+ */
3282
+ export async function createWalletUI(rootElement, options = {}) {
3283
+ await init(rootElement, options);
3284
+
3285
+ return {
3286
+ /** Open the login modal */
3287
+ openLogin() {
3288
+ const modal = document.getElementById('login-modal');
3289
+ if (modal) modal.classList.add('active');
3290
+ },
3291
+ /** Open the account / keys modal (requires login first) */
3292
+ openAccount() {
3293
+ const modal = document.getElementById('keys-modal');
3294
+ if (modal) modal.classList.add('active');
3295
+ },
3296
+ /** Remove all injected wallet UI elements from the DOM */
3297
+ destroy() {
3298
+ const container = document.getElementById('hd-wallet-ui-container');
3299
+ if (container) container.remove();
3300
+ },
3301
+ };
3302
+ }