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 +2 -2
- package/src/app.js +45 -1
- package/src/template.js +5 -1
- package/src/wallet-storage.js +13 -17
- package/styles/main.css +42 -0
- package/styles/widget.css +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hd-wallet-ui",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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">×</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">×</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">
|
package/src/wallet-storage.js
CHANGED
|
@@ -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
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
|
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;
|