From b101ca4210eadaca75c3f80ed253bb17e866a9ec Mon Sep 17 00:00:00 2001 From: Aitor Date: Tue, 2 Jun 2026 18:48:16 +0200 Subject: [PATCH] feat: rastro de procedencia paso 1 - seguir inputs hacia atras bajo demanda --- dashboard.html | 169 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 3 deletions(-) diff --git a/dashboard.html b/dashboard.html index 91059e4..a867ebe 100644 --- a/dashboard.html +++ b/dashboard.html @@ -118,8 +118,8 @@ fee: 4200, feerate: 16.8, size: 374, weight: 996, status: { confirmed:true, block_height:840811, block_time: Math.floor(Date.now()/1000) - 3891200 }, vin: [ - { sequence: 0xffffffff, prevout: { value:5000000, scriptpubkey_address:"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", scriptpubkey_type:"v0_p2wpkh" } }, - { sequence: 0xffffffff, prevout: { value:3000000, scriptpubkey_address:"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", scriptpubkey_type:"v0_p2wpkh" } }, + { sequence: 0xffffffff, txid:"d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5", vout:0, prevout: { value:5000000, scriptpubkey_address:"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", scriptpubkey_type:"v0_p2wpkh" } }, + { sequence: 0xffffffff, txid:"f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1", vout:1, prevout: { value:3000000, scriptpubkey_address:"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", scriptpubkey_type:"v0_p2wpkh" } }, ], vout: [ { value:7000000, scriptpubkey_address:"bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h", scriptpubkey_type:"v0_p2wpkh" }, @@ -2379,6 +2379,168 @@ ); } + // ── RASTRO DE PROCEDENCIA ────────────────────────────────────────────── + // Sigue una transacción hacia atrás, eslabón a eslabón, BAJO DEMANDA. + // El usuario pulsa "seguir rastro" en un input y se trae UNA transacción + // de origen. Caché en memoria de sesión para no repetir consultas. + // Paso 1: un solo salto hacia atrás por input. La base de todo lo demás. + function RastroProcedencia({tx, base}) { + const {get}=useApi(base); + // Caché en memoria: txid -> datos de esa tx ya consultada. Solo RAM. + const [cache,setCache]=useState({}); + // Estado por input: { [vinIndex]: {loading, eslabon, err} } + const [eslabones,setEslabones]=useState({}); + + const inputs = tx.vin || []; + + // Marcas baratas de un eslabón (lookup local + estructura, sin consultas extra) + const marcasDe = (txData) => { + const marcas = []; + // Cruce con ENTITY_INDEX (OFAC / mining) sobre inputs y outputs + const addrs = [ + ...(txData.vin||[]).map(v=>v.prevout?.scriptpubkey_address), + ...(txData.vout||[]).map(o=>o.scriptpubkey_address), + ].filter(Boolean); + let ofac=false, mining=false; + for(const a of addrs){ + const hit=ENTITY_INDEX.get(a); + if(hit?.cat==="ofac") ofac=true; + if(hit?.cat==="mining") mining=true; + } + if(ofac) marcas.push({txt:"⚠️ OFAC", color:C.red}); + if(mining) marcas.push({txt:"⛏️ minería", color:C.green}); + // ¿Es coinbase? (input sin prevout / con campo coinbase) -> fin de rama + const esCoinbase = (txData.vin||[]).some(v=>v.is_coinbase||v.coinbase) || (txData.vin||[]).length===0; + if(esCoinbase) marcas.push({txt:"⛏️ coinbase (origen)", color:C.green}); + // CoinJoin: lo detecta analyzeTx, lo añadimos abajo con la banda + return {marcas, esCoinbase}; + }; + + const seguir = async (i) => { + const vin = inputs[i]; + const origenTxid = vin?.txid; + if(!origenTxid){ + setEslabones(s=>({...s,[i]:{err:"Este input no indica su transacción de origen."}})); + return; + } + setEslabones(s=>({...s,[i]:{loading:true}})); + try{ + // ¿En caché? No repetir consulta al nodo. + let txData = cache[origenTxid]; + if(!txData){ + if(!base){ + // Modo demo: devolvemos el propio MOCK como "origen" simulado + txData = {...MOCK_TX, txid:origenTxid}; + } else { + txData = await get(`/api/tx/${origenTxid}`, null); + if(!txData) throw new Error("No se pudo traer la transacción de origen"); + } + setCache(c=>({...c,[origenTxid]:txData})); + } + // Info barata del eslabón + const ana = analyzeTx(txData); + const {marcas, esCoinbase} = marcasDe(txData); + if(ana?.band==="ALTA" && /coinjoin|whirlpool|mezcla/i.test(JSON.stringify(ana.checks||[]))){ + marcas.unshift({txt:"🔀 CoinJoin", color:C.blue}); + } + const fluyo = vin.prevout?.value || 0; + setEslabones(s=>({...s,[i]:{ + eslabon:{ + txid: origenTxid, + vout: vin.vout, + fluyo, + fecha: txData.status?.block_time || null, + band: ana?.band || null, + score: ana?.score ?? null, + marcas, + esCoinbase, + } + }})); + }catch(e){ + setEslabones(s=>({...s,[i]:{err:e.message}})); + } + }; + + const bandColor = (b) => b==="ALTA"?C.green : b==="MEDIA"?C.amber : b==="BAJA"?C.red : C.t2; + const abrev = (t) => t ? `${t.slice(0,10)}…${t.slice(-6)}` : "—"; + const mempoolUrl = (t) => base ? `${base}/tx/${t}` : `https://mempool.space/tx/${t}`; + + const limpiar = () => { setCache({}); setEslabones({}); }; + + return ( + +
+ Rastro de procedencia +
+ {Object.keys(eslabones).length>0&&( + + )} +
+
+ Sigue cada input hacia atrás, un salto a la vez. Cada consulta la pides tú, para no sobrecargar el nodo. +
+ + {inputs.map((vin,i)=>{ + const est = eslabones[i] || {}; + return ( +
+
+
+ Input #{i+1} + {fmt.num(vin.prevout?.value||0)} sat +
+ {!est.eslabon&&!est.loading&&( + + )} + {est.loading&&consultando…} +
+ + {est.err&&( +
{est.err}
+ )} + + {est.eslabon&&( +
+
+ + {abrev(est.eslabon.txid)} ↗ + + {est.eslabon.band&&( + + {est.eslabon.band} + + )} +
+
+ Fluyó {fmt.num(est.eslabon.fluyo)} sat hacia esta tx + {est.eslabon.fecha&& · {fmt.date(est.eslabon.fecha)}} +
+ {est.eslabon.marcas.length>0&&( +
+ {est.eslabon.marcas.map((m,j)=>( + {m.txt} + ))} +
+ )} + {est.eslabon.esCoinbase&&( +
Fin de la rama: estas monedas se crearon aquí (recompensa de minado).
+ )} +
+ )} +
+ ); + })} + + ); + } + // ── AUDITORÍA ────────────────────────────────────────────────────────── function Auditoria({base, initialQuery}) { const {get}=useApi(base); @@ -2450,6 +2612,7 @@
+ )} @@ -2772,7 +2935,7 @@ return (
Detector OP_RETURN
-
Indica si una transacción contiene datos arbitrarios (OP_RETURN) y su tamaño. No muestra el contenido: para auditar privacidad lo relevante es que existan, no qué dicen.
+
Indica si una transacción contiene datos arbitrarios (OP_RETURN) y su tamaño.
setTxid(e.target.value)} onKeyDown={e=>e.key==="Enter"&&decode()} placeholder="txid de una transacción"