diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d097318ca..0602e916a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain # Latest version available on this commit is 1.71.1 # Commit date is Aug 3, 2023 - uses: dtolnay/rust-toolchain@f361669954a8ecfc00a3443f35f9ac8e610ffc06 + uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 with: toolchain: ${{ steps.gettoolchain.outputs.toolchain }} diff --git a/LICENSE b/LICENSE index 9f8592854..e5b707840 100644 --- a/LICENSE +++ b/LICENSE @@ -1,29 +1,28 @@ -The Mempool Open Source Project -Copyright (c) 2019-2023 The Mempool Open Source Project Developers +The Mempool Open Source Project® +Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders This program is free software; you can redistribute it and/or modify it under -the terms of (at your option) either: +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License or any later version +approved by a proxy statement published on . - 1) the GNU Affero General Public License as published by the Free Software - Foundation, either version 3 of the License or any later version approved by a - proxy statement published on ; or +However, this copyright license does not include an implied right or license +to use any trademarks, service marks, logos, or trade names of Mempool Space K.K. +or any other contributor to The Mempool Open Source Project. - 2) the GNU General Public License as published by the Free Software - Foundation, either version 3 of the License or any later version approved by a - proxy statement published on . +The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, +Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full +Bitcoin ecosystem™, the mempool Logo, the mempool Square logo, the mempool Blocks +logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the +mempool.space Horizontal logo are registered trademarks or trademarks of Mempool +Space K.K in Japan, the United States, and/or other countries. -However, this copyright license does not include an implied right or license to -use our trademarks: The Mempool Open Source Project®, mempool.space™, the -mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal -Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered -trademarks or trademarks of Mempool Space K.K in Japan, the United States, -and/or other countries. See our full Trademark Policy and Guidelines for more -details, published on . +See our full Trademark Policy and Guidelines for more details, published on +. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details. You should have received a copy of both the GNU Affero General Public License -and the GNU General Public License along with this program. If not, see -. +along with this program. If not, see . diff --git a/README.md b/README.md index 9b56dc436..e5779416d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ It is an open-source project developed and operated for the benefit of the Bitco Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server. -**Most people should use a one-click install method.** Other install methods are meant for developers and others with experience managing servers. +Most people should use a one-click install method. + +Other install methods are meant for developers and others with experience managing servers. If you want support for your own production instance of Mempool, or if you'd like to have your own instance of Mempool run by the mempool.space team on their own global ISP infrastructure—check out Mempool Enterprise®. ## One-Click Installation diff --git a/backend/README.md b/backend/README.md index e5d9dabdd..d0376408f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,7 +2,7 @@ These instructions are mostly intended for developers. -If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool only provides support for custom setups to [enterprise sponsors](https://mempool.space/enterprise). +If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool only provides support for custom setups to project sponsors through [Mempool Enterprise®](https://mempool.space/enterprise). See other ways to set up Mempool on [the main README](/../../#installation-methods). diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index b6f8ab657..358a98c98 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,9 +1,12 @@ import * as bitcoinjs from 'bitcoinjs-lib'; import { Request } from 'express'; -import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces'; +import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; import config from '../config'; import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; +import rbfCache from './rbf-cache'; +import transactionUtils from './transaction-utils'; +import { isPoint } from '../utils/secp256k1'; export class Common { static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' @@ -138,6 +141,217 @@ export class Common { return matches; } + static 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 |= this.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 |= this.setSighashFlags(flags, witness[i]); + } else if (witness[i].length === 128) { + flags |= TransactionFlags.sighash_default; + } + } + } + return flags; + } + + static 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 + ); + } + + static setSegwitSighashFlags(flags: bigint, witness: string[]): bigint { + for (const w of witness) { + if (this.isDERSig(w)) { + flags |= this.setSighashFlags(flags, w); + } + } + return flags; + } + + static 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 (this.isDERSig(item)) { + flags |= this.setSighashFlags(flags, item); + } + } + return flags; + } + + static 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 + } + } + + static isBurnKey(pubkey: string): boolean { + return [ + '022222222222222222222222222222222222222222222222222222222222222222', + '033333333333333333333333333333333333333333333333333333333333333333', + '020202020202020202020202020202020202020202020202020202020202020202', + '030303030303030303030303030303030303030303030303030303030303030303', + ].includes(pubkey); + } + + static getTransactionFlags(tx: TransactionExtended): number { + let flags = tx.flags ? BigInt(tx.flags) : 0n; + + // Update variable flags (CPFP, RBF) + if (tx.ancestors?.length) { + flags |= TransactionFlags.cpfp_child; + } + if (tx.descendants?.length) { + flags |= TransactionFlags.cpfp_parent; + } + if (tx.replacement) { + flags |= TransactionFlags.replacement; + } + + // Already processed static flags, no need to do it again + if (tx.flags) { + return Number(flags); + } + + // Process static flags + if (tx.version === 1) { + flags |= TransactionFlags.v1; + } else if (tx.version === 2) { + flags |= TransactionFlags.v2; + } + const reusedAddresses: { [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': { + flags |= TransactionFlags.p2tr; + if (vin.witness.length > 2) { + const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - 2]); + if (asm?.includes('OP_0 OP_IF')) { + flags |= TransactionFlags.inscription; + } + } + } break; + } + + // sighash flags + if (vin.prevout?.scriptpubkey_type === 'v1_p2tr') { + flags |= this.setSchnorrSighashFlags(flags, vin.witness); + } else if (vin.witness) { + flags |= this.setSegwitSighashFlags(flags, vin.witness); + } else if (vin.scriptsig?.length) { + flags |= this.setLegacySighashFlags(flags, vin.scriptsig_asm || transactionUtils.convertScriptSigAsm(vin.scriptsig)); + } + + if (vin.prevout?.scriptpubkey_address) { + reusedAddresses[vin.prevout?.scriptpubkey_address] = (reusedAddresses[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; + 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 || transactionUtils.convertScriptSigAsm(vout.scriptpubkey); + for (const key of (asm?.split(' ') || [])) { + if (!hasFakePubkey && !key.startsWith('OP_')) { + hasFakePubkey = hasFakePubkey || this.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) { + reusedAddresses[vout.scriptpubkey_address] = (reusedAddresses[vout.scriptpubkey_address] || 0) + 1; + } + 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.values(reusedAddresses).reduce((acc, count) => Math.max(acc, count), 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; + } + + return Number(flags); + } + + static classifyTransaction(tx: TransactionExtended): TransactionClassified { + const flags = this.getTransactionFlags(tx); + tx.flags = flags; + return { + ...this.stripTransaction(tx), + flags, + }; + } + static stripTransaction(tx: TransactionExtended): TransactionStripped { return { txid: tx.txid, diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index 093f07f0d..202f8f4cb 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -256,6 +256,7 @@ class DiskCache { txs: rbfData.rbf.txs.map(([txid, entry]) => ({ value: entry })), trees: rbfData.rbf.trees, expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })), + mempool: memPool.getMempool(), }); } } catch (e) { diff --git a/backend/src/api/fee-api.ts b/backend/src/api/fee-api.ts index 5260e959a..0cab5a295 100644 --- a/backend/src/api/fee-api.ts +++ b/backend/src/api/fee-api.ts @@ -1,8 +1,10 @@ import { MempoolBlock } from '../mempool.interfaces'; -import { Common } from './common'; +import config from '../config'; import mempool from './mempool'; import projectedBlocks from './mempool-blocks'; +const isLiquid = config.MEMPOOL.NETWORK === 'liquid' || config.MEMPOOL.NETWORK === 'liquidtestnet'; + interface RecommendedFees { fastestFee: number, halfHourFee: number, @@ -14,8 +16,8 @@ interface RecommendedFees { class FeeApi { constructor() { } - defaultFee = Common.isLiquid() ? 0.1 : 1; - minimumIncrement = Common.isLiquid() ? 0.1 : 1; + defaultFee = isLiquid ? 0.1 : 1; + minimumIncrement = isLiquid ? 0.1 : 1; public getRecommendedFee(): RecommendedFees { const pBlocks = projectedBlocks.getMempoolBlocks(); diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 15f9b6cf7..a7f00f6e8 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -1,6 +1,6 @@ import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt'; import logger from '../logger'; -import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag } from '../mempool.interfaces'; +import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified } from '../mempool.interfaces'; import { Common, OnlineFeeStatsCalculator } from './common'; import config from '../config'; import { Worker } from 'worker_threads'; @@ -169,7 +169,7 @@ class MempoolBlocks { private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] { const mempoolBlockDeltas: MempoolBlockDelta[] = []; for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { - let added: TransactionStripped[] = []; + let added: TransactionClassified[] = []; let removed: string[] = []; const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = []; if (mempoolBlocks[i] && !prevBlocks[i]) { @@ -582,6 +582,7 @@ class MempoolBlocks { const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); this.mempoolBlocks = mempoolBlocks; this.mempoolBlockDeltas = deltas; + } return mempoolBlocks; @@ -599,7 +600,7 @@ class MempoolBlocks { medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE), feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength), transactionIds: transactionIds, - transactions: transactions.map((tx) => Common.stripTransaction(tx)), + transactions: transactions.map((tx) => Common.classifyTransaction(tx)), }; } diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index fa13db418..a5bc8407a 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -100,6 +100,9 @@ class Mempool { if (this.mempoolCache[txid].order == null) { this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid); } + for (const vin of this.mempoolCache[txid].vin) { + transactionUtils.addInnerScriptsToVin(vin); + } count++; if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) { await redisCache.$addTransaction(this.mempoolCache[txid]); diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index ad1762485..a087abbe0 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -97,6 +97,8 @@ class RbfCache { return; } + newTxExtended.replacement = true; + const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction; const newTime = newTxExtended.firstSeen || (Date.now() / 1000); newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe); @@ -368,14 +370,14 @@ class RbfCache { }; } - public async load({ txs, trees, expiring }): Promise { + public async load({ txs, trees, expiring, mempool }): Promise { try { txs.forEach(txEntry => { this.txs.set(txEntry.value.txid, txEntry.value); }); this.staleCount = 0; for (const deflatedTree of trees) { - await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); } expiring.forEach(expiringEntry => { if (this.txs.has(expiringEntry.key)) { @@ -413,7 +415,7 @@ class RbfCache { return deflated; } - async importTree(root, txid, deflated, txs: Map, mined: boolean = false): Promise { + async importTree(mempool, root, txid, deflated, txs: Map, mined: boolean = false): Promise { const treeInfo = deflated[txid]; const replaces: RbfTree[] = []; @@ -426,9 +428,12 @@ class RbfCache { // recursively reconstruct child trees for (const childId of treeInfo.replaces) { - const replaced = await this.importTree(root, childId, deflated, txs, mined); + const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); if (replaced) { this.replacedBy.set(replaced.tx.txid, txid); + if (mempool[replaced.tx.txid]) { + mempool[replaced.tx.txid].replacement = true; + } replaces.push(replaced); if (replaced.mined) { mined = true; diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index 82ce34ad1..edfd2142b 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -222,6 +222,7 @@ class RedisCache { txs: rbfTxs, trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), expiring: rbfExpirations, + mempool: memPool.getMempool(), }); } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index cb212512c..c93372ded 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -61,13 +61,13 @@ export interface MempoolBlock { export interface MempoolBlockWithTransactions extends MempoolBlock { transactionIds: string[]; - transactions: TransactionStripped[]; + transactions: TransactionClassified[]; } export interface MempoolBlockDelta { - added: TransactionStripped[]; + added: TransactionClassified[]; removed: string[]; - changed: { txid: string, rate: number | undefined }[]; + changed: { txid: string, rate: number | undefined, flags?: number }[]; } interface VinStrippedToScriptsig { @@ -94,7 +94,9 @@ export interface TransactionExtended extends IEsploraApi.Transaction { vsize: number, }; acceleration?: boolean; + replacement?: boolean; uid?: number; + flags?: number; } export interface MempoolTransactionExtended extends TransactionExtended { @@ -190,6 +192,45 @@ export interface TransactionStripped { rate?: number; // effective fee rate } +export interface TransactionClassified extends TransactionStripped { + flags: number; +} + +// binary flags for transaction classification +export const TransactionFlags = { + // features + rbf: 0b00000001n, + no_rbf: 0b00000010n, + v1: 0b00000100n, + v2: 0b00001000n, + // address types + p2pk: 0b00000001_00000000n, + p2ms: 0b00000010_00000000n, + p2pkh: 0b00000100_00000000n, + p2sh: 0b00001000_00000000n, + p2wpkh: 0b00010000_00000000n, + p2wsh: 0b00100000_00000000n, + p2tr: 0b01000000_00000000n, + // behavior + cpfp_parent: 0b00000001_00000000_00000000n, + cpfp_child: 0b00000010_00000000_00000000n, + replacement: 0b00000100_00000000_00000000n, + // data + op_return: 0b00000001_00000000_00000000_00000000n, + fake_pubkey: 0b00000010_00000000_00000000_00000000n, + inscription: 0b00000100_00000000_00000000_00000000n, + // heuristics + coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, + consolidation: 0b00000010_00000000_00000000_00000000_00000000n, + batch_payout: 0b00000100_00000000_00000000_00000000_00000000n, + // sighash + sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n, + sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n, + sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n, + sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n, + sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, +}; + export interface BlockExtension { totalFees: number; medianFee: number; // median fee rate diff --git a/backend/src/utils/secp256k1.ts b/backend/src/utils/secp256k1.ts new file mode 100644 index 000000000..cc731f17d --- /dev/null +++ b/backend/src/utils/secp256k1.ts @@ -0,0 +1,74 @@ +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 ( + !( + // 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; + } +} \ No newline at end of file diff --git a/contributors/0xBEEFCAF3.txt b/contributors/0xBEEFCAF3.txt new file mode 100644 index 000000000..e00999be1 --- /dev/null +++ b/contributors/0xBEEFCAF3.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 17, 2023. + +Signed: 0xBEEFCAF3 diff --git a/contributors/takuabonn.txt b/contributors/takuabonn.txt new file mode 100644 index 000000000..331019e91 --- /dev/null +++ b/contributors/takuabonn.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 13, 2023. + +Signed: takuabonn \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 8dc1f264a..444324af8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -124,7 +124,7 @@ Corresponding `docker-compose.yml` overrides: environment: MEMPOOL_NETWORK: "" MEMPOOL_BACKEND: "" - MEMPOOL_HTTP_PORT: "" + BACKEND_HTTP_PORT: "" MEMPOOL_SPAWN_CLUSTER_PROCS: "" MEMPOOL_API_URL_PREFIX: "" MEMPOOL_POLL_RATE_MS: "" diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index e3ad51116..98dcb4c31 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -406,27 +406,23 @@ + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html new file mode 100644 index 000000000..7b1c2f9e5 --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -0,0 +1,33 @@ +
+ + beta + + +
+ +
+ + + +
+
+
+ +
{{ group.label }}
+
+ + + +
+
+
+
+ + + + + +
+
\ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss new file mode 100644 index 000000000..6406a1d93 --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -0,0 +1,142 @@ +.block-filters { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + padding: 1em; + z-index: 10; + pointer-events: none; + + .filter-bar, .active-tags { + display: flex; + flex-direction: row; + align-items: center; + } + + .active-tags { + flex-wrap: wrap; + row-gap: 0.25em; + margin-left: 0.5em; + } + + .info-badges { + display: flex; + flex-direction: row; + align-items: center; + float: right; + + &:hover, &:active { + text-decoration: none; + } + } + + .menu-toggle { + width: 2em; + height: 2em; + padding: 0px 1px; + opacity: 0; + cursor: pointer; + color: white; + background: none; + border: solid 2px white; + border-radius: 0.35em; + pointer-events: all; + } + + .filter-menu { + h5 { + font-size: 0.8rem; + color: white; + margin: 0; + margin-top: 0.5em; + } + } + + .filter-group { + display: flex; + flex-direction: row; + flex-wrap: wrap; + row-gap: 0.25em; + margin-bottom: 0.5em; + } + + .filter-tag { + font-size: 0.9em; + background: #181b2daf; + border: solid 1px #105fb0; + color: white; + border-radius: 0.2rem; + padding: 0.2em 0.5em; + transition: background-color 300ms; + margin-right: 0.25em; + pointer-events: all; + + &.selected { + background-color: #105fb0; + } + } + + :host-context(.block-overview-graph:hover) &, &:hover, &:active { + .menu-toggle { + opacity: 0.5; + background: #181b2d; + + &:hover { + opacity: 1; + background: #181b2d7f; + } + } + + &.menu-open, &.filters-active { + .menu-toggle { + opacity: 1; + background: none; + + &:hover { + background: #181b2d7f; + } + } + } + } + + &.menu-open, &.filters-active { + .menu-toggle { + opacity: 1; + background: none; + + &:hover { + background: #181b2d7f; + } + } + } + + &.menu-open { + pointer-events: all; + background: #181b2d7f; + } + + &.small { + .filter-tag { + font-size: 0.8em; + } + } + + &.vsmall { + .filter-menu { + margin-top: 0.25em; + h5 { + display: none; + } + } + .filter-tag { + font-size: 0.7em; + } + } + + &.tiny { + .filter-tag { + font-size: 0.5em; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts new file mode 100644 index 000000000..ce0dd76ab --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -0,0 +1,70 @@ +import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core'; +import { FilterGroups, TransactionFilters } from '../../shared/filters.utils'; + + +@Component({ + selector: 'app-block-filters', + templateUrl: './block-filters.component.html', + styleUrls: ['./block-filters.component.scss'], +}) +export class BlockFiltersComponent implements OnChanges { + @Input() cssWidth: number = 800; + @Output() onFilterChanged: EventEmitter = new EventEmitter(); + + filters = TransactionFilters; + filterGroups = FilterGroups; + activeFilters: string[] = []; + filterFlags: { [key: string]: boolean } = {}; + menuOpen: boolean = false; + + constructor( + private cd: ChangeDetectorRef, + ) {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes.cssWidth) { + this.cd.markForCheck(); + } + } + + toggleFilter(key): void { + const filter = this.filters[key]; + this.filterFlags[key] = !this.filterFlags[key]; + if (this.filterFlags[key]) { + // remove any other flags in the same toggle group + if (filter.toggle) { + this.activeFilters.forEach(f => { + if (this.filters[f].toggle === filter.toggle) { + this.filterFlags[f] = false; + } + }); + this.activeFilters = this.activeFilters.filter(f => this.filters[f].toggle !== filter.toggle); + } + // add new active filter + this.activeFilters.push(key); + } else { + // remove active filter + this.activeFilters = this.activeFilters.filter(f => f != key); + } + this.onFilterChanged.emit(this.getBooleanFlags()); + } + + getBooleanFlags(): bigint | null { + let flags = 0n; + for (const key of Object.keys(this.filterFlags)) { + if (this.filterFlags[key]) { + flags |= this.filters[key].flag; + } + } + return flags || null; + } + + @HostListener('document:click', ['$event']) + onClick(event): boolean { + // click away from menu + if (!event.target.closest('button')) { + this.menuOpen = false; + } + return true; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html index a625a0385..9f5e7cb47 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html @@ -13,5 +13,6 @@ [auditEnabled]="auditHighlighting" [blockConversion]="blockConversion" > + diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index 1fc173a2d..8a449a121 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -8,6 +8,19 @@ import { Color, Position } from './sprite-types'; import { Price } from '../../services/price.service'; import { StateService } from '../../services/state.service'; import { Subscription } from 'rxjs'; +import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; + +const unmatchedOpacity = 0.2; +const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedAuditFeeColors = defaultAuditFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedMarginalFeeColors = defaultMarginalFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedAuditColors = { + censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity), + missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity), + added: setOpacity(defaultAuditColors.added, unmatchedOpacity), + selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity), + accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity), +}; @Component({ selector: 'app-block-overview-graph', @@ -26,6 +39,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On @Input() mirrorTxid: string | void; @Input() unavailable: boolean = false; @Input() auditHighlighting: boolean = false; + @Input() showFilters: boolean = false; + @Input() filterFlags: bigint | null = null; @Input() blockConversion: Price; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @@ -92,9 +107,21 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On if (changes.auditHighlighting) { this.setHighlightingEnabled(this.auditHighlighting); } - if (changes.overrideColor) { + if (changes.overrideColor && this.scene) { this.scene.setColorFunction(this.overrideColors); } + if ((changes.filterFlags || changes.showFilters) && this.scene) { + this.setFilterFlags(this.filterFlags); + } + } + + setFilterFlags(flags: bigint | null): void { + if (flags != null) { + this.scene.setColorFunction(this.getFilterColorFunction(flags)); + } else { + this.scene.setColorFunction(this.overrideColors); + } + this.start(); } ngOnDestroy(): void { @@ -374,6 +401,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On onPointerMove(event) { if (event.target === this.canvas.nativeElement) { this.setPreviewTx(event.offsetX, event.offsetY, false); + } else { + this.onPointerLeave(event); } } @@ -474,6 +503,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On onTxHover(hoverId: string) { this.txHoverEvent.emit(hoverId); } + + getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { + return (tx: TxView) => { + if ((tx.bigintFlags & flags) === flags) { + return defaultColorFunction(tx); + } else { + return defaultColorFunction( + tx, + unmatchedFeeColors, + unmatchedAuditFeeColors, + unmatchedMarginalFeeColors, + unmatchedAuditColors + ); + } + }; + } } // WebGL shader attributes diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index 77b7c2e05..cb589527d 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -2,19 +2,7 @@ import { FastVertexArray } from './fast-vertex-array'; import TxView from './tx-view'; import { TransactionStripped } from '../../interfaces/websocket.interface'; import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; -import { feeLevels, mempoolFeeColors } from '../../app.constants'; -import { darken, desaturate, hexToColor } from './utils'; - -const feeColors = mempoolFeeColors.map(hexToColor); -const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); -const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); -const auditColors = { - censored: hexToColor('f344df'), - missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), - added: hexToColor('0099ff'), - selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), - accelerated: hexToColor('8F5FF6'), -}; +import { defaultColorFunction } from './utils'; export default class BlockScene { scene: { count: number, offset: { x: number, y: number}}; @@ -27,6 +15,7 @@ export default class BlockScene { configAnimationOffset: number | null; animationOffset: number; highlightingEnabled: boolean; + filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n; width: number; height: number; gridWidth: number; @@ -78,7 +67,7 @@ export default class BlockScene { } setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { - this.getColor = colorFunction; + this.getColor = colorFunction || defaultColorFunction; this.dirty = true; if (this.initialised && this.scene) { this.updateColors(performance.now(), 50); @@ -277,6 +266,20 @@ export default class BlockScene { this.animateUntil = Math.max(this.animateUntil, tx.update(update)); } + private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void { + if (tx.dirty || this.dirty) { + const txColor = this.getColor(tx); + this.applyTxUpdate(tx, { + display: { + color: txColor + }, + duration: animate ? (duration || this.animationDuration) : 1, + start: startTime, + delay: animate ? delay : 0, + }); + } + } + private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left', animate: boolean = true): void { if (tx.dirty || this.dirty) { this.saveGridToScreenPosition(tx); @@ -325,7 +328,7 @@ export default class BlockScene { } else { this.applyTxUpdate(tx, { display: { - position: tx.screenPosition + position: tx.screenPosition, }, duration: animate ? this.animationDuration : 0, minDuration: animate ? (this.animationDuration / 2) : 0, @@ -903,49 +906,4 @@ class BlockLayout { function feeRateDescending(a: TxView, b: TxView) { return b.feerate - a.feerate; -} - -function defaultColorFunction(tx: TxView): Color { - const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate - const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; - const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; - // Normal mode - if (!tx.scene?.highlightingEnabled) { - if (tx.acc) { - return auditColors.accelerated; - } else { - return feeLevelColor; - } - return feeLevelColor; - } - // Block audit - switch(tx.status) { - case 'censored': - return auditColors.censored; - case 'missing': - case 'sigop': - case 'rbf': - return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; - case 'fresh': - case 'freshcpfp': - return auditColors.missing; - case 'added': - return auditColors.added; - case 'selected': - return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; - case 'accelerated': - return auditColors.accelerated; - case 'found': - if (tx.context === 'projected') { - return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; - } else { - return feeLevelColor; - } - default: - if (tx.acc) { - return auditColors.accelerated; - } else { - return feeLevelColor; - } - } } \ No newline at end of file diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index 4e2d855e6..da36b9880 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -1,9 +1,9 @@ import TxSprite from './tx-sprite'; import { FastVertexArray } from './fast-vertex-array'; -import { TransactionStripped } from '../../interfaces/websocket.interface'; import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; import { hexToColor } from './utils'; import BlockScene from './block-scene'; +import { TransactionStripped } from '../../interfaces/node-api.interface'; const hoverTransitionTime = 300; const defaultHoverColor = hexToColor('1bd8f4'); @@ -29,6 +29,7 @@ export default class TxView implements TransactionStripped { feerate: number; acc?: boolean; rate?: number; + bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; scene?: BlockScene; @@ -57,6 +58,7 @@ export default class TxView implements TransactionStripped { this.acc = tx.acc; this.rate = tx.rate; this.status = tx.status; + this.bigintFlags = tx.flags ? BigInt(tx.flags) : 0n; this.initialised = false; this.vertexArray = scene.vertexArray; diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index a0bb8e868..9c800ad85 100644 --- a/frontend/src/app/components/block-overview-graph/utils.ts +++ b/frontend/src/app/components/block-overview-graph/utils.ts @@ -1,4 +1,6 @@ +import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { Color } from './sprite-types'; +import TxView from './tx-view'; export function hexToColor(hex: string): Color { return { @@ -25,5 +27,75 @@ export function darken(color: Color, amount: number): Color { g: color.g * amount, b: color.b * amount, a: color.a, + }; +} + +export function setOpacity(color: Color, opacity: number): Color { + return { + ...color, + a: opacity + }; +} + +// precomputed colors +export const defaultFeeColors = mempoolFeeColors.map(hexToColor); +export const defaultAuditFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); +export const defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); +export const defaultAuditColors = { + censored: hexToColor('f344df'), + missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), + added: hexToColor('0099ff'), + selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), + accelerated: hexToColor('8F5FF6'), +}; + +export function defaultColorFunction( + tx: TxView, + feeColors: Color[] = defaultFeeColors, + auditFeeColors: Color[] = defaultAuditFeeColors, + marginalFeeColors: Color[] = defaultMarginalFeeColors, + auditColors: { [status: string]: Color } = defaultAuditColors +): Color { + const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate + const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; + const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; + // Normal mode + if (!tx.scene?.highlightingEnabled) { + if (tx.acc) { + return auditColors.accelerated; + } else { + return feeLevelColor; + } + return feeLevelColor; + } + // Block audit + switch(tx.status) { + case 'censored': + return auditColors.censored; + case 'missing': + case 'sigop': + case 'rbf': + return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; + case 'fresh': + case 'freshcpfp': + return auditColors.missing; + case 'added': + return auditColors.added; + case 'selected': + return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; + case 'accelerated': + return auditColors.accelerated; + case 'found': + if (tx.context === 'projected') { + return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; + } else { + return feeLevelColor; + } + default: + if (tx.acc) { + return auditColors.accelerated; + } else { + return feeLevelColor; + } } } \ No newline at end of file diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts index 65d0f984c..a6e2a2697 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts @@ -1,7 +1,7 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core'; -import { TransactionStripped } from '../../interfaces/websocket.interface'; import { Position } from '../../components/block-overview-graph/sprite-types.js'; import { Price } from '../../services/price.service'; +import { TransactionStripped } from '../../interfaces/node-api.interface.js'; @Component({ selector: 'app-block-overview-tooltip', diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 8bcf23ace..e908d5b24 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -42,7 +42,7 @@ Hash - ‎{{ block.id | shortenString : 13 }} + ‎{{ block.id | shortenString : 13 }} Timestamp diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html index 1e0cba48c..85e7eebb1 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html @@ -5,6 +5,7 @@ [blockLimit]="stateService.blockVSize" [orientation]="timeLtr ? 'right' : 'left'" [flip]="true" + [showFilters]="showFilters" [overrideColors]="overrideColors" (txClickEvent)="onTxClick($event)" > diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 09eac989e..4beda043a 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -18,6 +18,7 @@ import TxView from '../block-overview-graph/tx-view'; }) export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { @Input() index: number; + @Input() showFilters: boolean = false; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Output() txPreviewEvent = new EventEmitter(); diff --git a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html index 9d51ff4e9..2fafb31cd 100644 --- a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html +++ b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html @@ -1,5 +1,5 @@
- +
\ No newline at end of file diff --git a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.ts b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.ts index ebeb0801c..a671033cf 100644 --- a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.ts +++ b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.ts @@ -27,6 +27,7 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy { autofit: boolean = false; resolution: number = 80; index: number = 0; + filterFlags: bigint | null = 0n; routeParamsSubscription: Subscription; queryParamsSubscription: Subscription; @@ -38,6 +39,8 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy { ) { } ngOnInit(): void { + window['setFlags'] = this.setFilterFlags.bind(this); + this.websocketService.want(['blocks', 'mempool-blocks']); this.routeParamsSubscription = this.route.paramMap @@ -82,4 +85,8 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy { this.routeParamsSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe(); } + + setFilterFlags(flags: bigint | null) { + this.filterFlags = flags; + } } diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.html b/frontend/src/app/components/mempool-block/mempool-block.component.html index b089a6d74..d2aa1aed2 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.html +++ b/frontend/src/app/components/mempool-block/mempool-block.component.html @@ -46,7 +46,9 @@
- +
+ +
diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index c11bedacd..bb6e7791f 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { StateService } from '../../services/state.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { switchMap, map, tap, filter } from 'rxjs/operators'; @@ -28,6 +28,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { public stateService: StateService, private seoService: SeoService, private websocketService: WebsocketService, + private cd: ChangeDetectorRef, ) { this.webGlEnabled = detectWebGL(); } diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index a99dc084e..446d8be5a 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -174,7 +174,7 @@ export class SearchFormComponent implements OnInit { const addressPrefixSearchResults = result[0]; const lightningResults = result[1]; - const matchesBlockHeight = this.regexBlockheight.test(searchText); + const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight; const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date'; const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText); const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); @@ -217,7 +217,7 @@ export class SearchFormComponent implements OnInit { selectedResult(result: any): void { if (typeof result === 'string') { this.search(result); - } else if (typeof result === 'number') { + } else if (typeof result === 'number' && result <= this.stateService.latestBlockHeight) { this.navigate('/block/', result.toString()); } else if (result.alias) { this.navigate('/lightning/node/', result.public_key); @@ -232,8 +232,10 @@ export class SearchFormComponent implements OnInit { this.isSearching = true; if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) { this.navigate('/address/', searchText); - } else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) { + } else if (this.regexBlockhash.test(searchText)) { this.navigate('/block/', searchText); + } else if (this.regexBlockheight.test(searchText)) { + parseInt(searchText) <= this.stateService.latestBlockHeight ? this.navigate('/block/', searchText) : this.isSearching = false; } else if (this.regexTransaction.test(searchText)) { const matches = this.regexTransaction.exec(searchText); if (this.network === 'liquid' || this.network === 'liquidtestnet') { diff --git a/frontend/src/app/components/svg-images/svg-images.component.html b/frontend/src/app/components/svg-images/svg-images.component.html index 5e8d7d29f..11dfe1d79 100644 --- a/frontend/src/app/components/svg-images/svg-images.component.html +++ b/frontend/src/app/components/svg-images/svg-images.component.html @@ -84,6 +84,14 @@
+ + + + + + + + diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.component.html b/frontend/src/app/components/trademark-policy/trademark-policy.component.html index 3a3da15dd..9d08c634d 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.component.html +++ b/frontend/src/app/components/trademark-policy/trademark-policy.component.html @@ -8,7 +8,7 @@

Trademark Policy and Guidelines

The Mempool Open Source Project ®
-
Updated: July 19, 2021
+
Updated: December 7, 2023

@@ -56,7 +56,12 @@ Mempool Space K.K. The Mempool Open Source Project + Mempool Accelerator + Mempool Enterprise + Mempool Liquidity mempool.space + Be your own explorer + Explore the full Bitcoin ecosystem
@@ -89,11 +94,16 @@

The mempool Square Logo



- +

The mempool Blocks Logo



+ +

+

The mempool Blocks 3 | 2 Logo

+

+

@@ -304,8 +314,7 @@

Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:

-

“The Mempool Space K.K.™, The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”

- +

"The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."

  • What to Do When You See Abuse

  • diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index ce0b7eaef..97be0c2b1 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -8911,6 +8911,13 @@ export const faqData = [ fragment: "what-is-block-health", title: "What is block health?", }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "how-do-mempool-goggles-work", + title: "How do Mempool Goggles work?", + }, { type: "category", category: "self-hosting", diff --git a/frontend/src/app/docs/api-docs/api-docs.component.html b/frontend/src/app/docs/api-docs/api-docs.component.html index 49493d210..49b11ad7b 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.html +++ b/frontend/src/app/docs/api-docs/api-docs.component.html @@ -279,6 +279,95 @@

    Because of this feature's resource usage and availability requirements, it is only supported on official mempool.space instances.

    + +

    Mempool Goggles are a set of filters that can be applied to the mempool block visualizations to highlight different types of transactions.

    +

    There are currently 25 different Mempool Goggles filters, grouped into six categories:

    +
    +
    Features
    +
    +
    +
    RBF enabled
    +
    The transaction opts-in to BIP-125 replaceability.
    +
    RBF disabled
    +
    The transaction does not opt-in to BIP-125 replaceability.
    +
    Version 1
    +
    The default version for most transactions.
    +
    Version 2
    +
    Required for transactions which use OP_CHECKSEQUENCEVERIFY relative timelocks.
    +
    +
    + +
    Address Types
    +
    +
    +
    P2PK
    +
    Pay-to-public-key. A legacy output format most commonly found in old coinbase transactions.
    +
    Bare multisig
    +
    A legacy form of multisig, most commonly used for data embedding schemes (see also "Fake pubkey").
    +
    P2PKH
    +
    Pay-to-public-key-hash. A legacy address type that locks outputs to a public key.
    +
    P2SH
    +
    Pay-to-script-hash. A legacy address type that locks outputs to a redeem script.
    +
    P2WPKH
    +
    Pay-to-witness-public-key-hash. The SegWit version of P2PKH.
    +
    P2WSH
    +
    Pay-to-witness-script-hash. The SegWit version of P2SH.
    +
    Taproot
    +
    Addresses using the SegWit V1 format added in the Taproot upgrade.
    +
    +
    + +
    Behavior
    +
    +
    +
    Paid for by child
    +
    The transaction's effective fee rate has been increased by a higher rate CPFP child.
    +
    Pays for parent
    +
    The transaction bumps the effective fee rate of a lower rate CPFP ancestor.
    +
    Replacement
    +
    The transaction replaced a prior version via RBF.
    +
    +
    + +
    Data
    +
    + Different methods of embedding arbitrary data in a Bitcoin transaction. +
    +
    OP_RETURN
    +
    Fake pubkey
    +
    Data may be embedded in an invalid public key in a P2PK or Bare multisig output. This is a heuristic filter and can be prone to false positives and false negatives.
    +
    Inscription
    +
    Data is embedded in the witness script of a taproot input.
    +
    +
    + +
    Heuristics
    +
    + These filters match common types of transactions according to subjective criteria. +
    +
    Coinjoin
    +
    A type of collaborative privacy-improving transaction.
    +
    Consolidation
    +
    The transaction condenses many inputs into a few outputs.
    +
    Batch payment
    +
    The transaction sends coins from a few inputs to many outputs.
    +
    +
    + +
    Sighash Flags
    +
    + Different ways of signing inputs to Bitcoin transactions. Note that selecting multiple sighash filters will highlight transactions in which each sighash flag is used, but not necessarily in the same input. +
    +
    sighash_all
    +
    sighash_none
    +
    sighash_single
    +
    sighash_default
    +
    sighash_anyonecanpay
    +
    +
    +
    +
    + The official mempool.space website is operated by The Mempool Open Source Project. See more information on our About page. There are also many unofficial instances of this website operated by individual members of the Bitcoin community. diff --git a/frontend/src/app/docs/api-docs/api-docs.component.scss b/frontend/src/app/docs/api-docs/api-docs.component.scss index f90274046..8a4150262 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.scss +++ b/frontend/src/app/docs/api-docs/api-docs.component.scss @@ -389,3 +389,44 @@ h3 { margin-bottom: 4rem; } } + +/* styles for nested definition lists */ +dl { + margin: 0; + padding: 0; +} + +dt { + font-weight: bold; + color: #4a68b9; + padding: 5px 0; +} + +dd { + padding: 2px 0; + + & > dl { + padding-left: 1em; + border-left: 2px solid #4a68b9; + margin-left: 1em; + margin-top: 5px; + } + + & > dl > dt { + display: inline; + font-weight: normal; + color: #e83e8c; + font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New; + text-transform: uppercase; + + &:before { + content: ""; + display: block; + } + } + + & > dl > dd { + display: inline; + margin-left: 1em; + } +} diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 862272330..e225eb758 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -180,6 +180,7 @@ export interface TransactionStripped { value: number; rate?: number; // effective fee rate acc?: boolean; + flags?: number | null; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; } diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 1d0414de7..20bc42bde 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -90,6 +90,7 @@ export interface TransactionStripped { value: number; acc?: boolean; // is accelerated? rate?: number; // effective fee rate + flags?: number; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; } diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index fda93d446..ee55fb75d 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs'; import { StateService } from '../services/state.service'; import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface'; @@ -9,6 +9,8 @@ import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesP }) export class LightningApiService { private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet + + private requestCache = new Map, expiry: number }>; constructor( private httpClient: HttpClient, @@ -23,6 +25,46 @@ export class LightningApiService { }); } + private generateCacheKey(functionName: string, params: any[]): string { + return functionName + JSON.stringify(params); + } + + // delete expired cache entries + private cleanExpiredCache(): void { + this.requestCache.forEach((value, key) => { + if (value.expiry < Date.now()) { + this.requestCache.delete(key); + } + }); + } + + cachedRequest Observable>( + apiFunction: F, + expireAfter: number, // in ms + ...params: Parameters + ): Observable { + this.cleanExpiredCache(); + + const cacheKey = this.generateCacheKey(apiFunction.name, params); + if (!this.requestCache.has(cacheKey)) { + const subject = new BehaviorSubject(null); + this.requestCache.set(cacheKey, { subject, expiry: Date.now() + expireAfter }); + + apiFunction.bind(this)(...params).pipe( + tap(data => { + subject.next(data as T); + }), + catchError((error) => { + subject.error(error); + return of(null); + }), + shareReplay(1), + ).subscribe(); + } + + return this.requestCache.get(cacheKey).subject.asObservable().pipe(filter(val => val !== null), take(1)); + } + getNode$(publicKey: string): Observable { return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey); } diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index 7352d884d..30f786b16 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -82,9 +82,9 @@ export class NodesNetworksChartComponent implements OnInit { firstRun = false; this.miningWindowPreference = timespan; this.isLoading = true; - return this.lightningApiService.listStatistics$(timespan) + return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan) .pipe( - tap((response) => { + tap((response:any) => { const data = response.body; const chartData = { tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]), diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index 7417a35cd..0e4f66ca0 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -81,9 +81,9 @@ export class LightningStatisticsChartComponent implements OnInit { firstRun = false; this.miningWindowPreference = timespan; this.isLoading = true; - return this.lightningApiService.listStatistics$(timespan) + return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan) .pipe( - tap((response) => { + tap((response:any) => { const data = response.body; this.prepareChartOptions({ channel_count: data.map(val => [val.added * 1000, val.channel_count]), diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts new file mode 100644 index 000000000..0b652a192 --- /dev/null +++ b/frontend/src/app/shared/filters.utils.ts @@ -0,0 +1,88 @@ +export interface Filter { + key: string, + label: string, + flag: bigint, + toggle?: string, + group?: string, + important?: boolean, +} + +// binary flags for transaction classification +export const TransactionFlags = { + // features + rbf: 0b00000001n, + no_rbf: 0b00000010n, + v1: 0b00000100n, + v2: 0b00001000n, + multisig: 0b00010000n, + // address types + p2pk: 0b00000001_00000000n, + p2ms: 0b00000010_00000000n, + p2pkh: 0b00000100_00000000n, + p2sh: 0b00001000_00000000n, + p2wpkh: 0b00010000_00000000n, + p2wsh: 0b00100000_00000000n, + p2tr: 0b01000000_00000000n, + // behavior + cpfp_parent: 0b00000001_00000000_00000000n, + cpfp_child: 0b00000010_00000000_00000000n, + replacement: 0b00000100_00000000_00000000n, + // data + op_return: 0b00000001_00000000_00000000_00000000n, + fake_pubkey: 0b00000010_00000000_00000000_00000000n, + inscription: 0b00000100_00000000_00000000_00000000n, + // heuristics + coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, + consolidation: 0b00000010_00000000_00000000_00000000_00000000n, + batch_payout: 0b00000100_00000000_00000000_00000000_00000000n, + // sighash + sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n, + sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n, + sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n, + sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n, + sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, +}; + +export const TransactionFilters: { [key: string]: Filter } = { + /* features */ + rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true }, + no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf', important: true }, + v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' }, + v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' }, + // multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig }, + /* address types */ + p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true }, + p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true }, + p2pkh: { key: 'p2pkh', label: 'P2PKH', flag: TransactionFlags.p2pkh, important: true }, + p2sh: { key: 'p2sh', label: 'P2SH', flag: TransactionFlags.p2sh, important: true }, + p2wpkh: { key: 'p2wpkh', label: 'P2WPKH', flag: TransactionFlags.p2wpkh, important: true }, + p2wsh: { key: 'p2wsh', label: 'P2WSH', flag: TransactionFlags.p2wsh, important: true }, + p2tr: { key: 'p2tr', label: 'Taproot', flag: TransactionFlags.p2tr, important: true }, + /* behavior */ + cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent, important: true }, + cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child, important: true }, + replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true }, + /* data */ + op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true }, + fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey }, + inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription, important: true }, + /* heuristics */ + coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true }, + consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation }, + batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout }, + /* 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 }, + sighash_single: { key: 'sighash_single', label: 'sighash_single', flag: TransactionFlags.sighash_single }, + sighash_default: { key: 'sighash_default', label: 'sighash_default', flag: TransactionFlags.sighash_default }, + sighash_acp: { key: 'sighash_acp', label: 'sighash_anyonecanpay', flag: TransactionFlags.sighash_acp }, +}; + +export const FilterGroups: { label: string, filters: Filter[]}[] = [ + { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'multisig'] }, + { label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] }, + { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement'] }, + { label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] }, + { label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] }, + { label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] }, +].map(group => ({ label: group.label, filters: group.filters.map(filter => TransactionFilters[filter] || null).filter(f => f != null) })); \ No newline at end of file diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 52123f995..f092e81bc 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -44,6 +44,7 @@ import { StartComponent } from '../components/start/start.component'; import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; +import { BlockFiltersComponent } from '../components/block-filters/block-filters.component'; import { AddressComponent } from '../components/address/address.component'; import { SearchFormComponent } from '../components/search-form/search-form.component'; import { AddressLabelsComponent } from '../components/address-labels/address-labels.component'; @@ -141,6 +142,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir StartComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, + BlockFiltersComponent, TransactionsListComponent, AddressComponent, SearchFormComponent, @@ -266,6 +268,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir StartComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, + BlockFiltersComponent, TransactionsListComponent, AddressComponent, SearchFormComponent, @@ -319,7 +322,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir ClockFaceComponent, OnlyVsizeDirective, - OnlyWeightDirective + OnlyWeightDirective, ] }) export class SharedModule { diff --git a/frontend/src/resources/mempool-blocks-2-3-logo.jpeg b/frontend/src/resources/mempool-blocks-2-3-logo.jpeg new file mode 100644 index 000000000..9ec0525d3 Binary files /dev/null and b/frontend/src/resources/mempool-blocks-2-3-logo.jpeg differ diff --git a/frontend/src/resources/mempool-blocks-3-2-logo.jpeg b/frontend/src/resources/mempool-blocks-3-2-logo.jpeg new file mode 100644 index 000000000..298cf8a01 Binary files /dev/null and b/frontend/src/resources/mempool-blocks-3-2-logo.jpeg differ diff --git a/production/README.md b/production/README.md index 87b8bb0a1..8e325bb1b 100644 --- a/production/README.md +++ b/production/README.md @@ -2,7 +2,9 @@ These instructions are for setting up a serious production Mempool website for Bitcoin (mainnet, testnet, signet), Liquid (mainnet, testnet), and Bisq. -Again, this setup is no joke—home users should use [one of the other installation methods](../#installation-methods). Support is only provided to [enterprise sponsors](https://mempool.space/enterprise). +Again, this setup is no joke—home users should use [one of the other installation methods](../#installation-methods). Support is only provided to project sponsors through [Mempool Enterprise®](https://mempool.space/enterprise). + +You can also have the mempool.space team run a highly-performant and highly-available instance of Mempool for you on their own global ISP infrastructure. See Mempool Enterprise® for more details. ### Server Hardware diff --git a/production/mempool-frontend-config.liquid.json b/production/mempool-frontend-config.liquid.json index 6a7c79d52..1a4fc2998 100644 --- a/production/mempool-frontend-config.liquid.json +++ b/production/mempool-frontend-config.liquid.json @@ -11,7 +11,6 @@ "LIQUID_WEBSITE_URL": "https://liquid.network", "BISQ_WEBSITE_URL": "https://bisq.markets", "ITEMS_PER_PAGE": 25, - "BLOCK_WEIGHT_UNITS": 300000, "MEMPOOL_BLOCKS_AMOUNT": 2, "KEEP_BLOCKS_AMOUNT": 16 }