feat: distancia a entidad en el rastro + manejo correcto de coinbase como origen
This commit is contained in:
@@ -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
|
## [1.3.0] — 2026-06-02
|
||||||
|
|
||||||
### Añadido
|
### Añadido
|
||||||
|
|||||||
+63
-10
@@ -2427,7 +2427,7 @@
|
|||||||
if(!origenTxid){ setHijos(s=>({...s,[i]:{err:"Este input no indica su transacción de origen."}})); return; }
|
if(!origenTxid){ setHijos(s=>({...s,[i]:{err:"Este input no indica su transacción de origen."}})); return; }
|
||||||
setHijos(s=>({...s,[i]:{loading:true}}));
|
setHijos(s=>({...s,[i]:{loading:true}}));
|
||||||
try{
|
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");
|
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}}}));
|
setHijos(s=>({...s,[i]:{child:{txData:child, fluyo:vin.prevout?.value||0, vout:vin.vout}}}));
|
||||||
}catch(e){ setHijos(s=>({...s,[i]:{err:e.message}})); }
|
}catch(e){ setHijos(s=>({...s,[i]:{err:e.message}})); }
|
||||||
@@ -2490,19 +2490,47 @@
|
|||||||
|
|
||||||
function RastroProcedencia({tx, base}) {
|
function RastroProcedencia({tx, base}) {
|
||||||
const {get}=useApi(base);
|
const {get}=useApi(base);
|
||||||
const cacheRef = useRef({}); // txid -> txData, compartida
|
const cacheRef = useRef({}); // txid -> {txData, depth}, compartida
|
||||||
const [raiz,setRaiz]=useState({}); // {vinIndex: {loading, child, err}}
|
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.
|
// Trae una tx (de caché o del nodo) y registra a qué profundidad se exploró.
|
||||||
const getTx = useCallback(async (txid)=>{
|
// Compartida por todos los eslabones.
|
||||||
if(cacheRef.current[txid]) return cacheRef.current[txid];
|
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;
|
let data;
|
||||||
if(!base){ data={...MOCK_TX, txid}; }
|
if(!base){ data={...MOCK_TX, txid}; }
|
||||||
else { data=await get(`/api/tx/${txid}`, null); }
|
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;
|
return data;
|
||||||
},[base,get]);
|
},[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<minDist[cat])) minDist[cat]=depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.entries(minDist).map(([cat,d])=>({cat,dist:d}));
|
||||||
|
},[explorados]);
|
||||||
|
|
||||||
const inputs = tx.vin || [];
|
const inputs = tx.vin || [];
|
||||||
|
|
||||||
const seguir = async (i) => {
|
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; }
|
if(!origenTxid){ setRaiz(s=>({...s,[i]:{err:"Este input no indica su transacción de origen."}})); return; }
|
||||||
setRaiz(s=>({...s,[i]:{loading:true}}));
|
setRaiz(s=>({...s,[i]:{loading:true}}));
|
||||||
try{
|
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");
|
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}}}));
|
setRaiz(s=>({...s,[i]:{child:{txData:child, fluyo:vin.prevout?.value||0, vout:vin.vout}}}));
|
||||||
}catch(e){ setRaiz(s=>({...s,[i]:{err:e.message}})); }
|
}catch(e){ setRaiz(s=>({...s,[i]:{err:e.message}})); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const limpiar = () => { cacheRef.current={}; setRaiz({}); };
|
const limpiar = () => { cacheRef.current={}; setRaiz({}); setExplorados(0); };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -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.
|
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.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{inputs.map((vin,i)=>{
|
{resumenDistancia.length>0&&(
|
||||||
|
<div style={{marginBottom:12,padding:"10px 12px",background:C.bg,borderRadius:6,border:`1px solid ${C.border}`}}>
|
||||||
|
<div style={{fontSize:"0.58rem",color:C.t2,fontFamily:"monospace",textTransform:"uppercase",letterSpacing:"0.1em",marginBottom:6}}>De lo explorado, lo más cercano</div>
|
||||||
|
<div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
|
||||||
|
{resumenDistancia.map((r,j)=>(
|
||||||
|
<span key={j} style={{fontSize:"0.6rem",fontFamily:"monospace",color:C.t1,border:`1px solid ${C.border}`,borderRadius:4,padding:"2px 8px"}}>
|
||||||
|
{r.cat} · a {r.dist} {r.dist===1?"salto":"saltos"}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize:"0.55rem",color:C.t3,marginTop:6,lineHeight:1.4}}>
|
||||||
|
Solo cuenta las ramas que has seguido. Puede haber más sin explorar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ¿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)) ? (
|
||||||
|
<div style={{padding:"12px 14px",background:C.bg,borderRadius:6,border:`1px solid ${C.green}30`}}>
|
||||||
|
<div style={{fontSize:"0.64rem",color:C.green,fontFamily:"monospace",marginBottom:4}}>⛏️ Origen del rastro: transacción coinbase</div>
|
||||||
|
<div style={{fontSize:"0.6rem",color:C.t2,lineHeight:1.5}}>
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : inputs.map((vin,i)=>{
|
||||||
const est=raiz[i]||{};
|
const est=raiz[i]||{};
|
||||||
return (
|
return (
|
||||||
<div key={i} style={{marginBottom:10,paddingLeft:10,borderLeft:`2px solid ${C.border}`}}>
|
<div key={i} style={{marginBottom:10,paddingLeft:10,borderLeft:`2px solid ${C.border}`}}>
|
||||||
|
|||||||
Reference in New Issue
Block a user