rmi-backend/static/investigate.html

152 lines
9.3 KiB
HTML

<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RMI Investigative Framework</title>
<style>
:root{--bg:#0a0a0f;--fg:#e0e0e0;--accent:#6c5ce7;--danger:#e74c3c;--card:#14141f;--border:#2a2a3a;--muted:#888;--success:#27ae60}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:system-ui,sans-serif}
header{background:var(--card);border-bottom:1px solid var(--border);padding:16px 24px;display:flex;align-items:center;gap:16px}
.logo{font-size:20px;font-weight:700;color:var(--accent)}
.badge{background:var(--accent);color:#fff;padding:2px 8px;border-radius:12px;font-size:11px}
main{max-width:960px;margin:0 auto;padding:32px 24px}
h1{font-size:28px;margin-bottom:8px}
.sub{color:var(--muted);margin-bottom:24px}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;margin-bottom:24px}
label{display:block;font-size:13px;color:var(--muted);margin-bottom:6px;font-weight:500}
input,select{width:100%;padding:12px;background:#1a1a2e;border:1px solid var(--border);border-radius:8px;color:var(--fg);font-size:15px}
.row{display:flex;gap:12px}.row>*{flex:1}
button{padding:12px 28px;background:var(--accent);color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer}
button:hover{opacity:.9}button:disabled{opacity:.4;cursor:not-allowed}
.result{animation:fadeIn .3s ease}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.tag{display:inline-block;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600;margin:2px}
.tag-cex{background:#27ae60;color:#fff}.tag-dex{background:#2980b9;color:#fff}
.tag-mixer{background:#e74c3c;color:#fff}.tag-bridge{background:#8e44ad;color:#fff}
.tag-eoa{background:#7f8c8d;color:#fff}.tag-contract{background:#d35400;color:#fff}
.tag-solana{background:#9945FF;color:#fff}
.hop{padding:12px;background:#1a1a2e;border-radius:8px;margin:8px 0;border-left:3px solid var(--accent)}
.addr{font-family:monospace;font-size:13px;color:var(--accent);word-break:break-all}
.spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite;margin-right:8px}
@keyframes spin{to{transform:rotate(360deg)}}
.stats{display:flex;gap:16px;flex-wrap:wrap}
.stat{background:#1a1a2e;padding:12px 16px;border-radius:8px}
.stat-value{font-size:20px;font-weight:700}.stat-label{font-size:11px;color:var(--muted)}
.source-badge{font-size:10px;background:#2a2a3a;padding:2px 6px;border-radius:4px;margin-left:8px;color:var(--muted)}
.fallback-notice{background:#1a1a0e;border:1px solid #f39c12;padding:12px;border-radius:8px;font-size:13px;color:#f39c12;margin-top:12px}
</style>
</head>
<body>
<header><span class="logo">RMI</span><span>Investigative Framework</span><span class="badge">Solana + EVM</span></header>
<main>
<h1>Trace Crypto Funding Sources</h1>
<p class="sub">Trace wallets across Solana and 9 EVM chains. Multi-provider fallback ensures you never run out of data.</p>
<div class="card">
<div class="row">
<div><label>Wallet Address</label><input type="text" id="address" placeholder="0x... or Solana address" value="vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg"></div>
<div style="max-width:220px">
<label>Chain</label>
<select id="chain" onchange="updatePlaceholder()">
<option value="solana">Solana</option>
<option value="ethereum">Ethereum</option><option value="bsc">BSC</option>
<option value="polygon">Polygon</option><option value="base">Base</option>
<option value="arbitrum">Arbitrum</option><option value="optimism">Optimism</option>
<option value="avalanche">Avalanche</option><option value="fantom">Fantom</option><option value="gnosis">Gnosis</option>
</select>
</div>
</div>
<div style="margin-top:16px;display:flex;gap:12px">
<button onclick="trace()" id="traceBtn">Trace Funding</button>
<button onclick="scan()" id="scanBtn" style="background:#2a2a3a">Full Investigation</button>
</div>
</div>
<div id="result"></div><div id="error"></div>
</main>
<script>
function updatePlaceholder(){
const chain=document.getElementById('chain').value;
const inp=document.getElementById('address');
if(chain==='solana'){inp.placeholder='Solana address...';if(!inp.value.startsWith('0x'))inp.value=''}
else{inp.placeholder='0x...'}
}
async function api(path,body){
const btns=document.querySelectorAll('button');btns.forEach(b=>b.disabled=true);
document.getElementById('error').innerHTML='';
document.getElementById('result').innerHTML='<p><span class="spinner"></span> Querying all providers...</p>';
try{
const r=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d=await r.json();
if(!r.ok)throw new Error(d.detail||'Request failed');
return d;
}catch(e){
document.getElementById('error').innerHTML=`<div style="background:#2c1010;border:1px solid var(--danger);border-radius:8px;padding:16px;color:#e74c3c">${e.message}</div>`;
document.getElementById('result').innerHTML='';
}finally{btns.forEach(b=>b.disabled=false)}
}
function tagClass(t){const m={cex:'tag-cex',dex:'tag-dex',mixer:'tag-mixer',bridge:'tag-bridge',eoa:'tag-eoa',contract:'tag-contract'};return m[t]||''}
async function trace(){
const chain=document.getElementById('chain').value;
const data=await api('/api/v1/investigate/trace',{address:document.getElementById('address').value.trim(),chain});
if(!data)return;
if(chain==='solana'){
document.getElementById('result').innerHTML=`
<div class="card result"><h3>Solana Wallet Trace</h3>
<div class="stats" style="margin-top:12px">
<div class="stat"><div class="stat-value">${data.first_tx?data.first_tx.slice(0,10)+'...':'N/A'}</div><div class="stat-label">First TX</div></div>
<div class="stat"><div class="stat-value">${(data.balance_change_sol||0).toFixed(4)}</div><div class="stat-label">SOL Change</div></div>
<div class="stat"><div class="stat-value">${data.total_value_usd?('$'+data.total_value_usd.toFixed(2)):'N/A'}</div><div class="stat-label">Portfolio Value</div></div>
</div>
<div style="margin-top:12px">
<span class="tag tag-solana">Solana</span>
<span class="source-badge">via ${data.source||'?'}</span>
${data.label?`<span class="tag" style="background:#2a2a3a">${data.label}</span>`:''}
${data.is_cex?'<span class="tag tag-cex">CEX</span>':''}
</div>
</div>`;
} else {
document.getElementById('result').innerHTML=`
<div class="card result"><h3>EVM Funding Trace</h3>
<div class="stats" style="margin-top:12px">
<div class="stat"><div class="stat-value">${(data.source_type||'?').toUpperCase()}</div><div class="stat-label">Source Type</div></div>
<div class="stat"><div class="stat-value">${data.confidence||0}%</div><div class="stat-label">Confidence</div></div>
<div class="stat"><div class="stat-value">${(data.funding_amount_eth||0).toFixed(6)}</div><div class="stat-label">ETH</div></div>
<div class="stat"><div class="stat-value">${(data.hops||[]).length}</div><div class="stat-label">Hops</div></div>
</div>
${data.source_address?`<div style="margin-top:12px"><label>Source</label><div class="addr">${data.source_address}</div><span class="tag ${tagClass(data.source_type)}">${data.source_type}</span></div>`:''}
${(data.hops||[]).map((h,i)=>`<div class="hop"><strong>Hop ${h.depth}</strong> ← <span class="addr">${h.address}</span> <span style="color:var(--muted)">${(h.amount_eth||0).toFixed(6)} ETH</span></div>`).join('')}
${data.errors?.length?`<div class="fallback-notice">⚠ Fallback used — some providers unavailable</div>`:''}
</div>`;
}
}
async function scan(){
const chain=document.getElementById('chain').value;
const data=await api('/api/v1/investigate/scan',{address:document.getElementById('address').value.trim(),chain});
if(!data)return;
const tr=data.trace||{};
const risk=data.risk||{};
const labels=data.labels||[];
document.getElementById('result').innerHTML=`
<div class="card result"><h3>Full Investigation — ${data.chain.toUpperCase()}</h3>
<div class="stats" style="margin-top:12px">
<div class="stat"><div class="stat-value">${(tr.source_type||tr.source||'?').toUpperCase()}</div><div class="stat-label">Source</div></div>
<div class="stat"><div class="stat-value">${tr.confidence||0}%</div><div class="stat-label">Confidence</div></div>
${risk?`<div class="stat"><div class="stat-value" style="color:${risk.is_honeypot?'var(--danger)':'var(--success)'}">${risk.is_honeypot?'DANGER':'SAFE'}</div><div class="stat-label">Risk</div></div>`:''}
<div class="stat"><div class="stat-value">${labels.length}</div><div class="stat-label">Labels</div></div>
</div>
${risk?.risks?.length?`<div style="margin-top:8px">${risk.risks.map(r=>`<span class="tag" style="background:#2c1010;color:var(--danger)">${r}</span>`).join(' ')}</div>`:''}
${labels.map(l=>`<div class="hop"><span class="addr">${(l.address||'').slice(0,16)}...</span> → ${l.label}</div>`).join('')}
<div class="fallback-notice">🔍 Data sourced from ${tr.source||'multiple providers'} with automatic fallback</div>
</div>`;
}
</script>
</body>
</html>