mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 02:11:49 +01:00
Add Goggles filters tags to the transaction page
This commit is contained in:
parent
abbc8a134b
commit
00dcff50ee
@ -91,6 +91,7 @@
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-container *ngTemplateOutlet="goggles"></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -168,6 +169,7 @@
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-container *ngTemplateOutlet="goggles"></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -563,3 +565,17 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #goggles>
|
||||
<tr *ngIf="((auditStatus && auditStatus.accelerated) || accelerationInfo || (tx && tx.acceleration)) || filters.length">
|
||||
<td class="td-width">
|
||||
<span class="goggles-icon"><app-svg-images name="goggles" width="100%" height="100%"></app-svg-images></span>
|
||||
</td>
|
||||
<td class="wrap-cell">
|
||||
<span *ngIf="((auditStatus && auditStatus.accelerated) || accelerationInfo || (tx && tx.acceleration))" class="badge badge-accelerated mr-1" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||
<ng-container *ngFor="let filter of filters;">
|
||||
<span class="badge badge-primary filter-tag mr-1">{{ filter.label }}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
@ -310,3 +310,8 @@
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.goggles-icon {
|
||||
display: block;
|
||||
width: 2.2em;
|
||||
}
|
@ -21,6 +21,8 @@ import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { getTransactionFlags } from '../../shared/transaction.utils';
|
||||
import { Filter, toFilters, TransactionFlags } from '../../shared/filters.utils';
|
||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration } from '../../interfaces/node-api.interface';
|
||||
import { LiquidUnblinding } from './liquid-ublinding';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
@ -89,6 +91,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
adjustedVsize: number | null;
|
||||
pool: Pool | null;
|
||||
auditStatus: AuditStatus | null;
|
||||
filters: Filter[] = [];
|
||||
showCpfpDetails = false;
|
||||
fetchCpfp$ = new Subject<string>();
|
||||
fetchRbfHistory$ = new Subject<string>();
|
||||
@ -677,6 +680,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
|
||||
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
|
||||
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
|
||||
this.tx.flags = getTransactionFlags(this.tx);
|
||||
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
|
||||
} else {
|
||||
this.segwitEnabled = false;
|
||||
this.taprootEnabled = false;
|
||||
@ -723,6 +728,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.hasEffectiveFeeRate = false;
|
||||
this.rbfInfo = null;
|
||||
this.rbfReplaces = [];
|
||||
this.filters = [];
|
||||
this.showCpfpDetails = false;
|
||||
this.accelerationInfo = null;
|
||||
this.txInBlockIndex = null;
|
||||
|
@ -27,6 +27,7 @@ export interface Transaction {
|
||||
_channels?: TransactionChannels;
|
||||
price?: Price;
|
||||
sigops?: number;
|
||||
flags?: bigint;
|
||||
}
|
||||
|
||||
export interface TransactionChannels {
|
||||
|
@ -6,6 +6,7 @@ export interface Filter {
|
||||
group?: string,
|
||||
important?: boolean,
|
||||
tooltip?: boolean,
|
||||
txPage?: boolean,
|
||||
}
|
||||
|
||||
export type FilterMode = 'and' | 'or';
|
||||
@ -74,40 +75,40 @@ export function toFilters(flags: bigint): Filter[] {
|
||||
|
||||
export const TransactionFilters: { [key: string]: Filter } = {
|
||||
/* features */
|
||||
rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true, tooltip: true, },
|
||||
no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf', important: true, tooltip: true, },
|
||||
v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version', tooltip: true, },
|
||||
v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version', tooltip: true, },
|
||||
v3: { key: 'v3', label: 'Version 3', flag: TransactionFlags.v3, toggle: 'version', tooltip: true, },
|
||||
nonstandard: { key: 'nonstandard', label: 'Non-Standard', flag: TransactionFlags.nonstandard, important: true, tooltip: true, },
|
||||
rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true, tooltip: true, txPage: false, },
|
||||
no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf', important: true, tooltip: true, txPage: false, },
|
||||
v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version', tooltip: true, txPage: false, },
|
||||
v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version', tooltip: true, txPage: false, },
|
||||
v3: { key: 'v3', label: 'Version 3', flag: TransactionFlags.v3, toggle: 'version', tooltip: true, txPage: false, },
|
||||
nonstandard: { key: 'nonstandard', label: 'Non-Standard', flag: TransactionFlags.nonstandard, important: true, tooltip: true, txPage: true, },
|
||||
/* address types */
|
||||
p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true, tooltip: true, },
|
||||
p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true, tooltip: true, },
|
||||
p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true, tooltip: true, txPage: true, },
|
||||
p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true, tooltip: true, txPage: true, },
|
||||
p2pkh: { key: 'p2pkh', label: 'P2PKH', flag: TransactionFlags.p2pkh, important: true, tooltip: false, },
|
||||
p2sh: { key: 'p2sh', label: 'P2SH', flag: TransactionFlags.p2sh, important: true, tooltip: false, },
|
||||
p2wpkh: { key: 'p2wpkh', label: 'P2WPKH', flag: TransactionFlags.p2wpkh, important: true, tooltip: false, },
|
||||
p2wsh: { key: 'p2wsh', label: 'P2WSH', flag: TransactionFlags.p2wsh, important: true, tooltip: false, },
|
||||
p2tr: { key: 'p2tr', label: 'Taproot', flag: TransactionFlags.p2tr, important: true, tooltip: false, },
|
||||
/* behavior */
|
||||
cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent, important: true, tooltip: true, },
|
||||
cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child, important: true, tooltip: true, },
|
||||
replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true, tooltip: true, },
|
||||
cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent, important: true, tooltip: true, txPage: false, },
|
||||
cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child, important: true, tooltip: true, txPage: false, },
|
||||
replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true, tooltip: true, txPage: false, },
|
||||
acceleration: window?.['__env']?.ACCELERATOR ? { key: 'acceleration', label: 'Accelerated', flag: TransactionFlags.acceleration, important: false } : undefined,
|
||||
/* data */
|
||||
op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true, tooltip: true, },
|
||||
fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey, tooltip: true, },
|
||||
inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription, important: true, tooltip: true, },
|
||||
fake_scripthash: { key: 'fake_scripthash', label: 'Fake scripthash', flag: TransactionFlags.fake_scripthash, tooltip: true,},
|
||||
op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true, tooltip: true, txPage: true, },
|
||||
fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey, tooltip: true, txPage: true, },
|
||||
inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription, important: true, tooltip: true, txPage: true, },
|
||||
fake_scripthash: { key: 'fake_scripthash', label: 'Fake scripthash', flag: TransactionFlags.fake_scripthash, tooltip: true, txPage: true,},
|
||||
/* heuristics */
|
||||
coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true, tooltip: true, },
|
||||
consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation, tooltip: true, },
|
||||
batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout, tooltip: true, },
|
||||
coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true, tooltip: true, txPage: true, },
|
||||
consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation, tooltip: true, txPage: true, },
|
||||
batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout, tooltip: true, txPage: true, },
|
||||
/* sighash */
|
||||
sighash_all: { key: 'sighash_all', label: 'sighash_all', flag: TransactionFlags.sighash_all },
|
||||
sighash_none: { key: 'sighash_none', label: 'sighash_none', flag: TransactionFlags.sighash_none, tooltip: true, },
|
||||
sighash_single: { key: 'sighash_single', label: 'sighash_single', flag: TransactionFlags.sighash_single, tooltip: true, },
|
||||
sighash_none: { key: 'sighash_none', label: 'sighash_none', flag: TransactionFlags.sighash_none, tooltip: true },
|
||||
sighash_single: { key: 'sighash_single', label: 'sighash_single', flag: TransactionFlags.sighash_single, tooltip: true },
|
||||
sighash_default: { key: 'sighash_default', label: 'sighash_default', flag: TransactionFlags.sighash_default },
|
||||
sighash_acp: { key: 'sighash_acp', label: 'sighash_anyonecanpay', flag: TransactionFlags.sighash_acp, tooltip: true, },
|
||||
sighash_acp: { key: 'sighash_acp', label: 'sighash_anyonecanpay', flag: TransactionFlags.sighash_acp, tooltip: true },
|
||||
};
|
||||
|
||||
export const FilterGroups: { label: string, filters: Filter[]}[] = [
|
||||
|
281
frontend/src/app/shared/script.utils.ts
Normal file
281
frontend/src/app/shared/script.utils.ts
Normal file
@ -0,0 +1,281 @@
|
||||
const opcodes = {
|
||||
OP_FALSE: 0,
|
||||
OP_0: 0,
|
||||
OP_PUSHDATA1: 76,
|
||||
OP_PUSHDATA2: 77,
|
||||
OP_PUSHDATA4: 78,
|
||||
OP_1NEGATE: 79,
|
||||
OP_PUSHNUM_NEG1: 79,
|
||||
OP_RESERVED: 80,
|
||||
OP_TRUE: 81,
|
||||
OP_1: 81,
|
||||
OP_2: 82,
|
||||
OP_3: 83,
|
||||
OP_4: 84,
|
||||
OP_5: 85,
|
||||
OP_6: 86,
|
||||
OP_7: 87,
|
||||
OP_8: 88,
|
||||
OP_9: 89,
|
||||
OP_10: 90,
|
||||
OP_11: 91,
|
||||
OP_12: 92,
|
||||
OP_13: 93,
|
||||
OP_14: 94,
|
||||
OP_15: 95,
|
||||
OP_16: 96,
|
||||
OP_PUSHNUM_1: 81,
|
||||
OP_PUSHNUM_2: 82,
|
||||
OP_PUSHNUM_3: 83,
|
||||
OP_PUSHNUM_4: 84,
|
||||
OP_PUSHNUM_5: 85,
|
||||
OP_PUSHNUM_6: 86,
|
||||
OP_PUSHNUM_7: 87,
|
||||
OP_PUSHNUM_8: 88,
|
||||
OP_PUSHNUM_9: 89,
|
||||
OP_PUSHNUM_10: 90,
|
||||
OP_PUSHNUM_11: 91,
|
||||
OP_PUSHNUM_12: 92,
|
||||
OP_PUSHNUM_13: 93,
|
||||
OP_PUSHNUM_14: 94,
|
||||
OP_PUSHNUM_15: 95,
|
||||
OP_PUSHNUM_16: 96,
|
||||
OP_NOP: 97,
|
||||
OP_VER: 98,
|
||||
OP_IF: 99,
|
||||
OP_NOTIF: 100,
|
||||
OP_VERIF: 101,
|
||||
OP_VERNOTIF: 102,
|
||||
OP_ELSE: 103,
|
||||
OP_ENDIF: 104,
|
||||
OP_VERIFY: 105,
|
||||
OP_RETURN: 106,
|
||||
OP_TOALTSTACK: 107,
|
||||
OP_FROMALTSTACK: 108,
|
||||
OP_2DROP: 109,
|
||||
OP_2DUP: 110,
|
||||
OP_3DUP: 111,
|
||||
OP_2OVER: 112,
|
||||
OP_2ROT: 113,
|
||||
OP_2SWAP: 114,
|
||||
OP_IFDUP: 115,
|
||||
OP_DEPTH: 116,
|
||||
OP_DROP: 117,
|
||||
OP_DUP: 118,
|
||||
OP_NIP: 119,
|
||||
OP_OVER: 120,
|
||||
OP_PICK: 121,
|
||||
OP_ROLL: 122,
|
||||
OP_ROT: 123,
|
||||
OP_SWAP: 124,
|
||||
OP_TUCK: 125,
|
||||
OP_CAT: 126,
|
||||
OP_SUBSTR: 127,
|
||||
OP_LEFT: 128,
|
||||
OP_RIGHT: 129,
|
||||
OP_SIZE: 130,
|
||||
OP_INVERT: 131,
|
||||
OP_AND: 132,
|
||||
OP_OR: 133,
|
||||
OP_XOR: 134,
|
||||
OP_EQUAL: 135,
|
||||
OP_EQUALVERIFY: 136,
|
||||
OP_RESERVED1: 137,
|
||||
OP_RESERVED2: 138,
|
||||
OP_1ADD: 139,
|
||||
OP_1SUB: 140,
|
||||
OP_2MUL: 141,
|
||||
OP_2DIV: 142,
|
||||
OP_NEGATE: 143,
|
||||
OP_ABS: 144,
|
||||
OP_NOT: 145,
|
||||
OP_0NOTEQUAL: 146,
|
||||
OP_ADD: 147,
|
||||
OP_SUB: 148,
|
||||
OP_MUL: 149,
|
||||
OP_DIV: 150,
|
||||
OP_MOD: 151,
|
||||
OP_LSHIFT: 152,
|
||||
OP_RSHIFT: 153,
|
||||
OP_BOOLAND: 154,
|
||||
OP_BOOLOR: 155,
|
||||
OP_NUMEQUAL: 156,
|
||||
OP_NUMEQUALVERIFY: 157,
|
||||
OP_NUMNOTEQUAL: 158,
|
||||
OP_LESSTHAN: 159,
|
||||
OP_GREATERTHAN: 160,
|
||||
OP_LESSTHANOREQUAL: 161,
|
||||
OP_GREATERTHANOREQUAL: 162,
|
||||
OP_MIN: 163,
|
||||
OP_MAX: 164,
|
||||
OP_WITHIN: 165,
|
||||
OP_RIPEMD160: 166,
|
||||
OP_SHA1: 167,
|
||||
OP_SHA256: 168,
|
||||
OP_HASH160: 169,
|
||||
OP_HASH256: 170,
|
||||
OP_CODESEPARATOR: 171,
|
||||
OP_CHECKSIG: 172,
|
||||
OP_CHECKSIGVERIFY: 173,
|
||||
OP_CHECKMULTISIG: 174,
|
||||
OP_CHECKMULTISIGVERIFY: 175,
|
||||
OP_NOP1: 176,
|
||||
OP_NOP2: 177,
|
||||
OP_CHECKLOCKTIMEVERIFY: 177,
|
||||
OP_CLTV: 177,
|
||||
OP_NOP3: 178,
|
||||
OP_CHECKSEQUENCEVERIFY: 178,
|
||||
OP_CSV: 178,
|
||||
OP_NOP4: 179,
|
||||
OP_NOP5: 180,
|
||||
OP_NOP6: 181,
|
||||
OP_NOP7: 182,
|
||||
OP_NOP8: 183,
|
||||
OP_NOP9: 184,
|
||||
OP_NOP10: 185,
|
||||
OP_CHECKSIGADD: 186,
|
||||
OP_PUBKEYHASH: 253,
|
||||
OP_PUBKEY: 254,
|
||||
OP_INVALIDOPCODE: 255,
|
||||
};
|
||||
// add unused opcodes
|
||||
for (let i = 187; i <= 255; i++) {
|
||||
opcodes[`OP_RETURN_${i}`] = i;
|
||||
}
|
||||
|
||||
export { opcodes };
|
||||
|
||||
/** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */
|
||||
export function parseMultisigScript(script: string): void | { m: number, n: number } {
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
const ops = script.split(' ');
|
||||
if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') {
|
||||
return;
|
||||
}
|
||||
const opN = ops.pop();
|
||||
if (!opN) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||
if (ops.length < n * 2 + 1) {
|
||||
return;
|
||||
}
|
||||
// pop n public keys
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop() || '')) {
|
||||
return;
|
||||
}
|
||||
if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop() || '')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const opM = ops.pop();
|
||||
if (!opM) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
|
||||
if (ops.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { m, n };
|
||||
}
|
||||
|
||||
export function getVarIntLength(n: number): number {
|
||||
if (n < 0xfd) {
|
||||
return 1;
|
||||
} else if (n <= 0xffff) {
|
||||
return 3;
|
||||
} else if (n <= 0xffffffff) {
|
||||
return 5;
|
||||
} else {
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
|
||||
function powMod(x: bigint, power: number, modulo: bigint): bigint {
|
||||
for (let i = 0; i < power; i++) {
|
||||
x = (x * x) % modulo;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
function sqrtMod(x: bigint, P: bigint): bigint {
|
||||
const b2 = (x * x * x) % P;
|
||||
const b3 = (b2 * b2 * x) % P;
|
||||
const b6 = (powMod(b3, 3, P) * b3) % P;
|
||||
const b9 = (powMod(b6, 3, P) * b3) % P;
|
||||
const b11 = (powMod(b9, 2, P) * b2) % P;
|
||||
const b22 = (powMod(b11, 11, P) * b11) % P;
|
||||
const b44 = (powMod(b22, 22, P) * b22) % P;
|
||||
const b88 = (powMod(b44, 44, P) * b44) % P;
|
||||
const b176 = (powMod(b88, 88, P) * b88) % P;
|
||||
const b220 = (powMod(b176, 44, P) * b44) % P;
|
||||
const b223 = (powMod(b220, 3, P) * b3) % P;
|
||||
const t1 = (powMod(b223, 23, P) * b22) % P;
|
||||
const t2 = (powMod(t1, 6, P) * b2) % P;
|
||||
const root = powMod(t2, 2, P);
|
||||
return root;
|
||||
}
|
||||
|
||||
const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F`);
|
||||
|
||||
/**
|
||||
* This function tells whether the point given is a DER encoded point on the ECDSA curve.
|
||||
* @param {string} pointHex The point as a hex string (*must not* include a '0x' prefix)
|
||||
* @returns {boolean} true if the point is on the SECP256K1 curve
|
||||
*/
|
||||
export function isPoint(pointHex: string): boolean {
|
||||
if (!pointHex?.length) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!(
|
||||
// is uncompressed
|
||||
(
|
||||
(pointHex.length === 130 && pointHex.startsWith('04')) ||
|
||||
// OR is compressed
|
||||
(pointHex.length === 66 &&
|
||||
(pointHex.startsWith('02') || pointHex.startsWith('03')))
|
||||
)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function modified slightly from noble-curves
|
||||
|
||||
|
||||
// Now we know that pointHex is a 33 or 65 byte hex string.
|
||||
const isCompressed = pointHex.length === 66;
|
||||
|
||||
const x = BigInt(`0x${pointHex.slice(2, 66)}`);
|
||||
if (x >= curveP) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isCompressed) {
|
||||
const y = BigInt(`0x${pointHex.slice(66, 130)}`);
|
||||
if (y >= curveP) {
|
||||
return false;
|
||||
}
|
||||
// Just check y^2 = x^3 + 7 (secp256k1 curve)
|
||||
return (y * y) % curveP === (x * x * x + 7n) % curveP;
|
||||
} else {
|
||||
// Get unaltered y^2 (no mod p)
|
||||
const ySquared = (x * x * x + 7n) % curveP;
|
||||
// Try to sqrt it, it will round down if not perfect root
|
||||
const y = sqrtMod(ySquared, curveP);
|
||||
// If we square and it's equal, then it was a perfect root and valid point.
|
||||
return (y * y) % curveP === ySquared;
|
||||
}
|
||||
}
|
418
frontend/src/app/shared/transaction.utils.ts
Normal file
418
frontend/src/app/shared/transaction.utils.ts
Normal file
@ -0,0 +1,418 @@
|
||||
import { TransactionFlags } from './filters.utils';
|
||||
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { CpfpInfo, RbfInfo } from '../interfaces/node-api.interface';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
const MIN_STANDARD_TX_NONWITNESS_SIZE = 65;
|
||||
const MAX_P2SH_SIGOPS = 15;
|
||||
const MAX_STANDARD_P2WSH_STACK_ITEMS = 100;
|
||||
const MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80;
|
||||
const MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80;
|
||||
const MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600;
|
||||
const MAX_STANDARD_SCRIPTSIG_SIZE = 1650;
|
||||
const DUST_RELAY_TX_FEE = 3;
|
||||
const MAX_OP_RETURN_RELAY = 83;
|
||||
const DEFAULT_PERMIT_BAREMULTISIG = true;
|
||||
|
||||
export function countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
|
||||
if (!script?.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let sigops = 0;
|
||||
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
|
||||
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
|
||||
|
||||
// count OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY
|
||||
if (isRawScript) {
|
||||
// in scriptPubKey or scriptSig, always worth 20
|
||||
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
|
||||
} else {
|
||||
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
|
||||
const matches = script.matchAll(/(?:OP_(?:PUSHNUM_)?(\d+))? OP_CHECKMULTISIG/g);
|
||||
for (const match of matches) {
|
||||
const n = parseInt(match[1]);
|
||||
if (Number.isInteger(n)) {
|
||||
sigops += n;
|
||||
} else {
|
||||
sigops += 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return witness ? sigops : (sigops * 4);
|
||||
}
|
||||
|
||||
export function setSchnorrSighashFlags(flags: bigint, witness: string[]): bigint {
|
||||
// no witness items
|
||||
if (!witness?.length) {
|
||||
return flags;
|
||||
}
|
||||
const hasAnnex = witness.length > 1 && witness[witness.length - 1].startsWith('50');
|
||||
if (witness?.length === (hasAnnex ? 2 : 1)) {
|
||||
// keypath spend, signature is the only witness item
|
||||
if (witness[0].length === 130) {
|
||||
flags |= setSighashFlags(flags, witness[0]);
|
||||
} else {
|
||||
flags |= TransactionFlags.sighash_default;
|
||||
}
|
||||
} else {
|
||||
// scriptpath spend, all items except for the script, control block and annex could be signatures
|
||||
for (let i = 0; i < witness.length - (hasAnnex ? 3 : 2); i++) {
|
||||
// handle probable signatures
|
||||
if (witness[i].length === 130) {
|
||||
flags |= setSighashFlags(flags, witness[i]);
|
||||
} else if (witness[i].length === 128) {
|
||||
flags |= TransactionFlags.sighash_default;
|
||||
}
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
export function isDERSig(w: string): boolean {
|
||||
// heuristic to detect probable DER signatures
|
||||
return (w.length >= 18
|
||||
&& w.startsWith('30') // minimum DER signature length is 8 bytes + sighash flag (see https://mempool.space/testnet/tx/c6c232a36395fa338da458b86ff1327395a9afc28c5d2daa4273e410089fd433)
|
||||
&& ['01', '02', '03', '81', '82', '83'].includes(w.slice(-2)) // signature must end with a valid sighash flag
|
||||
&& (w.length === (2 * parseInt(w.slice(2, 4), 16)) + 6) // second byte encodes the combined length of the R and S components
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates most standardness rules
|
||||
*
|
||||
* returns true early if any standardness rule is violated, otherwise false
|
||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||
*/
|
||||
export function isNonStandard(tx: Transaction): boolean {
|
||||
// version
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// tx-size
|
||||
if (tx.weight > MAX_STANDARD_TX_WEIGHT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// tx-size-small
|
||||
if (getNonWitnessSize(tx) < MIN_STANDARD_TX_NONWITNESS_SIZE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// bad-txns-too-many-sigops
|
||||
if (tx.sigops && tx.sigops > MAX_STANDARD_TX_SIGOPS_COST) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// input validation
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.is_coinbase) {
|
||||
// standardness rules don't apply to coinbase transactions
|
||||
return false;
|
||||
}
|
||||
// scriptsig-size
|
||||
if ((vin.scriptsig.length / 2) > MAX_STANDARD_SCRIPTSIG_SIZE) {
|
||||
return true;
|
||||
}
|
||||
// scriptsig-not-pushonly
|
||||
if (vin.scriptsig_asm) {
|
||||
for (const op of vin.scriptsig_asm.split(' ')) {
|
||||
if (opcodes[op] && opcodes[op] > opcodes['OP_16']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// bad-txns-nonstandard-inputs
|
||||
if (vin.prevout?.scriptpubkey_type === 'p2sh') {
|
||||
// TODO: evaluate script (https://github.com/bitcoin/bitcoin/blob/1ac627c485a43e50a9a49baddce186ee3ad4daad/src/policy/policy.cpp#L177)
|
||||
// countScriptSigops returns the witness-scaled sigops, so divide by 4 before comparison with MAX_P2SH_SIGOPS
|
||||
const sigops = (countScriptSigops(vin.inner_redeemscript_asm || '') / 4);
|
||||
if (sigops > MAX_P2SH_SIGOPS) {
|
||||
return true;
|
||||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
|
||||
// output validation
|
||||
let opreturnCount = 0;
|
||||
for (const vout of tx.vout) {
|
||||
// scriptpubkey
|
||||
if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
|
||||
// (non-standard output type)
|
||||
return true;
|
||||
} else if (vout.scriptpubkey_type === 'multisig') {
|
||||
if (!DEFAULT_PERMIT_BAREMULTISIG) {
|
||||
// bare-multisig
|
||||
return true;
|
||||
}
|
||||
const mOfN = parseMultisigScript(vout.scriptpubkey_asm);
|
||||
if (!mOfN || mOfN.n < 1 || mOfN.n > 3 || mOfN.m < 1 || mOfN.m > mOfN.n) {
|
||||
// (non-standard bare multisig threshold)
|
||||
return true;
|
||||
}
|
||||
} else if (vout.scriptpubkey_type === 'op_return') {
|
||||
opreturnCount++;
|
||||
if ((vout.scriptpubkey.length / 2) > MAX_OP_RETURN_RELAY) {
|
||||
// over default datacarrier limit
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// dust
|
||||
// (we could probably hardcode this for the different output types...)
|
||||
if (vout.scriptpubkey_type !== 'op_return') {
|
||||
let dustSize = (vout.scriptpubkey.length / 2);
|
||||
// add varint length overhead
|
||||
dustSize += getVarIntLength(dustSize);
|
||||
// add value size
|
||||
dustSize += 8;
|
||||
if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) {
|
||||
dustSize += 67;
|
||||
} else {
|
||||
dustSize += 148;
|
||||
}
|
||||
if (vout.value < (dustSize * DUST_RELAY_TX_FEE)) {
|
||||
// under minimum output size
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// multi-op-return
|
||||
if (opreturnCount > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: non-mandatory-script-verify-flag
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getNonWitnessSize(tx: Transaction): number {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.witness?.length) {
|
||||
hasWitness = true;
|
||||
// witness count
|
||||
weight -= getVarIntLength(vin.witness.length);
|
||||
for (const witness of vin.witness) {
|
||||
// witness item size + content
|
||||
weight -= getVarIntLength(witness.length / 2) + (witness.length / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasWitness) {
|
||||
// marker & segwit flag
|
||||
weight -= 2;
|
||||
}
|
||||
return Math.ceil(weight / 4);
|
||||
}
|
||||
|
||||
export function setSegwitSighashFlags(flags: bigint, witness: string[]): bigint {
|
||||
for (const w of witness) {
|
||||
if (isDERSig(w)) {
|
||||
flags |= setSighashFlags(flags, w);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
export function setLegacySighashFlags(flags: bigint, scriptsig_asm: string): bigint {
|
||||
for (const item of scriptsig_asm.split(' ')) {
|
||||
// skip op_codes
|
||||
if (item.startsWith('OP_')) {
|
||||
continue;
|
||||
}
|
||||
// check pushed data
|
||||
if (isDERSig(item)) {
|
||||
flags |= setSighashFlags(flags, item);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
export function setSighashFlags(flags: bigint, signature: string): bigint {
|
||||
switch(signature.slice(-2)) {
|
||||
case '01': return flags | TransactionFlags.sighash_all;
|
||||
case '02': return flags | TransactionFlags.sighash_none;
|
||||
case '03': return flags | TransactionFlags.sighash_single;
|
||||
case '81': return flags | TransactionFlags.sighash_all | TransactionFlags.sighash_acp;
|
||||
case '82': return flags | TransactionFlags.sighash_none | TransactionFlags.sighash_acp;
|
||||
case '83': return flags | TransactionFlags.sighash_single | TransactionFlags.sighash_acp;
|
||||
default: return flags | TransactionFlags.sighash_default; // taproot only
|
||||
}
|
||||
}
|
||||
|
||||
export function isBurnKey(pubkey: string): boolean {
|
||||
return [
|
||||
'022222222222222222222222222222222222222222222222222222222222222222',
|
||||
'033333333333333333333333333333333333333333333333333333333333333333',
|
||||
'020202020202020202020202020202020202020202020202020202020202020202',
|
||||
'030303030303030303030303030303030303030303030303030303030303030303',
|
||||
].includes(pubkey);
|
||||
}
|
||||
|
||||
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
if (cpfpInfo) {
|
||||
if (cpfpInfo.ancestors.length) {
|
||||
flags |= TransactionFlags.cpfp_child;
|
||||
}
|
||||
if (cpfpInfo.descendants?.length) {
|
||||
flags |= TransactionFlags.cpfp_parent;
|
||||
}
|
||||
}
|
||||
if (replacement) {
|
||||
flags |= TransactionFlags.replacement;
|
||||
}
|
||||
|
||||
// Already processed static flags, no need to do it again
|
||||
if (tx.flags) {
|
||||
return flags;
|
||||
}
|
||||
|
||||
// Process static flags
|
||||
if (tx.version === 1) {
|
||||
flags |= TransactionFlags.v1;
|
||||
} else if (tx.version === 2) {
|
||||
flags |= TransactionFlags.v2;
|
||||
} else if (tx.version === 3) {
|
||||
flags |= TransactionFlags.v3;
|
||||
}
|
||||
const reusedInputAddresses: { [address: string ]: number } = {};
|
||||
const reusedOutputAddresses: { [address: string ]: number } = {};
|
||||
const inValues = {};
|
||||
const outValues = {};
|
||||
let rbf = false;
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.sequence < 0xfffffffe) {
|
||||
rbf = true;
|
||||
}
|
||||
switch (vin.prevout?.scriptpubkey_type) {
|
||||
case 'p2pk': flags |= TransactionFlags.p2pk; break;
|
||||
case 'multisig': flags |= TransactionFlags.p2ms; break;
|
||||
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
|
||||
case 'p2sh': flags |= TransactionFlags.p2sh; break;
|
||||
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
|
||||
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
||||
case 'v1_p2tr': {
|
||||
if (!vin.witness?.length) {
|
||||
throw new Error('Taproot input missing witness data');
|
||||
}
|
||||
flags |= TransactionFlags.p2tr;
|
||||
// in taproot, if the last witness item begins with 0x50, it's an annex
|
||||
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
|
||||
// script spends have more than one witness item, not counting the annex (if present)
|
||||
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
|
||||
// the script itself is the second-to-last witness item, not counting the annex
|
||||
const asm = vin.inner_witnessscript_asm;
|
||||
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
|
||||
if (asm?.includes('OP_0 OP_IF')) {
|
||||
flags |= TransactionFlags.inscription;
|
||||
}
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
// sighash flags
|
||||
if (vin.prevout?.scriptpubkey_type === 'v1_p2tr') {
|
||||
flags |= setSchnorrSighashFlags(flags, vin.witness);
|
||||
} else if (vin.witness) {
|
||||
flags |= setSegwitSighashFlags(flags, vin.witness);
|
||||
} else if (vin.scriptsig?.length) {
|
||||
flags |= setLegacySighashFlags(flags, vin.scriptsig_asm);
|
||||
}
|
||||
|
||||
if (vin.prevout?.scriptpubkey_address) {
|
||||
reusedInputAddresses[vin.prevout?.scriptpubkey_address] = (reusedInputAddresses[vin.prevout?.scriptpubkey_address] || 0) + 1;
|
||||
}
|
||||
inValues[vin.prevout?.value || Math.random()] = (inValues[vin.prevout?.value || Math.random()] || 0) + 1;
|
||||
}
|
||||
if (rbf) {
|
||||
flags |= TransactionFlags.rbf;
|
||||
} else {
|
||||
flags |= TransactionFlags.no_rbf;
|
||||
}
|
||||
let hasFakePubkey = false;
|
||||
let P2WSHCount = 0;
|
||||
let olgaSize = 0;
|
||||
for (const vout of tx.vout) {
|
||||
switch (vout.scriptpubkey_type) {
|
||||
case 'p2pk': {
|
||||
flags |= TransactionFlags.p2pk;
|
||||
// detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve)
|
||||
hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey?.slice(2, -2));
|
||||
} break;
|
||||
case 'multisig': {
|
||||
flags |= TransactionFlags.p2ms;
|
||||
// detect fake pubkeys (i.e. not valid DER points on the secp256k1 curve)
|
||||
const asm = vout.scriptpubkey_asm;
|
||||
for (const key of (asm?.split(' ') || [])) {
|
||||
if (!hasFakePubkey && !key.startsWith('OP_')) {
|
||||
hasFakePubkey = hasFakePubkey || isBurnKey(key) || !isPoint(key);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
|
||||
case 'p2sh': flags |= TransactionFlags.p2sh; break;
|
||||
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
|
||||
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
||||
case 'v1_p2tr': flags |= TransactionFlags.p2tr; break;
|
||||
case 'op_return': flags |= TransactionFlags.op_return; break;
|
||||
}
|
||||
if (vout.scriptpubkey_address) {
|
||||
reusedOutputAddresses[vout.scriptpubkey_address] = (reusedOutputAddresses[vout.scriptpubkey_address] || 0) + 1;
|
||||
}
|
||||
if (vout.scriptpubkey_type === 'v0_p2wsh') {
|
||||
if (!P2WSHCount) {
|
||||
olgaSize = parseInt(vout.scriptpubkey.slice(4, 8), 16);
|
||||
}
|
||||
P2WSHCount++;
|
||||
if (P2WSHCount === Math.ceil((olgaSize + 2) / 32)) {
|
||||
const nullBytes = (P2WSHCount * 32) - olgaSize - 2;
|
||||
if (vout.scriptpubkey.endsWith(''.padEnd(nullBytes * 2, '0'))) {
|
||||
flags |= TransactionFlags.fake_scripthash;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
P2WSHCount = 0;
|
||||
}
|
||||
outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1;
|
||||
}
|
||||
if (hasFakePubkey) {
|
||||
flags |= TransactionFlags.fake_pubkey;
|
||||
}
|
||||
|
||||
// fast but bad heuristic to detect possible coinjoins
|
||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||
if (!addressReuse && tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) {
|
||||
flags |= TransactionFlags.coinjoin;
|
||||
}
|
||||
// more than 5:1 input:output ratio
|
||||
if (tx.vin.length / tx.vout.length >= 5) {
|
||||
flags |= TransactionFlags.consolidation;
|
||||
}
|
||||
// less than 1:5 input:output ratio
|
||||
if (tx.vin.length / tx.vout.length <= 0.2) {
|
||||
flags |= TransactionFlags.batch_payout;
|
||||
}
|
||||
|
||||
if (isNonStandard(tx)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
Loading…
Reference in New Issue
Block a user