diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd1ec6..945210c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,26 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/): --- +## [1.4.0] — 2026-06-02 + +### Añadido + +- Distancia a una entidad en el rastro de procedencia. A medida que se exploran + eslabones, un resumen arriba del rastro indica a cuántos saltos está lo más + cercano de cada tipo marcado (OFAC, minería, CoinJoin) — por ejemplo "minería + · a 2 saltos". Es honesto sobre su alcance: solo cuenta las ramas que se han + seguido y avisa de que puede haber más sin explorar. + +### Corregido + +- Transacciones coinbase como origen del rastro. Antes mostraban un aviso + confuso ("este input no indica su transacción de origen"), como si fuera un + error. Ahora se reconocen como el principio natural del rastro: un mensaje + claro indica que esas monedas se crearon ahí como recompensa de minado y no + proceden de ninguna transacción anterior. + +--- + ## [1.3.0] — 2026-06-02 ### Añadido diff --git a/dashboard.html b/dashboard.html index e84dcd1..a27e311 100644 --- a/dashboard.html +++ b/dashboard.html @@ -2427,7 +2427,7 @@ if(!origenTxid){ setHijos(s=>({...s,[i]:{err:"Este input no indica su transacción de origen."}})); return; } setHijos(s=>({...s,[i]:{loading:true}})); try{ - const child=await getTx(origenTxid); + const child=await getTx(origenTxid, depth+1); if(!child) throw new Error("No se pudo traer la transacción de origen"); setHijos(s=>({...s,[i]:{child:{txData:child, fluyo:vin.prevout?.value||0, vout:vin.vout}}})); }catch(e){ setHijos(s=>({...s,[i]:{err:e.message}})); } @@ -2490,19 +2490,47 @@ function RastroProcedencia({tx, base}) { const {get}=useApi(base); - const cacheRef = useRef({}); // txid -> txData, compartida - const [raiz,setRaiz]=useState({}); // {vinIndex: {loading, child, err}} + const cacheRef = useRef({}); // txid -> {txData, depth}, compartida + const [raiz,setRaiz]=useState({}); // {vinIndex: {loading, child, err}} + const [explorados,setExplorados]=useState(0); // contador: sube al explorar, refresca resumen - // Trae una tx (de caché o del nodo). Compartida por todos los eslabones. - const getTx = useCallback(async (txid)=>{ - if(cacheRef.current[txid]) return cacheRef.current[txid]; + // Trae una tx (de caché o del nodo) y registra a qué profundidad se exploró. + // Compartida por todos los eslabones. + const getTx = useCallback(async (txid, depth)=>{ + const cached = cacheRef.current[txid]; + if(cached){ + // Si ya estaba pero a más profundidad, nos quedamos con la menor (más cercana) + if(depth!=null && depth < cached.depth){ cached.depth = depth; setExplorados(n=>n+1); } + return cached.txData; + } let data; if(!base){ data={...MOCK_TX, txid}; } else { data=await get(`/api/tx/${txid}`, null); } - if(data) cacheRef.current[txid]=data; + if(data){ cacheRef.current[txid]={txData:data, depth:depth??1}; setExplorados(n=>n+1); } return data; },[base,get]); + // Resumen de distancia a entidades: recorre lo YA explorado en la caché + // y busca la marca más cercana de cada tipo. Honesto: solo sabe de lo + // que se ha mirado, no afirma sobre ramas sin explorar. + const resumenDistancia = useMemo(()=>{ + void explorados; // dependencia: recalcular cuando se explora algo nuevo + const minDist = {}; // cat -> profundidad mínima + for(const txid in cacheRef.current){ + const {txData, depth} = cacheRef.current[txid]; + const ana = analyzeTx(txData); + const {marcas} = marcasDeTx(txData, ana); + for(const m of marcas){ + let cat=null; + if(m.txt.includes("OFAC")) cat="⚠️ OFAC"; + else if(m.txt.includes("minería")||m.txt.includes("coinbase")) cat="⛏️ minería"; + else if(m.txt.includes("CoinJoin")) cat="🔀 CoinJoin"; + if(cat && (minDist[cat]==null || depth({cat,dist:d})); + },[explorados]); + const inputs = tx.vin || []; const seguir = async (i) => { @@ -2511,13 +2539,13 @@ if(!origenTxid){ setRaiz(s=>({...s,[i]:{err:"Este input no indica su transacción de origen."}})); return; } setRaiz(s=>({...s,[i]:{loading:true}})); try{ - const child=await getTx(origenTxid); + const child=await getTx(origenTxid, 1); if(!child) throw new Error("No se pudo traer la transacción de origen"); setRaiz(s=>({...s,[i]:{child:{txData:child, fluyo:vin.prevout?.value||0, vout:vin.vout}}})); }catch(e){ setRaiz(s=>({...s,[i]:{err:e.message}})); } }; - const limpiar = () => { cacheRef.current={}; setRaiz({}); }; + const limpiar = () => { cacheRef.current={}; setRaiz({}); setExplorados(0); }; return ( @@ -2535,7 +2563,32 @@ Sigue cada input hacia atrás, un salto a la vez. Cada consulta la pides tú, para no sobrecargar el nodo. Desde cada eslabón puedes seguir bajando. - {inputs.map((vin,i)=>{ + {resumenDistancia.length>0&&( +
+
De lo explorado, lo más cercano
+
+ {resumenDistancia.map((r,j)=>( + + {r.cat} · a {r.dist} {r.dist===1?"salto":"saltos"} + + ))} +
+
+ Solo cuenta las ramas que has seguido. Puede haber más sin explorar. +
+
+ )} + + {/* ¿La propia tx analizada es coinbase? Entonces es el origen: no hay + nada hacia atrás, estas monedas se crearon aquí. */} + {((tx.vin||[]).some(v=>v.is_coinbase||v.coinbase) || (tx.vin||[]).length===0 || (tx.vin||[]).every(v=>!v.txid)) ? ( +
+
⛏️ Origen del rastro: transacción coinbase
+
+ Estas monedas se crearon aquí, como recompensa de minado. No vienen de ninguna transacción anterior — es el principio de su historia en la cadena. +
+
+ ) : inputs.map((vin,i)=>{ const est=raiz[i]||{}; return (