fix: cuadrar el denominador de la nota y ampliar la redondez; dos checks nuevos
El denominador de la nota incluía dos pesos que nunca restan (rbf, peeling) y excluía uno que sí (input_linkage, hasta 30), así que las deducciones podían superarlo en 17 puntos y dar negativo. Corregido en ambas direcciones, con los cortes de banda recalibrados a 79/54 para que ninguna transacción cambie de banda por el arreglo. 'Redondo' equivalía a 'múltiplo de 0,001 BTC' por dos condiciones redundantes, y se le escapaban pagos de 10.000 o 50.000 sats. Ahora se mide por ceros finales y la señal se gradúa. Dos checks nuevos. El de polvo avisaba de una vinculación futura y nunca miraba si estaba ocurriendo delante: ahora la detecta cuando el polvo se gasta junto a monedas normales. Y la consolidación pura (N entradas, 1 salida) no disparaba el check de inputs innecesarios; ahora se nombra, sin cobrarla dos veces porque input_linkage ya la penaliza. tests/test6.js cubre lo nuevo y la coherencia de la nota.
This commit is contained in:
+167
-31
@@ -1515,6 +1515,58 @@
|
||||
// no sostienen. Autocontenida (recalcula sus propias señales desde tx) para
|
||||
// poder llamarse tanto desde analyzeTx como, salto a salto, desde el motor
|
||||
// de rastreo forense, que necesita saber POR CUÁL output concreto seguir.
|
||||
// Cortes de banda de la nota de privacidad.
|
||||
//
|
||||
// Estaban en 75 y 45 cuando el denominador de la nota era 196. Ese
|
||||
// denominador ha cambiado a 233 al corregir los pesos y añadir dust_spent,
|
||||
// así que todas las notas suben ~4 puntos: mantener 75/45 habría relajado
|
||||
// el listón sin que nadie lo decidiera. Estos valores son los que hacen
|
||||
// que una transacción reciba hoy exactamente la misma banda que recibía
|
||||
// antes de tocar nada:
|
||||
// 75 sobre 196 -> 49 puntos de deducción -> 79 sobre 233
|
||||
// 45 sobre 196 -> 108 puntos de deducción -> 54 sobre 233
|
||||
//
|
||||
// AVISO ESTRUCTURAL: normalizar por la suma de todos los pesos tiene un
|
||||
// efecto perverso — cada check nuevo agranda el denominador y hace que
|
||||
// todos los problemas anteriores parezcan menos graves. Por eso hay que
|
||||
// recalibrar estos dos números cada vez que se añade un check, y por eso
|
||||
// conviene decidir algún día si la nota debe seguir normalizándose o
|
||||
// restar puntos directamente sobre 100 (como hace analyzeAddress).
|
||||
const BANDA_ALTA = 79;
|
||||
const BANDA_MEDIA = 54;
|
||||
|
||||
// ¿Es una cifra "redonda"? Devuelve la fuerza de la señal, no un sí/no.
|
||||
//
|
||||
// La versión anterior era `v % 1000000 === 0 || v % 100000 === 0 ||
|
||||
// v % 10000000 === 0`, donde la primera y la tercera condición sobran:
|
||||
// todo lo divisible por un millón lo es por cien mil. La expresión entera
|
||||
// equivalía a "múltiplo de 0,001 BTC", así que pagos tan redondos como
|
||||
// 10.000 o 50.000 sats —de lo más común con las comisiones de hoy— no se
|
||||
// veían. La heurística estaba infrautilizada, no equivocada.
|
||||
//
|
||||
// Ahora se mide por ceros finales y se gradúa: cuanto más redondo, más
|
||||
// improbable que sea un cambio (que es lo que sobra de una resta) y más
|
||||
// probable que sea el pago.
|
||||
//
|
||||
// LÍMITE QUE CONVIENE CONOCER: esto no ve el pago redondo en euros. Quien
|
||||
// paga 50 € manda una cantidad de BTC con todos sus decimales, y para esa
|
||||
// persona la heurística falla en silencio. Es una de las razones por las
|
||||
// que pagar en fiat convertido protege más de lo que parece.
|
||||
function roundness(sats) {
|
||||
if (!Number.isFinite(sats) || sats <= 0) return 0;
|
||||
if (sats % 100000000 === 0) return 4; // BTC enteros
|
||||
if (sats % 10000000 === 0) return 4; // 0,1 BTC
|
||||
if (sats % 1000000 === 0) return 3; // 0,01 BTC
|
||||
if (sats % 100000 === 0) return 3; // 0,001 BTC
|
||||
if (sats % 10000 === 0) return 2; // 0,0001 BTC
|
||||
if (sats % 1000 === 0) return 1; // 0,00001 BTC
|
||||
return 0;
|
||||
}
|
||||
// Umbral para considerar una salida "redonda" a efectos de heurística.
|
||||
// 2 = múltiplo de 10.000 sats. Por debajo (múltiplos de 1.000) la señal es
|
||||
// demasiado común para significar nada.
|
||||
const ROUND_MIN = 2;
|
||||
|
||||
function guessChangeOutput(tx, likelyCJ) {
|
||||
if (tx.vout.length !== 2 || likelyCJ) {
|
||||
return { identifiable: false, index: null, signals: 0, details: [], bonus: 0, certainty: null };
|
||||
@@ -1532,9 +1584,7 @@
|
||||
return (a[i-1].scriptpubkey||"") <= (v.scriptpubkey||"");
|
||||
});
|
||||
const isBip69 = isBip69Inputs && isBip69Outputs;
|
||||
const roundOutputs = tx.vout.filter(v =>
|
||||
v.value % 1000000 === 0 || v.value % 100000 === 0 || v.value % 10000000 === 0
|
||||
);
|
||||
const roundOutputs = tx.vout.filter(v => roundness(v.value) >= ROUND_MIN);
|
||||
const inputAddrs = tx.vin.map(v=>v.prevout?.scriptpubkey_address).filter(Boolean);
|
||||
const inputAddrSet = new Set(inputAddrs);
|
||||
const totalIn = tx.vin.reduce((s,v)=>s+(v.prevout?v.prevout.value:0),0);
|
||||
@@ -1638,14 +1688,28 @@
|
||||
|
||||
function analyzeTx(tx) {
|
||||
const checks = [];
|
||||
// Pesos que SÍ restan puntos. La regla es que todo lo que esté aquí
|
||||
// pueda aparecer en `deductions`, y que todo lo que aparezca en
|
||||
// `deductions` esté aquí. Antes no se cumplía en ninguna dirección:
|
||||
// · rbf (5) y peeling (8) vivían aquí pero son informativos y nunca
|
||||
// restan — trece puntos de denominador imposibles de alcanzar, que
|
||||
// inflaban todas las notas.
|
||||
// · input_linkage restaba hasta 30 desde una variable suelta que no
|
||||
// estaba en este objeto, así que ni siquiera entraba en el
|
||||
// denominador: las deducciones podían superarlo en 17 puntos y dar
|
||||
// una nota negativa, tapada solo por el Math.max(0, …).
|
||||
// Con los dos desajustes corregidos, un peso de 25 sobre un total de
|
||||
// 213 significa lo que parece.
|
||||
const weights = {
|
||||
input_reuse: 25, input_type_mixing: 14, output_type_mismatch: 8,
|
||||
round_numbers: 10, rbf: 5, peeling: 8,
|
||||
unnecessary_input: 12, dust: 8, change_detection: 10,
|
||||
input_reuse: 25, input_linkage: 30, input_type_mixing: 14,
|
||||
output_type_mismatch: 8, round_numbers: 10,
|
||||
unnecessary_input: 12, dust: 8, dust_spent: 20, change_detection: 10,
|
||||
wallet_fingerprint: 6, batch_payment: 45, op_return: 30, legacy_type: 15,
|
||||
};
|
||||
// entity_ofac y rbf no aparecen aquí a propósito: ninguno de los dos
|
||||
// reduce tu privacidad. Ver sus checks para el razonamiento.
|
||||
// Checks informativos: no restan nunca, así que no tienen peso ni entran
|
||||
// en el denominador. entity_ofac y rbf están aquí por decisión razonada
|
||||
// (ninguno reduce tu privacidad); peeling porque una tx suelta 1-in/2-out
|
||||
// no es una cadena de peeling. Ver cada check para el razonamiento.
|
||||
let deductions = 0;
|
||||
|
||||
const totalIn = tx.vin.reduce((s,v)=>s+(v.prevout?v.prevout.value:0),0);
|
||||
@@ -1764,12 +1828,14 @@
|
||||
const nVinculadas = uniqueInputAddrs.size;
|
||||
const hayVinculacion = nVinculadas > 1 && !likelyCJ;
|
||||
// Escalonado: consolidar dos monedas es rutina; juntar decenas es otra
|
||||
// cosa. El peso sube con el número de direcciones que quedan atadas.
|
||||
// cosa. El peso sube con el número de direcciones que quedan atadas,
|
||||
// hasta el máximo declarado en weights.input_linkage — que es también
|
||||
// lo que aporta al denominador de la nota.
|
||||
const pesoVinculacion = !hayVinculacion ? 0
|
||||
: nVinculadas >= 20 ? 30
|
||||
: nVinculadas >= 10 ? 22
|
||||
: nVinculadas >= 5 ? 15
|
||||
: 8;
|
||||
: nVinculadas >= 20 ? weights.input_linkage
|
||||
: nVinculadas >= 10 ? Math.round(weights.input_linkage * 0.73)
|
||||
: nVinculadas >= 5 ? Math.round(weights.input_linkage * 0.5)
|
||||
: Math.round(weights.input_linkage * 0.27);
|
||||
checks.push({
|
||||
id:"input_linkage", label:"Direcciones que quedan vinculadas entre sí", certainty:"CERTEZA",
|
||||
pass: !hayVinculacion,
|
||||
@@ -1840,20 +1906,24 @@
|
||||
// Sin penalización por no usar CoinJoin — es una técnica avanzada, no un error
|
||||
|
||||
// ── 5. Output de valor redondo ────────────────────────────────────
|
||||
const roundOutputs = tx.vout.filter(v =>
|
||||
v.value % 1000000 === 0 || v.value % 100000 === 0 || v.value % 10000000 === 0
|
||||
);
|
||||
// La fuerza de la señal escala con lo redonda que sea la cifra: pagar
|
||||
// 0,5 BTC clavados delata mucho más que pagar 0,0123 BTC. El peso va
|
||||
// del 60% al 100% del máximo según el nivel devuelto por roundness().
|
||||
const roundOutputs = tx.vout.filter(v => roundness(v.value) >= ROUND_MIN);
|
||||
const hasRound = roundOutputs.length > 0 && tx.vout.length === 2 && !likelyCJ;
|
||||
const roundLevel = hasRound ? Math.max(...roundOutputs.map(v => roundness(v.value))) : 0;
|
||||
const roundNames = { 2: "múltiplo de 0,0001 BTC", 3: "múltiplo de 0,001 BTC", 4: "múltiplo de 0,1 BTC" };
|
||||
const pesoRedondo = Math.round(weights.round_numbers * (roundLevel >= 4 ? 1 : roundLevel === 3 ? 0.8 : 0.6));
|
||||
checks.push({
|
||||
id:"round_numbers", label:"Output de valor redondo", certainty:"PROBABLE", pass:!hasRound,
|
||||
actionability: "evitable",
|
||||
detail: hasRound
|
||||
? `Hecho (certeza): uno de los dos outputs vale exactamente ${(roundOutputs[0].value/1e8).toFixed(6)} BTC, una cifra redonda. Interpretación (probable): las personas pagan cantidades redondas y el cambio sale de la resta, así que ese suele ser el pago y el otro el cambio. No siempre: una consolidación o un pago calculado al céntimo rompen la regla. Consecuencia: si acierta, queda señalada la dirección de cambio del emisor — y con ella, por dónde seguir sus gastos futuros.`
|
||||
? `Hecho (certeza): uno de los dos outputs vale exactamente ${(roundOutputs[0].value/1e8).toFixed(8).replace(/0+$/,"0")} BTC — ${roundNames[roundLevel] || "una cifra redonda"}. Interpretación (probable): las personas pagan cantidades redondas y el cambio sale de la resta, así que ese suele ser el pago y el otro el cambio. No siempre: una consolidación o un pago calculado al céntimo rompen la regla. Consecuencia: si acierta, queda señalada la dirección de cambio del emisor — y con ella, por dónde seguir sus gastos futuros.`
|
||||
: "No se detectan outputs de valor redondo en una estructura de 2 outputs.",
|
||||
didactic: "Las personas pagan cantidades redondas (0,001, 0,01, 0,1 BTC) y el cambio es lo que sobra, con todos sus decimales. De ahí la regla: en una transacción de dos salidas, la redonda suele ser el pago y la irregular el cambio. Suele, no siempre — hay consolidaciones y pagos calculados al céntimo que la desmienten. Cuando acierta, identifica la dirección de cambio del emisor, que es la puerta para seguir sus gastos posteriores.",
|
||||
penalty: weights.round_numbers,
|
||||
didactic: "Las personas pagan cantidades redondas (0,001, 0,01, 0,1 BTC) y el cambio es lo que sobra, con todos sus decimales. De ahí la regla: en una transacción de dos salidas, la redonda suele ser el pago y la irregular el cambio. Cuanto más redonda la cifra, más fuerte la señal. Suele, no siempre — hay consolidaciones y pagos calculados al céntimo que la desmienten. Y hay un punto ciego que conviene conocer: si pagas una cantidad redonda en euros o dólares, en BTC sale un número con todos sus decimales y esta heurística no ve nada.",
|
||||
penalty: pesoRedondo,
|
||||
});
|
||||
if (hasRound) deductions += weights.round_numbers;
|
||||
if (hasRound) deductions += pesoRedondo;
|
||||
|
||||
// ── 6. RBF ────────────────────────────────────────────────────────
|
||||
const rbfEnabled = tx.vin.some(v=>typeof v.sequence==="number" && v.sequence < 0xfffffffe);
|
||||
@@ -1893,19 +1963,37 @@
|
||||
unnecessaryDetail = `Uno de los ${tx.vin.length} inputs habría sido suficiente para cubrir el pago. Los inputs adicionales revelan consolidación de UTXOs y ligan esos fondos al mismo propietario.`;
|
||||
}
|
||||
}
|
||||
// Consolidación pura: N entradas y UNA sola salida, sin cambio. Es el
|
||||
// acto que más privacidad destruye de golpe y hasta ahora pasaba este
|
||||
// check sin despeinarse: la condición de arriba exige que una entrada
|
||||
// cubra la mayor salida `y sobre` (val < totalIn * 0.99), y en una
|
||||
// consolidación la salida vale casi lo mismo que la suma de entradas,
|
||||
// así que ninguna entrada la cubre sola. Comprobado con 20 entradas a
|
||||
// 1 salida: no se disparaba.
|
||||
//
|
||||
// No suma penalización propia a propósito. La vinculación que provoca
|
||||
// ya la cobra input_linkage, y cobrarla dos veces sería repetir el error
|
||||
// que este mismo repaso corrigió en la detección de cambio. Lo que
|
||||
// faltaba no era castigo, era nombre.
|
||||
const esConsolidacion = !likelyCJ && tx.vin.length >= 3 && tx.vout.length === 1;
|
||||
checks.push({
|
||||
id:"unnecessary_input", label:"Inputs innecesarios (consolidación revelada)", certainty:"PROBABLE",
|
||||
pass: likelyCJ ? true : !hasUnnecessaryInput,
|
||||
id:"unnecessary_input",
|
||||
label: esConsolidacion ? "Consolidación de UTXOs" : "Inputs innecesarios (consolidación revelada)",
|
||||
certainty: esConsolidacion ? "CERTEZA" : "PROBABLE",
|
||||
pass: likelyCJ ? true : !(hasUnnecessaryInput || esConsolidacion),
|
||||
informational: likelyCJ,
|
||||
actionability: likelyCJ ? null : "evitable",
|
||||
detail: likelyCJ
|
||||
? `En un CoinJoin todos los inputs son necesarios por diseño del protocolo — cada participante aporta los suyos. Esta heurística no aplica.`
|
||||
: (hasUnnecessaryInput ? unnecessaryDetail : "No se detectan inputs claramente innecesarios para cubrir el pago."),
|
||||
: esConsolidacion
|
||||
? `Hecho (certeza): ${tx.vin.length} entradas y una sola salida, sin cambio. Esto no es un pago: es una consolidación, juntar monedas sueltas en una. Interpretación (certeza para un observador): las ${tx.vin.length} entradas son del mismo dueño, y aquí no cabe la excepción del CoinJoin porque no hay nada que mezclar. Consecuencia: es el gesto que más historial ata de una sola vez, y suele hacerse cuando las comisiones están baratas — justo cuando menos se piensa en lo que cuesta en privacidad. La penalización la aplica el check de vinculación, que mide cuántas direcciones quedan atadas.`
|
||||
: (hasUnnecessaryInput ? unnecessaryDetail : "No se detectan inputs claramente innecesarios para cubrir el pago."),
|
||||
didactic: "Si una transacción aporta más entradas de las que hacían falta para cubrir el pago, lo habitual es que el emisor estuviera consolidando monedas sueltas. Gastarlas juntas es lo que las delata: cualquiera que mire asume que son del mismo dueño. Esa suposición —la heurística CIOH— es la más usada del análisis de cadena y acierta casi siempre, pero no es una ley: un CoinJoin o un pago colaborativo la desmienten por diseño. Por eso aquí se marca como probable y no como certeza.",
|
||||
penalty: likelyCJ ? 0 : weights.unnecessary_input,
|
||||
penalty: (likelyCJ || esConsolidacion) ? 0 : weights.unnecessary_input,
|
||||
});
|
||||
// En CoinJoin múltiples inputs son la estructura del protocolo — no penaliza.
|
||||
if (hasUnnecessaryInput && !likelyCJ) deductions += weights.unnecessary_input;
|
||||
// En una consolidación tampoco resta aquí: ya lo cobra input_linkage.
|
||||
if (hasUnnecessaryInput && !likelyCJ && !esConsolidacion) deductions += weights.unnecessary_input;
|
||||
|
||||
// ── 8. Patrón de pago simple (posible eslabón de peeling chain) ────
|
||||
// OJO: una sola tx 1-in/2-out NO es una peeling chain — es el pago más
|
||||
@@ -1975,6 +2063,50 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── 9-bis. El dusting materializado ───────────────────────────────
|
||||
// El check anterior avisa de que una salida diminuta "quedará vinculada
|
||||
// con las demás SI se gasta junto a ellas". Esa frase describía un
|
||||
// futuro hipotético y la app nunca miraba si ese futuro estaba
|
||||
// ocurriendo delante de ella — pese a tener el dato en la mano.
|
||||
//
|
||||
// Aquí se mira. Si entre las ENTRADAS de esta transacción hay una moneda
|
||||
// de tamaño polvo gastada junto a otras mayores, el ataque ya no es una
|
||||
// hipótesis: se acaba de consumar, y se puede decir con CERTEZA.
|
||||
//
|
||||
// Guardias: no aplica en CoinJoin (las entradas son de gente distinta) y
|
||||
// exige al menos otra entrada que no sea polvo — gastar solo polvo, sin
|
||||
// nada más, no revela ninguna vinculación nueva.
|
||||
const entradasPolvo = tx.vin.filter(v => {
|
||||
const val = v.prevout?.value;
|
||||
if (typeof val !== "number") return false;
|
||||
return val < Math.max(DUSTING_CEIL, dustThreshold(v.prevout?.scriptpubkey_type));
|
||||
});
|
||||
const entradasNormales = tx.vin.length - entradasPolvo.length;
|
||||
const polvoGastadoJunto = !likelyCJ && entradasPolvo.length > 0 && entradasNormales > 0;
|
||||
if (polvoGastadoJunto) {
|
||||
const dirsPolvo = [...new Set(entradasPolvo.map(v=>v.prevout?.scriptpubkey_address).filter(Boolean))];
|
||||
checks.push({
|
||||
id:"dust_spent", label:"Se gasta polvo junto a otras monedas", certainty:"CERTEZA", pass:false,
|
||||
actionability: "evitable",
|
||||
detail: `Hecho (certeza): entre las entradas hay ${entradasPolvo.length} moneda(s) de tamaño polvo (${entradasPolvo.map(v=>v.prevout.value+" sat").join(", ")}) gastada(s) junto a otras ${entradasNormales} de importe normal. Interpretación (certeza para un observador): quien envió ese polvo estaba esperando exactamente esto. Al firmar todas las entradas juntas se demuestra que la dirección que recibió el polvo y las demás son del mismo dueño. Consecuencia: si el remitente del polvo era un analista, acaba de confirmar la vinculación que buscaba — y a diferencia del aviso preventivo, esto ya no se puede deshacer.`,
|
||||
didactic: "Un ataque de dusting no hace daño al llegar: hace daño cuando lo gastas. El atacante manda una cantidad ínfima a tu dirección y espera. Si algún día esa moneda entra en una transacción junto a otras tuyas, la heurística CIOH hace el resto y tus direcciones quedan atadas en público. Por eso el consejo ante el polvo recibido es no tocarlo y congelarlo en el monedero si tu wallet lo permite. Aquí ya no es un consejo preventivo: la transacción que estás mirando lo hizo.",
|
||||
penalty: weights.dust_spent,
|
||||
});
|
||||
deductions += weights.dust_spent;
|
||||
} else {
|
||||
checks.push({
|
||||
id:"dust_spent", label:"Se gasta polvo junto a otras monedas", certainty:"CERTEZA", pass:true,
|
||||
actionability: "evitable",
|
||||
detail: likelyCJ
|
||||
? "En un CoinJoin las entradas pertenecen a participantes distintos, así que gastar monedas pequeñas junto a otras no vincula nada. La heurística no aplica."
|
||||
: entradasPolvo.length > 0
|
||||
? "Hay entradas de tamaño polvo, pero no se mezclan con monedas de importe normal, así que no se revela ninguna vinculación nueva."
|
||||
: "Ninguna de las entradas es de tamaño polvo.",
|
||||
didactic: "Un ataque de dusting no hace daño al llegar: hace daño cuando lo gastas. Mientras el polvo siga quieto en tu monedero, la vinculación que busca el atacante no se ha producido.",
|
||||
penalty: weights.dust_spent, informational: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 10. Change detection — señales combinadas (ver guessChangeOutput) ──
|
||||
const changeGuess = guessChangeOutput(tx, likelyCJ);
|
||||
const changeSignals = changeGuess.signals;
|
||||
@@ -2236,13 +2368,13 @@
|
||||
const maxPossible = Object.values(weights).reduce((a,b)=>a+b,0);
|
||||
const rawScore = Math.max(0, Math.min(100, Math.round(100 - (deductions / maxPossible) * 100)));
|
||||
// OP_RETURN es incompatible con privacidad aceptable — fuerza banda BAJA como máximo.
|
||||
const score = hasOpReturn ? Math.min(rawScore, 44) : rawScore;
|
||||
const score = hasOpReturn ? Math.min(rawScore, BANDA_MEDIA - 1) : rawScore;
|
||||
|
||||
return {
|
||||
score, checks,
|
||||
band: score>=75?"ALTA":score>=45?"MEDIA":"BAJA",
|
||||
summary: score>=75?"Privacidad aceptable":score>=45?"Privacidad mejorable":"Privacidad baja",
|
||||
summaryColor: score>=75?C.green:score>=45?C.amber:C.red,
|
||||
band: score>=BANDA_ALTA?"ALTA":score>=BANDA_MEDIA?"MEDIA":"BAJA",
|
||||
summary: score>=BANDA_ALTA?"Privacidad aceptable":score>=BANDA_MEDIA?"Privacidad mejorable":"Privacidad baja",
|
||||
summaryColor: score>=BANDA_ALTA?C.green:score>=BANDA_MEDIA?C.amber:C.red,
|
||||
wallets: detectedWallets,
|
||||
};
|
||||
}
|
||||
@@ -2274,6 +2406,10 @@
|
||||
const funded = addr.chain_stats ? addr.chain_stats.funded_txo_sum : 0;
|
||||
checks.push({ id:"volume", label:"Volumen de actividad", certainty:"CERTEZA", pass:true, detail:`Total recibido: ${(funded/1e8).toFixed(4)} BTC en ${txCount} transacciones.`, penalty:0, informational:true });
|
||||
score = Math.max(0, Math.min(100, score));
|
||||
// Cortes propios, a propósito distintos de BANDA_ALTA/BANDA_MEDIA. Esta
|
||||
// nota se calcula restando puntos directamente sobre 100, sin el
|
||||
// denominador normalizado del analizador de transacciones, así que 75/45
|
||||
// aquí ya están en la escala correcta. No unificar sin recalibrar.
|
||||
return { score, checks, band: score>=75?"ALTA":score>=45?"MEDIA":"BAJA", summary: score >= 75 ? "Privacidad aceptable" : score >= 45 ? "Privacidad mejorable" : "Privacidad baja", summaryColor: score >= 75 ? C.green : score >= 45 ? C.amber : C.red, wallets: [] };
|
||||
}
|
||||
|
||||
@@ -2706,8 +2842,8 @@
|
||||
{analysis.unused
|
||||
? "Esta dirección no se ha usado todavía. No tiene historial on-chain que analizar."
|
||||
: tx
|
||||
? (score>=75?"La transacción presenta buenas propiedades de privacidad.":score>=45?"La transacción es funcional, pero tiene características que reducen su privacidad.":"Esta transacción tiene características que facilitan su rastreo.")
|
||||
: (score>=75?"La dirección presenta buenas propiedades de privacidad.":score>=45?"La dirección es funcional, pero tiene características que reducen su privacidad.":"Esta dirección tiene características que facilitan su rastreo.")}
|
||||
? (analysis.band==="ALTA"?"La transacción presenta buenas propiedades de privacidad.":analysis.band==="MEDIA"?"La transacción es funcional, pero tiene características que reducen su privacidad.":"Esta transacción tiene características que facilitan su rastreo.")
|
||||
: (analysis.band==="ALTA"?"La dirección presenta buenas propiedades de privacidad.":analysis.band==="MEDIA"?"La dirección es funcional, pero tiene características que reducen su privacidad.":"Esta dirección tiene características que facilitan su rastreo.")}
|
||||
</div>
|
||||
{wallets&&wallets.length>0&&(
|
||||
<div style={{marginTop:6,fontSize:"0.62rem",color:C.t2,fontFamily:"monospace"}}>
|
||||
|
||||
Reference in New Issue
Block a user