fix: el informe de wallet avisa cuando el escaneo quedó incompleto

Mismo patrón que el fallo del peritaje, en la pieza central del proyecto.
scanWallet usaba .catch(()=>[]) en sus tres consultas, así que una dirección
que el nodo no pudo servir era indistinguible de una sin actividad: sus txs no
se traían, no contaban para reutilización ni clusters, y el informe daba su
valoración sin mencionar que faltaban datos.

El sesgo iba siempre al optimismo — menos actividad vista, mejor nota. Ahora
se registran los fallos y se avisa antes de la banda de salud, en la UI y en
el export MD.
This commit is contained in:
Aitor
2026-07-27 14:06:08 +02:00
parent 6f750a1327
commit b57c44e5c0
2 changed files with 77 additions and 13 deletions
+14
View File
@@ -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
+63 -13
View File
@@ -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</button>
<button onClick={()=>{
const {health,totalTxs,activeCount,reusedAddrs,clusters,history}=walletReport;
const {health,totalTxs,activeCount,reusedAddrs,clusters,history,scanErrors}=walletReport;
const lines=[
`# Informe de Wallet — Txoko`,``,
...((scanErrors&&scanErrors.length>0)?[
`> **AVISO — informe incompleto.** ${scanErrors.length} consulta(s) al nodo fallaron durante el escaneo.`,
`> Las direcciones no leídas se comportan como si no tuvieran actividad, así que la valoración`,
`> de abajo es **optimista**: puede faltar reutilización, vinculaciones o transacciones enteras.`,
`> Repite el escaneo antes de darla por buena.`,``,
...scanErrors.map(e=>`> - ${e.addr} (${e.branch}) — ${e.message} [${e.phase}]`),``,
]:[]),
`## Salud general: ${health.band}`,``,health.msg,``,
`- Transacciones analizadas: ${totalTxs}`,
`- Direcciones con actividad: ${activeCount}`,
@@ -5000,6 +5024,32 @@
</div>
</div>
{/* Aviso de escaneo incompleto — va ANTES de la banda a
propósito: una valoración de salud leída sin saber que
faltan datos es peor que no tener valoración. El sesgo
siempre va hacia el optimismo, porque una dirección no
leída parece una dirección sin actividad. */}
{walletReport.scanErrors&&walletReport.scanErrors.length>0&&(
<div style={{padding:"10px 14px",background:C.redMuted,border:`1px solid ${C.red}40`,borderRadius:8,marginBottom:14}}>
<div style={{fontSize:"0.66rem",color:C.red,fontFamily:"monospace",fontWeight:700,marginBottom:6}}>
⚠ Informe incompleto — {walletReport.scanErrors.length} consulta(s) al nodo fallaron
</div>
<div style={{fontSize:"0.64rem",color:C.t1,lineHeight:1.6,marginBottom:8}}>
Las direcciones que no se pudieron leer se comportan aquí como si no tuvieran actividad. Eso hace que la valoración de abajo sea <strong>optimista</strong>: puede faltar reutilización, vinculaciones o transacciones enteras que sí existen. No la des por buena sin repetir el escaneo.
</div>
<div style={{display:"flex",flexDirection:"column",gap:2}}>
{walletReport.scanErrors.slice(0,6).map((e,i)=>(
<div key={i} style={{fontSize:"0.56rem",color:C.t2,fontFamily:"monospace",wordBreak:"break-all"}}>
{e.addr} ({e.branch}) — {e.message} · {e.phase}
</div>
))}
{walletReport.scanErrors.length>6&&(
<div style={{fontSize:"0.56rem",color:C.t2,fontFamily:"monospace"}}>…y {walletReport.scanErrors.length-6} más</div>
)}
</div>
</div>
)}
{/* Bloque 1: Salud general */}
<div style={{display:"flex",alignItems:"center",gap:14,padding:"12px 14px",background:C.bgCard,borderRadius:8,marginBottom:14,border:`1px solid ${walletReport.health.color}30`}}>
<div style={{width:48,height:48,borderRadius:"50%",border:`3px solid ${walletReport.health.color}`,display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0}}>