From fcc19c31e12991745c6421c340e4eb7283e7a8b0 Mon Sep 17 00:00:00 2001 From: Aitor Date: Thu, 16 Jul 2026 12:43:32 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20peritaje=20forense=20=E2=80=94=20pesta?= =?UTF-8?q?=C3=B1a=20UI=20y=20componente=20PeritajeForense?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nueva pestaña "PERITAJE" junto a las existentes. Formulario de entrada (txid:vout, importe estimado robado opcional, declaración en texto libre), botón "Iniciar rastreo forense" con progreso por salto, y render completo del informe (resumen, declaración, cronología, direcciones atribuidas, fondos sin gastar, aviso de ramas truncadas con botón "seguir más saltos", conclusiones, recomendaciones, metodología, anexo de verificación) con export JSON/MD inline, mismo patrón que el informe de wallet. Verificado en navegador (servidor estático local): - Balance de sintaxis del bloque Babel correcto, sin errores de transpilación JSX en consola. - Validación del formulario (txid vacío/inválido, nodo no conectado) funciona. - Motor completo probado con datos sintéticos vía consola: caso simple (1 salto, cambio detectado correctamente, 2 ramas terminan en UTXO sin gastar) y caso de cadena de peeling de 3 saltos (detectada con certeza CERTEZA, direcciones de cambio atribuidas con fundamento correcto, 4 fondos sin gastar localizados en las hojas). Cierra la implementación del módulo Peritaje descrito en TRASPASO.md. --- dashboard.html | 311 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 310 insertions(+), 1 deletion(-) diff --git a/dashboard.html b/dashboard.html index 54d6338..805f8b9 100644 --- a/dashboard.html +++ b/dashboard.html @@ -4919,6 +4919,313 @@ ); } + // ── PERITAJE ───────────────────────────────────────────────────────── + function PeritajeForense({base}) { + const {get} = useApi(base); + const [txidInput, setTxidInput] = useState(""); + const [voutInput, setVoutInput] = useState("0"); + const [amountInput, setAmountInput] = useState(""); + const [declaracion, setDeclaracion] = useState(""); + const [maxHops, setMaxHops] = useState(8); + const [scanning, setScanning] = useState(false); + const [progress, setProgress] = useState(""); + const [error, setError] = useState(null); + const [graph, setGraph] = useState(null); + const [report, setReport] = useState(null); + + const iniciar = async (hops) => { + const txid = txidInput.trim(); + if (!/^[0-9a-f]{64}$/i.test(txid)) { setError("Introduce un txid de origen válido (64 caracteres hex)."); return; } + const vout = parseInt(voutInput, 10); + if (!Number.isInteger(vout) || vout < 0) { setError("El vout debe ser un número entero ≥ 0."); return; } + if (!base) { setError("Necesitas conectar tu nodo (CONFIG) para rastrear — el peritaje consulta tu Mempool self-hosted, no funciona en modo demo."); return; } + setError(null); setScanning(true); setGraph(null); setReport(null); + const effectiveHops = hops || maxHops; + try { + const amountStolen = amountInput.trim() ? Math.round(parseFloat(amountInput)*1e8) : null; + const g = await buildForensicGraph({ + get, originTxid: txid, originVout: vout, amountStolen, + maxHops: effectiveHops, + onProgress: (msg) => setProgress(msg), + }); + setGraph(g); + setReport(buildForensicReport(g, declaracion)); + } catch(e) { + setError(e.message); + } + setScanning(false); setProgress(""); + }; + + const seguirMasSaltos = () => { + const next = maxHops + 4; + setMaxHops(next); + iniciar(next); + }; + + const limpiar = () => { setGraph(null); setReport(null); setError(null); }; + + const exportJSON = () => { + const exportData = { + summary: report.summary, declaracion: report.declaracion, + chronology: report.chronology, attributed: report.attributed, + unspentFunds: report.unspentFunds, conclusions: report.conclusions, + recommendations: report.recommendations, methodology: report.methodology, + verificationAppendix: report.verificationAppendix, + }; + const blob = new Blob([JSON.stringify(exportData,null,2)], {type:"application/json"}); + const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "peritaje-forense.json"; a.click(); + }; + + const exportMD = () => { + const { summary, declaracion:decl, chronology, attributed, unspentFunds, conclusions, recommendations, methodology, verificationAppendix } = report; + const lines = [ + `# Peritaje forense — Txoko`, ``, + `## Resumen`, ``, + `- Origen: ${summary.originTxid}:${summary.originVout}`, + `- Dirección de origen: ${summary.originAddress}`, + `- Monto rastreado: ${summary.amountTraced} sats`, + summary.amountStolen ? `- Monto declarado como robado: ${summary.amountStolen} sats` : null, + `- Saltos: ${summary.hops} · Transacciones: ${summary.txCount} · Direcciones tocadas: ${summary.addressesTouched}`, + `- Generado: ${new Date(summary.generatedAt*1000).toLocaleString("es-ES")}`, ``, + decl ? `## Declaración del afectado (DECLARACION)\n\n${decl.text}\n` : null, + `## Cronología`, ``, + ...chronology.map(c => `- [HECHO] ${c.txid.slice(0,16)}… · salto ${c.hop} · ${c.amount} sats · ${c.blockTime?new Date(c.blockTime*1000).toLocaleDateString("es-ES"):"sin confirmar"} — banda [INFERENCIA${c.band.certainty?"/"+c.band.certainty:""}] ${c.band.value}, huella [INFERENCIA${c.fingerprint.certainty?"/"+c.fingerprint.certainty:""}] ${c.fingerprint.value}`), + ``, + `## Direcciones atribuidas al actor`, ``, + ...(attributed.length===0 ? ["No se atribuyen direcciones adicionales con fundamento suficiente."] : + attributed.map(a => `- [INFERENCIA] ${a.address} — ${a.fundamentos.map(f=>`${f.basis} (${f.certainty})`).join("; ")}`)), + ``, + `## Fondos localizados sin gastar`, ``, + ...(unspentFunds.length===0 ? ["Ninguno detectado en las ramas exploradas."] : + unspentFunds.map(u => `- [HECHO] ${u.address} — ${u.amount} sats (tx ${u.txid.slice(0,16)}…, vout ${u.vout})`)), + ``, + `## Conclusiones`, ``, + ...conclusions.map((c,i) => `${i+1}. [${c.level}${c.certainty?"/"+c.certainty:""}] ${c.text}`), + ``, + `## Recomendaciones`, ``, + ...recommendations.map(r => `- ${r.text}`), + ``, + `## Metodología`, ``, + `**Niveles de certeza:** ${methodology.levels}`, ``, + `**Heurísticas usadas:** ${methodology.heuristics}`, ``, + `**Límites:** ${methodology.limits}`, ``, + `## Anexo de verificación`, ``, + `**Txids:**`, ...verificationAppendix.txids.map(t=>`- ${t}`), ``, + `**Direcciones:**`, ...verificationAppendix.addresses.map(a=>`- ${a}`), + ].filter(l => l !== null); + const blob = new Blob([lines.join("\n")], {type:"text/markdown"}); + const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "peritaje-forense.md"; a.click(); + }; + + return ( +
+ {!base&&} + PERITAJE FORENSE + + +
+ Rastrea fondos robados o perdidos hacia adelante, salto a salto, desde la transacción de origen hasta un punto de parada natural (custodio identificado, dilución, CoinJoin, o fondos aún sin gastar). Todo el rastreo corre sobre tu propio nodo — nada sale de tu red. +
+
+
+ setTxidInput(e.target.value)} + placeholder="txid de origen (64 caracteres hex)" + style={{flex:3,background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none"}} + /> + setVoutInput(e.target.value)} type="number" min="0" + placeholder="vout" title="Índice del output robado dentro de esa transacción" + style={{flex:1,background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none"}} + /> +
+ setAmountInput(e.target.value)} type="number" step="0.00000001" min="0" + placeholder="Importe estimado robado en BTC (opcional — activa la detección de dilución)" + style={{background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none"}} + /> +