152 lines
9.3 KiB
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>
|