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.
@@ -0,0 +1,745 @@
1
+ /**
2
+ * Trust UI Components
3
+ *
4
+ * UI components for displaying and interacting with blockchain trust graph
5
+ */
6
+
7
+ import {
8
+ TrustLevel,
9
+ TrustLevelNames,
10
+ scanBitcoinTrustTransactions,
11
+ scanSolanaTrustTransactions,
12
+ scanEthereumTrustTransactions,
13
+ buildTrustGraph,
14
+ calculateTrustScore,
15
+ analyzeTrustRelationships,
16
+ } from './blockchain-trust.js';
17
+
18
+ // =============================================================================
19
+ // Helpers
20
+ // =============================================================================
21
+
22
+ export function truncatePubkey(pubkey, prefixLen = 12, suffixLen = 8) {
23
+ if (!pubkey) return '';
24
+ if (pubkey.length <= prefixLen + suffixLen + 3) return pubkey;
25
+ return `${pubkey.slice(0, prefixLen)}...${pubkey.slice(-suffixLen)}`;
26
+ }
27
+
28
+ export function truncateTxHash(txHash, prefixLen = 10, suffixLen = 6) {
29
+ if (!txHash) return '';
30
+ if (txHash.length <= prefixLen + suffixLen + 3) return txHash;
31
+ return `${txHash.slice(0, prefixLen)}...${txHash.slice(-suffixLen)}`;
32
+ }
33
+
34
+ function explorerTxUrl(chain, txHash) {
35
+ switch (chain) {
36
+ case 'btc':
37
+ return `https://blockstream.info/tx/${txHash}`;
38
+ case 'eth':
39
+ return `https://etherscan.io/tx/${txHash}`;
40
+ case 'sol':
41
+ return `https://solscan.io/tx/${txHash}`;
42
+ default:
43
+ return `https://blockstream.info/tx/${txHash}`;
44
+ }
45
+ }
46
+
47
+ function chainBadge(chain) {
48
+ const labels = { btc: 'BTC', eth: 'ETH', sol: 'SOL' };
49
+ const label = labels[chain] || chain?.toUpperCase() || '???';
50
+ return `<span class="chain-badge chain-${chain || 'unknown'}">${label}</span>`;
51
+ }
52
+
53
+ function trustLevelBadge(level) {
54
+ const name = TrustLevelNames[level] || 'Unknown';
55
+ const cls = name.toLowerCase().replace(/\s+/g, '-');
56
+ return `<span class="trust-level-badge trust-level-${cls}">${name}</span>`;
57
+ }
58
+
59
+ function directionIndicator(direction) {
60
+ switch (direction) {
61
+ case 'outbound':
62
+ return '<span class="trust-direction" title="Outbound">&rarr;</span>';
63
+ case 'inbound':
64
+ return '<span class="trust-direction" title="Inbound">&larr;</span>';
65
+ case 'mutual':
66
+ return '<span class="trust-direction" title="Mutual">&harr;</span>';
67
+ default:
68
+ return '<span class="trust-direction">--</span>';
69
+ }
70
+ }
71
+
72
+ function closeModal(modal) {
73
+ modal.classList.remove('active');
74
+ setTimeout(() => modal.remove(), 200);
75
+ }
76
+
77
+ // =============================================================================
78
+ // 1. renderTrustList
79
+ // =============================================================================
80
+
81
+ export function renderTrustList(container, relationships, ownAddresses) {
82
+ container.innerHTML = '';
83
+
84
+ if (!relationships || relationships.length === 0) {
85
+ container.innerHTML = '<div class="trust-empty">No trust relationships found.</div>';
86
+ return;
87
+ }
88
+
89
+ const list = document.createElement('div');
90
+ list.className = 'trust-list';
91
+
92
+ for (const rel of relationships) {
93
+ const row = document.createElement('div');
94
+ row.className = 'trust-row';
95
+
96
+ const ownSet = new Set(
97
+ Array.isArray(ownAddresses)
98
+ ? ownAddresses
99
+ : Object.values(ownAddresses || {}).flat()
100
+ );
101
+
102
+ const isOutbound = ownSet.has(rel.from);
103
+ const isInbound = ownSet.has(rel.to);
104
+ const direction = isOutbound && isInbound ? 'mutual'
105
+ : isOutbound ? 'outbound'
106
+ : isInbound ? 'inbound'
107
+ : 'outbound';
108
+
109
+ const displayAddress = direction === 'inbound' ? rel.from : rel.to;
110
+ const chain = rel.chain || rel.network || 'btc';
111
+
112
+ // Header
113
+ const header = document.createElement('div');
114
+ header.className = 'trust-row-header';
115
+ header.innerHTML = `
116
+ <span class="trust-row-address" title="${displayAddress}">${truncatePubkey(displayAddress)}</span>
117
+ ${chainBadge(chain)}
118
+ ${trustLevelBadge(rel.level)}
119
+ ${directionIndicator(direction)}
120
+ <span class="trust-row-expand">
121
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
122
+ <polyline points="6 9 12 15 18 9"/>
123
+ </svg>
124
+ </span>
125
+ `;
126
+
127
+ // Detail
128
+ const detail = document.createElement('div');
129
+ detail.className = 'trust-row-detail';
130
+
131
+ const txs = rel.transactions || (rel.txHash ? [rel] : []);
132
+ const txRows = txs.map(tx => {
133
+ const ts = tx.timestamp ? new Date(tx.timestamp).toLocaleString() : '--';
134
+ const hash = tx.txHash || tx.hash || '';
135
+ const txChain = tx.chain || tx.network || chain;
136
+ const url = explorerTxUrl(txChain, hash);
137
+ return `
138
+ <div class="trust-tx-row">
139
+ <span class="trust-tx-time">${ts}</span>
140
+ <a class="trust-tx-link" href="${url}" target="_blank" rel="noopener">${truncateTxHash(hash)}</a>
141
+ </div>
142
+ `;
143
+ }).join('');
144
+
145
+ const revokeBtn = direction !== 'inbound'
146
+ ? `<button class="glass-btn glass-btn-sm trust-revoke-btn" data-address="${displayAddress}">Revoke</button>`
147
+ : '';
148
+
149
+ detail.innerHTML = `
150
+ <div class="trust-detail-address">
151
+ <label>Full Address</label>
152
+ <code>${displayAddress}</code>
153
+ </div>
154
+ <div class="trust-detail-txs">
155
+ <label>Transactions</label>
156
+ ${txRows || '<span class="trust-no-txs">No transactions recorded</span>'}
157
+ </div>
158
+ ${revokeBtn ? `<div class="trust-detail-actions">${revokeBtn}</div>` : ''}
159
+ `;
160
+
161
+ // Toggle expand
162
+ header.addEventListener('click', () => {
163
+ const wasExpanded = row.classList.contains('expanded');
164
+ // Collapse all others
165
+ list.querySelectorAll('.trust-row.expanded').forEach(r => r.classList.remove('expanded'));
166
+ if (!wasExpanded) row.classList.add('expanded');
167
+ });
168
+
169
+ row.appendChild(header);
170
+ row.appendChild(detail);
171
+ list.appendChild(row);
172
+ }
173
+
174
+ container.appendChild(list);
175
+ }
176
+
177
+ // =============================================================================
178
+ // 2. showEstablishTrustModal
179
+ // =============================================================================
180
+
181
+ // Address format detection
182
+ function detectChainFromAddress(address) {
183
+ if (!address) return null;
184
+ const a = address.trim();
185
+ // Bitcoin: starts with 1, 3, or bc1
186
+ if (/^(1|3)[a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(a)) return 'btc';
187
+ if (/^bc1[a-z0-9]{25,90}$/.test(a)) return 'btc';
188
+ // Ethereum: 0x + 40 hex chars
189
+ if (/^0x[0-9a-fA-F]{40}$/.test(a)) return 'eth';
190
+ // Solana: base58, 32-44 chars, no 0/O/I/l
191
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a)) return 'sol';
192
+ return null;
193
+ }
194
+
195
+ // Simple VCF parser for trust modal
196
+ function parseVCFForTrust(vcfText) {
197
+ const lines = vcfText.replace(/\r?\n /g, '').split(/\r?\n/);
198
+ const result = { name: null, email: null, org: null, photo: null, keys: [], addresses: [] };
199
+
200
+ for (const line of lines) {
201
+ const colonIdx = line.indexOf(':');
202
+ if (colonIdx === -1) continue;
203
+ const prop = line.substring(0, colonIdx).toUpperCase();
204
+ const value = line.substring(colonIdx + 1);
205
+
206
+ if (prop === 'FN') {
207
+ result.name = value;
208
+ } else if (prop.startsWith('EMAIL')) {
209
+ result.email = value;
210
+ } else if (prop.startsWith('ORG')) {
211
+ result.org = value.replace(/;/g, ', ');
212
+ } else if (prop.startsWith('PHOTO')) {
213
+ if (prop.includes('VALUE=URI') || value.startsWith('data:') || value.startsWith('http')) {
214
+ result.photo = value;
215
+ } else if (prop.includes('ENCODING=B') || prop.includes('ENCODING=b')) {
216
+ const typeMatch = prop.match(/TYPE=(\w+)/i);
217
+ const imgType = typeMatch ? typeMatch[1].toLowerCase() : 'jpeg';
218
+ result.photo = `data:image/${imgType};base64,${value}`;
219
+ }
220
+ } else if (prop.startsWith('KEY') || prop.startsWith('X-CRYPTO') || prop.startsWith('X-KEY')) {
221
+ result.keys.push(value);
222
+ const chain = detectChainFromAddress(value);
223
+ if (chain) result.addresses.push({ address: value, chain });
224
+ }
225
+ }
226
+
227
+ // Also scan NOTE or other fields for addresses
228
+ return result;
229
+ }
230
+
231
+ const TRUST_LEVEL_CONFIG = [
232
+ { value: TrustLevel.NEVER, name: 'Never Trust', desc: 'Block this address from all interactions', color: '#ef4444', border: 'rgba(239, 68, 68, 0.4)' },
233
+ { value: TrustLevel.UNKNOWN, name: 'Unknown', desc: 'No opinion on this address yet', color: '#9ca3af', border: 'rgba(107, 114, 128, 0.4)' },
234
+ { value: TrustLevel.MARGINAL, name: 'Marginal', desc: 'Somewhat trusted, proceed with caution', color: '#fbbf24', border: 'rgba(245, 158, 11, 0.4)' },
235
+ { value: TrustLevel.FULL, name: 'Full Trust', desc: 'Highly trusted, verified relationship', color: '#6ee7b7', border: 'rgba(16, 185, 129, 0.4)' },
236
+ { value: TrustLevel.ULTIMATE, name: 'Ultimate', desc: 'Your own address or absolute trust', color: '#a78bfa', border: 'rgba(139, 92, 246, 0.4)' },
237
+ ];
238
+
239
+ export function showEstablishTrustModal(onConfirm) {
240
+ let vcfData = null;
241
+
242
+ const modal = document.createElement('div');
243
+ modal.className = 'modal trust-modal establish-trust-modal';
244
+
245
+ const levelOptionsHtml = TRUST_LEVEL_CONFIG.map((lvl, i) => `
246
+ <label class="trust-level-option" style="--level-color: ${lvl.color}; --level-border: ${lvl.border}">
247
+ <input type="radio" name="trust-level" value="${lvl.value}" ${i === 2 ? 'checked' : ''}>
248
+ <span class="trust-level-indicator" style="background: ${lvl.color}"></span>
249
+ <span class="trust-level-label">
250
+ <span class="trust-level-name">${lvl.name}</span>
251
+ <span class="trust-level-desc">${lvl.desc}</span>
252
+ </span>
253
+ </label>
254
+ `).join('');
255
+
256
+ modal.innerHTML = `
257
+ <div class="modal-glass">
258
+ <div class="modal-header">
259
+ <h3>Establish Trust</h3>
260
+ <button class="modal-close">&times;</button>
261
+ </div>
262
+ <div class="modal-body">
263
+
264
+ <div class="trust-input-section">
265
+ <label class="trust-section-label">Recipient</label>
266
+ <div class="trust-input-tabs">
267
+ <button class="trust-input-tab active" data-tab="address">Paste Address</button>
268
+ <button class="trust-input-tab" data-tab="vcf">Import vCard</button>
269
+ </div>
270
+
271
+ <div class="trust-tab-panel" id="trust-address-panel">
272
+ <input type="text" id="trust-recipient" class="trust-address-input invalid" placeholder="BTC, ETH, or SOL address" autocomplete="off" spellcheck="false" />
273
+ <div class="trust-address-status" id="trust-address-status">
274
+ <span id="trust-address-status-text"></span>
275
+ </div>
276
+ </div>
277
+
278
+ <div class="trust-tab-panel" id="trust-vcf-panel" style="display:none">
279
+ <label class="trust-vcf-dropzone" id="trust-vcf-dropzone">
280
+ <input type="file" id="trust-vcf-input" accept=".vcf,.vcard" style="display:none" />
281
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
282
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>
283
+ </svg>
284
+ <span>Drop .vcf file or click to browse</span>
285
+ </label>
286
+ <div class="trust-vcf-summary" id="trust-vcf-summary" style="display:none"></div>
287
+ </div>
288
+ </div>
289
+
290
+ <div class="trust-input-section">
291
+ <label class="trust-section-label">Trust Level</label>
292
+ <div class="trust-level-options">
293
+ ${levelOptionsHtml}
294
+ </div>
295
+ </div>
296
+
297
+ <div class="trust-modal-actions">
298
+ <button class="glass-btn" id="trust-cancel">Cancel</button>
299
+ <button class="glass-btn primary" id="trust-confirm">Publish Transaction</button>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ `;
304
+
305
+ document.body.appendChild(modal);
306
+ requestAnimationFrame(() => modal.classList.add('active'));
307
+
308
+ const closeBtn = modal.querySelector('.modal-close');
309
+ const cancelBtn = modal.querySelector('#trust-cancel');
310
+ const confirmBtn = modal.querySelector('#trust-confirm');
311
+ const recipientInput = modal.querySelector('#trust-recipient');
312
+ const addressStatus = modal.querySelector('#trust-address-status');
313
+ const addressStatusText = modal.querySelector('#trust-address-status-text');
314
+ const vcfInput = modal.querySelector('#trust-vcf-input');
315
+ const vcfSummary = modal.querySelector('#trust-vcf-summary');
316
+ const vcfDropzone = modal.querySelector('#trust-vcf-dropzone');
317
+ const addressPanel = modal.querySelector('#trust-address-panel');
318
+ const vcfPanel = modal.querySelector('#trust-vcf-panel');
319
+
320
+ let detectedNetwork = null;
321
+ const fees = { btc: '~0.0001 BTC', sol: '~0.000005 SOL', eth: '~0.001 ETH' };
322
+ const chainNames = { btc: 'Bitcoin', eth: 'Ethereum', sol: 'Solana' };
323
+
324
+ const close = () => closeModal(modal);
325
+ closeBtn.addEventListener('click', close);
326
+ cancelBtn.addEventListener('click', close);
327
+
328
+ // Tab switching
329
+ modal.querySelectorAll('.trust-input-tab').forEach(tab => {
330
+ tab.addEventListener('click', () => {
331
+ modal.querySelectorAll('.trust-input-tab').forEach(t => t.classList.remove('active'));
332
+ tab.classList.add('active');
333
+ const isVcf = tab.dataset.tab === 'vcf';
334
+ addressPanel.style.display = isVcf ? 'none' : '';
335
+ vcfPanel.style.display = isVcf ? '' : 'none';
336
+ });
337
+ });
338
+
339
+ // Address auto-detect chain with validation
340
+ recipientInput.addEventListener('input', () => {
341
+ const val = recipientInput.value.trim();
342
+ if (!val) {
343
+ recipientInput.classList.add('invalid');
344
+ recipientInput.classList.remove('valid');
345
+ addressStatus.className = 'trust-address-status';
346
+ addressStatusText.textContent = '';
347
+ detectedNetwork = null;
348
+ return;
349
+ }
350
+ const chain = detectChainFromAddress(val);
351
+ if (chain) {
352
+ detectedNetwork = chain;
353
+ recipientInput.classList.remove('invalid');
354
+ recipientInput.classList.add('valid');
355
+ addressStatus.className = 'trust-address-status detected';
356
+ addressStatusText.textContent = `${chainNames[chain]} (${fees[chain]})`;
357
+ } else {
358
+ detectedNetwork = null;
359
+ recipientInput.classList.add('invalid');
360
+ recipientInput.classList.remove('valid');
361
+ addressStatus.className = 'trust-address-status invalid';
362
+ addressStatusText.textContent = 'Unrecognized address format';
363
+ }
364
+ });
365
+
366
+ // VCF file handling
367
+ function handleVCFFile(file) {
368
+ if (!file) return;
369
+ const reader = new FileReader();
370
+ reader.onload = (e) => {
371
+ vcfData = parseVCFForTrust(e.target.result);
372
+ vcfDropzone.style.display = 'none';
373
+ vcfSummary.style.display = 'block';
374
+
375
+ let html = '<div class="trust-vcf-card">';
376
+ if (vcfData.photo) {
377
+ html += `<img class="trust-vcf-photo" src="${vcfData.photo}" alt="" />`;
378
+ }
379
+ html += '<div class="trust-vcf-info">';
380
+ if (vcfData.name) html += `<div class="trust-vcf-name">${vcfData.name}</div>`;
381
+ if (vcfData.org) html += `<div class="trust-vcf-org">${vcfData.org}</div>`;
382
+ if (vcfData.email) html += `<div class="trust-vcf-email">${vcfData.email}</div>`;
383
+ html += '</div></div>';
384
+
385
+ if (vcfData.addresses.length > 0) {
386
+ html += '<label class="trust-section-label" style="margin-top:12px">Select Address</label>';
387
+ html += '<div class="trust-vcf-addresses">';
388
+ vcfData.addresses.forEach((a, i) => {
389
+ const labels = { btc: 'Bitcoin', eth: 'Ethereum', sol: 'Solana' };
390
+ html += `
391
+ <label class="trust-vcf-addr-option">
392
+ <input type="radio" name="vcf-address" value="${i}" ${i === 0 ? 'checked' : ''} />
393
+ <span class="chain-badge chain-${a.chain}">${a.chain.toUpperCase()}</span>
394
+ <code>${truncatePubkey(a.address)}</code>
395
+ </label>`;
396
+ });
397
+ html += '</div>';
398
+ } else if (vcfData.keys.length > 0) {
399
+ html += `<div class="trust-vcf-note">Found ${vcfData.keys.length} key(s) but no recognized blockchain addresses.</div>`;
400
+ } else {
401
+ html += '<div class="trust-vcf-note">No blockchain addresses found in this vCard.</div>';
402
+ }
403
+
404
+ html += `<button class="glass-btn glass-btn-sm trust-vcf-clear" id="trust-vcf-clear">Remove</button>`;
405
+ vcfSummary.innerHTML = html;
406
+
407
+ // Set detected network from first address
408
+ if (vcfData.addresses.length > 0) {
409
+ detectedNetwork = vcfData.addresses[0].chain;
410
+ }
411
+
412
+ // Handle address radio changes
413
+ vcfSummary.querySelectorAll('input[name="vcf-address"]').forEach(radio => {
414
+ radio.addEventListener('change', () => {
415
+ const addr = vcfData.addresses[parseInt(radio.value, 10)];
416
+ if (addr) detectedNetwork = addr.chain;
417
+ });
418
+ });
419
+
420
+ modal.querySelector('#trust-vcf-clear')?.addEventListener('click', () => {
421
+ vcfData = null;
422
+ vcfSummary.style.display = 'none';
423
+ vcfDropzone.style.display = '';
424
+ vcfInput.value = '';
425
+ });
426
+ };
427
+ reader.readAsText(file);
428
+ }
429
+
430
+ vcfInput.addEventListener('change', (e) => handleVCFFile(e.target.files[0]));
431
+
432
+ vcfDropzone.addEventListener('dragover', (e) => { e.preventDefault(); vcfDropzone.classList.add('dragover'); });
433
+ vcfDropzone.addEventListener('dragleave', () => vcfDropzone.classList.remove('dragover'));
434
+ vcfDropzone.addEventListener('drop', (e) => {
435
+ e.preventDefault();
436
+ vcfDropzone.classList.remove('dragover');
437
+ handleVCFFile(e.dataTransfer.files[0]);
438
+ });
439
+
440
+ // Confirm
441
+ confirmBtn.addEventListener('click', () => {
442
+ let recipientAddress;
443
+ let network = detectedNetwork;
444
+ const isVcfTab = modal.querySelector('.trust-input-tab.active')?.dataset.tab === 'vcf';
445
+
446
+ if (isVcfTab && vcfData && vcfData.addresses.length > 0) {
447
+ const selectedRadio = vcfSummary.querySelector('input[name="vcf-address"]:checked');
448
+ const idx = selectedRadio ? parseInt(selectedRadio.value, 10) : 0;
449
+ recipientAddress = vcfData.addresses[idx].address;
450
+ network = vcfData.addresses[idx].chain;
451
+ } else {
452
+ recipientAddress = recipientInput.value.trim();
453
+ }
454
+
455
+ if (!recipientAddress || !network) {
456
+ recipientInput.focus();
457
+ return;
458
+ }
459
+ const level = parseInt(modal.querySelector('input[name="trust-level"]:checked').value, 10);
460
+
461
+ onConfirm({ level, network, recipientAddress });
462
+ close();
463
+ });
464
+ }
465
+
466
+ // =============================================================================
467
+ // 3. showRevokeTrustModal
468
+ // =============================================================================
469
+
470
+ export function showRevokeTrustModal(originalTxHash, onConfirm) {
471
+ const modal = document.createElement('div');
472
+ modal.className = 'modal trust-modal';
473
+ modal.innerHTML = `
474
+ <div class="modal-glass">
475
+ <div class="modal-header">
476
+ <h3>Revoke Trust</h3>
477
+ <button class="modal-close">&times;</button>
478
+ </div>
479
+ <div class="modal-body">
480
+ <div class="trust-warning">
481
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
482
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
483
+ <line x1="12" y1="9" x2="12" y2="13"/>
484
+ <line x1="12" y1="17" x2="12.01" y2="17"/>
485
+ </svg>
486
+ <p>This will publish a revocation transaction on-chain. The original trust relationship will be marked as revoked and will no longer contribute to trust scores.</p>
487
+ <p><strong>This action is permanent and cannot be undone.</strong></p>
488
+ </div>
489
+
490
+ <div class="trust-tx-hash">
491
+ <label>Original Transaction</label>
492
+ <code>${truncateTxHash(originalTxHash)}</code>
493
+ </div>
494
+
495
+ <div class="trust-actions">
496
+ <button class="glass-btn" id="revoke-cancel">Cancel</button>
497
+ <button class="glass-btn danger" id="revoke-confirm">Publish Revocation</button>
498
+ </div>
499
+ </div>
500
+ </div>
501
+ `;
502
+
503
+ document.body.appendChild(modal);
504
+ requestAnimationFrame(() => modal.classList.add('active'));
505
+
506
+ const closeBtn = modal.querySelector('.modal-close');
507
+ const cancelBtn = modal.querySelector('#revoke-cancel');
508
+ const confirmBtn = modal.querySelector('#revoke-confirm');
509
+
510
+ const close = () => closeModal(modal);
511
+
512
+ closeBtn.addEventListener('click', close);
513
+ cancelBtn.addEventListener('click', close);
514
+
515
+ confirmBtn.addEventListener('click', () => {
516
+ onConfirm({ originalTxHash });
517
+ close();
518
+ });
519
+ }
520
+
521
+ // =============================================================================
522
+ // 4. showRulesModal
523
+ // =============================================================================
524
+
525
+ const RULE_CONDITION_TYPES = [
526
+ { value: 'mutual_tx_count', label: 'Mutual Transaction Count' },
527
+ { value: 'last_interaction_days', label: 'Days Since Last Interaction' },
528
+ { value: 'address_blocklist', label: 'Address Blocklist' },
529
+ { value: 'bidirectional_trust', label: 'Bidirectional Trust' },
530
+ ];
531
+
532
+ const SEVERITY_OPTIONS = ['info', 'warn', 'block'];
533
+
534
+ function buildRuleRow(rule, index) {
535
+ const conditionOptions = RULE_CONDITION_TYPES.map(ct =>
536
+ `<option value="${ct.value}" ${rule.type === ct.value ? 'selected' : ''}>${ct.label}</option>`
537
+ ).join('');
538
+
539
+ const levelOptions = Object.entries(TrustLevelNames).map(([val, name]) =>
540
+ `<option value="${val}" ${String(rule.resultLevel) === String(val) ? 'selected' : ''}>${name}</option>`
541
+ ).join('');
542
+
543
+ const severityOptions = SEVERITY_OPTIONS.map(s =>
544
+ `<option value="${s}" ${rule.severity === s ? 'selected' : ''}>${s}</option>`
545
+ ).join('');
546
+
547
+ return `
548
+ <div class="rule-row" data-index="${index}">
549
+ <div class="rule-fields">
550
+ <div class="rule-field">
551
+ <label>Condition</label>
552
+ <select class="glass-select rule-type">${conditionOptions}</select>
553
+ </div>
554
+ <div class="rule-field">
555
+ <label>Threshold</label>
556
+ <input type="number" class="glass-input rule-threshold" value="${rule.params?.threshold ?? 0}" min="0" />
557
+ </div>
558
+ <div class="rule-field">
559
+ <label>Result Level</label>
560
+ <select class="glass-select rule-result-level">${levelOptions}</select>
561
+ </div>
562
+ <div class="rule-field">
563
+ <label>Severity</label>
564
+ <select class="glass-select rule-severity">${severityOptions}</select>
565
+ </div>
566
+ <div class="rule-field rule-field-actions">
567
+ <button class="glass-btn glass-btn-sm rule-delete-btn" data-index="${index}" title="Delete rule">&times;</button>
568
+ </div>
569
+ </div>
570
+ </div>
571
+ `;
572
+ }
573
+
574
+ export function showRulesModal(rules, onSave) {
575
+ let currentRules = (rules || []).map((r, i) => ({
576
+ id: r.id || `rule-${i}`,
577
+ type: r.type || 'mutual_tx_count',
578
+ params: { threshold: r.params?.threshold ?? 0 },
579
+ resultLevel: r.resultLevel ?? TrustLevel.MARGINAL,
580
+ severity: r.severity || 'info',
581
+ description: r.description || '',
582
+ }));
583
+
584
+ const modal = document.createElement('div');
585
+ modal.className = 'modal trust-modal rules-modal';
586
+
587
+ function renderRules() {
588
+ const rulesHtml = currentRules.map((r, i) => buildRuleRow(r, i)).join('');
589
+ modal.innerHTML = `
590
+ <div class="modal-glass">
591
+ <div class="modal-header">
592
+ <h3>Trust Rules</h3>
593
+ <button class="modal-close">&times;</button>
594
+ </div>
595
+ <div class="modal-body">
596
+ <div class="rules-list">
597
+ ${rulesHtml || '<div class="rules-empty">No rules defined. Add a rule below.</div>'}
598
+ </div>
599
+ <div class="rules-toolbar">
600
+ <button class="glass-btn glass-btn-sm" id="rules-add">+ Add Rule</button>
601
+ </div>
602
+ <div class="trust-actions">
603
+ <button class="glass-btn" id="rules-cancel">Cancel</button>
604
+ <button class="glass-btn primary" id="rules-save">Save Rules</button>
605
+ </div>
606
+ </div>
607
+ </div>
608
+ `;
609
+ bindRulesEvents();
610
+ }
611
+
612
+ function readRulesFromDom() {
613
+ const rows = modal.querySelectorAll('.rule-row');
614
+ rows.forEach((row, i) => {
615
+ if (currentRules[i]) {
616
+ currentRules[i].type = row.querySelector('.rule-type').value;
617
+ currentRules[i].params.threshold = parseInt(row.querySelector('.rule-threshold').value, 10) || 0;
618
+ currentRules[i].resultLevel = parseInt(row.querySelector('.rule-result-level').value, 10);
619
+ currentRules[i].severity = row.querySelector('.rule-severity').value;
620
+ }
621
+ });
622
+ }
623
+
624
+ function bindRulesEvents() {
625
+ const close = () => closeModal(modal);
626
+
627
+ modal.querySelector('.modal-close').addEventListener('click', close);
628
+ modal.querySelector('#rules-cancel').addEventListener('click', close);
629
+
630
+ modal.querySelector('#rules-add').addEventListener('click', () => {
631
+ readRulesFromDom();
632
+ currentRules.push({
633
+ id: `rule-${Date.now()}`,
634
+ type: 'mutual_tx_count',
635
+ params: { threshold: 0 },
636
+ resultLevel: TrustLevel.MARGINAL,
637
+ severity: 'info',
638
+ description: '',
639
+ });
640
+ renderRules();
641
+ });
642
+
643
+ modal.querySelectorAll('.rule-delete-btn').forEach(btn => {
644
+ btn.addEventListener('click', () => {
645
+ readRulesFromDom();
646
+ const idx = parseInt(btn.getAttribute('data-index'), 10);
647
+ currentRules.splice(idx, 1);
648
+ renderRules();
649
+ });
650
+ });
651
+
652
+ modal.querySelector('#rules-save').addEventListener('click', () => {
653
+ readRulesFromDom();
654
+ onSave(currentRules);
655
+ close();
656
+ });
657
+ }
658
+
659
+ document.body.appendChild(modal);
660
+ renderRules();
661
+ requestAnimationFrame(() => modal.classList.add('active'));
662
+ }
663
+
664
+ // =============================================================================
665
+ // 5. scanAllTrustTransactions
666
+ // =============================================================================
667
+
668
+ export async function scanAllTrustTransactions(addresses) {
669
+ const allTxs = [];
670
+
671
+ if (addresses.btc) {
672
+ const btcTxs = await scanBitcoinTrustTransactions(addresses.btc);
673
+ allTxs.push(...btcTxs);
674
+ }
675
+
676
+ if (addresses.sol) {
677
+ const solTxs = await scanSolanaTrustTransactions(addresses.sol);
678
+ allTxs.push(...solTxs);
679
+ }
680
+
681
+ if (addresses.eth) {
682
+ const ethTxs = await scanEthereumTrustTransactions(addresses.eth);
683
+ allTxs.push(...ethTxs);
684
+ }
685
+
686
+ return allTxs;
687
+ }
688
+
689
+ // =============================================================================
690
+ // 6. exportTrustData
691
+ // =============================================================================
692
+
693
+ export function exportTrustData(trustTransactions, xpub) {
694
+ const payload = {
695
+ exportDate: new Date().toISOString(),
696
+ xpub: xpub || null,
697
+ chainInfo: {
698
+ btc: 'Bitcoin mainnet',
699
+ sol: 'Solana mainnet-beta',
700
+ eth: 'Ethereum mainnet',
701
+ },
702
+ transactions: trustTransactions || [],
703
+ };
704
+
705
+ const json = JSON.stringify(payload, null, 2);
706
+ const blob = new Blob([json], { type: 'application/json' });
707
+ const url = URL.createObjectURL(blob);
708
+
709
+ const a = document.createElement('a');
710
+ a.href = url;
711
+ a.download = `trust-export-${Date.now()}.trust.json`;
712
+ document.body.appendChild(a);
713
+ a.click();
714
+ document.body.removeChild(a);
715
+ URL.revokeObjectURL(url);
716
+ }
717
+
718
+ // =============================================================================
719
+ // 7. importTrustData
720
+ // =============================================================================
721
+
722
+ export function importTrustData(file) {
723
+ return new Promise((resolve, reject) => {
724
+ if (!file) {
725
+ reject(new Error('No file provided'));
726
+ return;
727
+ }
728
+
729
+ const reader = new FileReader();
730
+ reader.onload = (e) => {
731
+ try {
732
+ const data = JSON.parse(e.target.result);
733
+ if (!data.transactions || !Array.isArray(data.transactions)) {
734
+ reject(new Error('Invalid trust data: missing transactions array'));
735
+ return;
736
+ }
737
+ resolve(data.transactions);
738
+ } catch (err) {
739
+ reject(new Error(`Failed to parse trust data: ${err.message}`));
740
+ }
741
+ };
742
+ reader.onerror = () => reject(new Error('Failed to read file'));
743
+ reader.readAsText(file);
744
+ });
745
+ }