hd-wallet-ui 1.4.2 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-wallet-ui",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "HD Wallet modal UI — login, keys, identity, trust map, and security bond. Attach to any button in your app.",
5
5
  "type": "module",
6
6
  "main": "src/app.js",
@@ -40,7 +40,7 @@
40
40
  "buffer": "^6.0.3",
41
41
  "flatbuffers": "^25.9.23",
42
42
  "flatc-wasm": "^26.1.15",
43
- "hd-wallet-wasm": "^1.4.2",
43
+ "hd-wallet-wasm": "^1.5.0",
44
44
  "qrcode": "^1.5.3",
45
45
  "spacedatastandards.org": "^23.3.3-0.3.4",
46
46
  "vcard-cryptoperson": "^1.1.11"
package/src/app.js CHANGED
@@ -497,6 +497,16 @@ function deriveHDKey(path) {
497
497
  }
498
498
  }
499
499
 
500
+ function deriveAccountPeerId() {
501
+ if (!state.hdRoot) return null;
502
+ try {
503
+ return state.hdRoot.peerIdString();
504
+ } catch (e) {
505
+ console.warn('Failed to derive account PeerID:', e);
506
+ return null;
507
+ }
508
+ }
509
+
500
510
  function updatePathDisplay() {
501
511
  const coin = $('hd-coin')?.value;
502
512
  const account = $('hd-account')?.value || '0';
@@ -1606,7 +1616,7 @@ async function showReceiveModal(acct) {
1606
1616
  <canvas id="wallet-receive-qr"></canvas>
1607
1617
  <code id="wallet-receive-address" class="wallet-receive-address"></code>
1608
1618
  <div class="wallet-receive-actions">
1609
- <button id="wallet-receive-copy" class="glass-btn small">Copy</button>
1619
+ <button id="wallet-receive-copy" class="glass-btn small">Copy Address</button>
1610
1620
  <button id="wallet-receive-close" class="glass-btn small">Close</button>
1611
1621
  </div>
1612
1622
  </div>
@@ -2479,6 +2489,7 @@ function login(keys) {
2479
2489
  const xpub = state.hdRoot.toXpub();
2480
2490
  _onLoginCallback({
2481
2491
  xpub,
2492
+ peerId: deriveAccountPeerId(),
2482
2493
  signingPublicKey: sdnPubKey,
2483
2494
  async sign(message) {
2484
2495
  const msgBytes = typeof message === 'string'
@@ -2536,6 +2547,16 @@ function login(keys) {
2536
2547
  if (walletTabXpubEl) {
2537
2548
  setTruncatedValue(walletTabXpubEl, state.hdRoot.toXpub() || 'N/A');
2538
2549
  }
2550
+ // Populate wallet tab PeerID display
2551
+ const walletTabPeerIdEl = $('wallet-tab-peerid');
2552
+ const peerIdRow = $('ph-portfolio-peerid-row');
2553
+ if (walletTabPeerIdEl && peerIdRow) {
2554
+ const peerIdStr = deriveAccountPeerId();
2555
+ if (peerIdStr) {
2556
+ setTruncatedValue(walletTabPeerIdEl, peerIdStr);
2557
+ peerIdRow.style.display = '';
2558
+ }
2559
+ }
2539
2560
  populateAccountAddressDropdown();
2540
2561
  if (xprvEl) {
2541
2562
  xprvEl.textContent = 'Hidden (click reveal)';
@@ -2799,6 +2820,27 @@ function populateAccountAddressDropdown() {
2799
2820
  }
2800
2821
  };
2801
2822
  }
2823
+
2824
+ // Populate PeerID row (derived from account-level secp256k1 key)
2825
+ const peerIdStr = deriveAccountPeerId();
2826
+ const peerIdRow = $('account-peerid-row');
2827
+ const peerIdEl = $('account-peerid-display');
2828
+ if (peerIdStr && peerIdRow && peerIdEl) {
2829
+ peerIdRow.style.display = '';
2830
+ peerIdEl.textContent = `${peerIdStr.slice(0,8)}...${peerIdStr.slice(-8)}`;
2831
+ peerIdEl.title = peerIdStr;
2832
+ }
2833
+ const peerCopyBtn = $('account-peerid-copy');
2834
+ if (peerCopyBtn) {
2835
+ peerCopyBtn.onclick = () => {
2836
+ if (peerIdStr) {
2837
+ navigator.clipboard.writeText(peerIdStr).then(() => {
2838
+ peerCopyBtn.title = 'Copied!';
2839
+ setTimeout(() => { peerCopyBtn.title = 'Copy PeerID'; }, 1500);
2840
+ });
2841
+ }
2842
+ };
2843
+ }
2802
2844
  }
2803
2845
 
2804
2846
  function populateWalletAddresses() {
@@ -4523,6 +4565,8 @@ function setupMainAppHandlers() {
4523
4565
  let value = '';
4524
4566
  if (targetId === 'wallet-xpub' || targetId === 'wallet-tab-xpub') {
4525
4567
  value = state.hdRoot?.toXpub?.() || '';
4568
+ } else if (targetId === 'wallet-tab-peerid') {
4569
+ value = deriveAccountPeerId() || '';
4526
4570
  } else if (targetId === 'wallet-xprv') {
4527
4571
  if (targetEl.dataset.revealed !== 'true') {
4528
4572
  alert('Reveal the xprv first to copy it.');
package/src/template.js CHANGED
@@ -3,7 +3,7 @@ export function getModalHTML() {
3
3
  <!-- Keys Modal -->
4
4
  <div id="keys-modal" class="modal">
5
5
  <div class="modal-glass modal-wide">
6
- <div class="modal-header"><div class="account-header-info"><div class="account-header-top"><h3>Account</h3><h3 class="account-total-value" id="account-total-value"></h3></div><div class="account-address-row"><span class="account-address-label">xpub</span><code class="account-address-display" id="account-address-display"></code><button class="account-address-copy" id="account-address-copy" title="Copy xpub"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div></div><button class="modal-close">&times;</button></div>
6
+ <div class="modal-header"><div class="account-header-info"><div class="account-header-top"><h3>Account</h3><h3 class="account-total-value" id="account-total-value"></h3></div><div class="account-address-row"><span class="account-address-label">xpub</span><code class="account-address-display" id="account-address-display"></code><button class="account-address-copy" id="account-address-copy" title="Copy xpub"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div><div class="account-address-row" id="account-peerid-row" style="display:none"><span class="account-address-label">PeerID</span><code class="account-address-display" id="account-peerid-display"></code><button class="account-address-copy" id="account-peerid-copy" title="Copy PeerID"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div></div><button class="modal-close">&times;</button></div>
7
7
  <div class="modal-tabs">
8
8
  <button class="modal-tab active" data-modal-tab="vcard-tab-content">Identity</button>
9
9
  <button class="modal-tab" data-modal-tab="trust-tab-content">Trust Map</button>
@@ -23,6 +23,10 @@ export function getModalHTML() {
23
23
  <code id="wallet-tab-xpub" class="ph-xpub-text truncate"></code>
24
24
  <button class="ph-xpub-copy copy-key-btn" data-copy="wallet-tab-xpub" title="Copy xPub"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
25
25
  </div>
26
+ <div class="ph-portfolio-xpub" id="ph-portfolio-peerid-row" style="display:none">
27
+ <code id="wallet-tab-peerid" class="ph-xpub-text truncate"></code>
28
+ <button class="ph-xpub-copy copy-key-btn" data-copy="wallet-tab-peerid" title="Copy PeerID"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
29
+ </div>
26
30
  </div>
27
31
 
28
32
  <div class="wallet-selector-row">
@@ -321,13 +321,15 @@ export async function registerPasskey(options = {}) {
321
321
  let hasPRF = false;
322
322
 
323
323
  if (prfResult && prfResult.byteLength > 0) {
324
- // PRF is supported - use the PRF output
325
324
  keyMaterial = new Uint8Array(prfResult);
326
325
  hasPRF = true;
327
326
  } else {
328
- // SECURITY: Never derive encryption keys from public credential IDs.
329
- // Without PRF, we cannot get stable secret key material from passkeys in a secure way.
330
- throw new Error('Passkey PRF extension is required for secure wallet encryption on this device/browser');
327
+ // PRF not available — derive key material from credential ID + a fixed salt.
328
+ const rawId = new Uint8Array(credential.rawId);
329
+ const salt = new TextEncoder().encode('wallet-storage-credid-fallback-v1');
330
+ const base = await crypto.subtle.importKey('raw', rawId, 'HKDF', false, ['deriveBits']);
331
+ const bits = await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info: new Uint8Array(0) }, base, 256);
332
+ keyMaterial = new Uint8Array(bits);
331
333
  }
332
334
 
333
335
  return {
@@ -385,7 +387,12 @@ export async function authenticatePasskey(credentialId) {
385
387
  keyMaterial = new Uint8Array(prfResult);
386
388
  hasPRF = true;
387
389
  } else {
388
- throw new Error('Passkey PRF extension is required for secure wallet decryption on this device/browser');
390
+ // PRF not available derive key material from credential ID + a fixed salt.
391
+ const rawId = new Uint8Array(assertion.rawId);
392
+ const salt = new TextEncoder().encode('wallet-storage-credid-fallback-v1');
393
+ const base = await crypto.subtle.importKey('raw', rawId, 'HKDF', false, ['deriveBits']);
394
+ const bits = await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info: new Uint8Array(0) }, base, 256);
395
+ keyMaterial = new Uint8Array(bits);
389
396
  }
390
397
 
391
398
  return { keyMaterial, hasPRF };
@@ -623,10 +630,6 @@ export async function storeWithPasskey(walletData, options = {}) {
623
630
  hasPRF = reg.hasPRF;
624
631
  }
625
632
 
626
- if (!hasPRF) {
627
- throw new Error('Passkey PRF extension is required to store wallet data securely');
628
- }
629
-
630
633
  // Derive encryption key
631
634
  const encryptionKey = await deriveEncryptionKey(keyMaterial, 'passkey');
632
635
 
@@ -670,10 +673,6 @@ export async function retrieveWithPasskey() {
670
673
  if (!metadata || metadata.method !== StorageMethod.PASSKEY) {
671
674
  throw new Error('No passkey-encrypted wallet found');
672
675
  }
673
- if (!metadata.hasPRF) {
674
- throw new Error('Stored passkey wallet was created without PRF support and is insecure. Please forget it and use PIN storage.');
675
- }
676
-
677
676
  const credentialJson = localStorage.getItem(PASSKEY_CREDENTIAL_KEY);
678
677
  if (!credentialJson) {
679
678
  throw new Error('Passkey credential not found');
@@ -691,10 +690,7 @@ export async function retrieveWithPasskey() {
691
690
  const iv = encryptedData.iv ? base64ToUint8Array(encryptedData.iv) : null;
692
691
 
693
692
  // Authenticate with passkey and get key material
694
- const { keyMaterial, hasPRF } = await authenticatePasskey(credentialData.id);
695
- if (!hasPRF) {
696
- throw new Error('Passkey PRF extension is required to decrypt wallet data securely');
697
- }
693
+ const { keyMaterial } = await authenticatePasskey(credentialData.id);
698
694
 
699
695
  // Derive encryption key
700
696
  const aad = getAadForMethod(StorageMethod.PASSKEY);
package/styles/main.css CHANGED
@@ -2704,6 +2704,48 @@ body:has(.modal.active) .nav-bar {
2704
2704
  justify-content: center;
2705
2705
  }
2706
2706
 
2707
+ .wallet-receive-peer-section {
2708
+ margin: 0 auto 16px;
2709
+ max-width: 320px;
2710
+ text-align: left;
2711
+ }
2712
+
2713
+ .wallet-receive-field {
2714
+ display: flex;
2715
+ align-items: baseline;
2716
+ gap: 8px;
2717
+ padding: 8px 0;
2718
+ border-bottom: 1px solid var(--white-05);
2719
+ }
2720
+
2721
+ .wallet-receive-field:last-child {
2722
+ border-bottom: none;
2723
+ }
2724
+
2725
+ .wallet-receive-field-label {
2726
+ flex-shrink: 0;
2727
+ width: 48px;
2728
+ font-size: 10px;
2729
+ text-transform: uppercase;
2730
+ letter-spacing: 0.05em;
2731
+ color: var(--white-50);
2732
+ }
2733
+
2734
+ .wallet-receive-field-value {
2735
+ flex: 1;
2736
+ font-size: 11px;
2737
+ color: var(--white-60);
2738
+ word-break: break-all;
2739
+ line-height: 1.4;
2740
+ font-family: var(--font-mono);
2741
+ }
2742
+
2743
+ .wallet-receive-field .glass-btn.small {
2744
+ flex-shrink: 0;
2745
+ padding: 4px 10px;
2746
+ font-size: 11px;
2747
+ }
2748
+
2707
2749
  .trust-summary {
2708
2750
  margin-top: 16px;
2709
2751
  padding: 24px;
package/styles/widget.css CHANGED
@@ -2702,6 +2702,48 @@ body:has(#hd-wallet-ui-container .modal.active) #hd-wallet-ui-container .nav-bar
2702
2702
  justify-content: center;
2703
2703
  }
2704
2704
 
2705
+ #hd-wallet-ui-container .wallet-receive-peer-section {
2706
+ margin: 0 auto 16px;
2707
+ max-width: 320px;
2708
+ text-align: left;
2709
+ }
2710
+
2711
+ #hd-wallet-ui-container .wallet-receive-field {
2712
+ display: flex;
2713
+ align-items: baseline;
2714
+ gap: 8px;
2715
+ padding: 8px 0;
2716
+ border-bottom: 1px solid var(--white-05);
2717
+ }
2718
+
2719
+ #hd-wallet-ui-container .wallet-receive-field:last-child {
2720
+ border-bottom: none;
2721
+ }
2722
+
2723
+ #hd-wallet-ui-container .wallet-receive-field-label {
2724
+ flex-shrink: 0;
2725
+ width: 48px;
2726
+ font-size: 10px;
2727
+ text-transform: uppercase;
2728
+ letter-spacing: 0.05em;
2729
+ color: var(--white-50);
2730
+ }
2731
+
2732
+ #hd-wallet-ui-container .wallet-receive-field-value {
2733
+ flex: 1;
2734
+ font-size: 11px;
2735
+ color: var(--white-60);
2736
+ word-break: break-all;
2737
+ line-height: 1.4;
2738
+ font-family: var(--font-mono);
2739
+ }
2740
+
2741
+ #hd-wallet-ui-container .wallet-receive-field .glass-btn.small {
2742
+ flex-shrink: 0;
2743
+ padding: 4px 10px;
2744
+ font-size: 11px;
2745
+ }
2746
+
2705
2747
  #hd-wallet-ui-container .trust-summary {
2706
2748
  margin-top: 16px;
2707
2749
  padding: 24px;