feat: cierra las cinco heurísticas ausentes del repaso
La comisión como huella: una comisión absoluta redonda o una tarifa entera no salen de un estimador, salen de alguien que las escribió a mano. Informativo y sin penalizar, igual que RBF — castigarlo empujaría a elegir comisiones peores para disimular. Cambio con más de dos salidas, solo el caso inequívoco: si exactamente una salida comparte tipo con las entradas no hay nada que adivinar, y con más salidas la coincidencia por azar es todavía más improbable. POSIBLE, no PROBABLE, porque es una sola señal. Autoenvíos en el informe de wallet: solo cabe ahí, porque saber que todas las salidas son tuyas exige conocer el wallet entero. Avisa solo cuando ataron más de una dirección — mover una a otra no vincula nada nuevo.
This commit is contained in:
@@ -10,6 +10,41 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/):
|
|||||||
- **MENOR** — características nuevas que no rompen lo anterior
|
- **MENOR** — características nuevas que no rompen lo anterior
|
||||||
- **PARCHE** — arreglos de errores
|
- **PARCHE** — arreglos de errores
|
||||||
|
|
||||||
|
---
|
||||||
|
## [1.18.0] — 2026-08-08
|
||||||
|
### Añadido
|
||||||
|
- **La comisión como huella.** El sat/vB que eliges dice algo del software y de
|
||||||
|
quien lo maneja, y el dato estaba en cada transacción sin que nadie lo
|
||||||
|
mirase. Se detectan dos firmas: una comisión **absoluta** redonda (10.000
|
||||||
|
sats clavados — un estimador nunca da eso, calcula tarifa por tamaño y
|
||||||
|
devuelve números feos) y una **tarifa** prácticamente entera (20,00 sat/vB,
|
||||||
|
que sale de teclear un número redondo en la casilla).
|
||||||
|
- Informativo y sin penalización, por el mismo motivo que se decidió con RBF:
|
||||||
|
la señal es débil, y bajar la nota por ella empujaría a elegir comisiones
|
||||||
|
peores solo para disimular. Mal negocio.
|
||||||
|
- **El cambio ya se identifica en transacciones de más de dos salidas, cuando
|
||||||
|
el caso es inequívoco.** Antes la detección se plantaba en seco por encima de
|
||||||
|
dos salidas. Estaba bien argumentado, pero se tiraba información sólida: si
|
||||||
|
de N salidas **exactamente una** comparte tipo de script con las entradas, no
|
||||||
|
hay nada que adivinar — y con más salidas la coincidencia por azar es aún más
|
||||||
|
improbable que con dos. También cuenta el caso de una salida que vuelve a una
|
||||||
|
dirección ya gastada. Se marca POSIBLE y no PROBABLE: es una sola señal, y
|
||||||
|
hay monederos que devuelven el cambio a un tipo distinto del que gastan.
|
||||||
|
- **Aviso de autoenvíos que ataron direcciones**, en el informe de wallet. Es
|
||||||
|
una comprobación que solo cabe ahí: para saber que *todas* las salidas son
|
||||||
|
tuyas hace falta conocer tu wallet entero, cosa que el analizador de una
|
||||||
|
transacción suelta no puede saber.
|
||||||
|
- Detecta las transacciones donde entradas y salidas son todas tuyas, y avisa
|
||||||
|
solo cuando hubo daño real: más de una dirección de entrada. Mover una
|
||||||
|
dirección a otra no vincula nada nuevo y no merece alarma.
|
||||||
|
- Es un gesto que suele hacerse creyendo que despista y hace lo contrario: no
|
||||||
|
hubo ningún pago, pero al firmar varias direcciones a la vez quedaron
|
||||||
|
enlazadas en público. Se paga una comisión por empeorar la propia
|
||||||
|
privacidad.
|
||||||
|
|
||||||
|
Con esto quedan cerradas las cinco heurísticas que el repaso del 2026-08-08
|
||||||
|
señaló como ausentes.
|
||||||
|
|
||||||
---
|
---
|
||||||
## [1.17.0] — 2026-08-08
|
## [1.17.0] — 2026-08-08
|
||||||
### Cambiado
|
### Cambiado
|
||||||
|
|||||||
+135
-5
@@ -1503,8 +1503,41 @@
|
|||||||
polvoRecibido.sort((a,b) => b.time - a.time);
|
polvoRecibido.sort((a,b) => b.time - a.time);
|
||||||
const polvoSinGastar = polvoRecibido.filter(p => !p.gastado);
|
const polvoSinGastar = polvoRecibido.filter(p => !p.gastado);
|
||||||
|
|
||||||
|
// ── Autoenvíos (mover dinero de una dirección tuya a otra tuya) ────
|
||||||
|
//
|
||||||
|
// Esta comprobación solo cabe aquí. El analizador de una transacción
|
||||||
|
// suelta no puede hacerla: para saber que TODAS las salidas son tuyas
|
||||||
|
// hace falta conocer tu wallet entero, que es justo lo que este informe
|
||||||
|
// tiene y aquel no.
|
||||||
|
//
|
||||||
|
// Es un gesto que la gente hace creyendo que limpia el rastro —"paso
|
||||||
|
// esto a otra dirección mía"— y hace exactamente lo contrario: gasta
|
||||||
|
// varias direcciones tuyas a la vez para juntarlas en otra tuya, y deja
|
||||||
|
// las tres atadas en público sin que haya habido ningún pago de por
|
||||||
|
// medio. Se paga una comisión por empeorar la propia privacidad.
|
||||||
|
const autoenvios = [];
|
||||||
|
for (const tx of txs) {
|
||||||
|
const ins = (tx.vin || []).map(v => v.prevout?.scriptpubkey_address).filter(Boolean);
|
||||||
|
const outs = (tx.vout || []).map(v => v.scriptpubkey_address).filter(Boolean);
|
||||||
|
if (ins.length === 0 || outs.length === 0) continue;
|
||||||
|
// Todas las entradas Y todas las salidas dentro del propio wallet.
|
||||||
|
if (!ins.every(a => myAddrSet.has(a)) || !outs.every(a => myAddrSet.has(a))) continue;
|
||||||
|
const dirsEntrada = new Set(ins);
|
||||||
|
autoenvios.push({
|
||||||
|
txid: tx.txid,
|
||||||
|
time: tx.status?.block_time || 0,
|
||||||
|
dirsEntrada: dirsEntrada.size,
|
||||||
|
dirsSalida: new Set(outs).size,
|
||||||
|
// Lo que de verdad importa: cuántas direcciones tuyas quedaron
|
||||||
|
// enlazadas por haber firmado juntas.
|
||||||
|
vinculadas: dirsEntrada.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
autoenvios.sort((a,b) => b.time - a.time);
|
||||||
|
const autoenviosDaninos = autoenvios.filter(a => a.vinculadas > 1);
|
||||||
|
|
||||||
return { totalTxs, activeCount: activeAddrs.length, reusedAddrs, clusters, linkReasons, history,
|
return { totalTxs, activeCount: activeAddrs.length, reusedAddrs, clusters, linkReasons, history,
|
||||||
polvoRecibido, polvoSinGastar,
|
polvoRecibido, polvoSinGastar, autoenvios, autoenviosDaninos,
|
||||||
health: { band: healthBand, color: healthColor, msg: healthMsg } };
|
health: { band: healthBand, color: healthColor, msg: healthMsg } };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1566,12 +1599,48 @@
|
|||||||
// demasiado común para significar nada.
|
// demasiado común para significar nada.
|
||||||
const ROUND_MIN = 2;
|
const ROUND_MIN = 2;
|
||||||
|
|
||||||
|
const SIN_CAMBIO = { identifiable: false, index: null, signals: 0, details: [], bonus: 0, certainty: null };
|
||||||
|
|
||||||
function guessChangeOutput(tx, likelyCJ) {
|
function guessChangeOutput(tx, likelyCJ) {
|
||||||
if (tx.vout.length !== 2 || likelyCJ) {
|
if (likelyCJ || tx.vout.length < 2) return SIN_CAMBIO;
|
||||||
return { identifiable: false, index: null, signals: 0, details: [], bonus: 0, certainty: null };
|
|
||||||
}
|
|
||||||
const inTypes = tx.vin.map(v=>v.prevout?.scriptpubkey_type).filter(Boolean);
|
const inTypes = tx.vin.map(v=>v.prevout?.scriptpubkey_type).filter(Boolean);
|
||||||
const dominantInType = inTypes[0];
|
const dominantInType = inTypes[0];
|
||||||
|
|
||||||
|
// ── Más de dos salidas: solo el caso inequívoco ────────────────────
|
||||||
|
//
|
||||||
|
// Con tres o más salidas la heurística general no vale, y la aplicación
|
||||||
|
// se plantaba en seco. Bien argumentado, pero se estaba tirando
|
||||||
|
// información sólida por prudencia: hay un subcaso donde no hay nada
|
||||||
|
// que adivinar.
|
||||||
|
//
|
||||||
|
// Si de N salidas EXACTAMENTE UNA comparte tipo de script con las
|
||||||
|
// entradas, esa es el cambio. El razonamiento es el mismo que en el caso
|
||||||
|
// de dos salidas: tu monedero devuelve el cambio a una dirección del
|
||||||
|
// mismo formato que las que gasta, mientras que los destinatarios usan
|
||||||
|
// el formato que les da la gana. Con más salidas la señal es incluso más
|
||||||
|
// limpia, porque la coincidencia por azar es más improbable.
|
||||||
|
//
|
||||||
|
// Se marca POSIBLE y no PROBABLE: es una sola señal, y hay monederos que
|
||||||
|
// devuelven el cambio a un tipo distinto del que gastan.
|
||||||
|
if (tx.vout.length > 2) {
|
||||||
|
const reusaIdx = tx.vout.findIndex(v =>
|
||||||
|
v.scriptpubkey_address && tx.vin.some(i => i.prevout?.scriptpubkey_address === v.scriptpubkey_address));
|
||||||
|
if (reusaIdx !== -1) {
|
||||||
|
return { identifiable: true, index: reusaIdx, signals: 2, bonus: 10, certainty: "PROBABLE",
|
||||||
|
details: [`de ${tx.vout.length} salidas, una vuelve a una dirección de las entradas — cambio casi seguro`] };
|
||||||
|
}
|
||||||
|
if (!dominantInType) return SIN_CAMBIO;
|
||||||
|
const coinciden = tx.vout.filter(v => v.scriptpubkey_type === dominantInType);
|
||||||
|
if (coinciden.length !== 1) return SIN_CAMBIO;
|
||||||
|
return {
|
||||||
|
identifiable: true,
|
||||||
|
index: tx.vout.findIndex(v => v.scriptpubkey_type === dominantInType),
|
||||||
|
signals: 1, bonus: 0, certainty: "POSIBLE",
|
||||||
|
details: [`de ${tx.vout.length} salidas, solo una comparte tipo de script (${dominantInType}) con las entradas`],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const isBip69Inputs = tx.vin.length < 2 || tx.vin.every((v,i,a) => {
|
const isBip69Inputs = tx.vin.length < 2 || tx.vin.every((v,i,a) => {
|
||||||
if (i===0) return true;
|
if (i===0) return true;
|
||||||
const pidA = a[i-1].txid||"", pidB = v.txid||"";
|
const pidA = a[i-1].txid||"", pidB = v.txid||"";
|
||||||
@@ -2140,7 +2209,7 @@
|
|||||||
? `Hecho (certeza): ${changeSignals} señales apuntan al output #${changeGuess.index} — ${changeDetails.join(" · ")}. Interpretación (probable): ese es el cambio, la moneda que vuelve a tu propio wallet. Consecuencia: quien lo identifique puede seguir tus gastos posteriores desde ahí.${correlatedProblem?" (penalización reducida por correlación con otros checks)":""}`
|
? `Hecho (certeza): ${changeSignals} señales apuntan al output #${changeGuess.index} — ${changeDetails.join(" · ")}. Interpretación (probable): ese es el cambio, la moneda que vuelve a tu propio wallet. Consecuencia: quien lo identifique puede seguir tus gastos posteriores desde ahí.${correlatedProblem?" (penalización reducida por correlación con otros checks)":""}`
|
||||||
: `Hecho (certeza): hay ${changeSignals} señales de que el cambio es identificable — ${changeDetails.join(" · ")}. Interpretación: las señales no coinciden en cuál de las dos salidas es el cambio, así que no se señala ninguna. Consecuencia: la transacción filtra que hay un cambio, aunque esta herramienta no se atreva a decir cuál — otro analista con más contexto sí podría.${correlatedProblem?" (penalización reducida por correlación con otros checks)":""}`)
|
: `Hecho (certeza): hay ${changeSignals} señales de que el cambio es identificable — ${changeDetails.join(" · ")}. Interpretación: las señales no coinciden en cuál de las dos salidas es el cambio, así que no se señala ninguna. Consecuencia: la transacción filtra que hay un cambio, aunque esta herramienta no se atreva a decir cuál — otro analista con más contexto sí podría.${correlatedProblem?" (penalización reducida por correlación con otros checks)":""}`)
|
||||||
: tx.vout.length > 2
|
: tx.vout.length > 2
|
||||||
? `Estructura de ${tx.vout.length} outputs — la identificación del cambio es menos fiable y no se afirma aquí. La herramienta no adivina el cambio en transacciones con más de 2 salidas para no dar una certeza que los datos no sostienen.`
|
? `Estructura de ${tx.vout.length} salidas. Con más de dos, esta herramienta solo se pronuncia si el caso es inequívoco —que una sola salida comparta tipo de script con las entradas, o que una vuelva a una dirección ya gastada— y aquí no lo es. Adivinar entre varias salidas daría una certeza que los datos no sostienen.`
|
||||||
: changeSignals === 1
|
: changeSignals === 1
|
||||||
? `Hay una señal, insuficiente para afirmar nada: ${changeDetails.join(" · ")}. Hace falta una segunda señal independiente para señalar un output, así que aquí no se señala ninguno. Conviene saber que la señal existe: un analista menos escrupuloso la daría por buena ella sola.`
|
? `Hay una señal, insuficiente para afirmar nada: ${changeDetails.join(" · ")}. Hace falta una segunda señal independiente para señalar un output, así que aquí no se señala ninguno. Conviene saber que la señal existe: un analista menos escrupuloso la daría por buena ella sola.`
|
||||||
: "No hay señales que permitan identificar el output de cambio.",
|
: "No hay señales que permitan identificar el output de cambio.",
|
||||||
@@ -2275,6 +2344,40 @@
|
|||||||
penalty: 0, informational: true,
|
penalty: 0, informational: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── 16-bis. La comisión como huella ───────────────────────────────
|
||||||
|
// El sat/vB que eliges dice algo del software y de quien lo maneja, y
|
||||||
|
// el dato estaba en la transacción sin que nadie lo mirase.
|
||||||
|
//
|
||||||
|
// Dos firmas distintas:
|
||||||
|
// · Comisión ABSOLUTA redonda (10.000 sats clavados). Un estimador
|
||||||
|
// nunca da eso: calcula tarifa × tamaño y sale un número feo. Una
|
||||||
|
// cifra redonda significa que alguien la escribió a mano.
|
||||||
|
// · TARIFA casi exacta (20,00 sat/vB). Sale de teclear un número
|
||||||
|
// redondo en la casilla de tarifa. No es exacta del todo porque el
|
||||||
|
// tamaño real difiere del estimado al firmar, de ahí la tolerancia.
|
||||||
|
//
|
||||||
|
// Informativo y sin penalización, por el mismo motivo que RBF: la señal
|
||||||
|
// es débil y castigarla empujaría a elegir comisiones peores para
|
||||||
|
// esconderla. Mal negocio, y esta herramienta no está para eso.
|
||||||
|
const vsizeReal = tx.weight ? Math.ceil(tx.weight / 4) : null;
|
||||||
|
const tarifaReal = (tx.fee && vsizeReal) ? tx.fee / vsizeReal : null;
|
||||||
|
const comisionRedonda = tx.fee ? roundness(tx.fee) >= 2 : false;
|
||||||
|
const tarifaEntera = tarifaReal !== null
|
||||||
|
&& Math.abs(tarifaReal - Math.round(tarifaReal)) < 0.03
|
||||||
|
&& Math.round(tarifaReal) >= 2;
|
||||||
|
const hayHuellaComision = comisionRedonda || tarifaEntera;
|
||||||
|
checks.push({
|
||||||
|
id:"fee_fingerprint", label:"La comisión como huella", certainty:"POSIBLE", pass:true,
|
||||||
|
actionability: "evitable",
|
||||||
|
detail: hayHuellaComision
|
||||||
|
? `Hecho (certeza): ${comisionRedonda ? `la comisión es de ${tx.fee} sats exactos` : `la tarifa sale a ${tarifaReal.toFixed(2)} sat/vB, prácticamente un número entero`}. Interpretación (posible): las cifras redondas no salen de un estimador automático —que calcula tarifa por tamaño y devuelve números feos— sino de alguien que las escribió a mano. Consecuencia: te mete en el grupo, más pequeño, de quien ajusta la comisión manualmente. Por sí sola no identifica a nadie; suma a un perfil junto con el resto de rasgos.`
|
||||||
|
: tarifaReal !== null
|
||||||
|
? `Tarifa de ${tarifaReal.toFixed(2)} sat/vB, sin forma de cifra elegida a mano. Compatible con un estimador automático, que es lo que hace la mayoría.`
|
||||||
|
: "No hay datos de comisión y tamaño para evaluar este rasgo.",
|
||||||
|
didactic: "El fingerprinting no busca una prueba, busca acumular rasgos hasta que el grupo de gente que los comparte es lo bastante pequeño. La comisión es uno de ellos y casi nadie lo piensa: teclear «20 sat/vB» deja una marca distinta a dejar que el monedero calcule. Aquí no penaliza a propósito — la señal es débil y bajar la nota por ella te empujaría a elegir comisiones peores solo para disimular, que es un mal negocio. Lo mismo que se decidió con RBF.",
|
||||||
|
penalty: 0, informational: true,
|
||||||
|
});
|
||||||
|
|
||||||
// ── 16. Tipo legacy (P2PKH / P2SH) ───────────────────────────────
|
// ── 16. Tipo legacy (P2PKH / P2SH) ───────────────────────────────
|
||||||
// Inputs en formato legacy (P2PKH = 1..., P2SH = 3...) revelan UTXOs antiguos
|
// Inputs en formato legacy (P2PKH = 1..., P2SH = 3...) revelan UTXOs antiguos
|
||||||
// o wallets que no han migrado a SegWit/Taproot. Mayor huella on-chain y
|
// o wallets que no han migrado a SegWit/Taproot. Mayor huella on-chain y
|
||||||
@@ -5669,6 +5772,33 @@
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Autoenvíos que ataron direcciones. Solo se muestra cuando
|
||||||
|
hubo daño real (más de una dirección de entrada): mover una
|
||||||
|
dirección a otra no vincula nada nuevo y no merece alarma. */}
|
||||||
|
{walletReport.autoenviosDaninos && walletReport.autoenviosDaninos.length > 0 && (
|
||||||
|
<div style={{padding:"10px 14px",background:C.bgCard,border:`1px solid ${C.amber}40`,borderRadius:8,marginBottom:14}}>
|
||||||
|
<div style={{fontSize:"0.64rem",color:C.amber,fontFamily:"monospace",fontWeight:700,marginBottom:5}}>
|
||||||
|
{walletReport.autoenviosDaninos.length} autoenvío(s) que ataron direcciones tuyas
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize:"0.62rem",color:C.t2,lineHeight:1.6,marginBottom:6}}>
|
||||||
|
Moviste dinero de varias direcciones tuyas a otra dirección tuya. Es un gesto que suele hacerse creyendo que despista, y hace lo contrario: no hubo ningún pago, pero al firmar varias direcciones a la vez quedaron enlazadas en público para siempre. Pagaste una comisión por empeorar tu propia privacidad.
|
||||||
|
</div>
|
||||||
|
<div style={{display:"flex",flexDirection:"column",gap:3}}>
|
||||||
|
{walletReport.autoenviosDaninos.slice(0,6).map(a=>(
|
||||||
|
<div key={a.txid} style={{fontSize:"0.58rem",color:C.t2,fontFamily:"monospace"}}>
|
||||||
|
<span style={{color:C.amber}}>{a.vinculadas} direcciones atadas</span>
|
||||||
|
{" · "}{a.time ? new Date(a.time*1000).toLocaleDateString("es-ES") : "sin confirmar"}
|
||||||
|
{" · "}<VerEnMempool base={base} tipo="tx" id={a.txid} texto={a.txid.slice(0,12)+"… ↗"}/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{walletReport.autoenviosDaninos.length>6&&<div style={{fontSize:"0.58rem",color:C.t2,fontFamily:"monospace"}}>…y {walletReport.autoenviosDaninos.length-6} más</div>}
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize:"0.6rem",color:C.t2,fontFamily:"monospace",marginTop:6,lineHeight:1.5}}>
|
||||||
|
Ya está hecho y no se deshace. Para adelante: si de verdad necesitas mover monedas entre direcciones tuyas, muévelas de una en una, y solo cuando su origen ya estuviera relacionado.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Bloque 1: Salud general */}
|
{/* 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={{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}}>
|
<div style={{width:48,height:48,borderRadius:"50%",border:`3px solid ${walletReport.health.color}`,display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0}}>
|
||||||
|
|||||||
@@ -87,6 +87,35 @@ T("CoinJoin: la guardia desactiva la heurística aunque haya señales",
|
|||||||
vout:[OUT(100000,"v1_p2tr","bc1pX"), OUT(99895000,"v0_p2wpkh","bc1qY")] },
|
vout:[OUT(100000,"v1_p2tr","bc1pX"), OUT(99895000,"v0_p2wpkh","bc1qY")] },
|
||||||
null, true);
|
null, true);
|
||||||
|
|
||||||
|
// ── Más de dos salidas: solo el caso inequívoco ─────────────────────────
|
||||||
|
// Antes la detección se plantaba en seco con más de 2 salidas. Ahora se
|
||||||
|
// pronuncia solo cuando no hay nada que adivinar.
|
||||||
|
console.log("\n=== Cambio con más de dos salidas ===\n");
|
||||||
|
|
||||||
|
T("3 salidas, solo una comparte tipo con las entradas: esa es el cambio",
|
||||||
|
{ vin:[IN(10000000,"v0_p2wpkh","bc1qA")],
|
||||||
|
vout:[OUT(3000000,"v1_p2tr","bc1pX"), OUT(2000000,"p2pkh","1Y"),
|
||||||
|
OUT(4990000,"v0_p2wpkh","bc1qCAMBIO")] },
|
||||||
|
"bc1qCAMBIO");
|
||||||
|
|
||||||
|
T("3 salidas, dos comparten tipo con las entradas: ambiguo, no se afirma",
|
||||||
|
{ vin:[IN(10000000,"v0_p2wpkh","bc1qA")],
|
||||||
|
vout:[OUT(3000000,"v0_p2wpkh","bc1qX"), OUT(2000000,"p2pkh","1Y"),
|
||||||
|
OUT(4990000,"v0_p2wpkh","bc1qZ")] },
|
||||||
|
null);
|
||||||
|
|
||||||
|
T("4 salidas, una vuelve a una dirección de las entradas: señal más fuerte",
|
||||||
|
{ vin:[IN(10000000,"v0_p2wpkh","bc1qA")],
|
||||||
|
vout:[OUT(3000000,"v0_p2wpkh","bc1qX"), OUT(2000000,"v0_p2wpkh","bc1qY"),
|
||||||
|
OUT(1000000,"v0_p2wpkh","bc1qZ"), OUT(3990000,"v0_p2wpkh","bc1qA")] },
|
||||||
|
"bc1qA");
|
||||||
|
|
||||||
|
T("CoinJoin con muchas salidas: la guardia sigue mandando",
|
||||||
|
{ vin:[IN(10000000,"v0_p2wpkh","bc1qA")],
|
||||||
|
vout:[OUT(3000000,"v1_p2tr","bc1pX"), OUT(2000000,"p2pkh","1Y"),
|
||||||
|
OUT(4990000,"v0_p2wpkh","bc1qCAMBIO")] },
|
||||||
|
null, true);
|
||||||
|
|
||||||
// ── Redondez ────────────────────────────────────────────────────────────
|
// ── Redondez ────────────────────────────────────────────────────────────
|
||||||
// La versión anterior era `v%1000000===0 || v%100000===0 || v%10000000===0`,
|
// La versión anterior era `v%1000000===0 || v%100000===0 || v%10000000===0`,
|
||||||
// donde la primera y la tercera condición sobran (todo múltiplo de un millón
|
// donde la primera y la tercera condición sobran (todo múltiplo de un millón
|
||||||
|
|||||||
@@ -160,6 +160,44 @@ ok("se etiqueta como consolidación, con certeza y no como probable",
|
|||||||
ok("no se cobra dos veces: la penalización la lleva input_linkage",
|
ok("no se cobra dos veces: la penalización la lleva input_linkage",
|
||||||
cu?.penalty === 0 && check(consol, "input_linkage")?.pass === false);
|
cu?.penalty === 0 && check(consol, "input_linkage")?.pass === false);
|
||||||
|
|
||||||
|
console.log("\n=== La comisión como huella (check nuevo) ===\n");
|
||||||
|
|
||||||
|
// Comisión absoluta redonda: nadie llega a 10.000 sats clavados con un
|
||||||
|
// estimador, que calcula tarifa por tamaño y devuelve números feos.
|
||||||
|
const feeRedonda = analyzeTx(TX(
|
||||||
|
[IN(5000000, "v1_p2tr", "bc1pA")],
|
||||||
|
[OUT(4990000, "v1_p2tr", "bc1pB")],
|
||||||
|
{ fee: 10000, weight: 600 }
|
||||||
|
));
|
||||||
|
ok("detecta una comisión absoluta redonda",
|
||||||
|
check(feeRedonda, "fee_fingerprint")?.pass === true &&
|
||||||
|
/10000 sats exactos/.test(check(feeRedonda, "fee_fingerprint")?.detail || ""),
|
||||||
|
check(feeRedonda, "fee_fingerprint")?.detail?.slice(0, 90));
|
||||||
|
|
||||||
|
// Tarifa entera: 20,00 sat/vB sale de teclear "20" en la casilla.
|
||||||
|
const feeEntera = analyzeTx(TX(
|
||||||
|
[IN(5000000, "v1_p2tr", "bc1pA")],
|
||||||
|
[OUT(4996000, "v1_p2tr", "bc1pB")],
|
||||||
|
{ fee: 3000, weight: 600 } // vsize 150 -> 20,00 sat/vB
|
||||||
|
));
|
||||||
|
ok("detecta una tarifa prácticamente entera",
|
||||||
|
/prácticamente un número entero|sats exactos/.test(check(feeEntera, "fee_fingerprint")?.detail || ""),
|
||||||
|
check(feeEntera, "fee_fingerprint")?.detail?.slice(0, 90));
|
||||||
|
|
||||||
|
// Un estimador automático deja números feos.
|
||||||
|
const feeEstimada = analyzeTx(TX(
|
||||||
|
[IN(5000000, "v1_p2tr", "bc1pA")],
|
||||||
|
[OUT(4996873, "v1_p2tr", "bc1pB")],
|
||||||
|
{ fee: 3127, weight: 601 }
|
||||||
|
));
|
||||||
|
ok("no ve huella donde hay un número feo de estimador",
|
||||||
|
/sin forma de cifra elegida a mano/.test(check(feeEstimada, "fee_fingerprint")?.detail || ""),
|
||||||
|
check(feeEstimada, "fee_fingerprint")?.detail?.slice(0, 90));
|
||||||
|
|
||||||
|
ok("la huella de comisión no penaliza (misma decisión que con RBF)",
|
||||||
|
check(feeRedonda, "fee_fingerprint")?.penalty === 0 &&
|
||||||
|
check(feeRedonda, "fee_fingerprint")?.informational === true);
|
||||||
|
|
||||||
console.log("\n=== Guardia de CoinJoin ===\n");
|
console.log("\n=== Guardia de CoinJoin ===\n");
|
||||||
|
|
||||||
// Whirlpool: 5 entradas, 5 salidas de la misma denominación.
|
// Whirlpool: 5 entradas, 5 salidas de la misma denominación.
|
||||||
|
|||||||
Reference in New Issue
Block a user