rmi-backend/app/wallet_memory/frontend.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

366 lines
18 KiB
Python

"""
WalletSafe Frontend - Entity explorer, wallet search, risk page.
===============================================================
Served at /walletsafe/ as a single-page app.
Hits /api/v1/wallet/* endpoints directly via fetch().
"""
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
router = APIRouter(tags=["walletsafe"])
WALLETSAFE_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WalletSafe - Wallet Intelligence Explorer</title>
<style>
:root {
--bg: #0a0a0f;
--card: #12121a;
--border: #1e1e2e;
--text: #e0e0e8;
--muted: #8888a0;
--accent: #6c5ce7;
--danger: #ff4757;
--warning: #ffa502;
--safe: #2ed573;
--info: #1e90ff;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { background:var(--bg); color:var(--text); font-family:'Inter',system-ui,sans-serif; min-height:100vh; }
nav { background:var(--card); border-bottom:1px solid var(--border); padding:12px 24px; display:flex; align-items:center; gap:24px; }
nav .logo { font-size:20px; font-weight:700; color:var(--accent); letter-spacing:-0.5px; }
nav .tab { color:var(--muted); cursor:pointer; padding:6px 14px; border-radius:6px; font-size:14px; transition:all .2s; }
nav .tab:hover { color:var(--text); background:var(--border); }
nav .tab.active { color:var(--accent); background:rgba(108,92,231,0.15); }
.container { max-width:1200px; margin:0 auto; padding:24px; }
/* Search bar */
.search-bar { display:flex; gap:12px; margin-bottom:24px; }
.search-bar input { flex:1; padding:12px 16px; background:var(--card); border:1px solid var(--border); border-radius:8px; color:var(--text); font-size:15px; outline:none; }
.search-bar input:focus { border-color:var(--accent); }
.search-bar select { padding:10px 12px; background:var(--card); border:1px solid var(--border); border-radius:8px; color:var(--text); font-size:14px; }
.search-bar button { padding:10px 24px; background:var(--accent); color:#fff; border:none; border-radius:8px; font-size:15px; cursor:pointer; font-weight:600; }
.search-bar button:hover { opacity:0.9; }
/* Cards */
.card { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:20px; margin-bottom:16px; }
.card h3 { font-size:16px; color:var(--muted); margin-bottom:12px; text-transform:uppercase; letter-spacing:0.5px; }
/* Risk gauge */
.risk-gauge { display:flex; align-items:center; gap:16px; margin:16px 0; }
.risk-score { font-size:48px; font-weight:800; line-height:1; }
.risk-label { font-size:18px; font-weight:600; padding:4px 12px; border-radius:6px; }
.risk-minimal { color:var(--safe); background:rgba(46,213,115,0.1); }
.risk-low { color:#7bed9f; background:rgba(123,237,159,0.1); }
.risk-medium { color:var(--warning); background:rgba(255,165,2,0.1); }
.risk-high { color:#ff6348; background:rgba(255,99,72,0.1); }
.risk-critical { color:var(--danger); background:rgba(255,71,87,0.15); }
.risk-unknown { color:var(--muted); background:rgba(136,136,160,0.1); }
/* Risk bar */
.risk-bar { height:8px; background:var(--border); border-radius:4px; overflow:hidden; margin-top:8px; }
.risk-bar-fill { height:100%; border-radius:4px; transition:width .5s ease; }
/* Profile grid */
.profile-grid { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
@media(max-width:768px) { .profile-grid { grid-template-columns:1fr; } }
.profile-field { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--border); }
.profile-field:last-child { border:none; }
.profile-field .label { color:var(--muted); font-size:13px; }
.profile-field .value { font-weight:500; font-size:14px; word-break:break-all; }
/* Tags */
.tag { display:inline-block; padding:3px 8px; border-radius:4px; font-size:12px; margin:2px; }
.tag-exchange { background:rgba(30,144,255,0.15); color:var(--info); }
.tag-scam { background:rgba(255,71,87,0.15); color:var(--danger); }
.tag-sanctioned { background:rgba(255,71,87,0.25); color:var(--danger); }
.tag-defi { background:rgba(108,92,231,0.15); color:var(--accent); }
.tag-bridge { background:rgba(255,165,2,0.15); color:var(--warning); }
.tag-mev { background:rgba(255,165,2,0.15); color:var(--warning); }
.tag-deployer { background:rgba(46,213,115,0.15); color:var(--safe); }
.tag-wallet { background:rgba(136,136,160,0.15); color:var(--muted); }
.tag-unknown { background:rgba(136,136,160,0.1); color:var(--muted); }
/* Wallet list */
.wallet-item { display:flex; align-items:center; gap:12px; padding:10px 0; border-bottom:1px solid var(--border); }
.wallet-item:last-child { border:none; }
.wallet-addr { font-family:monospace; font-size:13px; color:var(--info); cursor:pointer; }
.wallet-addr:hover { text-decoration:underline; }
.wallet-chain { font-size:11px; padding:2px 6px; border-radius:3px; background:rgba(108,92,231,0.15); color:var(--accent); }
/* Deployer history */
.deploy-item { padding:10px 0; border-bottom:1px solid var(--border); font-size:13px; }
.deploy-item:last-child { border:none; }
.deploy-token { font-family:monospace; color:var(--info); }
/* Loading state */
.loading { display:flex; align-items:center; justify-content:center; padding:60px; color:var(--muted); }
.spinner { width:24px; height:24px; border:3px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin 0.8s linear infinite; margin-right:12px; }
@keyframes spin { to { transform:rotate(360deg); } }
/* Empty state */
.empty { text-align:center; padding:60px; color:var(--muted); }
.empty h2 { font-size:20px; margin-bottom:8px; color:var(--text); }
/* Error */
.error { background:rgba(255,71,87,0.1); border:1px solid rgba(255,71,87,0.3); border-radius:8px; padding:16px; color:var(--danger); margin-bottom:16px; }
/* Stats bar */
.stats-bar { display:flex; gap:24px; margin-bottom:24px; flex-wrap:wrap; }
.stat-item { display:flex; flex-direction:column; }
.stat-value { font-size:28px; font-weight:700; }
.stat-label { font-size:12px; color:var(--muted); text-transform:uppercase; }
/* Entity graph placeholder */
.entity-graph { min-height:200px; display:flex; flex-wrap:wrap; gap:8px; padding:12px; }
.entity-node { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:8px 12px; font-size:12px; cursor:pointer; transition:all .2s; }
.entity-node:hover { border-color:var(--accent); }
.entity-node.center { border-color:var(--accent); background:rgba(108,92,231,0.1); }
.entity-node .addr { font-family:monospace; color:var(--info); }
.entity-node .method { color:var(--muted); font-size:10px; margin-top:2px; }
</style>
</head>
<body>
<nav>
<div class="logo">WalletSafe</div>
<div class="tab active" data-tab="search" onclick="switchTab('search')">Search</div>
<div class="tab" data-tab="dashboard" onclick="switchTab('dashboard')">Dashboard</div>
</nav>
<!-- Search Tab -->
<div id="tab-search" class="container">
<div class="search-bar">
<input id="search-input" type="text" placeholder="Enter wallet address (0x..., Solana...)" onkeydown="if(event.key==='Enter')searchWallet()">
<select id="chain-select">
<option value="solana">Solana</option>
<option value="ethereum">Ethereum</option>
<option value="bsc">BSC</option>
<option value="polygon">Polygon</option>
<option value="arbitrum">Arbitrum</option>
<option value="base">Base</option>
</select>
<button onclick="searchWallet()">Analyze</button>
</div>
<div id="search-results"></div>
<div id="empty-state" class="empty">
<h2>Wallet Intelligence Explorer</h2>
<p>Enter a wallet address to see risk scores, entity clusters, labels, and deployer history</p>
</div>
</div>
<!-- Dashboard Tab -->
<div id="tab-dashboard" class="container" style="display:none">
<div class="stats-bar" id="dashboard-stats"></div>
<div class="profile-grid" id="dashboard-content"></div>
</div>
<script>
const API = '/api/v1/wallet';
let currentTab = 'search';
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
document.getElementById('tab-search').style.display = tab === 'search' ? 'block' : 'none';
document.getElementById('tab-dashboard').style.display = tab === 'dashboard' ? 'block' : 'none';
if (tab === 'dashboard') loadDashboard();
}
async function api(endpoint, opts = {}) {
try {
const r = await fetch(API + endpoint, opts);
return await r.json();
} catch(e) {
console.error('API error:', e);
return null;
}
}
async function searchWallet() {
const addr = document.getElementById('search-input').value.trim();
const chain = document.getElementById('chain-select').value;
if (!addr) return;
const results = document.getElementById('search-results');
const empty = document.getElementById('empty-state');
empty.style.display = 'none';
results.innerHTML = '<div class="loading"><div class="spinner"></div>Analyzing wallet...</div>';
const profile = await api(`/${addr}/profile?chain=${chain}`);
if (!profile) {
results.innerHTML = '<div class="error">Failed to fetch wallet profile. Check the address and try again.</div>';
return;
}
results.innerHTML = renderProfile(profile, chain);
}
function renderProfile(p, chain) {
const score = p.risk_score || 0;
const level = p.risk_level || 'unknown';
const scoreColor = riskColor(score);
const barColor = riskBarColor(level);
let html = '';
// Risk Card
html += `<div class="card">
<h3>Risk Assessment</h3>
<div class="risk-gauge">
<div class="risk-score" style="color:${scoreColor}">${Math.round(score)}</div>
<div>
<span class="risk-label risk-${level}">${level.toUpperCase()}</span>
<div class="risk-bar" style="width:200px;margin-top:8px">
<div class="risk-bar-fill" style="width:${score}%;background:${barColor}"></div>
</div>
</div>
</div>
${p.scam_associations?.length ? `<div style="margin-top:12px"><strong>Scam Associations:</strong> ${p.scam_associations.map(s => `<span class="tag tag-scam">${s.type} (${s.source})</span>`).join(' ')}</div>` : ''}
</div>`;
// Profile Info
html += `<div class="profile-grid">
<div class="card">
<h3>Wallet Identity</h3>
<div class="profile-field"><span class="label">Address</span><span class="value" style="font-family:monospace;font-size:12px">${p.address}</span></div>
<div class="profile-field"><span class="label">Chain</span><span class="value">${p.chain}</span></div>
<div class="profile-field"><span class="label">Entity</span><span class="value">${p.entity_label || p.entity_id?.slice(0,16) + '...' || 'Unknown'}</span></div>
<div class="profile-field"><span class="label">Category</span><span class="value"><span class="tag tag-${p.entity_category || 'unknown'}">${p.entity_category || 'unknown'}</span></span></div>
<div class="profile-field"><span class="label">Confidence</span><span class="value">${(p.confidence * 100).toFixed(0)}%</span></div>
${p.total_transactions ? `<div class="profile-field"><span class="label">Transactions</span><span class="value">${p.total_transactions.toLocaleString()}</span></div>` : ''}
${p.total_volume ? `<div class="profile-field"><span class="label">Volume</span><span class="value">$${p.total_volume.toLocaleString()}</span></div>` : ''}
</div>
<div class="card">
<h3>Labels (${p.labels?.length || 0})</h3>
${p.labels?.length ? p.labels.map(l => `<div class="profile-field"><span class="label">${l.source}</span><span class="value"><span class="tag tag-${l.category || 'unknown'}">${l.label || l.category}</span></span></div>`).join('') : '<div style="color:var(--muted);padding:20px;text-align:center">No labels found</div>'}
</div>
</div>`;
// Linked Wallets / Entity
if (p.linked_wallets?.length) {
html += `<div class="card">
<h3>Linked Wallets (${p.linked_wallets.length})</h3>
<div class="entity-graph">
<div class="entity-node center">
<div class="addr">${p.address.slice(0,10)}...${p.address.slice(-6)}</div>
<div class="method">primary</div>
</div>
${p.linked_wallets.map(w => `<div class="entity-node" onclick="drillDown('${w.address}','${w.chain}')">
<div class="addr">${w.address.slice(0,10)}...${w.address.slice(-6)}</div>
<div class="method">${w.heuristic || w.method || 'linked'} · ${w.chain} · ${(w.confidence * 100).toFixed(0)}%</div>
</div>`).join('')}
</div>
</div>`;
}
// Cross-chain
if (p.cross_chain_presence?.length) {
html += `<div class="card"><h3>Cross-Chain Presence</h3>
${p.cross_chain_presence.map(b => `<div class="wallet-item">
<span class="wallet-addr" onclick="drillDown('${b.dest_address}','${b.dest_chain}')">${b.dest_address.slice(0,12)}...${b.dest_address.slice(-6)}</span>
<span class="wallet-chain">${b.dest_chain}</span>
<span style="color:var(--muted);font-size:12px">via ${b.bridge_name}</span>
</div>`).join('')}
</div>`;
}
// Deployer History
if (p.deployer_history?.length) {
html += `<div class="card"><h3>Deployer History (${p.deployer_history.length})</h3>
${p.deployer_history.map(d => `<div class="deploy-item">
<span class="deploy-token">${d.token_symbol || d.token_address?.slice(0,12)}</span>
<span style="margin-left:8px">${d.token_name || ''}</span>
${d.is_scam_related ? '<span class="tag tag-scam" style="margin-left:8px">SCAM</span>' : ''}
<span style="float:right;color:var(--muted)">${d.deployed_at ? new Date(d.deployed_at).toLocaleDateString() : ''} · ${d.outcome || 'unknown'}</span>
</div>`).join('')}
</div>`;
}
return html;
}
function drillDown(addr, chain) {
document.getElementById('search-input').value = addr;
document.getElementById('chain-select').value = chain;
searchWallet();
}
function riskColor(score) {
if (score >= 80) return 'var(--danger)';
if (score >= 60) return '#ff6348';
if (score >= 40) return 'var(--warning)';
if (score >= 20) return '#7bed9f';
return 'var(--safe)';
}
function riskBarColor(level) {
const m = {critical:'var(--danger)',high:'#ff6348',medium:'var(--warning)',low:'#7bed9f',minimal:'var(--safe)',unknown:'var(--muted)'};
return m[level] || 'var(--muted)';
}
async function loadDashboard() {
const stats = document.getElementById('dashboard-stats');
const content = document.getElementById('dashboard-content');
stats.innerHTML = '<div class="loading"><div class="spinner"></div>Loading...</div>';
const [health, labelStats] = await Promise.all([
api('/health'),
api('/labels/stats'),
]);
let statsHtml = '';
if (health) {
statsHtml += `
<div class="stat-item"><span class="stat-value" style="color:${health.storage?.redis ? 'var(--safe)' : 'var(--danger)'}">${health.storage?.redis ? 'Online' : 'Offline'}</span><span class="stat-label">Redis</span></div>
<div class="stat-item"><span class="stat-value" style="color:${health.storage?.clickhouse ? 'var(--safe)' : 'var(--warning)'}">${health.storage?.clickhouse ? 'Online' : 'Offline'}</span><span class="stat-label">ClickHouse</span></div>
<div class="stat-item"><span class="stat-value">${health.clustering?.wallets || 0}</span><span class="stat-label">Cluster Nodes</span></div>
`;
}
if (labelStats) {
const c = labelStats.last_counts || {};
statsHtml += `
<div class="stat-item"><span class="stat-value">${c.total || 0}</span><span class="stat-label">Total Labels</span></div>
<div class="stat-item"><span class="stat-value">${c.ofac || 0}</span><span class="stat-label">OFAC Sanctions</span></div>
<div class="stat-item"><span class="stat-value">${c.etherscan_combined || 0}</span><span class="stat-label">Etherscan Labels</span></div>
<div class="stat-item"><span class="stat-value">${c.solana_cex || 0}</span><span class="stat-label">Solana CEX</span></div>
`;
}
stats.innerHTML = statsHtml;
// Show recent high-risk wallets from Redis scan
content.innerHTML = `
<div class="card" style="grid-column:1/-1">
<h3>System Health</h3>
<div class="profile-field"><span class="label">Status</span><span class="value">${health?.status === 'ok' ? '<span style="color:var(--safe)">Operational</span>' : '<span style="color:var(--warning)">Degraded</span>'}</span></div>
<div class="profile-field"><span class="label">Redis</span><span class="value">${health?.storage?.redis ? '<span style="color:var(--safe)">Connected</span>' : '<span style="color:var(--danger)">Disconnected</span>'}</span></div>
<div class="profile-field"><span class="label">ClickHouse</span><span class="value">${health?.storage?.clickhouse ? '<span style="color:var(--safe)">Connected</span>' : '<span style="color:var(--warning)">Unavailable (Redis-only)</span>'}</span></div>
<div class="profile-field"><span class="label">Clustering Heuristics</span><span class="value">${health?.clustering?.heuristics || 6}</span></div>
<div class="profile-field"><span class="label">Label Sources</span><span class="value">9 (6 static + 3 live)</span></div>
<div class="profile-field"><span class="label">Label Import</span><span class="value">${labelStats?.loading ? '<span style="color:var(--warning)">Loading...</span>' : '<span style="color:var(--safe)">Complete</span>'}</span></div>
</div>
`;
}
</script>
</body>
</html>"""
@router.get("/walletsafe/")
async def walletsafe_root():
"""Serve the WalletSafe frontend."""
return HTMLResponse(content=WALLETSAFE_HTML)
@router.get("/walletsafe")
async def walletsafe_no_slash():
"""Redirect to /walletsafe/."""
return HTMLResponse(content=WALLETSAFE_HTML)