feat: distancia a entidad en el rastro + manejo correcto de coinbase como origen
This commit is contained in:
+63
-10
@@ -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<minDist[cat])) minDist[cat]=depth;
|
||||
}
|
||||
}
|
||||
return Object.entries(minDist).map(([cat,d])=>({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 (
|
||||
<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.
|
||||
</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]||{};
|
||||
return (
|
||||
<div key={i} style={{marginBottom:10,paddingLeft:10,borderLeft:`2px solid ${C.border}`}}>
|
||||
|
||||
Reference in New Issue
Block a user