diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b6222..e8ca64b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,20 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/): `mixer` del conjunto de direcciones del actor y del union-find, y la atribución por huella salta esos nodos. - Verificado con el mismo caso: de 10 direcciones atribuidas a 0. +- **El informe de wallet ya no da un aprobado optimista cuando faltan datos.** + Mismo patrón que el fallo del peritaje, en la pieza central del proyecto: + `scanWallet` usaba `.catch(()=>[])` en las tres consultas del escaneo, así que + una dirección que el nodo no pudo servir quedaba indistinguible de una + dirección sin actividad. Sus transacciones no se traían, no contaban para la + reutilización, no entraban en el union-find y no aparecían en el historial — + y el informe daba su valoración de salud sin mencionar que le faltaban datos. + El sesgo iba siempre hacia el optimismo: menos actividad vista es mejor nota. + - Las tres consultas usan ahora `getStrict` y registran los fallos en + `report.scanErrors`, con la dirección, la rama y en qué fase ocurrió. + - Aviso en rojo **antes** de la banda de salud (una valoración leída sin saber + que faltan datos es peor que ninguna valoración), y bloque equivalente al + principio del export Markdown. + - **Tampoco atribuye al actor las direcciones de un custodio.** Misma raíz que el fallo anterior, descubierta probando la poda por rama con un depósito real en Bitfinex: el cluster CIOH ya excluía las direcciones de custodio, pero la diff --git a/dashboard.html b/dashboard.html index aa981c9..f45fa37 100644 --- a/dashboard.html +++ b/dashboard.html @@ -4748,7 +4748,7 @@ // ── AUDITORÍA ────────────────────────────────────────────────────────── function Auditoria({base, initialQuery}) { - const {get}=useApi(base); + const {get, getStrict}=useApi(base); const [query,setQuery]=useState(initialQuery||""); const [tx,setTx]=useState(null); const [analysis,setAnalysis]=useState(null); @@ -4806,14 +4806,22 @@ const BATCH = 5; // direcciones por lote const PAUSE = 120; // ms entre lotes const active = []; // direcciones con actividad + // Consultas que el nodo no pudo servir. Se registran porque su + // ausencia SESGA el informe hacia el optimismo: una dirección que no + // se pudo leer parece una dirección sin actividad, y entonces su + // reutilización, sus vinculaciones y sus transacciones desaparecen + // del análisis. Decir "tu wallet está limpia" cuando en realidad + // faltan datos es peor que no decir nada. + const scanErrors = []; // Fase 1: estado ligero de cada dirección for (let i = 0; i < all.length; i += BATCH) { const slice = all.slice(i, i + BATCH); setWalletProgress(`Explorando direcciones ${Math.min(i+BATCH,all.length)}/${all.length}`); - const infos = await Promise.all(slice.map(s => - get(`/api/address/${s.addr}`, null).catch(()=>null) - )); + const infos = await Promise.all(slice.map(async s => { + try { return await getStrict(`/api/address/${s.addr}`); } + catch (e) { scanErrors.push({ addr:s.addr, branch:s.branch, message:e.message, phase:"estado de la dirección" }); return null; } + })); infos.forEach((info, j) => { const txc = info?.chain_stats?.tx_count || 0; if (txc > 0) active.push({ ...slice[j], info }); @@ -4829,14 +4837,21 @@ const slice = active.slice(i, i + BATCH); setWalletProgress(`Trayendo transacciones ${Math.min(i+BATCH,active.length)}/${active.length}`); await Promise.all(slice.map(async s => { - let page = await get(`/api/address/${s.addr}/txs`, []).catch(()=>[]); - let guard = 0; - while (Array.isArray(page) && page.length > 0 && guard < 8) { - page.forEach(t => { if (t && t.txid) txMap.set(t.txid, t); }); - if (page.length < 25) break; // última página - const last = page[page.length-1].txid; - page = await get(`/api/address/${s.addr}/txs/chain/${last}`, []).catch(()=>[]); - guard++; + try { + let page = await getStrict(`/api/address/${s.addr}/txs`); + let guard = 0; + while (Array.isArray(page) && page.length > 0 && guard < 8) { + page.forEach(t => { if (t && t.txid) txMap.set(t.txid, t); }); + if (page.length < 25) break; // última página + const last = page[page.length-1].txid; + page = await getStrict(`/api/address/${s.addr}/txs/chain/${last}`); + guard++; + } + } catch (e) { + // Perder el historial de una dirección con actividad conocida + // es lo más grave: sabemos que tiene transacciones y no hemos + // podido verlas. El informe queda incompleto por ahí. + scanErrors.push({ addr:s.addr, branch:s.branch, message:e.message, phase:"historial de transacciones", txCount:s.info?.chain_stats?.tx_count ?? null }); } })); if (i + BATCH < active.length) await new Promise(r => setTimeout(r, PAUSE)); @@ -4844,6 +4859,8 @@ setWalletProgress("Analizando vinculación…"); const report = buildWalletReport(active, [...txMap.values()], myAddrSet); + report.scanErrors = scanErrors; + report.scanComplete = scanErrors.length === 0; setWalletReport(report); } catch(e) { setWalletReport({ error: e.message }); @@ -4978,9 +4995,16 @@ const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="wallet-report.json";a.click(); }} style={{padding:"3px 10px",background:"none",border:`1px solid ${C.border}`,borderRadius:4,color:C.t2,fontFamily:"monospace",fontSize:"0.6rem",cursor:"pointer"}}>↓ JSON