From 2de16322ae2df5e975a6bebd566bc391bb6864a5 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 27 Nov 2024 17:54:07 +0100 Subject: [PATCH 001/114] Utils functions for decoding tx client side --- frontend/src/app/shared/transaction.utils.ts | 746 ++++++++++++++++++- 1 file changed, 744 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index b3678986b..8d7281a20 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -1,8 +1,9 @@ import { TransactionFlags } from '@app/shared/filters.utils'; -import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from '@app/shared/script.utils'; -import { Transaction } from '@interfaces/electrs.interface'; +import { getVarIntLength, parseMultisigScript, isPoint } from '@app/shared/script.utils'; +import { Transaction, Vin } from '@interfaces/electrs.interface'; import { CpfpInfo, RbfInfo, TransactionStripped } from '@interfaces/node-api.interface'; import { StateService } from '@app/services/state.service'; +import { Hash } from './sha256'; // Bitcoin Core default policy settings const MAX_STANDARD_TX_WEIGHT = 400_000; @@ -588,3 +589,744 @@ export function identifyPrioritizedTransactions(transactions: TransactionStrippe return { prioritized, deprioritized }; } + +function convertScriptSigAsm(hex: string): string { + + const buf = new Uint8Array(hex.length / 2); + for (let i = 0; i < buf.length; i++) { + buf[i] = parseInt(hex.substr(i * 2, 2), 16); + } + + const b = []; + let i = 0; + + while (i < buf.length) { + const op = buf[i]; + if (op >= 0x01 && op <= 0x4e) { + i++; + let push; + if (op === 0x4c) { + push = buf[i]; + b.push('OP_PUSHDATA1'); + i += 1; + } else if (op === 0x4d) { + push = buf[i] | (buf[i + 1] << 8); + b.push('OP_PUSHDATA2'); + i += 2; + } else if (op === 0x4e) { + push = buf[i] | (buf[i + 1] << 8) | (buf[i + 2] << 16) | (buf[i + 3] << 24); + b.push('OP_PUSHDATA4'); + i += 4; + } else { + push = op; + b.push('OP_PUSHBYTES_' + push); + } + + const data = buf.slice(i, i + push); + if (data.length !== push) { + break; + } + + b.push(uint8ArrayToHexString(data)); + i += data.length; + } else { + if (op === 0x00) { + b.push('OP_0'); + } else if (op === 0x4f) { + b.push('OP_PUSHNUM_NEG1'); + } else if (op === 0xb1) { + b.push('OP_CLTV'); + } else if (op === 0xb2) { + b.push('OP_CSV'); + } else if (op === 0xba) { + b.push('OP_CHECKSIGADD'); + } else { + const opcode = opcodes[op]; + if (opcode) { + b.push(opcode); + } else { + b.push('OP_RETURN_' + op); + } + } + i += 1; + } + } + + return b.join(' '); +} + +/** + * This function must only be called when we know the witness we are parsing + * is a taproot witness. + * @param witness An array of hex strings that represents the witness stack of + * the input. + * @returns null if the witness is not a script spend, and the hex string of + * the script item if it is a script spend. + */ +function witnessToP2TRScript(witness: string[]): string | null { + if (witness.length < 2) return null; + // Note: see BIP341 for parsing details of witness stack + + // If there are at least two witness elements, and the first byte of the + // last element is 0x50, this last element is called annex a and + // is removed from the witness stack. + const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50'; + // If there are at least two witness elements left, script path spending is used. + // Call the second-to-last stack element s, the script. + // (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack) + if (hasAnnex && witness.length < 3) return null; + const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; + return witness[positionOfScript]; +} + +export function addInnerScriptsToVin(vin: Vin): void { + if (!vin.prevout) { + return; + } + + if (vin.prevout.scriptpubkey_type === 'p2sh') { + const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0]; + vin.inner_redeemscript_asm = convertScriptSigAsm(redeemScript); + if (vin.witness && vin.witness.length > 2) { + const witnessScript = vin.witness[vin.witness.length - 1]; + vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript); + } + } + + if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) { + const witnessScript = vin.witness[vin.witness.length - 1]; + vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript); + } + + if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) { + const witnessScript = witnessToP2TRScript(vin.witness); + if (witnessScript !== null) { + vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript); + } + } +} + +function fromBuffer(buffer: Uint8Array, network: string): Transaction { + let offset = 0; + + function readInt8(): number { + if (offset + 1 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + return buffer[offset++]; + } + + function readInt16() { + if (offset + 2 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + const value = buffer[offset] | (buffer[offset + 1] << 8); + offset += 2; + return value; + } + + function readInt32(unsigned = false): number { + if (offset + 4 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + const value = buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24); + offset += 4; + if (unsigned) { + return value >>> 0; + } + return value; + } + + function readInt64(): bigint { + if (offset + 8 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + const low = BigInt(buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24)); + const high = BigInt(buffer[offset + 4] | (buffer[offset + 5] << 8) | (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24)); + offset += 8; + return (high << 32n) | (low & 0xffffffffn); + } + + function readVarInt(): bigint { + const first = readInt8(); + if (first < 0xfd) { + return BigInt(first); + } else if (first === 0xfd) { + return BigInt(readInt16()); + } else if (first === 0xfe) { + return BigInt(readInt32(true)); + } else if (first === 0xff) { + return readInt64(); + } else { + throw new Error("Invalid VarInt prefix"); + } + } + + function readSlice(n: number | bigint): Uint8Array { + const length = Number(n); + if (offset + length > buffer.length) { + throw new Error('Cannot read slice out of bounds'); + } + const slice = buffer.slice(offset, offset + length); + offset += length; + return slice; + } + + function readVarSlice(): Uint8Array { + return readSlice(readVarInt()); + } + + function readVector(): Uint8Array[] { + const count = readVarInt(); + const vector = []; + for (let i = 0; i < count; i++) { + vector.push(readVarSlice()); + } + return vector; + } + + // Parse raw transaction + const tx = { + status: { + confirmed: null, + block_height: null, + block_hash: null, + block_time: null, + } + } as Transaction; + + tx.version = readInt32(); + + const marker = readInt8(); + const flag = readInt8(); + + let hasWitnesses = false; + if ( + marker === 0x00 && + flag === 0x01 + ) { + hasWitnesses = true; + } else { + offset -= 2; + } + + const vinLen = readVarInt(); + tx.vin = []; + for (let i = 0; i < vinLen; ++i) { + const txid = uint8ArrayToHexString(readSlice(32).reverse()); + const vout = readInt32(true); + const scriptsig = uint8ArrayToHexString(readVarSlice()); + const sequence = readInt32(true); + const is_coinbase = txid === '0'.repeat(64); + const scriptsig_asm = convertScriptSigAsm(scriptsig); + tx.vin.push({ txid, vout, scriptsig, sequence, is_coinbase, scriptsig_asm, prevout: null }); + } + + const voutLen = readVarInt(); + tx.vout = []; + for (let i = 0; i < voutLen; ++i) { + const value = Number(readInt64()); + const scriptpubkeyArray = readVarSlice(); + const scriptpubkey = uint8ArrayToHexString(scriptpubkeyArray) + const scriptpubkey_asm = convertScriptSigAsm(scriptpubkey); + const toAddress = scriptPubKeyToAddress(scriptpubkey, network); + const scriptpubkey_type = toAddress.type; + const scriptpubkey_address = toAddress?.address; + tx.vout.push({ value, scriptpubkey, scriptpubkey_asm, scriptpubkey_type, scriptpubkey_address }); + } + + let witnessSize = 0; + if (hasWitnesses) { + const startOffset = offset; + for (let i = 0; i < vinLen; ++i) { + tx.vin[i].witness = readVector().map(uint8ArrayToHexString); + } + witnessSize = offset - startOffset + 2; + } + + tx.locktime = readInt32(true); + + if (offset !== buffer.length) { + throw new Error('Transaction has unexpected data'); + } + + tx.size = buffer.length; + tx.weight = (tx.size - witnessSize) * 3 + tx.size; + + tx.txid = txid(tx); + + return tx; +} + +export function decodeRawTransaction(rawtx: string, network: string): Transaction { + if (!rawtx.length || rawtx.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(rawtx)) { + throw new Error('Invalid hex string'); + } + + const buffer = new Uint8Array(rawtx.length / 2); + for (let i = 0; i < rawtx.length; i += 2) { + buffer[i / 2] = parseInt(rawtx.substring(i, i + 2), 16); + } + + return fromBuffer(buffer, network); +} + +function serializeTransaction(tx: Transaction): Uint8Array { + const result: number[] = []; + + // Add version + result.push(...intToBytes(tx.version, 4)); + + // Add input count and inputs + result.push(...varIntToBytes(tx.vin.length)); + for (const input of tx.vin) { + result.push(...hexStringToUint8Array(input.txid).reverse()); + result.push(...intToBytes(input.vout, 4)); + const scriptSig = hexStringToUint8Array(input.scriptsig); + result.push(...varIntToBytes(scriptSig.length)); + result.push(...scriptSig); + result.push(...intToBytes(input.sequence, 4)); + } + + // Add output count and outputs + result.push(...varIntToBytes(tx.vout.length)); + for (const output of tx.vout) { + result.push(...bigIntToBytes(BigInt(output.value), 8)); + const scriptPubKey = hexStringToUint8Array(output.scriptpubkey); + result.push(...varIntToBytes(scriptPubKey.length)); + result.push(...scriptPubKey); + } + + // Add locktime + result.push(...intToBytes(tx.locktime, 4)); + + return new Uint8Array(result); +} + +function txid(tx: Transaction): string { + const serializedTx = serializeTransaction(tx); + const hash1 = new Hash().update(serializedTx).digest(); + const hash2 = new Hash().update(hash1).digest(); + return uint8ArrayToHexString(hash2.reverse()); +} + +export function countSigops(transaction: Transaction): number { + let sigops = 0; + + for (const input of transaction.vin) { + if (input.scriptsig_asm) { + sigops += countScriptSigops(input.scriptsig_asm, true); + } + if (input.prevout) { + switch (true) { + case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'): + case input.prevout.scriptpubkey_type === 'v0_p2wpkh': + sigops += 1; + break; + + case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'): + case input.prevout.scriptpubkey_type === 'v0_p2wsh': + if (input.witness?.length) { + sigops += countScriptSigops(convertScriptSigAsm(input.witness[input.witness.length - 1]), false, true); + } + break; + + case input.prevout.scriptpubkey_type === 'p2sh': + if (input.inner_redeemscript_asm) { + sigops += countScriptSigops(input.inner_redeemscript_asm); + } + break; + } + } + } + + for (const output of transaction.vout) { + if (output.scriptpubkey_asm) { + sigops += countScriptSigops(output.scriptpubkey_asm, true); + } + } + + return sigops; +} + +function scriptPubKeyToAddress(scriptPubKey: string, network: string): { address: string, type: string } { + // P2PKH + if (/^76a914[0-9a-f]{40}88ac$/.test(scriptPubKey)) { + return { address: p2pkh(scriptPubKey.substring(6, 6 + 40), network), type: 'p2pkh' }; + } + // P2PK + if (/^21[0-9a-f]{66}ac$/.test(scriptPubKey) || /^41[0-9a-f]{130}ac$/.test(scriptPubKey)) { + return { address: null, type: 'p2pk' }; + } + // P2SH + if (/^a914[0-9a-f]{40}87$/.test(scriptPubKey)) { + return { address: p2sh(scriptPubKey.substring(4, 4 + 40), network), type: 'p2sh' }; + } + // P2WPKH + if (/^0014[0-9a-f]{40}$/.test(scriptPubKey)) { + return { address: p2wpkh(scriptPubKey.substring(4, 4 + 40), network), type: 'v0_p2wpkh' }; + } + // P2WSH + if (/^0020[0-9a-f]{64}$/.test(scriptPubKey)) { + return { address: p2wsh(scriptPubKey.substring(4, 4 + 64), network), type: 'v0_p2wsh' }; + } + // P2TR + if (/^5120[0-9a-f]{64}$/.test(scriptPubKey)) { + return { address: p2tr(scriptPubKey.substring(4, 4 + 64), network), type: 'v1_p2tr' }; + } + // multisig + if (/^[0-9a-f]+ae$/.test(scriptPubKey)) { + return { address: null, type: 'multisig' }; + } + // anchor + if (scriptPubKey === '51024e73') { + return { address: 'bc1pfeessrawgf', type: 'anchor' }; + } + // op_return + if (/^6a/.test(scriptPubKey)) { + return { address: null, type: 'op_return' }; + } + return { address: null, type: 'unknown' }; +} + +function p2pkh(pubKeyHash: string, network: string): string { + const pubkeyHashArray = hexStringToUint8Array(pubKeyHash); + const version = ['testnet', 'testnet4', 'signet'].includes(network) ? 0x6f : 0x00; + const versionedPayload = Uint8Array.from([version, ...pubkeyHashArray]); + const hash1 = new Hash().update(versionedPayload).digest(); + const hash2 = new Hash().update(hash1).digest(); + const checksum = hash2.slice(0, 4); + const finalPayload = Uint8Array.from([...versionedPayload, ...checksum]); + const bitcoinAddress = base58Encode(finalPayload); + return bitcoinAddress; +} + +function p2sh(scriptHash: string, network: string): string { + const scriptHashArray = hexStringToUint8Array(scriptHash); + const version = ['testnet', 'testnet4', 'signet'].includes(network) ? 0xc4 : 0x05; + const versionedPayload = Uint8Array.from([version, ...scriptHashArray]); + const hash1 = new Hash().update(versionedPayload).digest(); + const hash2 = new Hash().update(hash1).digest(); + const checksum = hash2.slice(0, 4); + const finalPayload = Uint8Array.from([...versionedPayload, ...checksum]); + const bitcoinAddress = base58Encode(finalPayload); + return bitcoinAddress; +} + +function p2wpkh(pubKeyHash: string, network: string): string { + const pubkeyHashArray = hexStringToUint8Array(pubKeyHash); + const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc'; + const version = 0; + const words = [version].concat(toWords(pubkeyHashArray)); + const bech32Address = bech32Encode(hrp, words); + return bech32Address; +} + +function p2wsh(scriptHash: string, network: string): string { + const scriptHashArray = hexStringToUint8Array(scriptHash); + const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc'; + const version = 0; + const words = [version].concat(toWords(scriptHashArray)); + const bech32Address = bech32Encode(hrp, words); + return bech32Address; +} + +function p2tr(pubKeyHash: string, network: string): string { + const pubkeyHashArray = hexStringToUint8Array(pubKeyHash); + const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc'; + const version = 1; + const words = [version].concat(toWords(pubkeyHashArray)); + const bech32Address = bech32Encode(hrp, words, 0x2bc830a3); + return bech32Address; +} + +// base58 encoding +function base58Encode(data: Uint8Array): string { + const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + let hexString = Array.from(data) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); + + let num = BigInt("0x" + hexString); + + let encoded = ""; + while (num > 0) { + const remainder = Number(num % 58n); + num = num / 58n; + encoded = BASE58_ALPHABET[remainder] + encoded; + } + + for (let byte of data) { + if (byte === 0) { + encoded = "1" + encoded; + } else { + break; + } + } + + return encoded; +} + +// bech32 encoding +// Adapted from https://github.com/bitcoinjs/bech32/blob/5ceb0e3d4625561a459c85643ca6947739b2d83c/src/index.ts +function bech32Encode(prefix: string, words: number[], constant: number = 1) { + const BECH32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + const checksum = createChecksum(prefix, words, constant); + const combined = words.concat(checksum); + let result = prefix + '1'; + for (let i = 0; i < combined.length; ++i) { + result += BECH32_ALPHABET.charAt(combined[i]); + } + return result; +} + +function polymodStep(pre) { + const GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; + const b = pre >> 25; + return ( + ((pre & 0x1ffffff) << 5) ^ + ((b & 1 ? GENERATORS[0] : 0) ^ + (b & 2 ? GENERATORS[1] : 0) ^ + (b & 4 ? GENERATORS[2] : 0) ^ + (b & 8 ? GENERATORS[3] : 0) ^ + (b & 16 ? GENERATORS[4] : 0)) + ); +} + +function prefixChk(prefix) { + let chk = 1; + for (let i = 0; i < prefix.length; ++i) { + const c = prefix.charCodeAt(i); + chk = polymodStep(chk) ^ (c >> 5); + } + chk = polymodStep(chk); + for (let i = 0; i < prefix.length; ++i) { + const c = prefix.charCodeAt(i); + chk = polymodStep(chk) ^ (c & 0x1f); + } + return chk; +} + +function createChecksum(prefix: string, words: number[], constant: number) { + const POLYMOD_CONST = constant; + let chk = prefixChk(prefix); + for (let i = 0; i < words.length; ++i) { + const x = words[i]; + chk = polymodStep(chk) ^ x; + } + for (let i = 0; i < 6; ++i) { + chk = polymodStep(chk); + } + chk ^= POLYMOD_CONST; + + const checksum = []; + for (let i = 0; i < 6; ++i) { + checksum.push((chk >> (5 * (5 - i))) & 31); + } + return checksum; +} + +function convertBits(data, fromBits, toBits, pad) { + let acc = 0; + let bits = 0; + const ret = []; + const maxV = (1 << toBits) - 1; + + for (let i = 0; i < data.length; ++i) { + const value = data[i]; + if (value < 0 || value >> fromBits) throw new Error('Invalid value'); + acc = (acc << fromBits) | value; + bits += fromBits; + while (bits >= toBits) { + bits -= toBits; + ret.push((acc >> bits) & maxV); + } + } + if (pad) { + if (bits > 0) { + ret.push((acc << (toBits - bits)) & maxV); + } + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxV)) { + throw new Error('Invalid data'); + } + return ret; +} + +function toWords(bytes) { + return convertBits(bytes, 8, 5, true); +} + +// Helper functions +function uint8ArrayToHexString(uint8Array: Uint8Array): string { + return Array.from(uint8Array).map(byte => byte.toString(16).padStart(2, '0')).join(''); +} + +function hexStringToUint8Array(hex: string): Uint8Array { + const buf = new Uint8Array(hex.length / 2); + for (let i = 0; i < buf.length; i++) { + buf[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return buf; +} + +function intToBytes(value: number, byteLength: number): number[] { + const bytes = []; + for (let i = 0; i < byteLength; i++) { + bytes.push((value >> (8 * i)) & 0xff); + } + return bytes; +} + +function bigIntToBytes(value: bigint, byteLength: number): number[] { + const bytes = []; + for (let i = 0; i < byteLength; i++) { + bytes.push(Number((value >> BigInt(8 * i)) & 0xffn)); + } + return bytes; +} + +function varIntToBytes(value: number | bigint): number[] { + const bytes = []; + + if (typeof value === 'number') { + if (value < 0xfd) { + bytes.push(value); + } else if (value <= 0xffff) { + bytes.push(0xfd, value & 0xff, (value >> 8) & 0xff); + } else if (value <= 0xffffffff) { + bytes.push(0xfe, ...intToBytes(value, 4)); + } + } else { + if (value < 0xfdn) { + bytes.push(Number(value)); + } else if (value <= 0xffffn) { + bytes.push(0xfd, Number(value & 0xffn), Number((value >> 8n) & 0xffn)); + } else if (value <= 0xffffffffn) { + bytes.push(0xfe, ...intToBytes(Number(value), 4)); + } else { + bytes.push(0xff, ...bigIntToBytes(value, 8)); + } + } + + return bytes; +} + +const opcodes = { + 0: 'OP_0', + 76: 'OP_PUSHDATA1', + 77: 'OP_PUSHDATA2', + 78: 'OP_PUSHDATA4', + 79: 'OP_PUSHNUM_NEG1', + 80: 'OP_RESERVED', + 81: 'OP_PUSHNUM_1', + 82: 'OP_PUSHNUM_2', + 83: 'OP_PUSHNUM_3', + 84: 'OP_PUSHNUM_4', + 85: 'OP_PUSHNUM_5', + 86: 'OP_PUSHNUM_6', + 87: 'OP_PUSHNUM_7', + 88: 'OP_PUSHNUM_8', + 89: 'OP_PUSHNUM_9', + 90: 'OP_PUSHNUM_10', + 91: 'OP_PUSHNUM_11', + 92: 'OP_PUSHNUM_12', + 93: 'OP_PUSHNUM_13', + 94: 'OP_PUSHNUM_14', + 95: 'OP_PUSHNUM_15', + 96: 'OP_PUSHNUM_16', + 97: 'OP_NOP', + 98: 'OP_VER', + 99: 'OP_IF', + 100: 'OP_NOTIF', + 101: 'OP_VERIF', + 102: 'OP_VERNOTIF', + 103: 'OP_ELSE', + 104: 'OP_ENDIF', + 105: 'OP_VERIFY', + 106: 'OP_RETURN', + 107: 'OP_TOALTSTACK', + 108: 'OP_FROMALTSTACK', + 109: 'OP_2DROP', + 110: 'OP_2DUP', + 111: 'OP_3DUP', + 112: 'OP_2OVER', + 113: 'OP_2ROT', + 114: 'OP_2SWAP', + 115: 'OP_IFDUP', + 116: 'OP_DEPTH', + 117: 'OP_DROP', + 118: 'OP_DUP', + 119: 'OP_NIP', + 120: 'OP_OVER', + 121: 'OP_PICK', + 122: 'OP_ROLL', + 123: 'OP_ROT', + 124: 'OP_SWAP', + 125: 'OP_TUCK', + 126: 'OP_CAT', + 127: 'OP_SUBSTR', + 128: 'OP_LEFT', + 129: 'OP_RIGHT', + 130: 'OP_SIZE', + 131: 'OP_INVERT', + 132: 'OP_AND', + 133: 'OP_OR', + 134: 'OP_XOR', + 135: 'OP_EQUAL', + 136: 'OP_EQUALVERIFY', + 137: 'OP_RESERVED1', + 138: 'OP_RESERVED2', + 139: 'OP_1ADD', + 140: 'OP_1SUB', + 141: 'OP_2MUL', + 142: 'OP_2DIV', + 143: 'OP_NEGATE', + 144: 'OP_ABS', + 145: 'OP_NOT', + 146: 'OP_0NOTEQUAL', + 147: 'OP_ADD', + 148: 'OP_SUB', + 149: 'OP_MUL', + 150: 'OP_DIV', + 151: 'OP_MOD', + 152: 'OP_LSHIFT', + 153: 'OP_RSHIFT', + 154: 'OP_BOOLAND', + 155: 'OP_BOOLOR', + 156: 'OP_NUMEQUAL', + 157: 'OP_NUMEQUALVERIFY', + 158: 'OP_NUMNOTEQUAL', + 159: 'OP_LESSTHAN', + 160: 'OP_GREATERTHAN', + 161: 'OP_LESSTHANOREQUAL', + 162: 'OP_GREATERTHANOREQUAL', + 163: 'OP_MIN', + 164: 'OP_MAX', + 165: 'OP_WITHIN', + 166: 'OP_RIPEMD160', + 167: 'OP_SHA1', + 168: 'OP_SHA256', + 169: 'OP_HASH160', + 170: 'OP_HASH256', + 171: 'OP_CODESEPARATOR', + 172: 'OP_CHECKSIG', + 173: 'OP_CHECKSIGVERIFY', + 174: 'OP_CHECKMULTISIG', + 175: 'OP_CHECKMULTISIGVERIFY', + 176: 'OP_NOP1', + 177: 'OP_CHECKLOCKTIMEVERIFY', + 178: 'OP_CHECKSEQUENCEVERIFY', + 179: 'OP_NOP4', + 180: 'OP_NOP5', + 181: 'OP_NOP6', + 182: 'OP_NOP7', + 183: 'OP_NOP8', + 184: 'OP_NOP9', + 185: 'OP_NOP10', + 186: 'OP_CHECKSIGADD', + 253: 'OP_PUBKEYHASH', + 254: 'OP_PUBKEY', + 255: 'OP_INVALIDOPCODE', +}; From 025b0585b483168f2ccfffd33e204f7ecfad8c71 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 27 Nov 2024 18:11:56 +0100 Subject: [PATCH 002/114] Preview transaction from raw data --- .../transaction-raw.component.html | 190 +++++++++++ .../transaction-raw.component.scss | 194 ++++++++++++ .../transaction/transaction-raw.component.ts | 296 ++++++++++++++++++ .../transaction/transaction.module.ts | 6 + .../transactions-list.component.ts | 12 +- frontend/src/app/route-guards.ts | 2 +- .../global-footer.component.html | 1 + .../truncate/truncate.component.html | 2 +- .../truncate/truncate.component.scss | 6 + .../components/truncate/truncate.component.ts | 1 + 10 files changed, 704 insertions(+), 6 deletions(-) create mode 100644 frontend/src/app/components/transaction/transaction-raw.component.html create mode 100644 frontend/src/app/components/transaction/transaction-raw.component.scss create mode 100644 frontend/src/app/components/transaction/transaction-raw.component.ts diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html new file mode 100644 index 000000000..15293e2dd --- /dev/null +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -0,0 +1,190 @@ +
+ + @if (!transaction) { + +

Preview Transaction

+ +
+
+ +
+ + + +

Error decoding transaction, reason: {{ error }}

+
+ } + + @if (transaction && !error && !isLoading) { +
+

Preview Transaction

+ + + + + + + + + +
+ + + +
+ +
+ +

{{ errorBroadcast }}

+ +
+ + @if (!hasPrevouts) { +
+ This transaction is missing prevouts data. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }} +
+ } + + + +
+ + +
+

Flow

+
+ + + +
+ +
+
+ + +
+
+ + + + +
+
+ +
+
+ + + + +
+
+

Inputs & Outputs

+
+ +
+ + +
+
+ + + + +
+

Details

+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
Size
Virtual size
Adjusted vsize + + + +
Weight
+
+
+ + + + + + + + + + + + + + + + + + + +
Version
Locktime
Sigops + + + +
Transaction hex
+
+
+
+ } + + @if (isLoading) { +
+
+

Loading transaction prevouts ({{ prevoutsLoadedCount }} / {{ prevoutsCount }})

+
+ } +
\ No newline at end of file diff --git a/frontend/src/app/components/transaction/transaction-raw.component.scss b/frontend/src/app/components/transaction/transaction-raw.component.scss new file mode 100644 index 000000000..5bbe5601e --- /dev/null +++ b/frontend/src/app/components/transaction/transaction-raw.component.scss @@ -0,0 +1,194 @@ +.label { + margin: 0 5px; +} + +.container-buttons { + align-self: center; +} + +.title-block { + flex-wrap: wrap; + align-items: baseline; + @media (min-width: 650px) { + flex-direction: row; + } + h1 { + margin: 0rem; + margin-right: 15px; + line-height: 1; + } +} + +.tx-link { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: baseline; + width: 0; + max-width: 100%; + margin-right: 0px; + margin-bottom: 0px; + margin-top: 8px; + @media (min-width: 651px) { + flex-grow: 1; + margin-bottom: 0px; + margin-right: 1em; + top: 1px; + position: relative; + } + @media (max-width: 650px) { + width: 100%; + order: 3; + } + + .txid { + width: 200px; + min-width: 200px; + flex-grow: 1; + } +} + +.container-xl { + margin-bottom: 40px; +} + +.row { + flex-direction: column; + @media (min-width: 850px) { + flex-direction: row; + } +} + +.box.hidden { + visibility: hidden; + height: 0px; + padding-top: 0px; + padding-bottom: 0px; + margin-top: 0px; + margin-bottom: 0px; +} + +.graph-container { + position: relative; + width: 100%; + background: var(--stat-box-bg); + padding: 10px 0; + padding-bottom: 0; +} + +.toggle-wrapper { + width: 100%; + text-align: center; + margin: 1.25em 0 0; +} + +.graph-toggle { + margin: auto; +} + +.table { + tr td { + padding: 0.75rem 0.5rem; + @media (min-width: 576px) { + padding: 0.75rem 0.75rem; + } + &:last-child { + text-align: right; + @media (min-width: 850px) { + text-align: left; + } + } + .btn { + display: block; + } + + &.wrap-cell { + white-space: normal; + } + } +} + +.effective-fee-container { + display: block; + @media (min-width: 768px){ + display: inline-block; + } + @media (max-width: 425px){ + display: flex; + flex-direction: column; + } +} + +.effective-fee-rating { + @media (max-width: 767px){ + margin-right: 0px !important; + } +} + +.title { + h2 { + line-height: 1; + margin: 0; + padding-bottom: 5px; + } +} + +.btn-outline-info { + margin-top: 5px; + @media (min-width: 768px){ + margin-top: 0px; + } +} + +.flow-toggle { + margin-top: -5px; + margin-left: 10px; + @media (min-width: 768px){ + display: inline-block; + margin-top: 0px; + margin-bottom: 0px; + } +} + +.subtitle-block { + display: flex; + flex-direction: row; + align-items: baseline; + justify-content: space-between; + + .title { + flex-shrink: 0; + } + + .title-buttons { + flex-shrink: 1; + text-align: right; + .btn { + margin-top: 0; + margin-bottom: 8px; + margin-left: 8px; + } + } +} + +.cpfp-details { + .txids { + width: 60%; + } + + @media (max-width: 500px) { + .txids { + width: 40%; + } + } +} + +.disabled { + opacity: 0.5; + pointer-events: none; +} + +.no-cursor { + cursor: default !important; + pointer-events: none; +} \ No newline at end of file diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts new file mode 100644 index 000000000..cac7b595f --- /dev/null +++ b/frontend/src/app/components/transaction/transaction-raw.component.ts @@ -0,0 +1,296 @@ +import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy, ChangeDetectorRef } from '@angular/core'; +import { Transaction } from '@interfaces/electrs.interface'; +import { StateService } from '../../services/state.service'; +import { Filter, toFilters } from '../../shared/filters.utils'; +import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils'; +import { ETA, EtaService } from '../../services/eta.service'; +import { combineLatest, firstValueFrom, map, Observable, startWith, Subscription } from 'rxjs'; +import { WebsocketService } from '../../services/websocket.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { ElectrsApiService } from '../../services/electrs-api.service'; +import { SeoService } from '../../services/seo.service'; +import { seoDescriptionNetwork } from '@app/shared/common.utils'; +import { ApiService } from '../../services/api.service'; +import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; + +@Component({ + selector: 'app-transaction-raw', + templateUrl: './transaction-raw.component.html', + styleUrls: ['./transaction-raw.component.scss'], +}) +export class TransactionRawComponent implements OnInit, OnDestroy { + + pushTxForm: UntypedFormGroup; + isLoading: boolean; + offlineMode: boolean = false; + transaction: Transaction; + error: string; + errorPrevouts: string; + hasPrevouts: boolean; + prevoutsLoadedCount: number = 0; + prevoutsCount: number; + isLoadingBroadcast: boolean; + errorBroadcast: string; + successBroadcast: boolean; + + isMobile: boolean; + @ViewChild('graphContainer') + graphContainer: ElementRef; + graphExpanded: boolean = false; + graphWidth: number = 1068; + graphHeight: number = 360; + inOutLimit: number = 150; + maxInOut: number = 0; + flowPrefSubscription: Subscription; + hideFlow: boolean = this.stateService.hideFlow.value; + flowEnabled: boolean; + adjustedVsize: number; + filters: Filter[] = []; + showCpfpDetails = false; + ETA$: Observable; + mempoolBlocksSubscription: Subscription; + + constructor( + public route: ActivatedRoute, + public router: Router, + public stateService: StateService, + public etaService: EtaService, + public electrsApi: ElectrsApiService, + public websocketService: WebsocketService, + public formBuilder: UntypedFormBuilder, + public cd: ChangeDetectorRef, + public seoService: SeoService, + public apiService: ApiService, + public relativeUrlPipe: RelativeUrlPipe, + ) {} + + ngOnInit(): void { + this.seoService.setTitle($localize`:@@meta.title.preview-tx:Preview Transaction`); + this.seoService.setDescription($localize`:@@meta.description.preview-tx:Preview a transaction to the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network using the transaction's raw hex data.`); + this.websocketService.want(['blocks', 'mempool-blocks']); + this.pushTxForm = this.formBuilder.group({ + txRaw: ['', Validators.required], + }); + } + + async decodeTransaction(): Promise { + this.resetState(); + this.isLoading = true; + try { + const tx = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network); + await this.fetchPrevouts(tx); + this.processTransaction(tx); + } catch (error) { + this.error = error.message; + } finally { + this.isLoading = false; + } + } + + async fetchPrevouts(transaction: Transaction): Promise { + if (this.offlineMode) { + return; + } + + this.prevoutsCount = transaction.vin.filter(input => !input.is_coinbase).length; + if (this.prevoutsCount === 0) { + this.hasPrevouts = true; + return; + } + + const txsToFetch: { [txid: string]: number } = transaction.vin.reduce((acc, input) => { + if (!input.is_coinbase) { + acc[input.txid] = (acc[input.txid] || 0) + 1; + } + return acc; + }, {} as { [txid: string]: number }); + + try { + + if (Object.keys(txsToFetch).length > 20) { + throw new Error($localize`:@@transaction.too-many-prevouts:Too many transactions to fetch (${Object.keys(txsToFetch).length})`); + } + + const fetchedTransactions = await Promise.all( + Object.keys(txsToFetch).map(txid => + firstValueFrom(this.electrsApi.getTransaction$(txid)) + .then(response => { + this.prevoutsLoadedCount += txsToFetch[txid]; + this.cd.markForCheck(); + return response; + }) + ) + ); + + const transactionsMap = fetchedTransactions.reduce((acc, transaction) => { + acc[transaction.txid] = transaction; + return acc; + }, {} as { [txid: string]: any }); + + const prevouts = transaction.vin.map((input, index) => ({ index, prevout: transactionsMap[input.txid]?.vout[input.vout] || null})); + + transaction.vin = transaction.vin.map((input, index) => { + if (!input.is_coinbase) { + input.prevout = prevouts.find(p => p.index === index)?.prevout; + addInnerScriptsToVin(input); + } + return input; + }); + this.hasPrevouts = true; + } catch (error) { + this.errorPrevouts = error.message; + } + } + + processTransaction(tx: Transaction): void { + this.transaction = tx; + + if (this.hasPrevouts) { + this.transaction.fee = this.transaction.vin.some(input => input.is_coinbase) + ? 0 + : this.transaction.vin.reduce((fee, input) => { + return fee + (input.prevout?.value || 0); + }, 0) - this.transaction.vout.reduce((sum, output) => sum + output.value, 0); + this.transaction.feePerVsize = this.transaction.fee / (this.transaction.weight / 4); + } + + this.transaction.flags = getTransactionFlags(this.transaction, null, null, null, this.stateService.network); + this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : []; + this.transaction.sigops = countSigops(this.transaction); + if (this.transaction.sigops >= 0) { + this.adjustedVsize = Math.max(this.transaction.weight / 4, this.transaction.sigops * 5); + } + + this.setupGraph(); + this.setFlowEnabled(); + this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => { + this.hideFlow = !!hide; + this.setFlowEnabled(); + }); + this.setGraphSize(); + + this.ETA$ = combineLatest([ + this.stateService.mempoolTxPosition$.pipe(startWith(null)), + this.stateService.mempoolBlocks$.pipe(startWith(null)), + this.stateService.difficultyAdjustment$.pipe(startWith(null)), + ]).pipe( + map(([position, mempoolBlocks, da]) => { + return this.etaService.calculateETA( + this.stateService.network, + this.transaction, + mempoolBlocks, + position, + da, + null, + null, + null + ); + }) + ); + + this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe(() => { + if (this.transaction) { + this.stateService.markBlock$.next({ + txid: this.transaction.txid, + txFeePerVSize: this.transaction.feePerVsize, + }); + } + }); + } + + async postTx(): Promise { + this.isLoadingBroadcast = true; + this.errorBroadcast = null; + return new Promise((resolve, reject) => { + this.apiService.postTransaction$(this.pushTxForm.get('txRaw').value) + .subscribe((result) => { + this.isLoadingBroadcast = false; + this.successBroadcast = true; + resolve(result); + }, + (error) => { + if (typeof error.error === 'string') { + const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"'); + this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error); + } else if (error.message) { + this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message; + } + this.isLoadingBroadcast = false; + reject(this.error); + }); + }); + } + + resetState() { + this.transaction = null; + this.error = null; + this.errorPrevouts = null; + this.errorBroadcast = null; + this.successBroadcast = false; + this.isLoading = false; + this.adjustedVsize = null; + this.filters = []; + this.hasPrevouts = false; + this.prevoutsLoadedCount = 0; + this.prevoutsCount = 0; + this.stateService.markBlock$.next({}); + this.mempoolBlocksSubscription?.unsubscribe(); + } + + resetForm() { + this.resetState(); + this.pushTxForm.reset(); + } + + @HostListener('window:resize', ['$event']) + setGraphSize(): void { + this.isMobile = window.innerWidth < 850; + if (this.graphContainer?.nativeElement && this.stateService.isBrowser) { + setTimeout(() => { + if (this.graphContainer?.nativeElement?.clientWidth) { + this.graphWidth = this.graphContainer.nativeElement.clientWidth; + } else { + setTimeout(() => { this.setGraphSize(); }, 1); + } + }, 1); + } else { + setTimeout(() => { this.setGraphSize(); }, 1); + } + } + + setupGraph() { + this.maxInOut = Math.min(this.inOutLimit, Math.max(this.transaction?.vin?.length || 1, this.transaction?.vout?.length + 1 || 1)); + this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80); + } + + toggleGraph() { + const showFlow = !this.flowEnabled; + this.stateService.hideFlow.next(!showFlow); + } + + setFlowEnabled() { + this.flowEnabled = !this.hideFlow; + } + + expandGraph() { + this.graphExpanded = true; + this.graphHeight = this.maxInOut * 15; + } + + collapseGraph() { + this.graphExpanded = false; + this.graphHeight = Math.min(360, this.maxInOut * 80); + } + + onOfflineModeChange(e): void { + this.offlineMode = !e.target.checked; + } + + ngOnDestroy(): void { + this.mempoolBlocksSubscription?.unsubscribe(); + this.flowPrefSubscription?.unsubscribe(); + this.stateService.markBlock$.next({}); + } + +} diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts index 80de0cf40..58b6493d8 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -9,6 +9,7 @@ import { TransactionExtrasModule } from '@components/transaction/transaction-ext import { GraphsModule } from '@app/graphs/graphs.module'; import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component'; import { AccelerateFeeGraphComponent } from '@components/accelerate-checkout/accelerate-fee-graph.component'; +import { TransactionRawComponent } from '@components/transaction/transaction-raw.component'; const routes: Routes = [ { @@ -16,6 +17,10 @@ const routes: Routes = [ redirectTo: '/', pathMatch: 'full', }, + { + path: 'preview', + component: TransactionRawComponent, + }, { path: ':id', component: TransactionComponent, @@ -49,6 +54,7 @@ export class TransactionRoutingModule { } TransactionDetailsComponent, AccelerateCheckout, AccelerateFeeGraphComponent, + TransactionRawComponent, ], exports: [ TransactionComponent, diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index b07546e5e..d1764442e 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -37,6 +37,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { @Input() addresses: string[] = []; @Input() rowLimit = 12; @Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block + @Input() txPreview = false; @Output() loadMore = new EventEmitter(); @@ -81,7 +82,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { this.refreshOutspends$ .pipe( switchMap((txIds) => { - if (!this.cached) { + if (!this.cached && !this.txPreview) { // break list into batches of 50 (maximum supported by esplora) const batches = []; for (let i = 0; i < txIds.length; i += 50) { @@ -119,7 +120,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { ), this.refreshChannels$ .pipe( - filter(() => this.stateService.networkSupportsLightning()), + filter(() => this.stateService.networkSupportsLightning() && !this.txPreview), switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)), catchError((error) => { // handle 404 @@ -187,7 +188,10 @@ export class TransactionsListComponent implements OnInit, OnChanges { } this.transactionsLength = this.transactions.length; - this.cacheService.setTxCache(this.transactions); + + if (!this.txPreview) { + this.cacheService.setTxCache(this.transactions); + } const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length; this.transactions.forEach((tx) => { @@ -347,7 +351,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { } loadMoreInputs(tx: Transaction): void { - if (!tx['@vinLoaded']) { + if (!tx['@vinLoaded'] && !this.txPreview) { this.electrsApiService.getTransaction$(tx.txid) .subscribe((newTx) => { tx['@vinLoaded'] = true; diff --git a/frontend/src/app/route-guards.ts b/frontend/src/app/route-guards.ts index 780e997db..81cbf03ae 100644 --- a/frontend/src/app/route-guards.ts +++ b/frontend/src/app/route-guards.ts @@ -14,7 +14,7 @@ class GuardService { trackerGuard(route: Route, segments: UrlSegment[]): boolean { const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode; const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments; - return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path)); + return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test', 'preview'].includes(path[1].path)); } } diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index d82bb8062..edce7bb88 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -76,6 +76,7 @@

Recent Blocks

Broadcast Transaction

Test Transaction

+

Preview Transaction

Connect to our Nodes

API Documentation

diff --git a/frontend/src/app/shared/components/truncate/truncate.component.html b/frontend/src/app/shared/components/truncate/truncate.component.html index 066f83244..b7e31483e 100644 --- a/frontend/src/app/shared/components/truncate/truncate.component.html +++ b/frontend/src/app/shared/components/truncate/truncate.component.html @@ -1,6 +1,6 @@ - + diff --git a/frontend/src/app/shared/components/truncate/truncate.component.scss b/frontend/src/app/shared/components/truncate/truncate.component.scss index 8c22dd836..739376ed2 100644 --- a/frontend/src/app/shared/components/truncate/truncate.component.scss +++ b/frontend/src/app/shared/components/truncate/truncate.component.scss @@ -37,6 +37,12 @@ max-width: 300px; overflow: hidden; } + + .disabled { + pointer-events: none; + opacity: 0.8; + color: #fff; + } } @media (max-width: 567px) { diff --git a/frontend/src/app/shared/components/truncate/truncate.component.ts b/frontend/src/app/shared/components/truncate/truncate.component.ts index 589f7aa36..f9ab34ee9 100644 --- a/frontend/src/app/shared/components/truncate/truncate.component.ts +++ b/frontend/src/app/shared/components/truncate/truncate.component.ts @@ -15,6 +15,7 @@ export class TruncateComponent { @Input() maxWidth: number = null; @Input() inline: boolean = false; @Input() textAlign: 'start' | 'end' = 'start'; + @Input() disabled: boolean = false; rtl: boolean; constructor( From 722eaa3e9638aaa712f35a662dc12715b0c9db77 Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 28 Nov 2024 12:07:05 +0100 Subject: [PATCH 003/114] Add note on borrowed code used for transaction decoding --- frontend/src/app/shared/transaction.utils.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index 8d7281a20..af1412838 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -590,6 +590,8 @@ export function identifyPrioritizedTransactions(transactions: TransactionStrippe return { prioritized, deprioritized }; } +// Adapted from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L254 +// Converts hex bitcoin script to ASM function convertScriptSigAsm(hex: string): string { const buf = new Uint8Array(hex.length / 2); @@ -655,6 +657,7 @@ function convertScriptSigAsm(hex: string): string { return b.join(' '); } +// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L327 /** * This function must only be called when we know the witness we are parsing * is a taproot witness. @@ -679,6 +682,8 @@ function witnessToP2TRScript(witness: string[]): string | null { return witness[positionOfScript]; } +// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L227 +// Fills inner_redeemscript_asm and inner_witnessscript_asm fields of fetched prevouts for decoded transactions export function addInnerScriptsToVin(vin: Vin): void { if (!vin.prevout) { return; @@ -706,6 +711,8 @@ export function addInnerScriptsToVin(vin: Vin): void { } } +// Adapted from bitcoinjs-lib at https://github.com/bitcoinjs/bitcoinjs-lib/blob/32e08aa57f6a023e995d8c4f0c9fbdc5f11d1fa0/ts_src/transaction.ts#L78 +// Reads buffer of raw transaction data function fromBuffer(buffer: Uint8Array, network: string): Transaction { let offset = 0; @@ -910,6 +917,7 @@ function txid(tx: Transaction): string { return uint8ArrayToHexString(hash2.reverse()); } +// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L177 export function countSigops(transaction: Transaction): number { let sigops = 0; @@ -1213,6 +1221,7 @@ function varIntToBytes(value: number | bigint): number[] { return bytes; } +// Inversed the opcodes object from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/utils/bitcoin-script.ts#L1 const opcodes = { 0: 'OP_0', 76: 'OP_PUSHDATA1', From 74ecd1aaac90789855ab1436b5d3eeba9ab830ff Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 28 Nov 2024 14:32:20 +0100 Subject: [PATCH 004/114] Fix missing prevouts message --- .../components/transaction/transaction-raw.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html index 15293e2dd..461d77bc4 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.html +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -43,7 +43,11 @@ @if (!hasPrevouts) {
- This transaction is missing prevouts data. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }} + @if (offlineMode) { + Prevouts are not loaded, some fields like fee rate cannot be displayed. + } @else { + Could not load prevouts. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }} + }
} From 727f22bc9d919e4b660f1134c6ed61f1fc92d149 Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 9 Dec 2024 23:24:11 +0100 Subject: [PATCH 005/114] Add backend endpoint to fetch prevouts --- backend/src/api/bitcoin/bitcoin.routes.ts | 50 ++++++++++++++- backend/src/api/transaction-utils.ts | 23 +++++++ .../transaction-raw.component.html | 4 +- .../transaction/transaction-raw.component.ts | 62 +++++++------------ frontend/src/app/services/api.service.ts | 4 ++ 5 files changed, 101 insertions(+), 42 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index d2d298e09..0dbd4fa27 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -12,7 +12,7 @@ import backendInfo from '../backend-info'; import transactionUtils from '../transaction-utils'; import { IEsploraApi } from './esplora-api.interface'; import loadingIndicators from '../loading-indicators'; -import { TransactionExtended } from '../../mempool.interfaces'; +import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; import logger from '../../logger'; import blocks from '../blocks'; import bitcoinClient from './bitcoin-client'; @@ -49,6 +49,7 @@ class BitcoinRoutes { .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) + .post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts) // Temporarily add txs/package endpoint for all backends until esplora supports it .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) ; @@ -824,6 +825,53 @@ class BitcoinRoutes { } } + private async $getPrevouts(req: Request, res: Response) { + try { + const outpoints = req.body; + if (!Array.isArray(outpoints) || outpoints.some((item) => !/^[a-fA-F0-9]{64}$/.test(item.txid) || typeof item.vout !== 'number')) { + return res.status(400).json({ error: 'Invalid input format' }); + } + + if (outpoints.length > 100) { + return res.status(400).json({ error: 'Too many prevouts requested' }); + } + + const result = Array(outpoints.length).fill(null); + const memPool = mempool.getMempool(); + + for (let i = 0; i < outpoints.length; i++) { + const outpoint = outpoints[i]; + let prevout: IEsploraApi.Vout | null = null; + let tx: MempoolTransactionExtended | null = null; + + const mempoolTx = memPool[outpoint.txid]; + if (mempoolTx) { + prevout = mempoolTx.vout[outpoint.vout]; + tx = mempoolTx; + } else { + const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false); + if (rawPrevout) { + prevout = { + value: Math.round(rawPrevout.value * 100000000), + scriptpubkey: rawPrevout.scriptPubKey.hex, + scriptpubkey_asm: rawPrevout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(rawPrevout.scriptPubKey.hex) : '', + scriptpubkey_type: transactionUtils.translateScriptPubKeyType(rawPrevout.scriptPubKey.type), + scriptpubkey_address: rawPrevout.scriptPubKey && rawPrevout.scriptPubKey.address ? rawPrevout.scriptPubKey.address : '', + }; + } + } + + if (prevout) { + result[i] = { prevout, tx }; + } + } + + res.json(result); + + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } } export default new BitcoinRoutes(); diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index 28fa72bba..519527d5c 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -420,6 +420,29 @@ class TransactionUtils { return { prioritized, deprioritized }; } + + // Copied from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/bitcoin/bitcoin-api.ts#L324 + public translateScriptPubKeyType(outputType: string): string { + const map = { + 'pubkey': 'p2pk', + 'pubkeyhash': 'p2pkh', + 'scripthash': 'p2sh', + 'witness_v0_keyhash': 'v0_p2wpkh', + 'witness_v0_scripthash': 'v0_p2wsh', + 'witness_v1_taproot': 'v1_p2tr', + 'nonstandard': 'nonstandard', + 'multisig': 'multisig', + 'anchor': 'anchor', + 'nulldata': 'op_return' + }; + + if (map[outputType]) { + return map[outputType]; + } else { + return 'unknown'; + } + } + } export default new TransactionUtils(); diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html index 461d77bc4..ca76d0e78 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.html +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -46,7 +46,7 @@ @if (offlineMode) { Prevouts are not loaded, some fields like fee rate cannot be displayed. } @else { - Could not load prevouts. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }} + Error loading prevouts. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }} } } @@ -188,7 +188,7 @@ @if (isLoading) {
-

Loading transaction prevouts ({{ prevoutsLoadedCount }} / {{ prevoutsCount }})

+

Loading transaction prevouts

} \ No newline at end of file diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts index cac7b595f..c9a0c2544 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.ts +++ b/frontend/src/app/components/transaction/transaction-raw.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy, ChangeDetectorRef } from '@angular/core'; -import { Transaction } from '@interfaces/electrs.interface'; +import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy } from '@angular/core'; +import { Transaction, Vout } from '@interfaces/electrs.interface'; import { StateService } from '../../services/state.service'; import { Filter, toFilters } from '../../shared/filters.utils'; import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils'; @@ -28,8 +28,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { error: string; errorPrevouts: string; hasPrevouts: boolean; - prevoutsLoadedCount: number = 0; - prevoutsCount: number; + missingPrevouts: string[]; isLoadingBroadcast: boolean; errorBroadcast: string; successBroadcast: boolean; @@ -59,7 +58,6 @@ export class TransactionRawComponent implements OnInit, OnDestroy { public electrsApi: ElectrsApiService, public websocketService: WebsocketService, public formBuilder: UntypedFormBuilder, - public cd: ChangeDetectorRef, public seoService: SeoService, public apiService: ApiService, public relativeUrlPipe: RelativeUrlPipe, @@ -93,52 +91,38 @@ export class TransactionRawComponent implements OnInit, OnDestroy { return; } - this.prevoutsCount = transaction.vin.filter(input => !input.is_coinbase).length; - if (this.prevoutsCount === 0) { + const prevoutsToFetch = transaction.vin.map((input) => ({ txid: input.txid, vout: input.vout })); + + if (!prevoutsToFetch.length || transaction.vin[0].is_coinbase) { this.hasPrevouts = true; return; } - const txsToFetch: { [txid: string]: number } = transaction.vin.reduce((acc, input) => { - if (!input.is_coinbase) { - acc[input.txid] = (acc[input.txid] || 0) + 1; - } - return acc; - }, {} as { [txid: string]: number }); - try { + this.missingPrevouts = []; - if (Object.keys(txsToFetch).length > 20) { - throw new Error($localize`:@@transaction.too-many-prevouts:Too many transactions to fetch (${Object.keys(txsToFetch).length})`); + const prevouts: { prevout: Vout, tx?: any }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch)); + + if (prevouts?.length !== prevoutsToFetch.length) { + throw new Error(); } - const fetchedTransactions = await Promise.all( - Object.keys(txsToFetch).map(txid => - firstValueFrom(this.electrsApi.getTransaction$(txid)) - .then(response => { - this.prevoutsLoadedCount += txsToFetch[txid]; - this.cd.markForCheck(); - return response; - }) - ) - ); - - const transactionsMap = fetchedTransactions.reduce((acc, transaction) => { - acc[transaction.txid] = transaction; - return acc; - }, {} as { [txid: string]: any }); - - const prevouts = transaction.vin.map((input, index) => ({ index, prevout: transactionsMap[input.txid]?.vout[input.vout] || null})); - transaction.vin = transaction.vin.map((input, index) => { - if (!input.is_coinbase) { - input.prevout = prevouts.find(p => p.index === index)?.prevout; + if (prevouts[index]) { + input.prevout = prevouts[index].prevout; addInnerScriptsToVin(input); + } else { + this.missingPrevouts.push(`${input.txid}:${input.vout}`); } return input; }); + + if (this.missingPrevouts.length) { + throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`); + } + this.hasPrevouts = true; - } catch (error) { + } catch (error) { this.errorPrevouts = error.message; } } @@ -207,6 +191,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { .subscribe((result) => { this.isLoadingBroadcast = false; this.successBroadcast = true; + this.transaction.txid = result; resolve(result); }, (error) => { @@ -232,8 +217,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.adjustedVsize = null; this.filters = []; this.hasPrevouts = false; - this.prevoutsLoadedCount = 0; - this.prevoutsCount = 0; + this.missingPrevouts = []; this.stateService.markBlock$.next({}); this.mempoolBlocksSubscription?.unsubscribe(); } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 3c8cf8807..ce0e67cbf 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -565,6 +565,10 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/acceleration/request/' + txid, ''); } + getPrevouts$(outpoints: {txid: string; vout: number}[]): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/prevouts', outpoints); + } + // Cache methods async setBlockAuditLoaded(hash: string) { this.blockAuditLoaded[hash] = true; From d852c48370066f3aba8b58853c7a12f0c969eeee Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 16 Dec 2024 18:06:01 +0100 Subject: [PATCH 006/114] Move 'related transactions' to dedicated component --- .../transaction/cpfp-info.component.html | 56 ++++++++++++++++++ .../transaction/cpfp-info.component.scss | 32 ++++++++++ .../transaction/cpfp-info.component.ts | 22 +++++++ .../transaction/transaction.component.html | 59 +------------------ .../transaction/transaction.component.scss | 12 ---- .../transaction/transaction.component.ts | 4 -- .../transaction/transaction.module.ts | 3 + 7 files changed, 114 insertions(+), 74 deletions(-) create mode 100644 frontend/src/app/components/transaction/cpfp-info.component.html create mode 100644 frontend/src/app/components/transaction/cpfp-info.component.scss create mode 100644 frontend/src/app/components/transaction/cpfp-info.component.ts diff --git a/frontend/src/app/components/transaction/cpfp-info.component.html b/frontend/src/app/components/transaction/cpfp-info.component.html new file mode 100644 index 000000000..55945c388 --- /dev/null +++ b/frontend/src/app/components/transaction/cpfp-info.component.html @@ -0,0 +1,56 @@ +
+
+

Related Transactions

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeTXIDVirtual sizeWeightFee rate
Descendant + +
Descendant + +
Ancestor + +
+
\ No newline at end of file diff --git a/frontend/src/app/components/transaction/cpfp-info.component.scss b/frontend/src/app/components/transaction/cpfp-info.component.scss new file mode 100644 index 000000000..df2b622e7 --- /dev/null +++ b/frontend/src/app/components/transaction/cpfp-info.component.scss @@ -0,0 +1,32 @@ +.title { + h2 { + line-height: 1; + margin: 0; + padding-bottom: 5px; + } +} + +.cpfp-details { + .txids { + width: 60%; + } + + @media (max-width: 500px) { + .txids { + width: 40%; + } + } +} + +.arrow-green { + color: var(--success); +} + +.arrow-red { + color: var(--red); +} + +.badge { + position: relative; + top: -1px; +} \ No newline at end of file diff --git a/frontend/src/app/components/transaction/cpfp-info.component.ts b/frontend/src/app/components/transaction/cpfp-info.component.ts new file mode 100644 index 000000000..3d122183b --- /dev/null +++ b/frontend/src/app/components/transaction/cpfp-info.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core'; +import { CpfpInfo } from '@interfaces/node-api.interface'; +import { Transaction } from '@interfaces/electrs.interface'; + +@Component({ + selector: 'app-cpfp-info', + templateUrl: './cpfp-info.component.html', + styleUrls: ['./cpfp-info.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CpfpInfoComponent implements OnInit { + @Input() cpfpInfo: CpfpInfo; + @Input() tx: Transaction; + + constructor() {} + + ngOnInit(): void {} + + roundToOneDecimal(cpfpTx: any): number { + return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); + } +} diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 8c2d9de01..099d7beb5 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -66,64 +66,7 @@ - -
-
-

Related Transactions

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeTXIDVirtual sizeWeightFee rate
Descendant - -
Descendant - -
Ancestor - -
-
-
+ diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index d35f26130..fed9f742c 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -227,18 +227,6 @@ } } -.cpfp-details { - .txids { - width: 60%; - } - - @media (max-width: 500px) { - .txids { - width: 40%; - } - } -} - .tx-list { .alert-link { display: block; diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 71ffaa2cd..50ff32340 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -1054,10 +1054,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.stateService.markBlock$.next({}); } - roundToOneDecimal(cpfpTx: any): number { - return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); - } - setupGraph() { this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1)); this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80); diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts index 58b6493d8..a05191346 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -10,6 +10,7 @@ import { GraphsModule } from '@app/graphs/graphs.module'; import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component'; import { AccelerateFeeGraphComponent } from '@components/accelerate-checkout/accelerate-fee-graph.component'; import { TransactionRawComponent } from '@components/transaction/transaction-raw.component'; +import { CpfpInfoComponent } from '@components/transaction/cpfp-info.component'; const routes: Routes = [ { @@ -55,12 +56,14 @@ export class TransactionRoutingModule { } AccelerateCheckout, AccelerateFeeGraphComponent, TransactionRawComponent, + CpfpInfoComponent, ], exports: [ TransactionComponent, TransactionDetailsComponent, AccelerateCheckout, AccelerateFeeGraphComponent, + CpfpInfoComponent, ] }) export class TransactionModule { } From 2987f86cd38214edda204b6ea428a039192619c1 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 17 Dec 2024 19:42:31 +0100 Subject: [PATCH 007/114] Compute decoded tx CPFP data in the backend --- backend/src/api/bitcoin/bitcoin.routes.ts | 44 +++++++++-- backend/src/api/cpfp.ts | 28 +++++++ .../transaction-raw.component.html | 15 +++- .../transaction/transaction-raw.component.ts | 79 +++++++++++++++---- frontend/src/app/services/api.service.ts | 4 + 5 files changed, 144 insertions(+), 26 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 0dbd4fa27..91dcf35c5 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -12,14 +12,14 @@ import backendInfo from '../backend-info'; import transactionUtils from '../transaction-utils'; import { IEsploraApi } from './esplora-api.interface'; import loadingIndicators from '../loading-indicators'; -import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; +import { TransactionExtended } from '../../mempool.interfaces'; import logger from '../../logger'; import blocks from '../blocks'; import bitcoinClient from './bitcoin-client'; import difficultyAdjustment from '../difficulty-adjustment'; import transactionRepository from '../../repositories/TransactionRepository'; import rbfCache from '../rbf-cache'; -import { calculateMempoolTxCpfp } from '../cpfp'; +import { calculateMempoolTxCpfp, calculateLocalTxCpfp } from '../cpfp'; import { handleError } from '../../utils/api'; class BitcoinRoutes { @@ -50,6 +50,7 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts) + .post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTx) // Temporarily add txs/package endpoint for all backends until esplora supports it .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) ; @@ -829,11 +830,11 @@ class BitcoinRoutes { try { const outpoints = req.body; if (!Array.isArray(outpoints) || outpoints.some((item) => !/^[a-fA-F0-9]{64}$/.test(item.txid) || typeof item.vout !== 'number')) { - return res.status(400).json({ error: 'Invalid input format' }); + return res.status(400).json({ message: 'Invalid input format' }); } if (outpoints.length > 100) { - return res.status(400).json({ error: 'Too many prevouts requested' }); + return res.status(400).json({ message: 'Too many prevouts requested' }); } const result = Array(outpoints.length).fill(null); @@ -842,12 +843,14 @@ class BitcoinRoutes { for (let i = 0; i < outpoints.length; i++) { const outpoint = outpoints[i]; let prevout: IEsploraApi.Vout | null = null; - let tx: MempoolTransactionExtended | null = null; + let unconfirmed: boolean | null = null; const mempoolTx = memPool[outpoint.txid]; if (mempoolTx) { - prevout = mempoolTx.vout[outpoint.vout]; - tx = mempoolTx; + if (outpoint.vout < mempoolTx.vout.length) { + prevout = mempoolTx.vout[outpoint.vout]; + unconfirmed = true; + } } else { const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false); if (rawPrevout) { @@ -858,11 +861,12 @@ class BitcoinRoutes { scriptpubkey_type: transactionUtils.translateScriptPubKeyType(rawPrevout.scriptPubKey.type), scriptpubkey_address: rawPrevout.scriptPubKey && rawPrevout.scriptPubKey.address ? rawPrevout.scriptPubKey.address : '', }; + unconfirmed = false; } } if (prevout) { - result[i] = { prevout, tx }; + result[i] = { prevout, unconfirmed }; } } @@ -872,6 +876,30 @@ class BitcoinRoutes { handleError(req, res, 500, e instanceof Error ? e.message : e); } } + + private getCpfpLocalTx(req: Request, res: Response) { + try { + const tx = req.body; + + if ( + !tx || typeof tx !== "object" || + !tx.txid || typeof tx.txid !== "string" || + typeof tx.weight !== "number" || + typeof tx.sigops !== "number" || + typeof tx.fee !== "number" || + !Array.isArray(tx.vin) || + !Array.isArray(tx.vout) + ) { + return res.status(400).json({ message: 'Invalid transaction format: missing or incorrect fields' }); + } + + const cpfpInfo = calculateLocalTxCpfp(tx, mempool.getMempool()); + res.json(cpfpInfo); + + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } } export default new BitcoinRoutes(); diff --git a/backend/src/api/cpfp.ts b/backend/src/api/cpfp.ts index 9da11328b..3421d9c7a 100644 --- a/backend/src/api/cpfp.ts +++ b/backend/src/api/cpfp.ts @@ -222,6 +222,34 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: }; } + +/** + * Takes an unbroadcasted transaction and a copy of the current mempool, and calculates an estimate + * of the CPFP data if the transaction were to enter the mempool. This only returns potential ancerstors + * and effective fee rate, and does not update the CPFP data of other transactions in the cluster. + */ +export function calculateLocalTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { + const ancestorMap = new Map(); + const graphTx = convertToGraphTx(tx, memPool.getSpendMap()); + ancestorMap.set(tx.txid, graphTx); + + const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap()); + const relativesMap = initializeRelatives(allRelatives); + const cluster = calculateCpfpCluster(tx.txid, relativesMap); + + let totalVsize = 0; + let totalFee = 0; + for (const tx of cluster.values()) { + totalVsize += tx.vsize; + totalFee += tx.fees.base; + } + + return { + ancestors: Array.from(cluster.get(tx.txid)?.ancestors.values() || []).map(ancestor => ({ txid: ancestor.txid, weight: ancestor.weight, fee: ancestor.fees.base })), + effectiveFeePerVsize: totalFee / totalVsize + } +} + /** * Given a root transaction and a list of in-mempool ancestors, * Calculate the CPFP cluster diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html index ca76d0e78..b6286779a 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.html +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -51,6 +51,12 @@ } + @if (errorCpfpInfo) { +
+ Error loading CPFP data. Reason: {{ errorCpfpInfo }} +
+ } + +
@@ -188,7 +197,9 @@ @if (isLoading) {
-

Loading transaction prevouts

+

+ Loading {{ isLoadingPrevouts ? 'transaction prevouts' : isLoadingCpfpInfo ? 'CPFP' : '' }} +

} \ No newline at end of file diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts index c9a0c2544..441a72f61 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.ts +++ b/frontend/src/app/components/transaction/transaction-raw.component.ts @@ -13,6 +13,7 @@ import { SeoService } from '../../services/seo.service'; import { seoDescriptionNetwork } from '@app/shared/common.utils'; import { ApiService } from '../../services/api.service'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; +import { CpfpInfo } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-transaction-raw', @@ -23,10 +24,13 @@ export class TransactionRawComponent implements OnInit, OnDestroy { pushTxForm: UntypedFormGroup; isLoading: boolean; + isLoadingPrevouts: boolean; + isLoadingCpfpInfo: boolean; offlineMode: boolean = false; transaction: Transaction; error: string; errorPrevouts: string; + errorCpfpInfo: string; hasPrevouts: boolean; missingPrevouts: string[]; isLoadingBroadcast: boolean; @@ -46,6 +50,10 @@ export class TransactionRawComponent implements OnInit, OnDestroy { flowEnabled: boolean; adjustedVsize: number; filters: Filter[] = []; + hasEffectiveFeeRate: boolean; + fetchCpfp: boolean; + cpfpInfo: CpfpInfo | null; + hasCpfp: boolean = false; showCpfpDetails = false; ETA$: Observable; mempoolBlocksSubscription: Subscription; @@ -78,6 +86,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { try { const tx = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network); await this.fetchPrevouts(tx); + await this.fetchCpfpInfo(tx); this.processTransaction(tx); } catch (error) { this.error = error.message; @@ -100,8 +109,9 @@ export class TransactionRawComponent implements OnInit, OnDestroy { try { this.missingPrevouts = []; + this.isLoadingPrevouts = true; - const prevouts: { prevout: Vout, tx?: any }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch)); + const prevouts: { prevout: Vout, unconfirmed: boolean }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch)); if (prevouts?.length !== prevoutsToFetch.length) { throw new Error(); @@ -121,27 +131,57 @@ export class TransactionRawComponent implements OnInit, OnDestroy { throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`); } + transaction.fee = transaction.vin.some(input => input.is_coinbase) + ? 0 + : transaction.vin.reduce((fee, input) => { + return fee + (input.prevout?.value || 0); + }, 0) - transaction.vout.reduce((sum, output) => sum + output.value, 0); + transaction.feePerVsize = transaction.fee / (transaction.weight / 4); + transaction.sigops = countSigops(transaction); + this.hasPrevouts = true; + this.isLoadingPrevouts = false; + this.fetchCpfp = prevouts.some(prevout => prevout?.unconfirmed); + } catch (error) { + this.errorPrevouts = error?.error?.message || error?.message; + this.isLoadingPrevouts = false; + } + } + + async fetchCpfpInfo(transaction: Transaction): Promise { + // Fetch potential cpfp data if all prevouts were parsed successfully and at least one of them is unconfirmed + if (this.hasPrevouts && this.fetchCpfp) { + try { + this.isLoadingCpfpInfo = true; + const cpfpInfo: CpfpInfo = await firstValueFrom(this.apiService.getCpfpLocalTx$({ + txid: transaction.txid, + weight: transaction.weight, + sigops: transaction.sigops, + fee: transaction.fee, + vin: transaction.vin, + vout: transaction.vout + })); + + if (cpfpInfo && cpfpInfo.ancestors.length > 0) { + const { ancestors, effectiveFeePerVsize } = cpfpInfo; + transaction.effectiveFeePerVsize = effectiveFeePerVsize; + this.cpfpInfo = { ancestors, effectiveFeePerVsize }; + this.hasCpfp = true; + this.hasEffectiveFeeRate = true; + } + this.isLoadingCpfpInfo = false; } catch (error) { - this.errorPrevouts = error.message; + this.errorCpfpInfo = error?.error?.message || error?.message; + this.isLoadingCpfpInfo = false; + } } } processTransaction(tx: Transaction): void { this.transaction = tx; - if (this.hasPrevouts) { - this.transaction.fee = this.transaction.vin.some(input => input.is_coinbase) - ? 0 - : this.transaction.vin.reduce((fee, input) => { - return fee + (input.prevout?.value || 0); - }, 0) - this.transaction.vout.reduce((sum, output) => sum + output.value, 0); - this.transaction.feePerVsize = this.transaction.fee / (this.transaction.weight / 4); - } - this.transaction.flags = getTransactionFlags(this.transaction, null, null, null, this.stateService.network); this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : []; - this.transaction.sigops = countSigops(this.transaction); if (this.transaction.sigops >= 0) { this.adjustedVsize = Math.max(this.transaction.weight / 4, this.transaction.sigops * 5); } @@ -155,16 +195,15 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.setGraphSize(); this.ETA$ = combineLatest([ - this.stateService.mempoolTxPosition$.pipe(startWith(null)), this.stateService.mempoolBlocks$.pipe(startWith(null)), this.stateService.difficultyAdjustment$.pipe(startWith(null)), ]).pipe( - map(([position, mempoolBlocks, da]) => { + map(([mempoolBlocks, da]) => { return this.etaService.calculateETA( this.stateService.network, this.transaction, mempoolBlocks, - position, + null, da, null, null, @@ -177,7 +216,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { if (this.transaction) { this.stateService.markBlock$.next({ txid: this.transaction.txid, - txFeePerVSize: this.transaction.feePerVsize, + txFeePerVSize: this.transaction.effectiveFeePerVsize || this.transaction.feePerVsize, }); } }); @@ -214,7 +253,15 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.errorBroadcast = null; this.successBroadcast = false; this.isLoading = false; + this.isLoadingPrevouts = false; + this.isLoadingCpfpInfo = false; + this.isLoadingBroadcast = false; this.adjustedVsize = null; + this.showCpfpDetails = false; + this.hasCpfp = false; + this.fetchCpfp = false; + this.cpfpInfo = null; + this.hasEffectiveFeeRate = false; this.filters = []; this.hasPrevouts = false; this.missingPrevouts = []; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index ce0e67cbf..698eede91 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -569,6 +569,10 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/prevouts', outpoints); } + getCpfpLocalTx$(tx: any): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp', tx); + } + // Cache methods async setBlockAuditLoaded(hash: string) { this.blockAuditLoaded[hash] = true; From 74fa3c7eb1654279c1454c04d1c47b6ad3827827 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 1 Jan 2025 16:51:34 +0000 Subject: [PATCH 008/114] conform getPrevouts and getCpfpLocalTx to new error handling standard --- backend/src/api/bitcoin/bitcoin.routes.ts | 25 +++++++++++-------- .../transaction/transaction-raw.component.ts | 5 ++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index f92b7cd0c..0e9016bde 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -936,11 +936,13 @@ class BitcoinRoutes { try { const outpoints = req.body; if (!Array.isArray(outpoints) || outpoints.some((item) => !/^[a-fA-F0-9]{64}$/.test(item.txid) || typeof item.vout !== 'number')) { - return res.status(400).json({ message: 'Invalid input format' }); + handleError(req, res, 400, 'Invalid outpoints format'); + return; } if (outpoints.length > 100) { - return res.status(400).json({ message: 'Too many prevouts requested' }); + handleError(req, res, 400, 'Too many outpoints requested'); + return; } const result = Array(outpoints.length).fill(null); @@ -955,7 +957,7 @@ class BitcoinRoutes { if (mempoolTx) { if (outpoint.vout < mempoolTx.vout.length) { prevout = mempoolTx.vout[outpoint.vout]; - unconfirmed = true; + unconfirmed = true; } } else { const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false); @@ -979,7 +981,7 @@ class BitcoinRoutes { res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get prevouts'); } } @@ -988,22 +990,23 @@ class BitcoinRoutes { const tx = req.body; if ( - !tx || typeof tx !== "object" || - !tx.txid || typeof tx.txid !== "string" || - typeof tx.weight !== "number" || - typeof tx.sigops !== "number" || - typeof tx.fee !== "number" || + !tx || typeof tx !== 'object' || + !tx.txid || typeof tx.txid !== 'string' || + typeof tx.weight !== 'number' || + typeof tx.sigops !== 'number' || + typeof tx.fee !== 'number' || !Array.isArray(tx.vin) || !Array.isArray(tx.vout) ) { - return res.status(400).json({ message: 'Invalid transaction format: missing or incorrect fields' }); + handleError(req, res, 400, 'Invalid transaction format'); + return; } const cpfpInfo = calculateLocalTxCpfp(tx, mempool.getMempool()); res.json(cpfpInfo); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to calculate CPFP info'); } } } diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts index 441a72f61..80f3eeb93 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.ts +++ b/frontend/src/app/components/transaction/transaction-raw.component.ts @@ -143,7 +143,8 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.isLoadingPrevouts = false; this.fetchCpfp = prevouts.some(prevout => prevout?.unconfirmed); } catch (error) { - this.errorPrevouts = error?.error?.message || error?.message; + console.log(error); + this.errorPrevouts = error?.error?.error || error?.message; this.isLoadingPrevouts = false; } } @@ -171,7 +172,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { } this.isLoadingCpfpInfo = false; } catch (error) { - this.errorCpfpInfo = error?.error?.message || error?.message; + this.errorCpfpInfo = error?.error?.error || error?.message; this.isLoadingCpfpInfo = false; } } From 5b331c144ba2498df23969a97fc257f227f1f9d8 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 8 Jan 2025 15:25:19 +0100 Subject: [PATCH 009/114] P2A address format decoding --- frontend/src/app/shared/transaction.utils.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index af1412838..b33d88c2f 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -988,7 +988,7 @@ function scriptPubKeyToAddress(scriptPubKey: string, network: string): { address } // anchor if (scriptPubKey === '51024e73') { - return { address: 'bc1pfeessrawgf', type: 'anchor' }; + return { address: p2a(network), type: 'anchor' }; } // op_return if (/^6a/.test(scriptPubKey)) { @@ -1048,6 +1048,15 @@ function p2tr(pubKeyHash: string, network: string): string { return bech32Address; } +function p2a(network: string): string { + const pubkeyHashArray = hexStringToUint8Array('4e73'); + const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc'; + const version = 1; + const words = [version].concat(toWords(pubkeyHashArray)); + const bech32Address = bech32Encode(hrp, words, 0x2bc830a3); + return bech32Address; +} + // base58 encoding function base58Encode(data: Uint8Array): string { const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; From af0c78be8129c0510dfcfe8827f17e13ec12d229 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 7 Jan 2025 15:01:36 +0100 Subject: [PATCH 010/114] Handle error from bitcoin client when querying prevouts --- backend/src/api/bitcoin/bitcoin.routes.ts | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 0e9016bde..545ad510c 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -960,16 +960,20 @@ class BitcoinRoutes { unconfirmed = true; } } else { - const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false); - if (rawPrevout) { - prevout = { - value: Math.round(rawPrevout.value * 100000000), - scriptpubkey: rawPrevout.scriptPubKey.hex, - scriptpubkey_asm: rawPrevout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(rawPrevout.scriptPubKey.hex) : '', - scriptpubkey_type: transactionUtils.translateScriptPubKeyType(rawPrevout.scriptPubKey.type), - scriptpubkey_address: rawPrevout.scriptPubKey && rawPrevout.scriptPubKey.address ? rawPrevout.scriptPubKey.address : '', - }; - unconfirmed = false; + try { + const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false); + if (rawPrevout) { + prevout = { + value: Math.round(rawPrevout.value * 100000000), + scriptpubkey: rawPrevout.scriptPubKey.hex, + scriptpubkey_asm: rawPrevout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(rawPrevout.scriptPubKey.hex) : '', + scriptpubkey_type: transactionUtils.translateScriptPubKeyType(rawPrevout.scriptPubKey.type), + scriptpubkey_address: rawPrevout.scriptPubKey && rawPrevout.scriptPubKey.address ? rawPrevout.scriptPubKey.address : '', + }; + unconfirmed = false; + } + } catch (e) { + // Ignore bitcoin client errors, just leave prevout as null } } From 6c95cd21491cfb377a14fd82f82e773a16d20d79 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 8 Jan 2025 15:07:37 +0100 Subject: [PATCH 011/114] Update local cpfp API to accept array of transactions --- backend/src/api/bitcoin/bitcoin.routes.ts | 23 +++++++++++-------- backend/src/api/cpfp.ts | 1 - .../transaction/transaction-raw.component.ts | 8 +++---- frontend/src/app/services/api.service.ts | 4 ++-- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 545ad510c..d49cd95b2 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -55,7 +55,7 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts) - .post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTx) + .post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTxs) // Temporarily add txs/package endpoint for all backends until esplora supports it .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) ; @@ -989,25 +989,30 @@ class BitcoinRoutes { } } - private getCpfpLocalTx(req: Request, res: Response) { + private getCpfpLocalTxs(req: Request, res: Response) { try { - const tx = req.body; + const transactions = req.body; - if ( + if (!Array.isArray(transactions) || transactions.some(tx => !tx || typeof tx !== 'object' || - !tx.txid || typeof tx.txid !== 'string' || + !/^[a-fA-F0-9]{64}$/.test(tx.txid) || typeof tx.weight !== 'number' || typeof tx.sigops !== 'number' || typeof tx.fee !== 'number' || !Array.isArray(tx.vin) || !Array.isArray(tx.vout) - ) { - handleError(req, res, 400, 'Invalid transaction format'); + )) { + handleError(req, res, 400, 'Invalid transactions format'); return; } - const cpfpInfo = calculateLocalTxCpfp(tx, mempool.getMempool()); - res.json(cpfpInfo); + if (transactions.length > 1) { + handleError(req, res, 400, 'More than one transaction is not supported yet'); + return; + } + + const cpfpInfo = calculateLocalTxCpfp(transactions[0], mempool.getMempool()); + res.json([cpfpInfo]); } catch (e) { handleError(req, res, 500, 'Failed to calculate CPFP info'); diff --git a/backend/src/api/cpfp.ts b/backend/src/api/cpfp.ts index 3421d9c7a..2b9f90542 100644 --- a/backend/src/api/cpfp.ts +++ b/backend/src/api/cpfp.ts @@ -222,7 +222,6 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: }; } - /** * Takes an unbroadcasted transaction and a copy of the current mempool, and calculates an estimate * of the CPFP data if the transaction were to enter the mempool. This only returns potential ancerstors diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts index 80f3eeb93..8917dce87 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.ts +++ b/frontend/src/app/components/transaction/transaction-raw.component.ts @@ -154,17 +154,17 @@ export class TransactionRawComponent implements OnInit, OnDestroy { if (this.hasPrevouts && this.fetchCpfp) { try { this.isLoadingCpfpInfo = true; - const cpfpInfo: CpfpInfo = await firstValueFrom(this.apiService.getCpfpLocalTx$({ + const cpfpInfo: CpfpInfo[] = await firstValueFrom(this.apiService.getCpfpLocalTx$([{ txid: transaction.txid, weight: transaction.weight, sigops: transaction.sigops, fee: transaction.fee, vin: transaction.vin, vout: transaction.vout - })); + }])); - if (cpfpInfo && cpfpInfo.ancestors.length > 0) { - const { ancestors, effectiveFeePerVsize } = cpfpInfo; + if (cpfpInfo?.[0]?.ancestors?.length) { + const { ancestors, effectiveFeePerVsize } = cpfpInfo[0]; transaction.effectiveFeePerVsize = effectiveFeePerVsize; this.cpfpInfo = { ancestors, effectiveFeePerVsize }; this.hasCpfp = true; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 698eede91..d958bfa25 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -569,8 +569,8 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/prevouts', outpoints); } - getCpfpLocalTx$(tx: any): Observable { - return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp', tx); + getCpfpLocalTx$(tx: any[]): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp', tx); } // Cache methods From 860bc7d14db79b9e64632d17bc479fa63cbd78e8 Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 9 Jan 2025 14:35:34 +0100 Subject: [PATCH 012/114] Merge calculateMempoolTxCpfp and calculateLocalTxCpfp --- backend/src/api/bitcoin/bitcoin.routes.ts | 4 +- backend/src/api/cpfp.ts | 60 +++++++++-------------- 2 files changed, 24 insertions(+), 40 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index d49cd95b2..cb1bb0696 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -19,7 +19,7 @@ import bitcoinClient from './bitcoin-client'; import difficultyAdjustment from '../difficulty-adjustment'; import transactionRepository from '../../repositories/TransactionRepository'; import rbfCache from '../rbf-cache'; -import { calculateMempoolTxCpfp, calculateLocalTxCpfp } from '../cpfp'; +import { calculateMempoolTxCpfp } from '../cpfp'; import { handleError } from '../../utils/api'; const TXID_REGEX = /^[a-f0-9]{64}$/i; @@ -1011,7 +1011,7 @@ class BitcoinRoutes { return; } - const cpfpInfo = calculateLocalTxCpfp(transactions[0], mempool.getMempool()); + const cpfpInfo = calculateMempoolTxCpfp(transactions[0], mempool.getMempool(), true); res.json([cpfpInfo]); } catch (e) { diff --git a/backend/src/api/cpfp.ts b/backend/src/api/cpfp.ts index 2b9f90542..953664fcc 100644 --- a/backend/src/api/cpfp.ts +++ b/backend/src/api/cpfp.ts @@ -167,8 +167,10 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran /** * Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for * that transaction (and all others in the same cluster) + * If the passed transaction is not guaranteed to be in the mempool, set localTx to true: this will + * prevent updating the CPFP data of other transactions in the cluster */ -export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { +export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }, localTx: boolean = false): CpfpInfo { if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) { tx.cpfpDirty = false; return { @@ -198,17 +200,26 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: totalFee += tx.fees.base; } const effectiveFeePerVsize = totalFee / totalVsize; - for (const tx of cluster.values()) { - mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; - mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); - mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); - mempool[tx.txid].bestDescendant = null; - mempool[tx.txid].cpfpChecked = true; - mempool[tx.txid].cpfpDirty = true; - mempool[tx.txid].cpfpUpdated = Date.now(); - } - tx = mempool[tx.txid]; + if (localTx) { + tx.effectiveFeePerVsize = effectiveFeePerVsize; + tx.ancestors = Array.from(cluster.get(tx.txid)?.ancestors.values() || []).map(ancestor => ({ txid: ancestor.txid, weight: ancestor.weight, fee: ancestor.fees.base })); + tx.descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !cluster.get(tx.txid)?.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); + tx.bestDescendant = null; + } else { + for (const tx of cluster.values()) { + mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; + mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); + mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); + mempool[tx.txid].bestDescendant = null; + mempool[tx.txid].cpfpChecked = true; + mempool[tx.txid].cpfpDirty = true; + mempool[tx.txid].cpfpUpdated = Date.now(); + } + + tx = mempool[tx.txid]; + + } return { ancestors: tx.ancestors || [], @@ -222,33 +233,6 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: }; } -/** - * Takes an unbroadcasted transaction and a copy of the current mempool, and calculates an estimate - * of the CPFP data if the transaction were to enter the mempool. This only returns potential ancerstors - * and effective fee rate, and does not update the CPFP data of other transactions in the cluster. - */ -export function calculateLocalTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { - const ancestorMap = new Map(); - const graphTx = convertToGraphTx(tx, memPool.getSpendMap()); - ancestorMap.set(tx.txid, graphTx); - - const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap()); - const relativesMap = initializeRelatives(allRelatives); - const cluster = calculateCpfpCluster(tx.txid, relativesMap); - - let totalVsize = 0; - let totalFee = 0; - for (const tx of cluster.values()) { - totalVsize += tx.vsize; - totalFee += tx.fees.base; - } - - return { - ancestors: Array.from(cluster.get(tx.txid)?.ancestors.values() || []).map(ancestor => ({ txid: ancestor.txid, weight: ancestor.weight, fee: ancestor.fees.base })), - effectiveFeePerVsize: totalFee / totalVsize - } -} - /** * Given a root transaction and a list of in-mempool ancestors, * Calculate the CPFP cluster From 9660723e96f0fe89867e116a01159e2fe151cc9b Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 02:39:48 +0000 Subject: [PATCH 013/114] Translate frontend/src/locale/messages.xlf in fr 100% reviewed source file: 'frontend/src/locale/messages.xlf' on 'fr'. --- frontend/src/locale/messages.fr.xlf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/locale/messages.fr.xlf b/frontend/src/locale/messages.fr.xlf index 51e7204b7..c193f296b 100644 --- a/frontend/src/locale/messages.fr.xlf +++ b/frontend/src/locale/messages.fr.xlf @@ -9847,7 +9847,7 @@ confirmation - confirmation + confirmation src/app/shared/components/confirmations/confirmations.component.html 4 From a62a3cc774093b9e23e3b31be37a32a93c345a51 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 06:29:40 +0000 Subject: [PATCH 014/114] Translate frontend/src/locale/messages.xlf in fr 100% reviewed source file: 'frontend/src/locale/messages.xlf' on 'fr'. --- frontend/src/locale/messages.fr.xlf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/locale/messages.fr.xlf b/frontend/src/locale/messages.fr.xlf index c193f296b..aac7eead9 100644 --- a/frontend/src/locale/messages.fr.xlf +++ b/frontend/src/locale/messages.fr.xlf @@ -1567,7 +1567,7 @@ Total Bid Boost - Augmentation totale des frais + Total frais ajoutés src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html 11 @@ -1728,7 +1728,7 @@ Bid Boost - Augmentation des frais + Frais ajoutés src/app/components/acceleration/accelerations-list/accelerations-list.component.html 17 @@ -6739,7 +6739,7 @@ Just now - Juste maintenant + À l'instant src/app/components/time/time.component.ts 111 From aad4783415f791ec50ef8fb8989a4312d05a91fe Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 4 Feb 2025 21:33:21 +0100 Subject: [PATCH 015/114] fix cors --- backend/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/index.ts b/backend/src/index.ts index dc6a8ae1a..3cc73afb7 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -131,6 +131,9 @@ class Server { this.app .use((req: Request, res: Response, next: NextFunction) => { res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'); + res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count,X-Mempool-Auth'); next(); }) .use(express.urlencoded({ extended: true })) From 5e0cbb084a391659b5872e090a54b55873d620c0 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 7 Feb 2025 11:34:18 +0100 Subject: [PATCH 016/114] add new fa icon --- frontend/src/app/shared/shared.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 76184113f..d937e6bbb 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, - faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard } from '@fortawesome/free-solid-svg-icons'; + faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '@components/menu/menu.component'; import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; @@ -464,5 +464,6 @@ export class SharedModule { library.addIcons(faRobot); library.addIcons(faShareNodes); library.addIcons(faCreditCard); + library.addIcons(faMicroscope); } } From 1779c672e359b0d678f49a139d64becdaccfcdf4 Mon Sep 17 00:00:00 2001 From: natsoni Date: Fri, 7 Feb 2025 16:52:46 +0100 Subject: [PATCH 017/114] Don't tweak scrollLeft if time is left to right --- .../app/components/start/start.component.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/start/start.component.ts b/frontend/src/app/components/start/start.component.ts index 31317cab5..7db1a75e1 100644 --- a/frontend/src/app/components/start/start.component.ts +++ b/frontend/src/app/components/start/start.component.ts @@ -194,14 +194,16 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy { applyScrollLeft(): void { if (this.blockchainContainer?.nativeElement?.scrollWidth) { let lastScrollLeft = null; - while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) { - lastScrollLeft = this.scrollLeft; - this.scrollLeft += this.pageWidth; - } - lastScrollLeft = null; - while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) { - lastScrollLeft = this.scrollLeft; - this.scrollLeft -= this.pageWidth; + if (!this.timeLtr) { + while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) { + lastScrollLeft = this.scrollLeft; + this.scrollLeft += this.pageWidth; + } + lastScrollLeft = null; + while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) { + lastScrollLeft = this.scrollLeft; + this.scrollLeft -= this.pageWidth; + } } this.blockchainContainer.nativeElement.scrollLeft = this.scrollLeft; } From 3691eda9d1311c49a3e4f9822df56c3d33995167 Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Sat, 8 Feb 2025 14:40:04 -0500 Subject: [PATCH 018/114] Add fortris to enterprise sponsors --- frontend/src/app/components/about/about.component.html | 9 +++++++++ frontend/src/app/components/about/about.component.scss | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 433fe1abb..51dad662f 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -146,6 +146,15 @@ Bull Bitcoin + + + Fortris + diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 6a76bf299..e756473a6 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -264,6 +264,6 @@ display: flex; flex-wrap: wrap; justify-content: center; - max-width: 800px; + max-width: 850px; } -} \ No newline at end of file +} From 6340dc571c6b5ae99eaadd275beddbe49502c3f2 Mon Sep 17 00:00:00 2001 From: natsoni Date: Sun, 9 Feb 2025 18:13:06 +0100 Subject: [PATCH 019/114] Don't show ETA on unbroadcasted txs, and placeholder for missing fee --- .../transaction-details.component.html | 8 +++---- .../transaction-details.component.ts | 1 + .../transaction-raw.component.html | 4 +--- .../transaction/transaction-raw.component.ts | 23 +------------------ 4 files changed, 7 insertions(+), 29 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html index c5609882c..78bba955c 100644 --- a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html +++ b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html @@ -153,7 +153,7 @@ @if (!isLoadingTx) { - @if (!replaced && !isCached) { + @if (!replaced && !isCached && !unbroadcasted) {
ETA @@ -184,7 +184,7 @@
} - } @else { + } @else if (!unbroadcasted){ }
@@ -213,11 +213,11 @@ @if (!isLoadingTx) {
Fee - {{ tx.fee | number }}
sats + {{ (tx.fee | number) ?? '-' }} sats @if (isAcceleration && accelerationInfo?.bidBoost ?? tx.feeDelta > 0) { +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} sats } - + } @else { diff --git a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.ts b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.ts index 2b539c154..c6260da48 100644 --- a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.ts +++ b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.ts @@ -38,6 +38,7 @@ export class TransactionDetailsComponent implements OnInit { @Input() replaced: boolean; @Input() isCached: boolean; @Input() ETA$: Observable; + @Input() unbroadcasted: boolean; @Output() accelerateClicked = new EventEmitter(); @Output() toggleCpfp$ = new EventEmitter(); diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html index b6286779a..450e18ecd 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.html +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -58,6 +58,7 @@ } ; mempoolBlocksSubscription: Subscription; constructor( public route: ActivatedRoute, public router: Router, public stateService: StateService, - public etaService: EtaService, public electrsApi: ElectrsApiService, public websocketService: WebsocketService, public formBuilder: UntypedFormBuilder, @@ -195,24 +192,6 @@ export class TransactionRawComponent implements OnInit, OnDestroy { }); this.setGraphSize(); - this.ETA$ = combineLatest([ - this.stateService.mempoolBlocks$.pipe(startWith(null)), - this.stateService.difficultyAdjustment$.pipe(startWith(null)), - ]).pipe( - map(([mempoolBlocks, da]) => { - return this.etaService.calculateETA( - this.stateService.network, - this.transaction, - mempoolBlocks, - null, - da, - null, - null, - null - ); - }) - ); - this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe(() => { if (this.transaction) { this.stateService.markBlock$.next({ From 27c28f939c31e50c3aad0dc4e014dab3ea4ecfec Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 10 Feb 2025 03:47:05 +0000 Subject: [PATCH 020/114] misc unfurl preview fixes --- .../address/address-preview.component.ts | 10 +++--- .../block/block-preview.component.html | 2 +- .../block/block-preview.component.ts | 23 ++++++++------ .../components/pool/pool-preview.component.ts | 28 +++++++++-------- .../transaction-preview.component.ts | 20 ++++++------ .../wallet/wallet-preview.component.ts | 20 ++++++------ .../channel/channel-preview.component.ts | 14 +++++---- .../group/group-preview.component.ts | 18 ++++++----- .../lightning/node/node-preview.component.ts | 14 +++++---- .../nodes-per-isp-preview.component.ts | 14 +++++---- .../src/app/services/opengraph.service.ts | 31 ++++++++++++------- 11 files changed, 111 insertions(+), 83 deletions(-) diff --git a/frontend/src/app/components/address/address-preview.component.ts b/frontend/src/app/components/address/address-preview.component.ts index bcc328787..1106d6096 100644 --- a/frontend/src/app/components/address/address-preview.component.ts +++ b/frontend/src/app/components/address/address-preview.component.ts @@ -36,6 +36,8 @@ export class AddressPreviewComponent implements OnInit, OnDestroy { sent = 0; totalUnspent = 0; + ogSession: number; + constructor( private route: ActivatedRoute, private electrsApiService: ElectrsApiService, @@ -58,7 +60,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy { .pipe( switchMap((params: ParamMap) => { this.rawAddress = params.get('id') || ''; - this.openGraphService.waitFor('address-data-' + this.rawAddress); + this.ogSession = this.openGraphService.waitFor('address-data-' + this.rawAddress); this.error = undefined; this.isLoadingAddress = true; this.loadedConfirmedTxCount = 0; @@ -79,7 +81,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy { this.isLoadingAddress = false; this.error = err; console.log(err); - this.openGraphService.fail('address-data-' + this.rawAddress); + this.openGraphService.fail({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession }); return of(null); }) ); @@ -97,7 +99,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy { this.address = address; this.updateChainStats(); this.isLoadingAddress = false; - this.openGraphService.waitOver('address-data-' + this.rawAddress); + this.openGraphService.waitOver({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession }); }) ) .subscribe(() => {}, @@ -105,7 +107,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy { console.log(error); this.error = error; this.isLoadingAddress = false; - this.openGraphService.fail('address-data-' + this.rawAddress); + this.openGraphService.fail({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession }); } ); } diff --git a/frontend/src/app/components/block/block-preview.component.html b/frontend/src/app/components/block/block-preview.component.html index 036ab8399..6ea8e3387 100644 --- a/frontend/src/app/components/block/block-preview.component.html +++ b/frontend/src/app/components/block/block-preview.component.html @@ -49,7 +49,7 @@
- + Miner diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts index 42a47f3c4..f5b31e846 100644 --- a/frontend/src/app/components/block/block-preview.component.ts +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -35,6 +35,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { overviewSubscription: Subscription; networkChangedSubscription: Subscription; + ogSession: number; + @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; constructor( @@ -53,8 +55,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { const block$ = this.route.paramMap.pipe( switchMap((params: ParamMap) => { this.rawId = params.get('id') || ''; - this.openGraphService.waitFor('block-viz-' + this.rawId); - this.openGraphService.waitFor('block-data-' + this.rawId); + this.ogSession = this.openGraphService.waitFor('block-viz-' + this.rawId); + this.ogSession = this.openGraphService.waitFor('block-data-' + this.rawId); const blockHash: string = params.get('id') || ''; this.block = undefined; @@ -86,8 +88,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { catchError((err) => { this.error = err; this.seoService.logSoft404(); - this.openGraphService.fail('block-data-' + this.rawId); - this.openGraphService.fail('block-viz-' + this.rawId); + this.openGraphService.fail({ event: 'block-data-' + this.rawId, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession }); return of(null); }), ); @@ -114,7 +116,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { this.isLoadingOverview = true; this.overviewError = null; - this.openGraphService.waitOver('block-data-' + this.rawId); + this.openGraphService.waitOver({ event: 'block-data-' + this.rawId, sessionId: this.ogSession }); }), throttleTime(50, asyncScheduler, { leading: true, trailing: true }), shareReplay({ bufferSize: 1, refCount: true }) @@ -129,7 +131,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { .pipe( catchError((err) => { this.overviewError = err; - this.openGraphService.fail('block-viz-' + this.rawId); + this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession }); return of([]); }), switchMap((transactions) => { @@ -138,7 +140,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { ), this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) - .pipe(catchError(() => { + .pipe( + catchError(() => { return of([]); })) : of([]) @@ -169,8 +172,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { this.error = error; this.isLoadingOverview = false; this.seoService.logSoft404(); - this.openGraphService.fail('block-viz-' + this.rawId); - this.openGraphService.fail('block-data-' + this.rawId); + this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'block-data-' + this.rawId, sessionId: this.ogSession }); if (this.blockGraph) { this.blockGraph.destroy(); } @@ -196,6 +199,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { } onGraphReady(): void { - this.openGraphService.waitOver('block-viz-' + this.rawId); + this.openGraphService.waitOver({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession }); } } diff --git a/frontend/src/app/components/pool/pool-preview.component.ts b/frontend/src/app/components/pool/pool-preview.component.ts index 93077120d..7478e5f6f 100644 --- a/frontend/src/app/components/pool/pool-preview.component.ts +++ b/frontend/src/app/components/pool/pool-preview.component.ts @@ -30,6 +30,8 @@ export class PoolPreviewComponent implements OnInit { slug: string = undefined; + ogSession: number; + constructor( @Inject(LOCALE_ID) public locale: string, private apiService: ApiService, @@ -47,22 +49,22 @@ export class PoolPreviewComponent implements OnInit { this.isLoading = true; this.imageLoaded = false; this.slug = slug; - this.openGraphService.waitFor('pool-hash-' + this.slug); - this.openGraphService.waitFor('pool-stats-' + this.slug); - this.openGraphService.waitFor('pool-chart-' + this.slug); - this.openGraphService.waitFor('pool-img-' + this.slug); + this.ogSession = this.openGraphService.waitFor('pool-hash-' + this.slug); + this.ogSession = this.openGraphService.waitFor('pool-stats-' + this.slug); + this.ogSession = this.openGraphService.waitFor('pool-chart-' + this.slug); + this.ogSession = this.openGraphService.waitFor('pool-img-' + this.slug); return this.apiService.getPoolHashrate$(this.slug) .pipe( switchMap((data) => { this.isLoading = false; this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate])); - this.openGraphService.waitOver('pool-hash-' + this.slug); + this.openGraphService.waitOver({ event: 'pool-hash-' + this.slug, sessionId: this.ogSession }); return [slug]; }), catchError(() => { this.isLoading = false; this.seoService.logSoft404(); - this.openGraphService.fail('pool-hash-' + this.slug); + this.openGraphService.fail({ event: 'pool-hash-' + this.slug, sessionId: this.ogSession }); return of([slug]); }) ); @@ -72,7 +74,7 @@ export class PoolPreviewComponent implements OnInit { catchError(() => { this.isLoading = false; this.seoService.logSoft404(); - this.openGraphService.fail('pool-stats-' + this.slug); + this.openGraphService.fail({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession }); return of(null); }) ); @@ -90,11 +92,11 @@ export class PoolPreviewComponent implements OnInit { } poolStats.pool.regexes = regexes.slice(0, -3); - this.openGraphService.waitOver('pool-stats-' + this.slug); + this.openGraphService.waitOver({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession }); const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg'; if (logoSrc === this.lastImgSrc) { - this.openGraphService.waitOver('pool-img-' + this.slug); + this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession }); } this.lastImgSrc = logoSrc; return Object.assign({ @@ -103,7 +105,7 @@ export class PoolPreviewComponent implements OnInit { }), catchError(() => { this.isLoading = false; - this.openGraphService.fail('pool-stats-' + this.slug); + this.openGraphService.fail({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession }); return of(null); }) ); @@ -170,16 +172,16 @@ export class PoolPreviewComponent implements OnInit { } onChartReady(): void { - this.openGraphService.waitOver('pool-chart-' + this.slug); + this.openGraphService.waitOver({ event: 'pool-chart-' + this.slug, sessionId: this.ogSession }); } onImageLoad(): void { this.imageLoaded = true; - this.openGraphService.waitOver('pool-img-' + this.slug); + this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession }); } onImageFail(): void { this.imageLoaded = false; - this.openGraphService.waitOver('pool-img-' + this.slug); + this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession }); } } diff --git a/frontend/src/app/components/transaction/transaction-preview.component.ts b/frontend/src/app/components/transaction/transaction-preview.component.ts index 0c51e0064..4746f9de7 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.ts +++ b/frontend/src/app/components/transaction/transaction-preview.component.ts @@ -43,6 +43,8 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { opReturns: Vout[]; extraData: 'none' | 'coinbase' | 'opreturn'; + ogSession: number; + constructor( private route: ActivatedRoute, private electrsApiService: ElectrsApiService, @@ -75,7 +77,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { ) .subscribe((cpfpInfo) => { this.cpfpInfo = cpfpInfo; - this.openGraphService.waitOver('cpfp-data-' + this.txId); + this.openGraphService.waitOver({ event: 'cpfp-data-' + this.txId, sessionId: this.ogSession }); }); this.subscription = this.route.paramMap @@ -83,8 +85,8 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { switchMap((params: ParamMap) => { const urlMatch = (params.get('id') || '').split(':'); this.txId = urlMatch[0]; - this.openGraphService.waitFor('tx-data-' + this.txId); - this.openGraphService.waitFor('tx-time-' + this.txId); + this.ogSession = this.openGraphService.waitFor('tx-data-' + this.txId); + this.ogSession = this.openGraphService.waitFor('tx-time-' + this.txId); this.seoService.setTitle( $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:` ); @@ -138,7 +140,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { .subscribe((tx: Transaction) => { if (!tx) { this.seoService.logSoft404(); - this.openGraphService.fail('tx-data-' + this.txId); + this.openGraphService.fail({ event: 'tx-data-' + this.txId, sessionId: this.ogSession }); return; } @@ -155,10 +157,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { if (tx.status.confirmed) { this.transactionTime = tx.status.block_time; - this.openGraphService.waitOver('tx-time-' + this.txId); + this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession }); } else if (!tx.status.confirmed && tx.firstSeen) { this.transactionTime = tx.firstSeen; - this.openGraphService.waitOver('tx-time-' + this.txId); + this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession }); } else { this.getTransactionTime(); } @@ -184,11 +186,11 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { } } - this.openGraphService.waitOver('tx-data-' + this.txId); + this.openGraphService.waitOver({ event: 'tx-data-' + this.txId, sessionId: this.ogSession }); }, (error) => { this.seoService.logSoft404(); - this.openGraphService.fail('tx-data-' + this.txId); + this.openGraphService.fail({ event: 'tx-data-' + this.txId, sessionId: this.ogSession }); this.error = error; this.isLoadingTx = false; } @@ -205,7 +207,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { ) .subscribe((transactionTimes) => { this.transactionTime = transactionTimes[0]; - this.openGraphService.waitOver('tx-time-' + this.txId); + this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession }); }); } diff --git a/frontend/src/app/components/wallet/wallet-preview.component.ts b/frontend/src/app/components/wallet/wallet-preview.component.ts index 0387822aa..bbf8ecf7a 100644 --- a/frontend/src/app/components/wallet/wallet-preview.component.ts +++ b/frontend/src/app/components/wallet/wallet-preview.component.ts @@ -125,6 +125,8 @@ export class WalletPreviewComponent implements OnInit, OnDestroy { sent = 0; chainBalance = 0; + ogSession: number; + constructor( private route: ActivatedRoute, private stateService: StateService, @@ -141,9 +143,9 @@ export class WalletPreviewComponent implements OnInit, OnDestroy { map((params: ParamMap) => params.get('wallet') as string), tap((walletName: string) => { this.walletName = walletName; - this.openGraphService.waitFor('wallet-addresses-' + this.walletName); - this.openGraphService.waitFor('wallet-data-' + this.walletName); - this.openGraphService.waitFor('wallet-txs-' + this.walletName); + this.ogSession = this.openGraphService.waitFor('wallet-addresses-' + this.walletName); + this.ogSession = this.openGraphService.waitFor('wallet-data-' + this.walletName); + this.ogSession = this.openGraphService.waitFor('wallet-txs-' + this.walletName); this.seoService.setTitle($localize`:@@wallet.component.browser-title:Wallet: ${walletName}:INTERPOLATION:`); this.seoService.setDescription($localize`:@@meta.description.bitcoin.wallet:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} wallet ${walletName}:INTERPOLATION:.`); }), @@ -152,9 +154,9 @@ export class WalletPreviewComponent implements OnInit, OnDestroy { this.error = err; this.seoService.logSoft404(); console.log(err); - this.openGraphService.fail('wallet-addresses-' + this.walletName); - this.openGraphService.fail('wallet-data-' + this.walletName); - this.openGraphService.fail('wallet-txs-' + this.walletName); + this.openGraphService.fail({ event: 'wallet-addresses-' + this.walletName, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'wallet-data-' + this.walletName, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'wallet-txs-' + this.walletName, sessionId: this.ogSession }); return of({}); }) )), @@ -185,13 +187,13 @@ export class WalletPreviewComponent implements OnInit, OnDestroy { this.walletSubscription = this.walletAddresses$.subscribe(wallet => { this.addressStrings = Object.keys(wallet); this.addresses = Object.values(wallet); - this.openGraphService.waitOver('wallet-addresses-' + this.walletName); + this.openGraphService.waitOver({ event: 'wallet-addresses-' + this.walletName, sessionId: this.ogSession }); }); this.walletSummary$ = this.wallet$.pipe( map(wallet => this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))), tap(() => { - this.openGraphService.waitOver('wallet-txs-' + this.walletName); + this.openGraphService.waitOver({ event: 'wallet-txs-' + this.walletName, sessionId: this.ogSession }); }) ); @@ -209,7 +211,7 @@ export class WalletPreviewComponent implements OnInit, OnDestroy { ); }), tap(() => { - this.openGraphService.waitOver('wallet-data-' + this.walletName); + this.openGraphService.waitOver({ event: 'wallet-data-' + this.walletName, sessionId: this.ogSession }); }) ); } diff --git a/frontend/src/app/lightning/channel/channel-preview.component.ts b/frontend/src/app/lightning/channel/channel-preview.component.ts index 84a85f9c6..2af0dcd57 100644 --- a/frontend/src/app/lightning/channel/channel-preview.component.ts +++ b/frontend/src/app/lightning/channel/channel-preview.component.ts @@ -18,6 +18,8 @@ export class ChannelPreviewComponent implements OnInit { channelGeo: number[] = []; shortId: string; + ogSession: number; + constructor( private lightningApiService: LightningApiService, private activatedRoute: ActivatedRoute, @@ -30,8 +32,8 @@ export class ChannelPreviewComponent implements OnInit { .pipe( switchMap((params: ParamMap) => { this.shortId = params.get('short_id') || ''; - this.openGraphService.waitFor('channel-map-' + this.shortId); - this.openGraphService.waitFor('channel-data-' + this.shortId); + this.ogSession = this.openGraphService.waitFor('channel-map-' + this.shortId); + this.ogSession = this.openGraphService.waitFor('channel-data-' + this.shortId); this.error = null; this.seoService.setTitle(`Channel: ${params.get('short_id')}`); this.seoService.setDescription($localize`:@@meta.description.lightning.channel:Overview for Lightning channel ${params.get('short_id')}. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.`); @@ -51,13 +53,13 @@ export class ChannelPreviewComponent implements OnInit { data.node_right.longitude, data.node_right.latitude, ]; } - this.openGraphService.waitOver('channel-data-' + this.shortId); + this.openGraphService.waitOver({ event: 'channel-data-' + this.shortId, sessionId: this.ogSession }); }), catchError((err) => { this.error = err; this.seoService.logSoft404(); - this.openGraphService.fail('channel-map-' + this.shortId); - this.openGraphService.fail('channel-data-' + this.shortId); + this.openGraphService.fail({ event: 'channel-map-' + this.shortId, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'channel-data-' + this.shortId, sessionId: this.ogSession }); return of(null); }) ); @@ -66,6 +68,6 @@ export class ChannelPreviewComponent implements OnInit { } onMapReady() { - this.openGraphService.waitOver('channel-map-' + this.shortId); + this.openGraphService.waitOver({ event: 'channel-map-' + this.shortId, sessionId: this.ogSession }); } } diff --git a/frontend/src/app/lightning/group/group-preview.component.ts b/frontend/src/app/lightning/group/group-preview.component.ts index 4b8f5ed77..4e7d56bbe 100644 --- a/frontend/src/app/lightning/group/group-preview.component.ts +++ b/frontend/src/app/lightning/group/group-preview.component.ts @@ -22,6 +22,8 @@ export class GroupPreviewComponent implements OnInit { slug: string; groupId: string; + ogSession: number; + constructor( private lightningApiService: LightningApiService, private activatedRoute: ActivatedRoute, @@ -37,8 +39,8 @@ export class GroupPreviewComponent implements OnInit { .pipe( switchMap((params: ParamMap) => { this.slug = params.get('slug'); - this.openGraphService.waitFor('ln-group-map-' + this.slug); - this.openGraphService.waitFor('ln-group-data-' + this.slug); + this.ogSession = this.openGraphService.waitFor('ln-group-map-' + this.slug); + this.ogSession = this.openGraphService.waitFor('ln-group-data-' + this.slug); if (this.slug === 'the-mempool-open-source-project') { this.groupId = 'mempool.space'; @@ -52,8 +54,8 @@ export class GroupPreviewComponent implements OnInit { description: '', }; this.seoService.logSoft404(); - this.openGraphService.fail('ln-group-map-' + this.slug); - this.openGraphService.fail('ln-group-data-' + this.slug); + this.openGraphService.fail({ event: 'ln-group-map-' + this.slug, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'ln-group-data-' + this.slug, sessionId: this.ogSession }); return of(null); } @@ -99,7 +101,7 @@ export class GroupPreviewComponent implements OnInit { const sumLiquidity = nodes.reduce((partialSum, a) => partialSum + parseInt(a.capacity, 10), 0); const sumChannels = nodes.reduce((partialSum, a) => partialSum + a.opened_channel_count, 0); - this.openGraphService.waitOver('ln-group-data-' + this.slug); + this.openGraphService.waitOver({ event: 'ln-group-data-' + this.slug, sessionId: this.ogSession }); return { nodes: nodes, @@ -109,8 +111,8 @@ export class GroupPreviewComponent implements OnInit { }), catchError(() => { this.seoService.logSoft404(); - this.openGraphService.fail('ln-group-map-' + this.slug); - this.openGraphService.fail('ln-group-data-' + this.slug); + this.openGraphService.fail({ event: 'ln-group-map-' + this.slug, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'ln-group-data-' + this.slug, sessionId: this.ogSession }); return of({ nodes: [], sumLiquidity: 0, @@ -121,7 +123,7 @@ export class GroupPreviewComponent implements OnInit { } onMapReady(): void { - this.openGraphService.waitOver('ln-group-map-' + this.slug); + this.openGraphService.waitOver({ event: 'ln-group-map-' + this.slug, sessionId: this.ogSession }); } } diff --git a/frontend/src/app/lightning/node/node-preview.component.ts b/frontend/src/app/lightning/node/node-preview.component.ts index 259313de6..7a45ea905 100644 --- a/frontend/src/app/lightning/node/node-preview.component.ts +++ b/frontend/src/app/lightning/node/node-preview.component.ts @@ -27,6 +27,8 @@ export class NodePreviewComponent implements OnInit { publicKeySize = 99; + ogSession: number; + constructor( private lightningApiService: LightningApiService, private activatedRoute: ActivatedRoute, @@ -43,8 +45,8 @@ export class NodePreviewComponent implements OnInit { .pipe( switchMap((params: ParamMap) => { this.publicKey = params.get('public_key'); - this.openGraphService.waitFor('node-map-' + this.publicKey); - this.openGraphService.waitFor('node-data-' + this.publicKey); + this.ogSession = this.openGraphService.waitFor('node-map-' + this.publicKey); + this.ogSession = this.openGraphService.waitFor('node-data-' + this.publicKey); return this.lightningApiService.getNode$(params.get('public_key')); }), map((node) => { @@ -76,15 +78,15 @@ export class NodePreviewComponent implements OnInit { this.socketTypes = Object.keys(socketTypesMap); node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count); - this.openGraphService.waitOver('node-data-' + this.publicKey); + this.openGraphService.waitOver({ event: 'node-data-' + this.publicKey, sessionId: this.ogSession }); return node; }), catchError(err => { this.error = err; this.seoService.logSoft404(); - this.openGraphService.fail('node-map-' + this.publicKey); - this.openGraphService.fail('node-data-' + this.publicKey); + this.openGraphService.fail({ event: 'node-map-' + this.publicKey, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'node-data-' + this.publicKey, sessionId: this.ogSession }); return [{ alias: this.publicKey, public_key: this.publicKey, @@ -102,6 +104,6 @@ export class NodePreviewComponent implements OnInit { } onMapReady() { - this.openGraphService.waitOver('node-map-' + this.publicKey); + this.openGraphService.waitOver({ event: 'node-map-' + this.publicKey, sessionId: this.ogSession }); } } diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts index 9fc071eb5..bab34ae8f 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts @@ -19,6 +19,8 @@ export class NodesPerISPPreview implements OnInit { id: string; error: Error; + ogSession: number; + constructor( private apiService: ApiService, private seoService: SeoService, @@ -32,8 +34,8 @@ export class NodesPerISPPreview implements OnInit { switchMap((params: ParamMap) => { this.id = params.get('isp'); this.isp = null; - this.openGraphService.waitFor('isp-map-' + this.id); - this.openGraphService.waitFor('isp-data-' + this.id); + this.ogSession = this.openGraphService.waitFor('isp-map-' + this.id); + this.ogSession = this.openGraphService.waitFor('isp-data-' + this.id); return this.apiService.getNodeForISP$(params.get('isp')); }), map(response => { @@ -75,7 +77,7 @@ export class NodesPerISPPreview implements OnInit { } topCountry.flag = getFlagEmoji(topCountry.iso); - this.openGraphService.waitOver('isp-data-' + this.id); + this.openGraphService.waitOver({ event: 'isp-data-' + this.id, sessionId: this.ogSession }); return { nodes: response.nodes, @@ -87,8 +89,8 @@ export class NodesPerISPPreview implements OnInit { catchError(err => { this.error = err; this.seoService.logSoft404(); - this.openGraphService.fail('isp-map-' + this.id); - this.openGraphService.fail('isp-data-' + this.id); + this.openGraphService.fail({ event: 'isp-map-' + this.id, sessionId: this.ogSession }); + this.openGraphService.fail({ event: 'isp-data-' + this.id, sessionId: this.ogSession }); return of({ nodes: [], sumLiquidity: 0, @@ -100,6 +102,6 @@ export class NodesPerISPPreview implements OnInit { } onMapReady() { - this.openGraphService.waitOver('isp-map-' + this.id); + this.openGraphService.waitOver({ event: 'isp-map-' + this.id, sessionId: this.ogSession }); } } diff --git a/frontend/src/app/services/opengraph.service.ts b/frontend/src/app/services/opengraph.service.ts index e969dd07a..47b9d87d4 100644 --- a/frontend/src/app/services/opengraph.service.ts +++ b/frontend/src/app/services/opengraph.service.ts @@ -12,8 +12,9 @@ import { LanguageService } from '@app/services/language.service'; export class OpenGraphService { network = ''; defaultImageUrl = ''; - previewLoadingEvents = {}; - previewLoadingCount = 0; + previewLoadingEvents = {}; // pending count per event type + previewLoadingCount = 0; // number of unique events pending + sessionId = 1; constructor( private ngZone: NgZone, @@ -45,7 +46,7 @@ export class OpenGraphService { // expose routing method to global scope, so we can access it from the unfurler window['ogService'] = { - loadPage: (path) => { return this.loadPage(path) } + loadPage: (path) => { return this.loadPage(path); } }; } @@ -77,7 +78,7 @@ export class OpenGraphService { } /// register an event that needs to resolve before we can take a screenshot - waitFor(event) { + waitFor(event: string): number { if (!this.previewLoadingEvents[event]) { this.previewLoadingEvents[event] = 1; this.previewLoadingCount++; @@ -85,24 +86,31 @@ export class OpenGraphService { this.previewLoadingEvents[event]++; } this.metaService.updateTag({ property: 'og:preview:loading', content: 'loading'}); + return this.sessionId; } // mark an event as resolved // if all registered events have resolved, signal we are ready for a screenshot - waitOver(event) { + waitOver({ event, sessionId }: { event: string, sessionId: number }) { + if (sessionId !== this.sessionId) { + return; + } if (this.previewLoadingEvents[event]) { this.previewLoadingEvents[event]--; if (this.previewLoadingEvents[event] === 0 && this.previewLoadingCount > 0) { - delete this.previewLoadingEvents[event] + delete this.previewLoadingEvents[event]; this.previewLoadingCount--; } - if (this.previewLoadingCount === 0) { - this.metaService.updateTag({ property: 'og:preview:ready', content: 'ready'}); - } + } + if (this.previewLoadingCount === 0) { + this.metaService.updateTag({ property: 'og:preview:ready', content: 'ready'}); } } - fail(event) { + fail({ event, sessionId }: { event: string, sessionId: number }) { + if (sessionId !== this.sessionId) { + return; + } if (this.previewLoadingEvents[event]) { this.metaService.updateTag({ property: 'og:preview:fail', content: 'fail'}); } @@ -111,6 +119,7 @@ export class OpenGraphService { resetLoading() { this.previewLoadingEvents = {}; this.previewLoadingCount = 0; + this.sessionId++; this.metaService.removeTag("property='og:preview:loading'"); this.metaService.removeTag("property='og:preview:ready'"); this.metaService.removeTag("property='og:preview:fail'"); @@ -122,7 +131,7 @@ export class OpenGraphService { this.resetLoading(); this.ngZone.run(() => { this.router.navigateByUrl(path); - }) + }); } } } From 831b923dda4244df789738471d64d05a1b377142 Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 10 Feb 2025 15:32:16 +0100 Subject: [PATCH 021/114] Update transaction preview messages --- .../components/transaction/transaction-raw.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html index 450e18ecd..7f8723353 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.html +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -8,10 +8,10 @@
- +

Error decoding transaction, reason: {{ error }}

@@ -44,9 +44,9 @@ @if (!hasPrevouts) {
@if (offlineMode) { - Prevouts are not loaded, some fields like fee rate cannot be displayed. + Missing prevouts are not loaded. Some fields like fee rate cannot be calculated. } @else { - Error loading prevouts. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }} + Error loading missing prevouts. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }} }
} From 4c20d2b180152c69abf15a790d77fd20142e570e Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 10 Feb 2025 15:33:21 +0100 Subject: [PATCH 022/114] Move broadcast button to alert banner --- .../transaction/transaction-raw.component.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html index 7f8723353..b761bc8a9 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.html +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -30,7 +30,6 @@
-
@@ -41,6 +40,16 @@
+
+ + + + This transaction is stored locally in your browser. Broadcast it to add it to the mempool. + + + +
+ @if (!hasPrevouts) {
@if (offlineMode) { From 80201c082127226226e955d56a236c615f3bef72 Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 10 Feb 2025 09:33:07 -1000 Subject: [PATCH 023/114] ops: Add new Bitcoin nodes in SG1 and HNL to bitcoin.conf --- production/bitcoin.conf | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/production/bitcoin.conf b/production/bitcoin.conf index 8fe17d921..adff1ef6b 100644 --- a/production/bitcoin.conf +++ b/production/bitcoin.conf @@ -12,6 +12,7 @@ rpcallowip=127.0.0.1 rpcuser=__BITCOIN_RPC_USER__ rpcpassword=__BITCOIN_RPC_PASS__ whitelist=127.0.0.1 +whitelist=209.146.50.0/23 whitelist=103.99.168.0/22 whitelist=2401:b140::/32 blocksxor=0 @@ -27,6 +28,10 @@ bind=0.0.0.0:8333 bind=[::]:8333 zmqpubrawblock=tcp://127.0.0.1:8334 zmqpubrawtx=tcp://127.0.0.1:8335 +#addnode=[2401:b140::92:201]:8333 +#addnode=[2401:b140::92:202]:8333 +#addnode=[2401:b140::92:203]:8333 +#addnode=[2401:b140::92:204]:8333 #addnode=[2401:b140:1::92:201]:8333 #addnode=[2401:b140:1::92:202]:8333 #addnode=[2401:b140:1::92:203]:8333 @@ -65,6 +70,10 @@ zmqpubrawtx=tcp://127.0.0.1:8335 #addnode=[2401:b140:4::92:210]:8333 #addnode=[2401:b140:4::92:211]:8333 #addnode=[2401:b140:4::92:212]:8333 +#addnode=[2401:b140:5::92:201]:8333 +#addnode=[2401:b140:5::92:202]:8333 +#addnode=[2401:b140:5::92:203]:8333 +#addnode=[2401:b140:5::92:204]:8333 [test] daemon=1 @@ -74,6 +83,10 @@ bind=0.0.0.0:18333 bind=[::]:18333 zmqpubrawblock=tcp://127.0.0.1:18334 zmqpubrawtx=tcp://127.0.0.1:18335 +#addnode=[2401:b140::92:201]:18333 +#addnode=[2401:b140::92:202]:18333 +#addnode=[2401:b140::92:203]:18333 +#addnode=[2401:b140::92:204]:18333 #addnode=[2401:b140:1::92:201]:18333 #addnode=[2401:b140:1::92:202]:18333 #addnode=[2401:b140:1::92:203]:18333 @@ -112,6 +125,10 @@ zmqpubrawtx=tcp://127.0.0.1:18335 #addnode=[2401:b140:4::92:210]:18333 #addnode=[2401:b140:4::92:211]:18333 #addnode=[2401:b140:4::92:212]:18333 +#addnode=[2401:b140:5::92:201]:18333 +#addnode=[2401:b140:5::92:202]:18333 +#addnode=[2401:b140:5::92:203]:18333 +#addnode=[2401:b140:5::92:204]:18333 [signet] daemon=1 @@ -121,6 +138,10 @@ bind=0.0.0.0:38333 bind=[::]:38333 zmqpubrawblock=tcp://127.0.0.1:38334 zmqpubrawtx=tcp://127.0.0.1:38335 +#addnode=[2401:b140::92:201]:38333 +#addnode=[2401:b140::92:202]:38333 +#addnode=[2401:b140::92:203]:38333 +#addnode=[2401:b140::92:204]:38333 #addnode=[2401:b140:1::92:201]:38333 #addnode=[2401:b140:1::92:202]:38333 #addnode=[2401:b140:1::92:203]:38333 @@ -161,6 +182,10 @@ zmqpubrawtx=tcp://127.0.0.1:38335 #addnode=[2401:b140:4::92:212]:38333 #addnode=[2401:b140:4::92:213]:38333 #addnode=[2401:b140:4::92:214]:38333 +#addnode=[2401:b140:5::92:201]:38333 +#addnode=[2401:b140:5::92:202]:38333 +#addnode=[2401:b140:5::92:203]:38333 +#addnode=[2401:b140:5::92:204]:38333 [testnet4] daemon=1 @@ -170,6 +195,10 @@ bind=0.0.0.0:48333 bind=[::]:48333 zmqpubrawblock=tcp://127.0.0.1:48334 zmqpubrawtx=tcp://127.0.0.1:48335 +#addnode=[2401:b140::92:201]:48333 +#addnode=[2401:b140::92:202]:48333 +#addnode=[2401:b140::92:203]:48333 +#addnode=[2401:b140::92:204]:48333 #addnode=[2401:b140:1::92:201]:48333 #addnode=[2401:b140:1::92:202]:48333 #addnode=[2401:b140:1::92:203]:48333 @@ -210,3 +239,7 @@ zmqpubrawtx=tcp://127.0.0.1:48335 #addnode=[2401:b140:4::92:212]:48333 #addnode=[2401:b140:4::92:213]:48333 #addnode=[2401:b140:4::92:214]:48333 +#addnode=[2401:b140:5::92:201]:48333 +#addnode=[2401:b140:5::92:202]:48333 +#addnode=[2401:b140:5::92:203]:48333 +#addnode=[2401:b140:5::92:204]:48333 From c47af1f8b22921c9379a145768b7c5c9fc3427ab Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Tue, 11 Feb 2025 15:30:51 -0500 Subject: [PATCH 024/114] =?UTF-8?q?Add=20Mempool=C2=AE=20to=20trademark=20?= =?UTF-8?q?guidelines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trademark-policy/trademark-policy.component.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 e12cbb8b2..82580be1c 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: August 19, 2024
+
Updated: February 11, 2025

@@ -59,6 +59,7 @@ Mempool Accelerator Mempool Enterprise Mempool Liquidity + Mempool mempool.space Be your own explorer Explore the full Bitcoin ecosystem @@ -340,7 +341,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 Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles 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."

+

"The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, Mempool®, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles 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

  • From d3b5c15f33fa4c54ec22bb5b9b27c6a2480ad78b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 12 Feb 2025 15:03:52 +0000 Subject: [PATCH 025/114] [ops] check for pool updates every hour --- production/mempool-config.mainnet.json | 1 + production/mempool-config.signet.json | 1 + production/mempool-config.testnet.json | 1 + production/mempool-config.testnet4.json | 1 + 4 files changed, 4 insertions(+) diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index b14e3cd07..9505601d2 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -14,6 +14,7 @@ "BLOCKS_SUMMARIES_INDEXING": true, "GOGGLES_INDEXING": true, "AUTOMATIC_POOLS_UPDATE": true, + "POOLS_UPDATE_DELAY": 3600, "AUDIT": true, "CPFP_INDEXING": true, "RUST_GBT": true, diff --git a/production/mempool-config.signet.json b/production/mempool-config.signet.json index a0a2353cb..952845ae9 100644 --- a/production/mempool-config.signet.json +++ b/production/mempool-config.signet.json @@ -9,6 +9,7 @@ "API_URL_PREFIX": "/api/v1/", "INDEXING_BLOCKS_AMOUNT": -1, "AUTOMATIC_POOLS_UPDATE": true, + "POOLS_UPDATE_DELAY": 3600, "AUDIT": true, "RUST_GBT": true, "POLL_RATE_MS": 1000, diff --git a/production/mempool-config.testnet.json b/production/mempool-config.testnet.json index 81cd61dc4..5f9f3abb9 100644 --- a/production/mempool-config.testnet.json +++ b/production/mempool-config.testnet.json @@ -9,6 +9,7 @@ "API_URL_PREFIX": "/api/v1/", "INDEXING_BLOCKS_AMOUNT": -1, "AUTOMATIC_POOLS_UPDATE": true, + "POOLS_UPDATE_DELAY": 3600, "AUDIT": true, "RUST_GBT": true, "POLL_RATE_MS": 1000, diff --git a/production/mempool-config.testnet4.json b/production/mempool-config.testnet4.json index 91373d223..2e79309ed 100644 --- a/production/mempool-config.testnet4.json +++ b/production/mempool-config.testnet4.json @@ -9,6 +9,7 @@ "API_URL_PREFIX": "/api/v1/", "INDEXING_BLOCKS_AMOUNT": -1, "AUTOMATIC_POOLS_UPDATE": true, + "POOLS_UPDATE_DELAY": 3600, "AUDIT": true, "RUST_GBT": true, "POLL_RATE_MS": 1000, From c626bd1ea2b51d8eed9787e9f701613a5887279a Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 19 Feb 2025 10:56:13 -0600 Subject: [PATCH 026/114] ops: Remove old X-Frame-Options HTTP header --- production/nginx/server-common.conf | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/production/nginx/server-common.conf b/production/nginx/server-common.conf index 9a2a582c0..5a0b17b4e 100644 --- a/production/nginx/server-common.conf +++ b/production/nginx/server-common.conf @@ -8,33 +8,28 @@ add_header Onion-Location http://$onion.onion$request_uri; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; # generate frame configuration from origin header -if ($frameOptions = '') +if ($contentSecurityPolicy = '') { - set $frameOptions "DENY"; - set $contentSecurityPolicy "frame-ancestors 'none'"; + set $contentSecurityPolicy "frame-ancestors 'self'"; } # used for iframes on https://mempool.space/network if ($http_referer ~ ^https://mempool.space/) { - set $frameOptions "ALLOW-FROM https://mempool.space"; set $contentSecurityPolicy "frame-ancestors https://mempool.space"; } # used for iframes on https://mempool.ninja/network if ($http_referer ~ ^https://mempool.ninja/) { - set $frameOptions "ALLOW-FROM https://mempool.ninja"; set $contentSecurityPolicy "frame-ancestors https://mempool.ninja"; } # used for iframes on https://wiz.biz/bitcoin/nodes if ($http_referer ~ ^https://wiz.biz/) { - set $frameOptions "ALLOW-FROM https://wiz.biz"; set $contentSecurityPolicy "frame-ancestors https://wiz.biz"; } # restrict usage of frames -add_header X-Frame-Options $frameOptions; add_header Content-Security-Policy $contentSecurityPolicy; # enable browser and proxy caching From 7671600455b94bcbc96907bb1bdf07bc39be094f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 19 Feb 2025 15:03:03 +0000 Subject: [PATCH 027/114] temporary twidget mirror --- .../twitter-widget.component.ts | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/components/twitter-widget/twitter-widget.component.ts b/frontend/src/app/components/twitter-widget/twitter-widget.component.ts index 06b50b1dc..8f5894ad0 100644 --- a/frontend/src/app/components/twitter-widget/twitter-widget.component.ts +++ b/frontend/src/app/components/twitter-widget/twitter-widget.component.ts @@ -34,29 +34,39 @@ export class TwitterWidgetComponent implements OnChanges { } setIframeSrc(): void { - if (this.handle) { - this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, - `https://syndication.x.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool` - + '&dnt=true' - + '&embedId=twitter-widget-0' - + '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D' - + '&frame=false' - + '&hideBorder=true' - + '&hideFooter=false' - + '&hideHeader=true' - + '&hideScrollBar=false' - + `&lang=${this.lang}` - + '&maxHeight=500px' - + '&origin=https%3A%2F%2Fmempool.space%2F' - // + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9' - + '&showHeader=false' - + '&showReplies=false' - + '&siteScreenName=mempool' - + '&theme=dark' - + '&transparent=true' - + '&widgetsVersion=2615f7e52b7e0%3A1702314776716' - )); + if (!this.handle) { + return; } + let url = `https://syndication.x.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool` + + '&dnt=true' + + '&embedId=twitter-widget-0' + + '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D' + + '&frame=false' + + '&hideBorder=true' + + '&hideFooter=false' + + '&hideHeader=true' + + '&hideScrollBar=false' + + `&lang=${this.lang}` + + '&maxHeight=500px' + + '&origin=https%3A%2F%2Fmempool.space%2F' + // + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9' + + '&showHeader=false' + + '&showReplies=false' + + '&siteScreenName=mempool' + + '&theme=dark' + + '&transparent=true' + + '&widgetsVersion=2615f7e52b7e0%3A1702314776716'; + switch (this.handle.toLowerCase()) { + case 'nayibbukele': + url = 'https://bitcoin.gob.sv/twidget'; + break; + case 'metaplanet_jp': + url = 'https://metaplanet.mempool.space/twidget'; + break; + default: + break; + } + this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, url)); } onReady(): void { From 6ec1cc3fd5bea16f0ecdf45c76c667939b35abe0 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 20 Feb 2025 22:08:14 +0700 Subject: [PATCH 028/114] Deprecating the tv view --- frontend/cypress/e2e/liquid/liquid.spec.ts | 5 -- .../e2e/liquidtestnet/liquidtestnet.spec.ts | 5 -- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 20 ----- .../liquid-master-page.component.html | 5 -- .../statistics/statistics.component.html | 3 - .../television/television.component.html | 25 ------ .../television/television.component.scss | 80 ----------------- .../television/television.component.ts | 86 ------------------- frontend/src/app/graphs/graphs.module.ts | 2 - .../src/app/graphs/graphs.routing.module.ts | 6 -- 10 files changed, 237 deletions(-) delete mode 100644 frontend/src/app/components/television/television.component.html delete mode 100644 frontend/src/app/components/television/television.component.scss delete mode 100644 frontend/src/app/components/television/television.component.ts diff --git a/frontend/cypress/e2e/liquid/liquid.spec.ts b/frontend/cypress/e2e/liquid/liquid.spec.ts index c7d2a92ee..4fb7431d9 100644 --- a/frontend/cypress/e2e/liquid/liquid.spec.ts +++ b/frontend/cypress/e2e/liquid/liquid.spec.ts @@ -57,11 +57,6 @@ describe('Liquid', () => { }); }); - it('loads the tv page - desktop', () => { - cy.visit(`${basePath}/tv`); - cy.waitForSkeletonGone(); - }); - it('loads the graphs page - mobile', () => { cy.visit(`${basePath}`) cy.waitForSkeletonGone(); diff --git a/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts b/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts index 54e355ce8..7befda49f 100644 --- a/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts +++ b/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts @@ -57,11 +57,6 @@ describe('Liquid Testnet', () => { cy.waitForSkeletonGone(); }); - it('loads the tv page - desktop', () => { - cy.visit(`${basePath}/tv`); - cy.waitForSkeletonGone(); - }); - it('loads the graphs page - mobile', () => { cy.visit(`${basePath}`) cy.waitForSkeletonGone(); diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 7e17c09cd..08a4741b3 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -415,26 +415,6 @@ describe('Mainnet', () => { }); }); - it('loads the tv screen - desktop', () => { - cy.viewport('macbook-16'); - cy.visit('/graphs/mempool'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.viewport('macbook-16'); - cy.get('.chart-holder'); - cy.get('.blockchain-wrapper').should('be.visible'); - cy.get('#mempool-block-0').should('be.visible'); - }); - }); - - it('loads the tv screen - mobile', () => { - cy.viewport('iphone-6'); - cy.visit('/tv'); - cy.waitForSkeletonGone(); - cy.get('.chart-holder'); - cy.get('.blockchain-wrapper').should('not.visible'); - }); - it('loads the api screen', () => { cy.visit('/'); cy.waitForSkeletonGone(); diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html index 7e39d9341..cd016471b 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html @@ -70,11 +70,6 @@
    - diff --git a/frontend/src/app/components/statistics/statistics.component.html b/frontend/src/app/components/statistics/statistics.component.html index 168a3c0c3..eb37cc858 100644 --- a/frontend/src/app/components/statistics/statistics.component.html +++ b/frontend/src/app/components/statistics/statistics.component.html @@ -16,9 +16,6 @@ - - -
    diff --git a/frontend/src/app/components/television/television.component.html b/frontend/src/app/components/television/television.component.html deleted file mode 100644 index 23dd18389..000000000 --- a/frontend/src/app/components/television/television.component.html +++ /dev/null @@ -1,25 +0,0 @@ -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    -
    -
    -
    -
    diff --git a/frontend/src/app/components/television/television.component.scss b/frontend/src/app/components/television/television.component.scss deleted file mode 100644 index 9a6cbcc24..000000000 --- a/frontend/src/app/components/television/television.component.scss +++ /dev/null @@ -1,80 +0,0 @@ - -.loading { - margin: auto; - width: 100%; - display: flex; - text-align: center; - justify-content: center; - height: 100vh; - align-items: center; -} - -#tv-wrapper { - height: 100vh; - overflow: hidden; - position: relative; -} - -.chart-holder { - position: relative; - height: 655px; - width: 100%; - margin: 30px auto 0; -} - -.blockchain-wrapper { - display: block; - height: 100%; - min-height: 240px; - position: relative; - top: 30px; - - .position-container { - position: absolute; - left: 0; - bottom: 170px; - transform: translateX(50vw); - } - - #divider { - width: 2px; - height: 175px; - left: 0; - top: -40px; - position: absolute; - img { - position: absolute; - left: -100px; - top: -28px; - } - } - - &.time-ltr { - .blocks-wrapper { - transform: scaleX(-1); - } - } -} - -:host-context(.ltr-layout) { - .blockchain-wrapper.time-ltr .blocks-wrapper, - .blockchain-wrapper .blocks-wrapper { - direction: ltr; - } -} - -:host-context(.rtl-layout) { - .blockchain-wrapper.time-ltr .blocks-wrapper, - .blockchain-wrapper .blocks-wrapper { - direction: rtl; - } -} - -.tv-container { - display: flex; - margin-top: 0px; - flex-direction: column; -} - - - diff --git a/frontend/src/app/components/television/television.component.ts b/frontend/src/app/components/television/television.component.ts deleted file mode 100644 index 1507f3d97..000000000 --- a/frontend/src/app/components/television/television.component.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { WebsocketService } from '@app/services/websocket.service'; -import { OptimizedMempoolStats } from '@interfaces/node-api.interface'; -import { StateService } from '@app/services/state.service'; -import { ApiService } from '@app/services/api.service'; -import { SeoService } from '@app/services/seo.service'; -import { ActivatedRoute } from '@angular/router'; -import { map, scan, startWith, switchMap, tap } from 'rxjs/operators'; -import { interval, merge, Observable, Subscription } from 'rxjs'; -import { ChangeDetectionStrategy } from '@angular/core'; - -@Component({ - selector: 'app-television', - templateUrl: './television.component.html', - styleUrls: ['./television.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class TelevisionComponent implements OnInit, OnDestroy { - - mempoolStats: OptimizedMempoolStats[] = []; - statsSubscription$: Observable; - fragment: string; - timeLtrSubscription: Subscription; - timeLtr: boolean = this.stateService.timeLtr.value; - - constructor( - private websocketService: WebsocketService, - private apiService: ApiService, - private stateService: StateService, - private seoService: SeoService, - private route: ActivatedRoute - ) { } - - refreshStats(time: number, fn: Observable) { - return interval(time).pipe(startWith(0), switchMap(() => fn)); - } - - ngOnInit() { - this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`); - this.seoService.setDescription($localize`:@@meta.description.tv:See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.`); - this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']); - - this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { - this.timeLtr = !!ltr; - }); - - this.statsSubscription$ = merge( - this.stateService.live2Chart$.pipe(map(stats => [stats])), - this.route.fragment - .pipe( - tap(fragment => { this.fragment = fragment ?? '2h'; }), - switchMap((fragment) => { - const minute = 60000; const hour = 3600000; - switch (fragment) { - case '24h': return this.apiService.list24HStatistics$(); - case '1w': return this.refreshStats(5 * minute, this.apiService.list1WStatistics$()); - case '1m': return this.refreshStats(30 * minute, this.apiService.list1MStatistics$()); - case '3m': return this.refreshStats(2 * hour, this.apiService.list3MStatistics$()); - case '6m': return this.refreshStats(3 * hour, this.apiService.list6MStatistics$()); - case '1y': return this.refreshStats(8 * hour, this.apiService.list1YStatistics$()); - case '2y': return this.refreshStats(8 * hour, this.apiService.list2YStatistics$()); - case '3y': return this.refreshStats(12 * hour, this.apiService.list3YStatistics$()); - default /* 2h */: return this.apiService.list2HStatistics$(); - } - }) - ) - ) - .pipe( - scan((mempoolStats, newStats) => { - if (newStats.length > 1) { - mempoolStats = newStats; - } else if (['2h', '24h'].includes(this.fragment)) { - mempoolStats.unshift(newStats[0]); - const now = Date.now() / 1000; - const start = now - (this.fragment === '2h' ? (2 * 60 * 60) : (24 * 60 * 60) ); - mempoolStats = mempoolStats.filter(p => p.added >= start); - } - return mempoolStats; - }) - ); - } - - ngOnDestroy() { - this.timeLtrSubscription.unsubscribe(); - } -} diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index f882b4221..8ebf06f7c 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -26,7 +26,6 @@ import { StatisticsComponent } from '@components/statistics/statistics.component import { MempoolBlockComponent } from '@components/mempool-block/mempool-block.component'; import { PoolRankingComponent } from '@components/pool-ranking/pool-ranking.component'; import { PoolComponent } from '@components/pool/pool.component'; -import { TelevisionComponent } from '@components/television/television.component'; import { DashboardComponent } from '@app/dashboard/dashboard.component'; import { CustomDashboardComponent } from '@components/custom-dashboard/custom-dashboard.component'; import { MiningDashboardComponent } from '@components/mining-dashboard/mining-dashboard.component'; @@ -56,7 +55,6 @@ import { CommonModule } from '@angular/common'; AcceleratorDashboardComponent, PoolComponent, PoolRankingComponent, - TelevisionComponent, StatisticsComponent, GraphsComponent, diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 886d55072..e8dbaece3 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -16,7 +16,6 @@ import { PoolRankingComponent } from '@components/pool-ranking/pool-ranking.comp import { PoolComponent } from '@components/pool/pool.component'; import { StartComponent } from '@components/start/start.component'; import { StatisticsComponent } from '@components/statistics/statistics.component'; -import { TelevisionComponent } from '@components/television/television.component'; import { DashboardComponent } from '@app/dashboard/dashboard.component'; import { CustomDashboardComponent } from '@components/custom-dashboard/custom-dashboard.component'; import { AccelerationFeesGraphComponent } from '@components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component'; @@ -180,11 +179,6 @@ const routes: Routes = [ }, ] }, - { - path: 'tv', - data: { networks: ['bitcoin', 'liquid'] }, - component: TelevisionComponent - }, ]; @NgModule({ From e40ca40ecbe1061bb9749ee0536550bc811e9050 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 20 Feb 2025 22:11:45 +0700 Subject: [PATCH 029/114] Remove tv icon dep --- frontend/src/app/shared/shared.module.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index d937e6bbb..0870eeabf 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, - faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, + faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, @@ -395,7 +395,6 @@ export class SharedModule { constructor(library: FaIconLibrary) { library.addIcons(faInfoCircle); library.addIcons(faChartArea); - library.addIcons(faTv); library.addIcons(faClock); library.addIcons(faTachometerAlt); library.addIcons(faCubes); From 650c3d949dd3e969fd7b1e9f08b129239c25b977 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 22 Feb 2025 22:25:52 +0700 Subject: [PATCH 030/114] remove tv mode tests --- frontend/cypress/e2e/signet/signet.spec.ts | 24 ------------------- .../cypress/e2e/testnet4/testnet4.spec.ts | 24 ------------------- 2 files changed, 48 deletions(-) diff --git a/frontend/cypress/e2e/signet/signet.spec.ts b/frontend/cypress/e2e/signet/signet.spec.ts index 11c47d14d..ae591c6a7 100644 --- a/frontend/cypress/e2e/signet/signet.spec.ts +++ b/frontend/cypress/e2e/signet/signet.spec.ts @@ -60,30 +60,6 @@ describe('Signet', () => { }); }); - describe.skip('tv mode', () => { - it('loads the tv screen - desktop', () => { - cy.viewport('macbook-16'); - cy.visit('/signet/graphs'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.get('.chart-holder').should('be.visible'); - cy.get('#mempool-block-0').should('be.visible'); - cy.get('.tv-only').should('not.exist'); - }); - }); - - it('loads the tv screen - mobile', () => { - cy.visit('/signet/graphs'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.viewport('iphone-8'); - cy.get('.chart-holder').should('be.visible'); - cy.get('.tv-only').should('not.exist'); - cy.get('#mempool-block-0').should('be.visible'); - }); - }); - }); - it('loads the api screen', () => { cy.visit('/signet'); cy.waitForSkeletonGone(); diff --git a/frontend/cypress/e2e/testnet4/testnet4.spec.ts b/frontend/cypress/e2e/testnet4/testnet4.spec.ts index c67d2414b..97af0e08e 100644 --- a/frontend/cypress/e2e/testnet4/testnet4.spec.ts +++ b/frontend/cypress/e2e/testnet4/testnet4.spec.ts @@ -60,30 +60,6 @@ describe('Testnet4', () => { }); }); - describe('tv mode', () => { - it('loads the tv screen - desktop', () => { - cy.viewport('macbook-16'); - cy.visit('/testnet4/graphs'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.wait(1000); - cy.get('.tv-only').should('not.exist'); - cy.get('#mempool-block-0').should('be.visible'); - }); - }); - - it('loads the tv screen - mobile', () => { - cy.visit('/testnet4/graphs'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.viewport('iphone-6'); - cy.wait(1000); - cy.get('.tv-only').should('not.exist'); - }); - }); - }); - - it('loads the api screen', () => { cy.visit('/testnet4'); cy.waitForSkeletonGone(); From d82a9f6c6a58366d2f022499d6b82644278356cf Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Tue, 25 Feb 2025 18:56:29 -0800 Subject: [PATCH 031/114] Tweak Docker workflow --- .github/workflows/on-tag.yml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 634a27ab9..ba9e1eb7b 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -2,7 +2,7 @@ name: Docker build on tag env: DOCKER_CLI_EXPERIMENTAL: enabled TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$" - DOCKER_BUILDKIT: 0 + DOCKER_BUILDKIT: 1 # Enable BuildKit for better performance COMPOSE_DOCKER_CLI_BUILD: 0 on: @@ -25,13 +25,12 @@ jobs: timeout-minutes: 120 name: Build and push to DockerHub steps: - # Workaround based on JonasAlfredsson/docker-on-tmpfs@v1.0.1 - name: Replace the current swap file shell: bash run: | - sudo swapoff /mnt/swapfile - sudo rm -v /mnt/swapfile - sudo fallocate -l 13G /mnt/swapfile + sudo swapoff /mnt/swapfile || true + sudo rm -f /mnt/swapfile + sudo fallocate -l 16G /mnt/swapfile sudo chmod 600 /mnt/swapfile sudo mkswap /mnt/swapfile sudo swapon /mnt/swapfile @@ -50,7 +49,7 @@ jobs: echo "Directory '/var/lib/docker' not found" exit 1 fi - sudo mount -t tmpfs -o size=10G tmpfs /var/lib/docker + sudo mount -t tmpfs -o size=12G tmpfs /var/lib/docker sudo systemctl restart docker sudo df -h | grep docker @@ -75,10 +74,16 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64,linux/arm64 id: qemu - name: Setup Docker buildx action uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + driver-opts: | + network=host id: buildx - name: Available platforms @@ -89,19 +94,20 @@ jobs: id: cache with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + key: ${{ runner.os }}-buildx-${{ matrix.service }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-${{ matrix.service }}- - name: Run Docker buildx for ${{ matrix.service }} against tag run: | docker buildx build \ --cache-from "type=local,src=/tmp/.buildx-cache" \ - --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --build-context rustgbt=./rust \ --build-context backend=./backend \ - --output "type=registry" ./${{ matrix.service }}/ \ - --build-arg commitHash=$SHORT_SHA + --output "type=registry,push=true" \ + --build-arg commitHash=$SHORT_SHA \ + ./${{ matrix.service }}/ \ No newline at end of file From e6f13766d3c05a910d3d146f457762a30f6c86ee Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Tue, 25 Feb 2025 19:05:28 -0800 Subject: [PATCH 032/114] Update Docker images --- docker/backend/Dockerfile | 29 ++++++++++++++++++----------- docker/frontend/Dockerfile | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 60d663f20..e56b07da3 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -1,20 +1,20 @@ -FROM node:20.15.0-buster-slim AS builder +FROM rust:1.84-bookworm AS builder ARG commitHash ENV MEMPOOL_COMMIT_HASH=${commitHash} WORKDIR /build + +RUN apt-get update && \ + apt-get install -y curl ca-certificates && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs build-essential python3 pkg-config && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + COPY . . -RUN apt-get update -RUN apt-get install -y build-essential python3 pkg-config curl ca-certificates - -# Install Rust via rustup -RUN CPU_ARCH=$(uname -m); if [ "$CPU_ARCH" = "armv7l" ]; then c_rehash; fi -#RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable -#Workaround to run on github actions from https://github.com/rust-lang/rustup/issues/2700#issuecomment-1367488985 -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sed 's#/proc/self/exe#\/bin\/sh#g' | sh -s -- -y --default-toolchain stable -ENV PATH="/root/.cargo/bin:$PATH" +ENV PATH="/usr/local/cargo/bin:$PATH" COPY --from=backend . . COPY --from=rustgbt . ../rust/ @@ -24,7 +24,14 @@ RUN npm install --omit=dev --omit=optional WORKDIR /build RUN npm run package -FROM node:20.15.0-buster-slim +FROM rust:1.84-bookworm AS runtime + +RUN apt-get update && \ + apt-get install -y curl ca-certificates && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* WORKDIR /backend diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index 8374ebe49..8d97c9dc6 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.15.0-buster-slim AS builder +FROM node:22-bookworm-slim AS builder ARG commitHash ENV DOCKER_COMMIT_HASH=${commitHash} From cfe7c93755c38a0837cc4744add1c6a821fc2160 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 02:13:31 +0000 Subject: [PATCH 033/114] Bump axios from 1.7.2 to 1.8.1 in /backend Bumps [axios](https://github.com/axios/axios) from 1.7.2 to 1.8.1. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.8.1) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- backend/package-lock.json | 14 +++++++------- backend/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 3f66fa25b..1aaa77f85 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "1.7.2", + "axios": "1.8.1", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", "express": "~4.21.1", @@ -2275,9 +2275,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -9459,9 +9459,9 @@ "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==" }, "axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/backend/package.json b/backend/package.json index ee5944f93..efc5a4501 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,7 +41,7 @@ "dependencies": { "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "1.7.2", + "axios": "1.8.1", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", "express": "~4.21.1", From 5116da2626432b4c6a0818671cee19897370d5f6 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Fri, 28 Feb 2025 23:20:40 -0800 Subject: [PATCH 034/114] Do not update the latest tag when building --- .github/workflows/on-tag.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index ba9e1eb7b..8a846631c 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -105,7 +105,7 @@ jobs: --cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ - --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ + # --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --build-context rustgbt=./rust \ --build-context backend=./backend \ --output "type=registry,push=true" \ From 9c358060aa0f881414a95ac9d52cf38d364845aa Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Fri, 28 Feb 2025 23:22:00 -0800 Subject: [PATCH 035/114] Add dispatch workflow to update the latest tag --- .../workflows/docker_update_latest_tag.yml | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 .github/workflows/docker_update_latest_tag.yml diff --git a/.github/workflows/docker_update_latest_tag.yml b/.github/workflows/docker_update_latest_tag.yml new file mode 100644 index 000000000..5d21697d5 --- /dev/null +++ b/.github/workflows/docker_update_latest_tag.yml @@ -0,0 +1,181 @@ +name: Docker - Update latest tag + +on: + workflow_dispatch: + inputs: + tag: + description: 'The Docker image tag to pull' + required: true + type: string + +jobs: + retag-and-push: + strategy: + matrix: + service: + - frontend + - backend + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + id: buildx + with: + install: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Get source image manifest and SHAs + id: source-manifest + run: | + set -e + echo "Fetching source manifest..." + MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }}) + if [ -z "$MANIFEST" ]; then + echo "No manifest found. Assuming single-arch image." + exit 1 + fi + + echo "Original source manifest:" + echo "$MANIFEST" | jq . + + AMD64_SHA=$(echo "$MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest') + ARM64_SHA=$(echo "$MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="arm64" and .platform.os=="linux") | .digest') + + if [ -z "$AMD64_SHA" ] || [ -z "$ARM64_SHA" ]; then + echo "Source image is not multi-arch (missing amd64 or arm64)" + exit 1 + fi + + echo "Source amd64 manifest digest: $AMD64_SHA" + echo "Source arm64 manifest digest: $ARM64_SHA" + + echo "amd64_sha=$AMD64_SHA" >> $GITHUB_OUTPUT + echo "arm64_sha=$ARM64_SHA" >> $GITHUB_OUTPUT + + - name: Pull and retag architecture-specific images + run: | + set -e + + docker buildx inspect --bootstrap + + # Remove any existing local images to avoid cache interference + echo "Removing existing local images if they exist..." + docker image rm ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }} || true + + # Pull amd64 image by digest + echo "Pulling amd64 image by digest..." + docker pull --platform linux/amd64 ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} + PULLED_AMD64_MANIFEST_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} --format '{{index .RepoDigests 0}}' | cut -d@ -f2) + PULLED_AMD64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} --format '{{.Id}}') + echo "Pulled amd64 manifest digest: $PULLED_AMD64_MANIFEST_DIGEST" + echo "Pulled amd64 image ID (sha256): $PULLED_AMD64_IMAGE_ID" + + # Pull arm64 image by digest + echo "Pulling arm64 image by digest..." + docker pull --platform linux/arm64 ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} + PULLED_ARM64_MANIFEST_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} --format '{{index .RepoDigests 0}}' | cut -d@ -f2) + PULLED_ARM64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} --format '{{.Id}}') + echo "Pulled arm64 manifest digest: $PULLED_ARM64_MANIFEST_DIGEST" + echo "Pulled arm64 image ID (sha256): $PULLED_ARM64_IMAGE_ID" + + # Tag the images + docker tag ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 + docker tag ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 + + # Verify tagged images + TAGGED_AMD64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 --format '{{.Id}}') + TAGGED_ARM64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 --format '{{.Id}}') + echo "Tagged amd64 image ID (sha256): $TAGGED_AMD64_IMAGE_ID" + echo "Tagged arm64 image ID (sha256): $TAGGED_ARM64_IMAGE_ID" + + - name: Push architecture-specific images + run: | + set -e + + echo "Pushing amd64 image..." + docker push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 + PUSHED_AMD64_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 --format '{{index .RepoDigests 0}}' | cut -d@ -f2) + echo "Pushed amd64 manifest digest (local): $PUSHED_AMD64_DIGEST" + + # Fetch manifest from registry after push + echo "Fetching pushed amd64 manifest from registry..." + PUSHED_AMD64_REGISTRY_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64) + PUSHED_AMD64_REGISTRY_DIGEST=$(echo "$PUSHED_AMD64_REGISTRY_MANIFEST" | jq -r '.config.digest') + echo "Pushed amd64 manifest digest (registry): $PUSHED_AMD64_REGISTRY_DIGEST" + + echo "Pushing arm64 image..." + docker push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 + PUSHED_ARM64_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 --format '{{index .RepoDigests 0}}' | cut -d@ -f2) + echo "Pushed arm64 manifest digest (local): $PUSHED_ARM64_DIGEST" + + # Fetch manifest from registry after push + echo "Fetching pushed arm64 manifest from registry..." + PUSHED_ARM64_REGISTRY_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64) + PUSHED_ARM64_REGISTRY_DIGEST=$(echo "$PUSHED_ARM64_REGISTRY_MANIFEST" | jq -r '.config.digest') + echo "Pushed arm64 manifest digest (registry): $PUSHED_ARM64_REGISTRY_DIGEST" + + - name: Create and push multi-arch manifest with original digests + run: | + set -e + + echo "Creating multi-arch manifest with original digests..." + docker manifest create ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest \ + ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} \ + ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} + + echo "Pushing multi-arch manifest..." + docker manifest push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest + + - name: Clean up intermediate tags + if: success() + run: | + docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 || true + docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 || true + docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }} || true + + - name: Verify final manifest + run: | + set -e + echo "Fetching final generated manifest..." + FINAL_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest) + echo "Generated final manifest:" + echo "$FINAL_MANIFEST" | jq . + + FINAL_AMD64_SHA=$(echo "$FINAL_MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest') + FINAL_ARM64_SHA=$(echo "$FINAL_MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="arm64" and .platform.os=="linux") | .digest') + + echo "Final amd64 manifest digest: $FINAL_AMD64_SHA" + echo "Final arm64 manifest digest: $FINAL_ARM64_SHA" + + # Compare all digests + echo "Comparing digests..." + echo "Source amd64 digest: ${{ steps.source-manifest.outputs.amd64_sha }}" + echo "Pulled amd64 manifest digest: $PULLED_AMD64_MANIFEST_DIGEST" + echo "Pushed amd64 manifest digest (local): $PUSHED_AMD64_DIGEST" + echo "Pushed amd64 manifest digest (registry): $PUSHED_AMD64_REGISTRY_DIGEST" + echo "Final amd64 digest: $FINAL_AMD64_SHA" + echo "Source arm64 digest: ${{ steps.source-manifest.outputs.arm64_sha }}" + echo "Pulled arm64 manifest digest: $PULLED_ARM64_MANIFEST_DIGEST" + echo "Pushed arm64 manifest digest (local): $PUSHED_ARM64_DIGEST" + echo "Pushed arm64 manifest digest (registry): $PUSHED_ARM64_REGISTRY_DIGEST" + echo "Final arm64 digest: $FINAL_ARM64_SHA" + + if [ "$FINAL_AMD64_SHA" != "${{ steps.source-manifest.outputs.amd64_sha }}" ] || [ "$FINAL_ARM64_SHA" != "${{ steps.source-manifest.outputs.arm64_sha }}" ]; then + echo "Error: Final manifest SHAs do not match source SHAs" + exit 1 + fi + + echo "Successfully created multi-arch ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest from ${{ github.event.inputs.tag }}" From c01e11899c78c14d3aa2a7c6371ed5015407f398 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 4 Mar 2025 16:25:54 +0100 Subject: [PATCH 036/114] PSBT support in transaction preview --- .../transaction-raw.component.html | 4 +- .../transaction/transaction-raw.component.ts | 92 +-- frontend/src/app/shared/transaction.utils.ts | 523 ++++++++++++++---- 3 files changed, 458 insertions(+), 161 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html index b761bc8a9..3bd8ee6d2 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.html +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -6,7 +6,7 @@
    - +
    @@ -192,7 +192,7 @@ Transaction hex - + diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts index 321b0ffe5..5ce170e12 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.ts +++ b/frontend/src/app/components/transaction/transaction-raw.component.ts @@ -22,6 +22,7 @@ import { CpfpInfo } from '../../interfaces/node-api.interface'; export class TransactionRawComponent implements OnInit, OnDestroy { pushTxForm: UntypedFormGroup; + rawHexTransaction: string; isLoading: boolean; isLoadingPrevouts: boolean; isLoadingCpfpInfo: boolean; @@ -81,10 +82,10 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.resetState(); this.isLoading = true; try { - const tx = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network); + const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network); await this.fetchPrevouts(tx); await this.fetchCpfpInfo(tx); - this.processTransaction(tx); + this.processTransaction(tx, hex); } catch (error) { this.error = error.message; } finally { @@ -93,57 +94,60 @@ export class TransactionRawComponent implements OnInit, OnDestroy { } async fetchPrevouts(transaction: Transaction): Promise { - if (this.offlineMode) { - return; - } + const prevoutsToFetch = transaction.vin.filter(input => !input.prevout).map((input) => ({ txid: input.txid, vout: input.vout })); - const prevoutsToFetch = transaction.vin.map((input) => ({ txid: input.txid, vout: input.vout })); + if (!prevoutsToFetch.length || transaction.vin[0].is_coinbase || this.offlineMode) { + this.hasPrevouts = !prevoutsToFetch.length || transaction.vin[0].is_coinbase; + this.fetchCpfp = this.hasPrevouts && !this.offlineMode; + } else { + try { + this.missingPrevouts = []; + this.isLoadingPrevouts = true; - if (!prevoutsToFetch.length || transaction.vin[0].is_coinbase) { - this.hasPrevouts = true; - return; - } + const prevouts: { prevout: Vout, unconfirmed: boolean }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch)); - try { - this.missingPrevouts = []; - this.isLoadingPrevouts = true; - - const prevouts: { prevout: Vout, unconfirmed: boolean }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch)); - - if (prevouts?.length !== prevoutsToFetch.length) { - throw new Error(); - } - - transaction.vin = transaction.vin.map((input, index) => { - if (prevouts[index]) { - input.prevout = prevouts[index].prevout; - addInnerScriptsToVin(input); - } else { - this.missingPrevouts.push(`${input.txid}:${input.vout}`); + if (prevouts?.length !== prevoutsToFetch.length) { + throw new Error(); } - return input; - }); - if (this.missingPrevouts.length) { - throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`); + let fetchIndex = 0; + transaction.vin.forEach(input => { + if (!input.prevout) { + const fetched = prevouts[fetchIndex]; + if (fetched) { + input.prevout = fetched.prevout; + } else { + this.missingPrevouts.push(`${input.txid}:${input.vout}`); + } + fetchIndex++; + } + }); + + if (this.missingPrevouts.length) { + throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`); + } + + this.hasPrevouts = true; + this.isLoadingPrevouts = false; + this.fetchCpfp = prevouts.some(prevout => prevout?.unconfirmed); + } catch (error) { + console.log(error); + this.errorPrevouts = error?.error?.error || error?.message; + this.isLoadingPrevouts = false; } + } + if (this.hasPrevouts) { transaction.fee = transaction.vin.some(input => input.is_coinbase) ? 0 : transaction.vin.reduce((fee, input) => { return fee + (input.prevout?.value || 0); }, 0) - transaction.vout.reduce((sum, output) => sum + output.value, 0); transaction.feePerVsize = transaction.fee / (transaction.weight / 4); - transaction.sigops = countSigops(transaction); - - this.hasPrevouts = true; - this.isLoadingPrevouts = false; - this.fetchCpfp = prevouts.some(prevout => prevout?.unconfirmed); - } catch (error) { - console.log(error); - this.errorPrevouts = error?.error?.error || error?.message; - this.isLoadingPrevouts = false; } + + transaction.vin.forEach(addInnerScriptsToVin); + transaction.sigops = countSigops(transaction); } async fetchCpfpInfo(transaction: Transaction): Promise { @@ -175,10 +179,11 @@ export class TransactionRawComponent implements OnInit, OnDestroy { } } - processTransaction(tx: Transaction): void { + processTransaction(tx: Transaction, hex: string): void { this.transaction = tx; + this.rawHexTransaction = hex; - this.transaction.flags = getTransactionFlags(this.transaction, null, null, null, this.stateService.network); + this.transaction.flags = getTransactionFlags(this.transaction, this.cpfpInfo, null, null, this.stateService.network); this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : []; if (this.transaction.sigops >= 0) { this.adjustedVsize = Math.max(this.transaction.weight / 4, this.transaction.sigops * 5); @@ -206,7 +211,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.isLoadingBroadcast = true; this.errorBroadcast = null; return new Promise((resolve, reject) => { - this.apiService.postTransaction$(this.pushTxForm.get('txRaw').value) + this.apiService.postTransaction$(this.rawHexTransaction) .subscribe((result) => { this.isLoadingBroadcast = false; this.successBroadcast = true; @@ -228,6 +233,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { resetState() { this.transaction = null; + this.rawHexTransaction = null; this.error = null; this.errorPrevouts = null; this.errorBroadcast = null; @@ -251,7 +257,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { resetForm() { this.resetState(); - this.pushTxForm.reset(); + this.pushTxForm.get('txRaw').setValue(''); } @HostListener('window:resize', ['$event']) diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index b33d88c2f..eafe8ae99 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -692,7 +692,7 @@ export function addInnerScriptsToVin(vin: Vin): void { if (vin.prevout.scriptpubkey_type === 'p2sh') { const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0]; vin.inner_redeemscript_asm = convertScriptSigAsm(redeemScript); - if (vin.witness && vin.witness.length > 2) { + if (vin.witness && vin.witness.length) { const witnessScript = vin.witness[vin.witness.length - 1]; vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript); } @@ -712,86 +712,15 @@ export function addInnerScriptsToVin(vin: Vin): void { } // Adapted from bitcoinjs-lib at https://github.com/bitcoinjs/bitcoinjs-lib/blob/32e08aa57f6a023e995d8c4f0c9fbdc5f11d1fa0/ts_src/transaction.ts#L78 -// Reads buffer of raw transaction data -function fromBuffer(buffer: Uint8Array, network: string): Transaction { +/** + * @param buffer The raw transaction data + * @param network + * @param inputs Additional information from a PSBT, if available + * @returns The decoded transaction object and the raw hex + */ +function fromBuffer(buffer: Uint8Array, network: string, inputs?: { key: Uint8Array; value: Uint8Array }[][]): { tx: Transaction, hex: string } { let offset = 0; - function readInt8(): number { - if (offset + 1 > buffer.length) { - throw new Error('Buffer out of bounds'); - } - return buffer[offset++]; - } - - function readInt16() { - if (offset + 2 > buffer.length) { - throw new Error('Buffer out of bounds'); - } - const value = buffer[offset] | (buffer[offset + 1] << 8); - offset += 2; - return value; - } - - function readInt32(unsigned = false): number { - if (offset + 4 > buffer.length) { - throw new Error('Buffer out of bounds'); - } - const value = buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24); - offset += 4; - if (unsigned) { - return value >>> 0; - } - return value; - } - - function readInt64(): bigint { - if (offset + 8 > buffer.length) { - throw new Error('Buffer out of bounds'); - } - const low = BigInt(buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24)); - const high = BigInt(buffer[offset + 4] | (buffer[offset + 5] << 8) | (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24)); - offset += 8; - return (high << 32n) | (low & 0xffffffffn); - } - - function readVarInt(): bigint { - const first = readInt8(); - if (first < 0xfd) { - return BigInt(first); - } else if (first === 0xfd) { - return BigInt(readInt16()); - } else if (first === 0xfe) { - return BigInt(readInt32(true)); - } else if (first === 0xff) { - return readInt64(); - } else { - throw new Error("Invalid VarInt prefix"); - } - } - - function readSlice(n: number | bigint): Uint8Array { - const length = Number(n); - if (offset + length > buffer.length) { - throw new Error('Cannot read slice out of bounds'); - } - const slice = buffer.slice(offset, offset + length); - offset += length; - return slice; - } - - function readVarSlice(): Uint8Array { - return readSlice(readVarInt()); - } - - function readVector(): Uint8Array[] { - const count = readVarInt(); - const vector = []; - for (let i = 0; i < count; i++) { - vector.push(readVarSlice()); - } - return vector; - } - // Parse raw transaction const tx = { status: { @@ -802,39 +731,47 @@ function fromBuffer(buffer: Uint8Array, network: string): Transaction { } } as Transaction; - tx.version = readInt32(); + [tx.version, offset] = readInt32(buffer, offset); - const marker = readInt8(); - const flag = readInt8(); + let marker, flag; + [marker, offset] = readInt8(buffer, offset); + [flag, offset] = readInt8(buffer, offset); - let hasWitnesses = false; - if ( - marker === 0x00 && - flag === 0x01 - ) { - hasWitnesses = true; + let isLegacyTransaction = true; + if (marker === 0x00 && flag === 0x01) { + isLegacyTransaction = false; } else { offset -= 2; } - const vinLen = readVarInt(); + let vinLen; + [vinLen, offset] = readVarInt(buffer, offset); + if (vinLen === 0) { + throw new Error('Transaction has no inputs'); + } tx.vin = []; for (let i = 0; i < vinLen; ++i) { - const txid = uint8ArrayToHexString(readSlice(32).reverse()); - const vout = readInt32(true); - const scriptsig = uint8ArrayToHexString(readVarSlice()); - const sequence = readInt32(true); + let txid, vout, scriptsig, sequence; + [txid, offset] = readSlice(buffer, offset, 32); + txid = uint8ArrayToHexString(txid.reverse()); + [vout, offset] = readInt32(buffer, offset, true); + [scriptsig, offset] = readVarSlice(buffer, offset); + scriptsig = uint8ArrayToHexString(scriptsig); + [sequence, offset] = readInt32(buffer, offset, true); const is_coinbase = txid === '0'.repeat(64); const scriptsig_asm = convertScriptSigAsm(scriptsig); tx.vin.push({ txid, vout, scriptsig, sequence, is_coinbase, scriptsig_asm, prevout: null }); } - const voutLen = readVarInt(); + let voutLen; + [voutLen, offset] = readVarInt(buffer, offset); tx.vout = []; for (let i = 0; i < voutLen; ++i) { - const value = Number(readInt64()); - const scriptpubkeyArray = readVarSlice(); - const scriptpubkey = uint8ArrayToHexString(scriptpubkeyArray) + let value, scriptpubkeyArray, scriptpubkey; + [value, offset] = readInt64(buffer, offset); + value = Number(value); + [scriptpubkeyArray, offset] = readVarSlice(buffer, offset); + scriptpubkey = uint8ArrayToHexString(scriptpubkeyArray); const scriptpubkey_asm = convertScriptSigAsm(scriptpubkey); const toAddress = scriptPubKeyToAddress(scriptpubkey, network); const scriptpubkey_type = toAddress.type; @@ -842,48 +779,303 @@ function fromBuffer(buffer: Uint8Array, network: string): Transaction { tx.vout.push({ value, scriptpubkey, scriptpubkey_asm, scriptpubkey_type, scriptpubkey_address }); } - let witnessSize = 0; - if (hasWitnesses) { - const startOffset = offset; + if (!isLegacyTransaction) { for (let i = 0; i < vinLen; ++i) { - tx.vin[i].witness = readVector().map(uint8ArrayToHexString); + let witness; + [witness, offset] = readVector(buffer, offset); + tx.vin[i].witness = witness.map(uint8ArrayToHexString); } - witnessSize = offset - startOffset + 2; } - tx.locktime = readInt32(true); + [tx.locktime, offset] = readInt32(buffer, offset, true); if (offset !== buffer.length) { throw new Error('Transaction has unexpected data'); } - tx.size = buffer.length; - tx.weight = (tx.size - witnessSize) * 3 + tx.size; + // Optionally add data from PSBT: prevouts, redeem/witness scripts and signatures + if (inputs) { + for (let i = 0; i < tx.vin.length; i++) { + const vin = tx.vin[i]; + const inputRecords = inputs[i]; - tx.txid = txid(tx); + const groups = { + nonWitnessUtxo: null, + witnessUtxo: null, + finalScriptSig: null, + finalScriptWitness: null, + redeemScript: null, + witnessScript: null, + partialSigs: [] + }; - return tx; -} + for (const record of inputRecords) { + switch (record.key[0]) { + case 0x00: + groups.nonWitnessUtxo = record; + break; + case 0x01: + groups.witnessUtxo = record; + break; + case 0x07: + groups.finalScriptSig = record; + break; + case 0x08: + groups.finalScriptWitness = record; + break; + case 0x04: + groups.redeemScript = record; + break; + case 0x05: + groups.witnessScript = record; + break; + case 0x02: + groups.partialSigs.push(record); + break; + } + } -export function decodeRawTransaction(rawtx: string, network: string): Transaction { - if (!rawtx.length || rawtx.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(rawtx)) { - throw new Error('Invalid hex string'); + // Fill prevout + if (groups.witnessUtxo && !vin.prevout) { + let value, scriptpubkeyArray, scriptpubkey, outputOffset = 0; + [value, outputOffset] = readInt64(groups.witnessUtxo.value, outputOffset); + value = Number(value); + [scriptpubkeyArray, outputOffset] = readVarSlice(groups.witnessUtxo.value, outputOffset); + scriptpubkey = uint8ArrayToHexString(scriptpubkeyArray); + const scriptpubkey_asm = convertScriptSigAsm(scriptpubkey); + const toAddress = scriptPubKeyToAddress(scriptpubkey, network); + const scriptpubkey_type = toAddress.type; + const scriptpubkey_address = toAddress?.address; + vin.prevout = { value, scriptpubkey, scriptpubkey_asm, scriptpubkey_type, scriptpubkey_address }; + } + if (groups.nonWitnessUtxo && !vin.prevout) { + const utxoTx = fromBuffer(groups.nonWitnessUtxo.value, network).tx; + vin.prevout = utxoTx.vout[vin.vout]; + } + + // Fill final scriptSig or witness + let finalizedScriptSig = false; + if (groups.finalScriptSig) { + vin.scriptsig = uint8ArrayToHexString(groups.finalScriptSig.value); + vin.scriptsig_asm = convertScriptSigAsm(vin.scriptsig); + finalizedScriptSig = true; + } + let finalizedWitness = false; + if (groups.finalScriptWitness) { + let witness = []; + let witnessOffset = 0; + [witness, witnessOffset] = readVector(groups.finalScriptWitness.value, witnessOffset); + vin.witness = witness.map(uint8ArrayToHexString); + finalizedWitness = true; + } + if (finalizedScriptSig && finalizedWitness) { + continue; + } + + // Fill redeem script and/or witness script + if (groups.redeemScript && !finalizedScriptSig) { + const redeemScript = groups.redeemScript.value; + if (redeemScript.length > 520) { + throw new Error("Redeem script must be <= 520 bytes"); + } + let pushOpcode; + if (redeemScript.length < 0x4c) { + pushOpcode = new Uint8Array([redeemScript.length]); + } else if (redeemScript.length <= 0xff) { + pushOpcode = new Uint8Array([0x4c, redeemScript.length]); // OP_PUSHDATA1 + } else { + pushOpcode = new Uint8Array([0x4d, redeemScript.length & 0xff, redeemScript.length >> 8]); // OP_PUSHDATA2 + } + vin.scriptsig = (vin.scriptsig || '') + uint8ArrayToHexString(pushOpcode) + uint8ArrayToHexString(redeemScript); + vin.scriptsig_asm = convertScriptSigAsm(vin.scriptsig); + } + if (groups.witnessScript && !finalizedWitness) { + vin.witness = (vin.witness || []).concat(uint8ArrayToHexString(groups.witnessScript.value)); + } + + + // Fill partial signatures + for (const record of groups.partialSigs) { + const scriptpubkey_type = vin.prevout?.scriptpubkey_type; + if (scriptpubkey_type === 'v0_p2wsh' && !finalizedWitness) { + vin.witness = vin.witness || []; + vin.witness.unshift(uint8ArrayToHexString(record.value)); + } + if (scriptpubkey_type === 'p2sh') { + const redeemScriptStr = vin.scriptsig_asm ? vin.scriptsig_asm.split(' ').reverse()[0] : ''; + if (redeemScriptStr.startsWith('00') && redeemScriptStr.length === 68 && vin.witness?.length) { + if (!finalizedWitness) { + vin.witness.unshift(uint8ArrayToHexString(record.value)); + } + } else { + if (!finalizedScriptSig) { + const signature = record.value; + if (signature.length > 73) { + throw new Error("Signature must be <= 73 bytes"); + } + const pushOpcode = new Uint8Array([signature.length]); + vin.scriptsig = uint8ArrayToHexString(pushOpcode) + uint8ArrayToHexString(signature) + (vin.scriptsig || ''); + vin.scriptsig_asm = convertScriptSigAsm(vin.scriptsig); + } + } + } + } + } } - const buffer = new Uint8Array(rawtx.length / 2); - for (let i = 0; i < rawtx.length; i += 2) { - buffer[i / 2] = parseInt(rawtx.substring(i, i + 2), 16); + // Calculate final size, weight, and txid + const hasWitness = tx.vin.some(vin => vin.witness?.length); + let witnessSize = 0; + if (hasWitness) { + for (let i = 0; i < tx.vin.length; ++i) { + const witnessItems = tx.vin[i].witness || []; + witnessSize += getVarIntLength(witnessItems.length); + for (const item of witnessItems) { + const witnessItem = hexStringToUint8Array(item); + witnessSize += getVarIntLength(witnessItem.length); + witnessSize += witnessItem.length; + } + } + witnessSize += 2; + } + + const rawHex = serializeTransaction(tx, hasWitness); + tx.size = rawHex.length; + tx.weight = (tx.size - witnessSize) * 3 + tx.size; + tx.txid = txid(tx); + + return { tx, hex: uint8ArrayToHexString(rawHex) }; +} + +/** + * Decodes a PSBT buffer into the unsigned raw transaction and input map + * @param psbtBuffer + * @returns + * - the unsigned transaction from a PSBT (txHex) + * - the full input map for each input in to fill signatures and prevouts later (inputs) + */ +function decodePsbt(psbtBuffer: Uint8Array): { rawTx: Uint8Array; inputs: { key: Uint8Array; value: Uint8Array }[][] } { + let offset = 0; + + // magic: "psbt" in ASCII + const expectedMagic = [0x70, 0x73, 0x62, 0x74]; + for (let i = 0; i < expectedMagic.length; i++) { + if (psbtBuffer[offset + i] !== expectedMagic[i]) { + throw new Error("Invalid PSBT magic bytes"); + } + } + offset += expectedMagic.length; + + const separator = psbtBuffer[offset]; + offset += 1; + if (separator !== 0xff) { + throw new Error("Invalid PSBT separator"); + } + + // GLOBAL MAP + let rawTx: Uint8Array | null = null; + while (offset < psbtBuffer.length) { + const [keyLen, newOffset] = readVarInt(psbtBuffer, offset); + offset = newOffset; + // key length of 0 means the end of the global map + if (keyLen === 0) { + break; + } + const key = psbtBuffer.slice(offset, offset + keyLen); + offset += keyLen; + const [valLen, newOffset2] = readVarInt(psbtBuffer, offset); + offset = newOffset2; + const value = psbtBuffer.slice(offset, offset + valLen); + offset += valLen; + + // Global key type 0x00 holds the unsigned transaction. + if (key[0] === 0x00) { + rawTx = value; + } + } + + if (!rawTx) { + throw new Error("Unsigned transaction not found in PSBT"); + } + + let numInputs: number; + let txOffset = 0; + // Skip version (4 bytes) + txOffset += 4; + if (rawTx[txOffset] === 0x00 && rawTx[txOffset + 1] === 0x01) { + txOffset += 2; + } + const [inputCount, newTxOffset] = readVarInt(rawTx, txOffset); + txOffset = newTxOffset; + numInputs = inputCount; + + // INPUT MAPS + const inputs: { key: Uint8Array; value: Uint8Array }[][] = []; + for (let i = 0; i < numInputs; i++) { + const inputRecords: { key: Uint8Array; value: Uint8Array }[] = []; + const seenKeys = new Set(); + while (offset < psbtBuffer.length) { + const [keyLen, newOffset] = readVarInt(psbtBuffer, offset); + offset = newOffset; + // key length of 0 means the end of the input map + if (keyLen === 0) { + break; + } + const key = psbtBuffer.slice(offset, offset + keyLen); + offset += keyLen; + + const keyHex = uint8ArrayToHexString(key); + if (seenKeys.has(keyHex)) { + throw new Error(`Duplicate key in input map`); + } + seenKeys.add(keyHex); + + const [valLen, newOffset2] = readVarInt(psbtBuffer, offset); + offset = newOffset2; + const value = psbtBuffer.slice(offset, offset + valLen); + offset += valLen; + + inputRecords.push({ key, value }); + } + inputs.push(inputRecords); + } + + return { rawTx, inputs }; +} + +export function decodeRawTransaction(input: string, network: string): { tx: Transaction, hex: string } { + if (!input.length) { + throw new Error('Empty input'); + } + + let buffer: Uint8Array; + if (input.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(input)) { + buffer = hexStringToUint8Array(input); + } else if (/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)|[A-Za-z0-9+/]{3}=)?$/.test(input)) { + buffer = base64ToUint8Array(input); + } else { + throw new Error('Invalid input: not a valid transaction or PSBT'); + } + + if (buffer[0] === 0x70 && buffer[1] === 0x73 && buffer[2] === 0x62 && buffer[3] === 0x74) { // PSBT magic bytes + const { rawTx, inputs } = decodePsbt(buffer); + return fromBuffer(rawTx, network, inputs); } return fromBuffer(buffer, network); } -function serializeTransaction(tx: Transaction): Uint8Array { +function serializeTransaction(tx: Transaction, includeWitness: boolean = true): Uint8Array { const result: number[] = []; // Add version result.push(...intToBytes(tx.version, 4)); + if (includeWitness) { + // Add SegWit marker and flag bytes (0x00, 0x01) + result.push(0x00, 0x01); + } + // Add input count and inputs result.push(...varIntToBytes(tx.vin.length)); for (const input of tx.vin) { @@ -904,6 +1096,18 @@ function serializeTransaction(tx: Transaction): Uint8Array { result.push(...scriptPubKey); } + if (includeWitness) { + for (const input of tx.vin) { + const witnessItems = input.witness || []; + result.push(...varIntToBytes(witnessItems.length)); + for (const item of witnessItems) { + const witnessBytes = hexStringToUint8Array(item); + result.push(...varIntToBytes(witnessBytes.length)); + result.push(...witnessBytes); + } + } + } + // Add locktime result.push(...intToBytes(tx.locktime, 4)); @@ -911,7 +1115,7 @@ function serializeTransaction(tx: Transaction): Uint8Array { } function txid(tx: Transaction): string { - const serializedTx = serializeTransaction(tx); + const serializedTx = serializeTransaction(tx, false); const hash1 = new Hash().update(serializedTx).digest(); const hash2 = new Hash().update(hash1).digest(); return uint8ArrayToHexString(hash2.reverse()); @@ -1188,6 +1392,11 @@ function hexStringToUint8Array(hex: string): Uint8Array { return buf; } +function base64ToUint8Array(base64: string): Uint8Array { + const binaryString = atob(base64); + return new Uint8Array([...binaryString].map(char => char.charCodeAt(0))); +} + function intToBytes(value: number, byteLength: number): number[] { const bytes = []; for (let i = 0; i < byteLength; i++) { @@ -1230,6 +1439,88 @@ function varIntToBytes(value: number | bigint): number[] { return bytes; } +function readInt8(buffer: Uint8Array, offset: number): [number, number] { + if (offset + 1 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + return [buffer[offset], offset + 1]; +} + +function readInt16(buffer: Uint8Array, offset: number): [number, number] { + if (offset + 2 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + return [buffer[offset] | (buffer[offset + 1] << 8), offset + 2]; +} + +function readInt32(buffer: Uint8Array, offset: number, unsigned: boolean = false): [number, number] { + if (offset + 4 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + const value = buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24); + return [unsigned ? value >>> 0 : value, offset + 4]; +} + +function readInt64(buffer: Uint8Array, offset: number): [bigint, number] { + if (offset + 8 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + const low = BigInt(buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24)); + const high = BigInt(buffer[offset + 4] | (buffer[offset + 5] << 8) | (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24)); + return [(high << 32n) | (low & 0xffffffffn), offset + 8]; +} + +function readVarInt(buffer: Uint8Array, offset: number): [number, number] { + const [first, newOffset] = readInt8(buffer, offset); + + if (first < 0xfd) { + return [first, newOffset]; + } else if (first === 0xfd) { + return readInt16(buffer, newOffset); + } else if (first === 0xfe) { + return readInt32(buffer, newOffset, true); + } else if (first === 0xff) { + const [bigValue, nextOffset] = readInt64(buffer, newOffset); + + if (bigValue > Number.MAX_SAFE_INTEGER) { + throw new Error("VarInt exceeds safe integer range"); + } + + const numValue = Number(bigValue); + return [numValue, nextOffset]; + } else { + throw new Error("Invalid VarInt prefix"); + } +} + +function readSlice(buffer: Uint8Array, offset: number, n: number | bigint): [Uint8Array, number] { + const length = Number(n); + if (offset + length > buffer.length) { + throw new Error('Cannot read slice out of bounds'); + } + const slice = buffer.slice(offset, offset + length); + return [slice, offset + length]; +} + +function readVarSlice(buffer: Uint8Array, offset: number): [Uint8Array, number] { + const [length, newOffset] = readVarInt(buffer, offset); + return readSlice(buffer, newOffset, length); +} + +function readVector(buffer: Uint8Array, offset: number): [Uint8Array[], number] { + const [count, newOffset] = readVarInt(buffer, offset); + let updatedOffset = newOffset; + const vector: Uint8Array[] = []; + + for (let i = 0; i < count; i++) { + const [slice, nextOffset] = readVarSlice(buffer, updatedOffset); + vector.push(slice); + updatedOffset = nextOffset; + } + + return [vector, updatedOffset]; +} + // Inversed the opcodes object from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/utils/bitcoin-script.ts#L1 const opcodes = { 0: 'OP_0', From 494be165ad70b3e2f8d3b82afde710e86424c202 Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 4 Mar 2025 09:25:39 -1000 Subject: [PATCH 037/114] Update latest tag on dockerhub --- .github/workflows/on-tag.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 8a846631c..ba9e1eb7b 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -105,7 +105,7 @@ jobs: --cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ - # --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ + --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --build-context rustgbt=./rust \ --build-context backend=./backend \ --output "type=registry,push=true" \ From c4e22a6225c04547666e3a0d962b3f9d6cab5db9 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 5 Mar 2025 04:18:31 +0000 Subject: [PATCH 038/114] disabled ON UPDATE for blocks_audits time field --- backend/src/api/database-migration.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 4f43bd9d2..299cd309b 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 95; + private static currentVersion = 96; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -1130,6 +1130,11 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)'); await this.updateToSchemaVersion(95); } + + if (databaseSchemaVersion < 96) { + await this.$executeQuery(`ALTER TABLE blocks_audits MODIFY time timestamp NOT NULL DEFAULT 0`); + await this.updateToSchemaVersion(96); + } } /** From ad140dc60a32a9ced5d2772d355731f60f2553c6 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 5 Mar 2025 15:29:54 +0100 Subject: [PATCH 039/114] Tapscript multisig parsing --- frontend/src/app/shared/script.utils.ts | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts index 731e0051b..62a7a5845 100644 --- a/frontend/src/app/shared/script.utils.ts +++ b/frontend/src/app/shared/script.utils.ts @@ -251,6 +251,11 @@ export function detectScriptTemplate(type: ScriptType, script_asm: string, witne return ScriptTemplates.multisig(multisig.m, multisig.n); } + const tapscriptMultisig = parseTapscriptMultisig(script_asm); + if (tapscriptMultisig) { + return ScriptTemplates.multisig(tapscriptMultisig.m, tapscriptMultisig.n); + } + return; } @@ -299,6 +304,62 @@ export function parseMultisigScript(script: string): undefined | { m: number, n: return { m, n }; } +export function parseTapscriptMultisig(script: string): undefined | { m: number, n: number } { + if (!script) { + return; + } + + const ops = script.split(' '); + // At minimum, one pubkey group (3 tokens) + m push + final opcode = 5 tokens + if (ops.length < 5) return; + + const finalOp = ops.pop(); + if (finalOp !== 'OP_NUMEQUAL' && finalOp !== 'OP_GREATERTHANOREQUAL') { + return; + } + + let m: number; + if (['OP_PUSHBYTES_1', 'OP_PUSHBYTES_2'].includes(ops[ops.length - 2])) { + const data = ops.pop(); + ops.pop(); + m = parseInt(data.match(/../g).reverse().join(''), 16); + } else if (ops[ops.length - 1].startsWith('OP_PUSHNUM_') || ops[ops.length - 1] === 'OP_0') { + m = parseInt(ops.pop().match(/[0-9]+/)?.[0], 10); + } else { + return; + } + + if (ops.length % 3 !== 0) { + return; + } + const n = ops.length / 3; + if (n < 1) { + return; + } + + for (let i = 0; i < n; i++) { + const push = ops.shift(); + const pubkey = ops.shift(); + const sigOp = ops.shift(); + + if (push !== 'OP_PUSHBYTES_32') { + return; + } + if (!/^[0-9a-fA-F]{64}$/.test(pubkey)) { + return; + } + if (sigOp !== (i === 0 ? 'OP_CHECKSIG' : 'OP_CHECKSIGADD')) { + return; + } + } + + if (ops.length) { + return; + } + + return { m, n }; +} + export function getVarIntLength(n: number): number { if (n < 0xfd) { return 1; From 55c09efb580f87849bc2c8c6d4a81b3345b0b17d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 6 Mar 2025 02:42:17 +0000 Subject: [PATCH 040/114] be your own explorer --- frontend/src/app/components/about/about.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 433fe1abb..3bd8960f5 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -12,6 +12,7 @@
    The Mempool Open Source Project ®

    Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.

    +
    Be your own explorer™
    From 0b1895664b43af0c2d0b78a8e4e2a868d5bfea8a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 6 Mar 2025 03:52:20 +0000 Subject: [PATCH 042/114] change staging proxy from fmt to va1 --- frontend/proxy.conf.staging.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/proxy.conf.staging.js b/frontend/proxy.conf.staging.js index 260b222c0..0165bed96 100644 --- a/frontend/proxy.conf.staging.js +++ b/frontend/proxy.conf.staging.js @@ -3,10 +3,10 @@ const fs = require('fs'); let PROXY_CONFIG = require('./proxy.conf'); PROXY_CONFIG.forEach(entry => { - const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space'; + const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.va1.mempool.space'; console.log(`e2e tests running against ${hostname}`); entry.target = entry.target.replace("mempool.space", hostname); - entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space"); + entry.target = entry.target.replace("liquid.network", "liquid-staging.va1.mempool.space"); }); module.exports = PROXY_CONFIG; From 3b9d9864cf9bf4f72f9b89b57aa4c5b28fe531f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:22:43 +0000 Subject: [PATCH 043/114] Bump mysql2 from 3.12.0 to 3.13.0 in /backend Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.12.0 to 3.13.0. - [Release notes](https://github.com/sidorares/node-mysql2/releases) - [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md) - [Commits](https://github.com/sidorares/node-mysql2/compare/v3.12.0...v3.13.0) --- updated-dependencies: - dependency-name: mysql2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- backend/package-lock.json | 14 +++++++------- backend/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 1aaa77f85..a4963d6f0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,7 +17,7 @@ "crypto-js": "~4.2.0", "express": "~4.21.1", "maxmind": "~4.3.11", - "mysql2": "~3.12.0", + "mysql2": "~3.13.0", "redis": "^4.7.0", "rust-gbt": "file:./rust-gbt", "socks-proxy-agent": "~7.0.0", @@ -6173,9 +6173,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mysql2": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", - "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz", + "integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -12337,9 +12337,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mysql2": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", - "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz", + "integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==", "requires": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", diff --git a/backend/package.json b/backend/package.json index efc5a4501..bcbc0f256 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,7 +46,7 @@ "crypto-js": "~4.2.0", "express": "~4.21.1", "maxmind": "~4.3.11", - "mysql2": "~3.12.0", + "mysql2": "~3.13.0", "rust-gbt": "file:./rust-gbt", "redis": "^4.7.0", "socks-proxy-agent": "~7.0.0", From 9d711c336a51a05b55b7a7b56ec78d2ad2af2b6d Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 9 Mar 2025 11:42:53 +0100 Subject: [PATCH 044/114] retreive -> retrieve --- backend/src/api/blocks.ts | 8 ++++---- backend/src/repositories/HashratesRepository.ts | 2 +- .../accelerate-checkout/accelerate-checkout.component.ts | 6 +++--- frontend/src/app/docs/api-docs/api-docs-data.ts | 2 +- frontend/src/app/services/services-api.service.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 102601594..beefc825b 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -1469,11 +1469,11 @@ class Blocks { if (rows && Array.isArray(rows)) { return rows.map(r => r.definition_hash); } else { - logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`); + logger.debug(`Unable to retrieve list of blocks.definition_hash from db (no result)`); return null; } } catch (e) { - logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`); + logger.debug(`Unable to retrieve list of blocks.definition_hash from db (exception: ${e})`); return null; } } @@ -1484,11 +1484,11 @@ class Blocks { if (rows && Array.isArray(rows)) { return rows.map(r => r.hash); } else { - logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`); + logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (no result)`); return null; } } catch (e) { - logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`); + logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (exception: ${e})`); return null; } } diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index ec44afebe..93aa2d53f 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -93,7 +93,7 @@ class HashratesRepository { const [rows]: any[] = await DB.query(query); return rows.map(row => row.timestamp); } catch (e) { - logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); + logger.err('Cannot retrieve indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); throw e; } } diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index ac6c7f147..bf70aebd3 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -525,7 +525,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (tokenResult?.status === 'OK') { const card = tokenResult.details?.card; if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { - console.error(`Cannot retreive payment card details`); + console.error(`Cannot retrieve payment card details`); this.accelerateError = 'apple_pay_no_card_details'; this.processing = false; return; @@ -643,7 +643,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (tokenResult?.status === 'OK') { const card = tokenResult.details?.card; if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { - console.error(`Cannot retreive payment card details`); + console.error(`Cannot retrieve payment card details`); this.accelerateError = 'apple_pay_no_card_details'; this.processing = false; return; @@ -936,7 +936,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingBtcpayInvoice = true; this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe( switchMap(response => { - return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId); + return this.servicesApiService.retrieveInvoice$(response.btcpayInvoiceId); }), catchError(error => { console.log(error); 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 c32baa3f7..5e9608bdf 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -11803,7 +11803,7 @@ export const restApiDocsData = [ fragment: "accelerator-cancel", title: "POST Cancel Acceleration (Pro)", description: { - default: "

    Sends a request to cancel an acceleration in the accelerating status.
    You can retreive eligible acceleration id using the history endpoint GET /api/v1/services/accelerator/history?status=accelerating." + default: "

    Sends a request to cancel an acceleration in the accelerating status.
    You can retrieve eligible acceleration id using the history endpoint GET /api/v1/services/accelerator/history?status=accelerating." }, urlString: "/v1/services/accelerator/cancel", showConditions: [""], diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 59dc92358..a9550e731 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -213,7 +213,7 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/payments/bitcoin`, params); } - retreiveInvoice$(invoiceId: string): Observable { + retrieveInvoice$(invoiceId: string): Observable { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/payments/bitcoin/invoice?id=${invoiceId}`); } From 658151e0e8cee063d5f60852aaf0f1d9db1f38cf Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 11 Mar 2025 12:46:00 +0900 Subject: [PATCH 045/114] ops: Update electrs patch path for FreeBSD prod build --- production/install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/production/install b/production/install index 05b12c08e..68eac6e5a 100755 --- a/production/install +++ b/production/install @@ -1314,7 +1314,7 @@ if [ "${BITCOIN_ELECTRS_INSTALL}" = ON ];then case $OS in FreeBSD) echo "[*] Patching Bitcoin Electrs code for FreeBSD" - osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" + osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" #osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak" #osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak" ;; @@ -1364,7 +1364,7 @@ if [ "${ELEMENTS_ELECTRS_INSTALL}" = ON ];then case $OS in FreeBSD) echo "[*] Patching Liquid Electrs code for FreeBSD" - osSudo "${ELEMENTS_USER}" sh -c "cd \"${ELEMENTS_HOME}/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" + osSudo "${ELEMENTS_USER}" sh -c "cd \"${ELEMENTS_HOME}/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" ;; Debian) ;; From 7adc0083af973c65c321e9bcb96bfa371c87e401 Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 11 Mar 2025 14:08:51 +0900 Subject: [PATCH 046/114] ops: Modify prod install to run even if mysql exists --- production/install | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/production/install b/production/install index 68eac6e5a..487850b37 100755 --- a/production/install +++ b/production/install @@ -562,7 +562,7 @@ zfsCreateFilesystems() zfs create -o "mountpoint=${MINFEE_HOME}" "${ZPOOL}/minfee" zfs create -o "mountpoint=${ELECTRS_HOME}" "${ZPOOL}/electrs" zfs create -o "mountpoint=${MEMPOOL_HOME}" "${ZPOOL}/mempool" - zfs create -o "mountpoint=${MYSQL_HOME}" "${ZPOOL}/mysql" + zfs create -o "mountpoint=${MYSQL_HOME}" "${ZPOOL}/mysql" || true zfs create -o "mountpoint=${BITCOIN_ELECTRS_HOME}" "${ZPOOL}/bitcoin/electrs" @@ -1907,34 +1907,34 @@ esac sleep 10 mysql << _EOF_ -create database mempool; +create database if not exists mempool; grant all on mempool.* to '${MEMPOOL_MAINNET_USER}'@'localhost' identified by '${MEMPOOL_MAINNET_PASS}'; -create database mempool_testnet; +create database if not exists mempool_testnet; grant all on mempool_testnet.* to '${MEMPOOL_TESTNET_USER}'@'localhost' identified by '${MEMPOOL_TESTNET_PASS}'; -create database mempool_testnet4; +create database if not exists mempool_testnet4; grant all on mempool_testnet4.* to '${MEMPOOL_TESTNET4_USER}'@'localhost' identified by '${MEMPOOL_TESTNET4_PASS}'; -create database mempool_signet; +create database if not exists mempool_signet; grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}'; -create database mempool_mainnet_lightning; +create database if not exists mempool_mainnet_lightning; grant all on mempool_mainnet_lightning.* to '${MEMPOOL_MAINNET_LIGHTNING_USER}'@'localhost' identified by '${MEMPOOL_MAINNET_LIGHTNING_PASS}'; -create database mempool_testnet_lightning; +create database if not exists mempool_testnet_lightning; grant all on mempool_testnet_lightning.* to '${MEMPOOL_TESTNET_LIGHTNING_USER}'@'localhost' identified by '${MEMPOOL_TESTNET_LIGHTNING_PASS}'; -create database mempool_signet_lightning; +create database if not exists mempool_signet_lightning; grant all on mempool_signet_lightning.* to '${MEMPOOL_SIGNET_LIGHTNING_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_LIGHTNING_PASS}'; -create database mempool_liquid; +create database if not exists mempool_liquid; grant all on mempool_liquid.* to '${MEMPOOL_LIQUID_USER}'@'localhost' identified by '${MEMPOOL_LIQUID_PASS}'; -create database mempool_liquidtestnet; +create database if not exists mempool_liquidtestnet; grant all on mempool_liquidtestnet.* to '${MEMPOOL_LIQUIDTESTNET_USER}'@'localhost' identified by '${MEMPOOL_LIQUIDTESTNET_PASS}'; -create database mempool_bisq; +create database if not exists mempool_bisq; grant all on mempool_bisq.* to '${MEMPOOL_BISQ_USER}'@'localhost' identified by '${MEMPOOL_BISQ_PASS}'; _EOF_ From 636b4c0da72d31ddde3e27bdfa7d1e078d62327c Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 11 Mar 2025 15:46:03 +0900 Subject: [PATCH 047/114] ops: Add missing if check for CLN in prod install --- production/install | 3 +++ 1 file changed, 3 insertions(+) diff --git a/production/install b/production/install index 487850b37..7c84e5956 100755 --- a/production/install +++ b/production/install @@ -1378,6 +1378,8 @@ fi # Core Lightning for Bitcoin # ############################## +if [ "${CLN_INSTALL}" = ON ];then + echo "[*] Installing Core Lightning" case $OS in FreeBSD) @@ -1418,6 +1420,7 @@ case $OS in ;; esac +fi ##################### # Bisq installation # From 9bef19449fd24dc0e6a47a24a58f7a7dcc7bc051 Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 11 Mar 2025 16:37:14 +0900 Subject: [PATCH 048/114] ops: Comment out old keybase commands in build script --- production/mempool-build-all | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/production/mempool-build-all b/production/mempool-build-all index 377deb316..10ad179ea 100755 --- a/production/mempool-build-all +++ b/production/mempool-build-all @@ -173,7 +173,7 @@ for repo in $frontend_repos;do done # notify everyone -echo "${HOSTNAME} updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.dev -echo "${HOSTNAME} updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general "mempool.ops.${LOCATION}" +#echo "${HOSTNAME} updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.dev +#echo "${HOSTNAME} updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general "mempool.ops.${LOCATION}" exit 0 From c79ef93413a46115522e801cb40590df8a2afe37 Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 11 Mar 2025 16:38:26 +0900 Subject: [PATCH 049/114] ops: Bump prod to bitcoin v28.1 + elements 23.2.6 --- production/install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/production/install b/production/install index 7c84e5956..f3bebb4e7 100755 --- a/production/install +++ b/production/install @@ -357,7 +357,7 @@ BITCOIN_REPO_URL=https://github.com/bitcoin/bitcoin BITCOIN_REPO_NAME=bitcoin BITCOIN_REPO_BRANCH=master #BITCOIN_LATEST_RELEASE=$(curl -s https://api.github.com/repos/bitcoin/bitcoin/releases/latest|grep tag_name|head -1|cut -d '"' -f4) -BITCOIN_LATEST_RELEASE=v28.0 +BITCOIN_LATEST_RELEASE=v28.1 echo -n '.' BISQ_REPO_URL=https://github.com/bisq-network/bisq @@ -378,7 +378,7 @@ ELEMENTS_REPO_URL=https://github.com/ElementsProject/elements ELEMENTS_REPO_NAME=elements ELEMENTS_REPO_BRANCH=master #ELEMENTS_LATEST_RELEASE=$(curl -s https://api.github.com/repos/ElementsProject/elements/releases/latest|grep tag_name|head -1|cut -d '"' -f4) -ELEMENTS_LATEST_RELEASE=elements-22.1.1 +ELEMENTS_LATEST_RELEASE=elements-23.2.6 echo -n '.' BITCOIN_ELECTRS_REPO_URL=https://github.com/mempool/electrs From 65b276678fb8c64ff0b2ac28d0384962a021b3dc Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 12 Mar 2025 09:35:55 +0900 Subject: [PATCH 050/114] ops: Add negative balance check to check script --- production/check | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/production/check b/production/check index 3f31e67ee..3da5e920d 100755 --- a/production/check +++ b/production/check @@ -18,28 +18,37 @@ check_mempoolfoss_frontend_md5_hash() { check_mempool_electrs_git_hash() { echo -n $(curl -s -i https://node${1}.${2}.mempool.space/api/mempool|grep -i x-powered-by|cut -d ' ' -f3|cut -d '-' -f3|tr -d '\r'|tr -d '\n') } +check_mempool_electrs_negative_balance() { + echo -n $(curl -s https://node${1}.${2}.mempool.space/api/address/35Ty15fzBPGQvKnXZMLYvr41Fq2FTdU54a|jq .chain_stats.spent_txo_sum|tr -d '\r'|tr -d '\n') +} check_liquid_electrs_git_hash() { echo -n $(curl -s -i --connect-to "::node${1}.${2}.mempool.space:443" https://liquid.network/api/mempool|grep -i x-powered-by|cut -d ' ' -f3|cut -d '-' -f3|tr -d '\r'|tr -d '\n') } -for site in fmt va1 fra tk7;do +check_contributors_md5_hash() { + echo -n $(curl -s --connect-to "::node${1}.${2}.mempool.space:443" https://mempool.space/api/v1/contributors|md5|cut -c1-8) +} +for site in va1 fra tk7 fmt;do echo "${site}" for node in 201 202 203 204 205 206 207 208 209 210 211 212 213 214;do [ "${site}" = "fmt" ] && [ "${node}" -gt 206 ] && continue [ "${site}" = "tk7" ] && [ "${node}" -gt 206 ] && continue echo -n "node${node}.${site}: " - check_mempoolspace_frontend_git_hash $node $site - echo -n " " - check_mempoolfoss_frontend_git_hash $node $site - echo -n " " - check_mempoolfoss_backend_git_hash $node $site - echo -n " " - check_mempoolspace_frontend_md5_hash $node $site - echo -n " " - check_mempoolfoss_frontend_md5_hash $node $site - echo -n " " +# check_mempoolspace_frontend_git_hash $node $site +# echo -n " " +# check_mempoolfoss_frontend_git_hash $node $site +# echo -n " " +# check_mempoolfoss_backend_git_hash $node $site +# echo -n " " +# check_mempoolspace_frontend_md5_hash $node $site +# echo -n " " +# check_mempoolfoss_frontend_md5_hash $node $site +# echo -n " " check_mempool_electrs_git_hash $node $site echo -n " " - check_liquid_electrs_git_hash $node $site +# check_liquid_electrs_git_hash $node $site +# echo -n " " +# check_contributors_md5_hash $node $site + check_mempool_electrs_negative_balance $node $site echo done done From 305d931d5c3fc5b87b1a2c08efbb4f634a3dee7f Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 12 Mar 2025 09:43:00 +0900 Subject: [PATCH 051/114] ops: Add more sites to check script --- production/check | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/production/check b/production/check index 3da5e920d..d1bba7826 100755 --- a/production/check +++ b/production/check @@ -27,9 +27,12 @@ check_liquid_electrs_git_hash() { check_contributors_md5_hash() { echo -n $(curl -s --connect-to "::node${1}.${2}.mempool.space:443" https://mempool.space/api/v1/contributors|md5|cut -c1-8) } -for site in va1 fra tk7 fmt;do +for site in va1 fra tk7 sg1 hnl;do echo "${site}" for node in 201 202 203 204 205 206 207 208 209 210 211 212 213 214;do + [ "${site}" = "fmt" ] && [ "${node}" = 203 ] && continue + [ "${site}" = "sg1" ] && [ "${node}" -gt 204 ] && continue + [ "${site}" = "hnl" ] && [ "${node}" -gt 204 ] && continue [ "${site}" = "fmt" ] && [ "${node}" -gt 206 ] && continue [ "${site}" = "tk7" ] && [ "${node}" -gt 206 ] && continue echo -n "node${node}.${site}: " From a152afb4af6aedc111b08c1e25ed9813e22656f9 Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 12 Mar 2025 11:51:03 +0900 Subject: [PATCH 052/114] ops: Fix nginx conf for elements unix socket paths --- production/nginx/nginx.conf | 4 ++-- production/nginx/server-esplora.conf | 4 ++-- production/nginx/upstream-esplora.conf | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/production/nginx/nginx.conf b/production/nginx/nginx.conf index 81c0c01d5..790bd6d73 100644 --- a/production/nginx/nginx.conf +++ b/production/nginx/nginx.conf @@ -99,8 +99,8 @@ http { set $mempoolTestnet "http://mempool-liquid-testnet"; # for blockstream/esplora daemon, see upstream-esplora.conf - set $esploraMainnet "http://esplora-liquid-mainnet"; - set $esploraTestnet "http://esplora-liquid-testnet"; + set $esploraMainnet "http://esplora-elements-liquid"; + set $esploraTestnet "http://esplora-elements-liquidtestnet"; # filesystem paths root /mempool/public_html/liquid/; diff --git a/production/nginx/server-esplora.conf b/production/nginx/server-esplora.conf index 38cdb0bc2..fdbea6bd5 100644 --- a/production/nginx/server-esplora.conf +++ b/production/nginx/server-esplora.conf @@ -9,7 +9,7 @@ server { listen 127.0.0.1:4001; access_log /dev/null; location / { - proxy_pass http://esplora-liquid-mainnet; + proxy_pass http://esplora-elements-liquid; } } server { @@ -30,6 +30,6 @@ server { listen 127.0.0.1:4004; access_log /dev/null; location / { - proxy_pass http://esplora-liquid-testnet; + proxy_pass http://esplora-elements-liquidtestnet; } } diff --git a/production/nginx/upstream-esplora.conf b/production/nginx/upstream-esplora.conf index 88ffa11bd..80b76df2d 100644 --- a/production/nginx/upstream-esplora.conf +++ b/production/nginx/upstream-esplora.conf @@ -1,7 +1,7 @@ upstream esplora-bitcoin-mainnet { server unix:/bitcoin/socket/esplora-bitcoin-mainnet fail_timeout=10s max_fails=10 weight=99999; } -upstream esplora-liquid-mainnet { +upstream esplora-elements-liquid { server unix:/elements/socket/esplora-elements-liquid fail_timeout=10s max_fails=10 weight=99999; } upstream esplora-bitcoin-testnet { @@ -13,6 +13,6 @@ upstream esplora-bitcoin-testnet4 { upstream esplora-bitcoin-signet { server unix:/bitcoin/socket/esplora-bitcoin-signet fail_timeout=10s max_fails=10 weight=99999; } -upstream esplora-liquid-testnet { +upstream esplora-elements-liquidtestnet { server unix:/elements/socket/esplora-elements-liquidtestnet fail_timeout=10s max_fails=10 weight=99999; } From 7d9e275803e90b621b4419561962086e5f41a3f2 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 12 Mar 2025 14:56:15 +0100 Subject: [PATCH 053/114] [accelerator] show square receipt if available --- .../accelerate-checkout.component.html | 19 +++++++++++++++++-- .../accelerate-checkout.component.ts | 16 ++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 4594fd9fc..2038d4b6c 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -567,14 +567,29 @@ } @else if (step === 'success') {

    -

    Your transaction is being accelerated!

    +

    + @if (accelerationResponse) { + Your transaction is being accelerated! + } @else { + Transaction is already being accelerated! + } +

    - Your transaction has been accepted for acceleration by our mining pool partners. + @if (accelerationResponse) { + Your transaction has been accepted for acceleration by our mining pool partners. + } @else { + Transaction has already been accepted for acceleration by our mining pool partners. + }
    + @if (accelerationResponse?.receiptUrl) { + + }

    diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index ac6c7f147..06d2fa4cd 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -87,6 +87,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { math = Math; isMobile: boolean = window.innerWidth <= 767.98; isProdDomain = false; + accelerationResponse: { receiptUrl: string | null } | undefined; private _step: CheckoutStep = 'summary'; simpleMode: boolean = true; @@ -194,11 +195,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.scrollToElement('acceleratePreviewAnchor', 'start'); } if (changes.accelerating && this.accelerating) { - if (this.step === 'processing' || this.step === 'paid') { - this.moveToStep('success', true); - } else { // Edge case where the transaction gets accelerated by someone else or on another session - this.closeModal(); - } + this.moveToStep('success', true); } } @@ -541,7 +538,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, costUSD ).subscribe({ - next: () => { + next: (response) => { + this.accelerationResponse = response; this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); @@ -668,7 +666,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { costUSD, verificationToken.userChallenged ).subscribe({ - next: () => { + next: (response) => { + this.accelerationResponse = response; this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); @@ -777,7 +776,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { costUSD, verificationToken.userChallenged ).subscribe({ - next: () => { + next: (response) => { + this.accelerationResponse = response; this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); From cac404ae9bbce0c6192fddf3a99bb8c79483f134 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 12 Mar 2025 15:26:44 +0100 Subject: [PATCH 054/114] [accelerator] make sure we cannot go back from 'success' step --- .../accelerate-checkout/accelerate-checkout.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 06d2fa4cd..0db40af82 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -200,7 +200,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } moveToStep(step: CheckoutStep, force: boolean = false): void { - if (this.isCheckoutLocked > 0 && !force) { + if (this.isCheckoutLocked > 0 && !force || this.step === 'success') { return; } this.processing = false; From 1121377c7a888d8a1d12859cf9488d3db4b56249 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 12 Mar 2025 15:27:00 +0100 Subject: [PATCH 055/114] [accelerator] add missing response from cashapp payment --- .../accelerate-checkout/accelerate-checkout.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 0db40af82..7fc1e88ef 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -870,7 +870,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { tokenResult.details.cashAppPay.referenceId, costUSD ).subscribe({ - next: () => { + next: (response) => { + this.accelerationResponse = response; this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); From 062c5ca03a0d1bf4e8678dc717b9df5b228a4955 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 12 Mar 2025 16:47:31 +0100 Subject: [PATCH 056/114] Trim input data in tx preview --- .../src/app/components/transaction/transaction-raw.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts index 5ce170e12..f7ae4a751 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.ts +++ b/frontend/src/app/components/transaction/transaction-raw.component.ts @@ -82,7 +82,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.resetState(); this.isLoading = true; try { - const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network); + const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value.trim(), this.stateService.network); await this.fetchPrevouts(tx); await this.fetchCpfpInfo(tx); this.processTransaction(tx, hex); From 4dff3adf1169d056f35acd5b3c28bb5fadbd850d Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 12 Mar 2025 16:49:38 +0100 Subject: [PATCH 057/114] Redirect to tx page 2 seconds after broadcast --- .../transaction-raw.component.html | 11 ++++--- .../transaction-raw.component.scss | 8 +++++ .../transaction/transaction-raw.component.ts | 31 ++++++++++++------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction-raw.component.html b/frontend/src/app/components/transaction/transaction-raw.component.html index 3bd8ee6d2..35701889b 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.html +++ b/frontend/src/app/components/transaction/transaction-raw.component.html @@ -30,7 +30,6 @@
    -
    @@ -40,14 +39,18 @@
    -
    +
    - + This transaction is stored locally in your browser. Broadcast it to add it to the mempool. + + Redirecting to transaction page... + - + +
    @if (!hasPrevouts) { diff --git a/frontend/src/app/components/transaction/transaction-raw.component.scss b/frontend/src/app/components/transaction/transaction-raw.component.scss index 5bbe5601e..a4b386cee 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.scss +++ b/frontend/src/app/components/transaction/transaction-raw.component.scss @@ -191,4 +191,12 @@ .no-cursor { cursor: default !important; pointer-events: none; +} + +.btn-broadcast { + margin-left: 5px; + @media (max-width: 567px) { + margin-left: 0; + margin-top: 5px; + } } \ No newline at end of file diff --git a/frontend/src/app/components/transaction/transaction-raw.component.ts b/frontend/src/app/components/transaction/transaction-raw.component.ts index f7ae4a751..2e4dd4868 100644 --- a/frontend/src/app/components/transaction/transaction-raw.component.ts +++ b/frontend/src/app/components/transaction/transaction-raw.component.ts @@ -3,7 +3,7 @@ import { Transaction, Vout } from '@interfaces/electrs.interface'; import { StateService } from '../../services/state.service'; import { Filter, toFilters } from '../../shared/filters.utils'; import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils'; -import { firstValueFrom, Subscription } from 'rxjs'; +import { catchError, firstValueFrom, Subscription, switchMap, tap, throwError, timer } from 'rxjs'; import { WebsocketService } from '../../services/websocket.service'; import { ActivatedRoute, Router } from '@angular/router'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; @@ -36,6 +36,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { isLoadingBroadcast: boolean; errorBroadcast: string; successBroadcast: boolean; + broadcastSubscription: Subscription; isMobile: boolean; @ViewChild('graphContainer') @@ -207,18 +208,22 @@ export class TransactionRawComponent implements OnInit, OnDestroy { }); } - async postTx(): Promise { + postTx(): void { this.isLoadingBroadcast = true; this.errorBroadcast = null; - return new Promise((resolve, reject) => { - this.apiService.postTransaction$(this.rawHexTransaction) - .subscribe((result) => { + + this.broadcastSubscription = this.apiService.postTransaction$(this.rawHexTransaction).pipe( + tap((txid: string) => { this.isLoadingBroadcast = false; this.successBroadcast = true; - this.transaction.txid = result; - resolve(result); - }, - (error) => { + this.transaction.txid = txid; + }), + switchMap((txid: string) => + timer(2000).pipe( + tap(() => this.router.navigate([this.relativeUrlPipe.transform('/tx/' + txid)])), + ) + ), + catchError((error) => { if (typeof error.error === 'string') { const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"'); this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error); @@ -226,9 +231,9 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message; } this.isLoadingBroadcast = false; - reject(this.error); - }); - }); + return throwError(() => error); + }) + ).subscribe(); } resetState() { @@ -253,6 +258,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.missingPrevouts = []; this.stateService.markBlock$.next({}); this.mempoolBlocksSubscription?.unsubscribe(); + this.broadcastSubscription?.unsubscribe(); } resetForm() { @@ -308,6 +314,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy { this.mempoolBlocksSubscription?.unsubscribe(); this.flowPrefSubscription?.unsubscribe(); this.stateService.markBlock$.next({}); + this.broadcastSubscription?.unsubscribe(); } } From 00e7e726bc7084933d8e757a69e46049b0eef7bb Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:31:10 -0500 Subject: [PATCH 058/114] Mark mempool accelerator as registered trademark --- LICENSE | 2 +- .../src/app/components/about/about.component.html | 2 +- .../accelerate-checkout.component.html | 2 +- .../privacy-policy/privacy-policy.component.html | 12 ++++++------ .../components/svg-images/svg-images.component.html | 4 ++-- .../terms-of-service/terms-of-service.component.html | 8 ++++---- .../trademark-policy/trademark-policy.component.html | 2 +- .../transaction-details.component.html | 4 ++-- .../src/app/docs/api-docs/api-docs.component.html | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/LICENSE b/LICENSE index 1c368c00a..b6e67e523 100644 --- a/LICENSE +++ b/LICENSE @@ -10,7 +10,7 @@ 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. -The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, +The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 3bd8960f5..5b53e94ab 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -451,7 +451,7 @@ Trademark Notice

    - The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research 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. + The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research 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.

    While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 4594fd9fc..4ac5aa24e 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -158,7 +158,7 @@ - Mempool Accelerator™ fees + Mempool Accelerator® fees diff --git a/frontend/src/app/components/privacy-policy/privacy-policy.component.html b/frontend/src/app/components/privacy-policy/privacy-policy.component.html index 06b09ad30..bfd6159c4 100644 --- a/frontend/src/app/components/privacy-policy/privacy-policy.component.html +++ b/frontend/src/app/components/privacy-policy/privacy-policy.component.html @@ -45,14 +45,14 @@
    -

    USING MEMPOOL ACCELERATOR™

    +

    USING MEMPOOL ACCELERATOR®

    -

    If you use Mempool Accelerator™ your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.

    +

    If you use Mempool Accelerator® your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.

    -

    When using Mempool Accelerator™ the mempool.space privacy policy will apply: https://mempool.space/privacy-policy.

    +

    When using Mempool Accelerator® the mempool.space privacy policy will apply: https://mempool.space/privacy-policy.


    - +

    SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE

    @@ -67,7 +67,7 @@
  • If you sign up for a subscription to Mempool Enterprise™ we also collect your company name which is not shared with any third-party.
  • -
  • If you sign up for an account on mempool.space and use Mempool Accelerator™ Pro your accelerated transactions will be associated with your account for the purposes of accounting.
  • +
  • If you sign up for an account on mempool.space and use Mempool Accelerator® Pro your accelerated transactions will be associated with your account for the purposes of accounting.
  • @@ -101,7 +101,7 @@

    We aim to retain your data only as long as necessary:

      -
    • An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator™ Pro account credit
    • +
    • An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator® Pro account credit
    • If an account meets the criteria for inactivity as defined above, we will automatically delete the associated account data after a period of 6 months of continuous inactivity, except in the case of payment disputes or account irregularities.
    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 76aa3de85..04b99dea6 100644 --- a/frontend/src/app/components/svg-images/svg-images.component.html +++ b/frontend/src/app/components/svg-images/svg-images.component.html @@ -137,7 +137,7 @@
    - Mempool Accelerator™ + Mempool Accelerator® @@ -695,4 +695,4 @@ - \ No newline at end of file + diff --git a/frontend/src/app/components/terms-of-service/terms-of-service.component.html b/frontend/src/app/components/terms-of-service/terms-of-service.component.html index 709605a9f..51f035436 100644 --- a/frontend/src/app/components/terms-of-service/terms-of-service.component.html +++ b/frontend/src/app/components/terms-of-service/terms-of-service.component.html @@ -67,9 +67,9 @@ -

    MEMPOOL ACCELERATOR™

    +

    MEMPOOL ACCELERATOR®

    -

    Mempool Accelerator™ enables members of the Bitcoin community to submit requests for transaction prioritization.

    +

    Mempool Accelerator® enables members of the Bitcoin community to submit requests for transaction prioritization.

    • Mempool will use reasonable commercial efforts to relay user acceleration requests to Mempool's mining pool partners, but it is at the discretion of Mempool and Mempool's mining pool partners to accept acceleration requests.
    • @@ -84,11 +84,11 @@
      -
    • All acceleration payments and Mempool Accelerator™ account credit top-ups are non-refundable.
    • +
    • All acceleration payments and Mempool Accelerator® account credit top-ups are non-refundable.

    • -
    • Mempool Accelerator™ account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.
    • +
    • Mempool Accelerator® account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.

    • 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 e12cbb8b2..f7da0a7a4 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.component.html +++ b/frontend/src/app/components/trademark-policy/trademark-policy.component.html @@ -340,7 +340,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 Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles 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."

      +

      "The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles 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/components/transaction/transaction-details/transaction-details.component.html b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html index 78bba955c..819e27d89 100644 --- a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html +++ b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html @@ -170,7 +170,7 @@ @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) { @@ -318,4 +318,4 @@ - \ No newline at end of file + 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 75e37a3bd..d359500a0 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.html +++ b/frontend/src/app/docs/api-docs/api-docs.component.html @@ -219,7 +219,7 @@ -

      To get your transaction confirmed quicker, you will need to increase its effective feerate.

      If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.

      If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).

      Another option to get your transaction confirmed more quickly is Mempool Accelerator™.

      +

      To get your transaction confirmed quicker, you will need to increase its effective feerate.

      If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.

      If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).

      Another option to get your transaction confirmed more quickly is Mempool Accelerator®.

      From 8efea6160135a6284142f911aaf03aab4045aaae Mon Sep 17 00:00:00 2001 From: wiz Date: Thu, 13 Mar 2025 09:49:40 +0900 Subject: [PATCH 059/114] ops: Add electrs popular-scripts cron jobs --- production/bitcoin.crontab | 9 +++++++++ production/elements.crontab | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/production/bitcoin.crontab b/production/bitcoin.crontab index a5bc64241..a17147f7b 100644 --- a/production/bitcoin.crontab +++ b/production/bitcoin.crontab @@ -1,7 +1,16 @@ +# start test network daemons on boot @reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 @reboot sleep 5 ; /usr/local/bin/bitcoind -testnet4 >/dev/null 2>&1 @reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1 + +# start electrs on boot @reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/start mainnet @reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/start testnet @reboot sleep 10 ; screen -dmS testnet4 /bitcoin/electrs/start testnet4 @reboot sleep 10 ; screen -dmS signet /bitcoin/electrs/start signet + +# daily update of popular-scripts +30 03 * * * $HOME/electrs/start testnet4 popular-scripts >/dev/null 2>&1 +31 03 * * * $HOME/electrs/start testnet popular-scripts >/dev/null 2>&1 +32 03 * * * $HOME/electrs/start signet popular-scripts >/dev/null 2>&1 +33 03 * * * $HOME/electrs/start mainnet popular-scripts >/dev/null 2>&1 diff --git a/production/elements.crontab b/production/elements.crontab index 4f837706e..6590dfbd7 100644 --- a/production/elements.crontab +++ b/production/elements.crontab @@ -8,3 +8,7 @@ # hourly asset update and electrs restart 6 * * * * cd $HOME/asset_registry_db && git pull --quiet origin master && cd $HOME/asset_registry_testnet_db && git pull --quiet origin master && killall electrs + +# daily update of popular-scripts +32 03 * * * $HOME/electrs/start liquid popular-scripts >/dev/null 2>&1 +33 03 * * * $HOME/electrs/start liquidtestnet popular-scripts >/dev/null 2>&1 From e94dc67b3187ebfbaa630e43805e2f4be306803a Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 13 Mar 2025 15:46:11 +0100 Subject: [PATCH 060/114] Update tapscript multisig minimum size --- frontend/src/app/shared/script.utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts index 62a7a5845..df50a4070 100644 --- a/frontend/src/app/shared/script.utils.ts +++ b/frontend/src/app/shared/script.utils.ts @@ -310,8 +310,10 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number, } const ops = script.split(' '); - // At minimum, one pubkey group (3 tokens) + m push + final opcode = 5 tokens - if (ops.length < 5) return; + // At minimum, 2 pubkey group (3 tokens) + m push + final opcode = 8 tokens + if (ops.length < 8) { + return; + } const finalOp = ops.pop(); if (finalOp !== 'OP_NUMEQUAL' && finalOp !== 'OP_GREATERTHANOREQUAL') { From 188096e651f745ef8a09d4091d1eb727282a50c4 Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 13 Mar 2025 16:38:53 +0100 Subject: [PATCH 061/114] Add opcodes that can be used for tapscript multisig --- frontend/src/app/shared/script.utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts index df50a4070..f0c4701db 100644 --- a/frontend/src/app/shared/script.utils.ts +++ b/frontend/src/app/shared/script.utils.ts @@ -316,7 +316,7 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number, } const finalOp = ops.pop(); - if (finalOp !== 'OP_NUMEQUAL' && finalOp !== 'OP_GREATERTHANOREQUAL') { + if (!['OP_NUMEQUAL', 'OP_NUMEQUALVERIFY', 'OP_GREATERTHANOREQUAL', 'OP_GREATERTHAN', 'OP_EQUAL', 'OP_EQUALVERIFY'].includes(finalOp)) { return; } @@ -331,6 +331,10 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number, return; } + if (finalOp === 'OP_GREATERTHAN') { + m += 1; + } + if (ops.length % 3 !== 0) { return; } From 76f31623feffd946c7072a208c990f1f68f6c9cc Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Thu, 13 Mar 2025 15:49:45 -0700 Subject: [PATCH 062/114] Don't tag as latest by default --- .github/workflows/on-tag.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index ba9e1eb7b..1447ec4ab 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -105,7 +105,6 @@ jobs: --cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ - --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --build-context rustgbt=./rust \ --build-context backend=./backend \ --output "type=registry,push=true" \ From 4d89cb01bd06a3176f9a19a793dd85fef8eaf93a Mon Sep 17 00:00:00 2001 From: wiz Date: Fri, 14 Mar 2025 13:34:09 +0900 Subject: [PATCH 063/114] ops: Tweak delay times for bitcoin.crontab --- production/bitcoin.crontab | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/production/bitcoin.crontab b/production/bitcoin.crontab index a17147f7b..63df3c52a 100644 --- a/production/bitcoin.crontab +++ b/production/bitcoin.crontab @@ -1,13 +1,13 @@ # start test network daemons on boot -@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 -@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet4 >/dev/null 2>&1 -@reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1 +@reboot sleep 10 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 +@reboot sleep 20 ; /usr/local/bin/bitcoind -testnet4 >/dev/null 2>&1 +@reboot sleep 30 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1 # start electrs on boot -@reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/start mainnet -@reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/start testnet -@reboot sleep 10 ; screen -dmS testnet4 /bitcoin/electrs/start testnet4 -@reboot sleep 10 ; screen -dmS signet /bitcoin/electrs/start signet +@reboot sleep 40 ; screen -dmS mainnet /bitcoin/electrs/start mainnet +@reboot sleep 50 ; screen -dmS testnet /bitcoin/electrs/start testnet +@reboot sleep 60 ; screen -dmS testnet4 /bitcoin/electrs/start testnet4 +@reboot sleep 70 ; screen -dmS signet /bitcoin/electrs/start signet # daily update of popular-scripts 30 03 * * * $HOME/electrs/start testnet4 popular-scripts >/dev/null 2>&1 From 322e81d3edcbea536274ea72110188e4f333bc40 Mon Sep 17 00:00:00 2001 From: natsoni Date: Fri, 14 Mar 2025 14:59:15 +0100 Subject: [PATCH 064/114] Parse tapscript unanimous n-of-n multisig --- frontend/src/app/shared/script.utils.ts | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts index f0c4701db..8453bdd63 100644 --- a/frontend/src/app/shared/script.utils.ts +++ b/frontend/src/app/shared/script.utils.ts @@ -256,6 +256,11 @@ export function detectScriptTemplate(type: ScriptType, script_asm: string, witne return ScriptTemplates.multisig(tapscriptMultisig.m, tapscriptMultisig.n); } + const tapscriptUnanimousMultisig = parseTapscriptUnanimousMultisig(script_asm); + if (tapscriptUnanimousMultisig) { + return ScriptTemplates.multisig(tapscriptUnanimousMultisig, tapscriptUnanimousMultisig); + } + return; } @@ -366,6 +371,53 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number, return { m, n }; } +export function parseTapscriptUnanimousMultisig(script: string): undefined | number { + if (!script) { + return; + } + + const ops = script.split(' '); + // At minimum, 2 pubkey group (3 tokens) = 6 tokens + if (ops.length < 6) { + return; + } + + if (ops.length % 3 !== 0) { + return; + } + + const n = ops.length / 3; + + for (let i = 0; i < n; i++) { + const pushOp = ops.shift(); + const pubkey = ops.shift(); + const sigOp = ops.shift(); + + if (pushOp !== 'OP_PUSHBYTES_32') { + return; + } + if (!/^[0-9a-fA-F]{64}$/.test(pubkey)) { + return; + } + if (i < n - 1) { + if (sigOp !== 'OP_CHECKSIGVERIFY') { + return; + } + } else { + // Last opcode can be either CHECKSIG or CHECKSIGVERIFY + if (!(sigOp === 'OP_CHECKSIGVERIFY' || sigOp === 'OP_CHECKSIG')) { + return; + } + } + } + + if (ops.length) { + return; + } + + return n; +} + export function getVarIntLength(n: number): number { if (n < 0xfd) { return 1; From 30003348ce182a53ea5e910bc0f331ea138f7202 Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 16 Mar 2025 12:49:26 +0900 Subject: [PATCH 065/114] ops: Fix premature socket close bug in nginx cache warmer scripts --- production/nginx-cache-heater | 2 +- production/nginx-cache-warmer | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/production/nginx-cache-heater b/production/nginx-cache-heater index 24ec8a061..e6dea270a 100755 --- a/production/nginx-cache-heater +++ b/production/nginx-cache-heater @@ -4,7 +4,7 @@ hostname=$(hostname) heat() { echo "$1" - curl -i -s "$1" | head -1 + curl -o /dev/null -s "$1" } heatURLs=( diff --git a/production/nginx-cache-warmer b/production/nginx-cache-warmer index f02091747..171f95430 100755 --- a/production/nginx-cache-warmer +++ b/production/nginx-cache-warmer @@ -6,19 +6,19 @@ slugs=(`curl -sSL https://${hostname}/api/v1/mining/pools/3y|jq -r -S '(.pools[] warmSlurp() { echo "$1" - curl -i -s -H 'User-Agent: Googlebot' "$1" | head -1 + curl -o /dev/null -s -H 'User-Agent: Googlebot' "$1" } warmUnfurl() { echo "$1" - curl -i -s -H 'User-Agent: Twitterbot' "$1" | head -1 + curl -o /dev/null -s -H 'User-Agent: Twitterbot' "$1" } warm() { echo "$1" - curl -i -s "$1" | head -1 + curl -o /dev/null -s "$1" } warmSlurpURLs=( From 54cf5ea75e8b2c91e18490b02dc1d9c3086fde9b Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 25 Mar 2025 23:04:52 +0700 Subject: [PATCH 066/114] Fix database diabled --- backend/src/api/blocks.ts | 4 ++-- backend/src/api/common.ts | 7 +++++++ backend/src/api/websocket-handler.ts | 18 +++++++++++++----- backend/src/index.ts | 4 +++- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 102601594..581850277 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -1391,7 +1391,7 @@ class Blocks { } public async $getBlockAuditSummary(hash: string): Promise { - if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { + if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && Common.auditIndexingEnabled()) { return BlocksAuditsRepository.$getBlockAudit(hash); } else { return null; @@ -1399,7 +1399,7 @@ class Blocks { } public async $getBlockTxAuditSummary(hash: string, txid: string): Promise { - if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { + if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && Common.auditIndexingEnabled()) { return BlocksAuditsRepository.$getBlockTxAudit(hash, txid); } else { return null; diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 50de63afc..f3569c44c 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -722,6 +722,13 @@ export class Common { ); } + static auditIndexingEnabled(): boolean { + return ( + Common.indexingEnabled() && + config.MEMPOOL.AUDIT === true + ); + } + static gogglesIndexingEnabled(): boolean { return ( Common.blocksSummariesIndexingEnabled() && diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 390896caa..09e56630a 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -1011,15 +1011,19 @@ class WebsocketHandler { const blockTransactions = structuredClone(transactions); this.printLogs(); - await statistics.runStatistics(); + if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) { + await statistics.runStatistics(); + } const _memPool = memPool.getMempool(); const candidateTxs = await memPool.getMempoolCandidates(); let candidates: GbtCandidates | undefined = (memPool.limitGBT && candidateTxs) ? { txs: candidateTxs, added: [], removed: [] } : undefined; let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool); - const accelerations = Object.values(mempool.getAccelerations()); - await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); + if (config.DATABASE.ENABLED) { + const accelerations = Object.values(mempool.getAccelerations()); + await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); + } const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); memPool.handleRbfTransactions(rbfTransactions); @@ -1095,7 +1099,9 @@ class WebsocketHandler { if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) { const firstSeen = getRecentFirstSeen(block.id); if (firstSeen) { - BlocksRepository.$saveFirstSeenTime(block.id, firstSeen); + if (config.DATABASE.ENABLED) { + BlocksRepository.$saveFirstSeenTime(block.id, firstSeen); + } block.extras.firstSeen = firstSeen; } } @@ -1392,7 +1398,9 @@ class WebsocketHandler { }); } - await statistics.runStatistics(); + if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) { + await statistics.runStatistics(); + } } public handleNewStratumJob(job: StratumJob): void { diff --git a/backend/src/index.ts b/backend/src/index.ts index dc6a8ae1a..1b2204c28 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -153,7 +153,9 @@ class Server { await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it await syncAssets.syncAssets$(); - await mempoolBlocks.updatePools$(); + if (config.DATABASE.ENABLED) { + await mempoolBlocks.updatePools$(); + } if (config.MEMPOOL.ENABLED) { if (config.MEMPOOL.CACHE_ENABLED) { await diskCache.$loadMempoolCache(); From 2945e47eba9583cf2826d2ca9f159f9707eec563 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 26 Mar 2025 23:05:43 +0100 Subject: [PATCH 067/114] [blocks] respect 404 error code instead of misleading 500 --- backend/src/api/bitcoin/bitcoin.routes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 73a14ba4e..3cf2923f1 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -406,8 +406,8 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.json(block); - } catch (e) { - handleError(req, res, 500, 'Failed to get block'); + } catch (e: any) { + handleError(req, res, e?.response?.status === 404 ? 404 : 500, 'Failed to get block'); } } From b153d21162aea4e51ee773fe96bf24137a14bf0b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 10 Mar 2025 04:35:52 +0000 Subject: [PATCH 068/114] automatically fetch enabled wallets from services backend --- backend/src/api/services/wallets.ts | 33 ++++++++++++++++++++++++++ backend/src/config.ts | 2 ++ production/mempool-config.mainnet.json | 1 + 3 files changed, 36 insertions(+) diff --git a/backend/src/api/services/wallets.ts b/backend/src/api/services/wallets.ts index dd4d7ebc9..f498a80ad 100644 --- a/backend/src/api/services/wallets.ts +++ b/backend/src/api/services/wallets.ts @@ -30,6 +30,7 @@ const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes class WalletApi { private wallets: Record = {}; private syncing = false; + private lastSync = 0; constructor() { this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => { @@ -47,7 +48,38 @@ class WalletApi { if (!config.WALLETS.ENABLED || this.syncing) { return; } + this.syncing = true; + + if (config.WALLETS.AUTO && (Date.now() - this.lastSync) > POLL_FREQUENCY) { + try { + // update list of active wallets + this.lastSync = Date.now(); + const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets`); + const walletList: string[] = response.data; + if (walletList) { + // create a quick lookup dictionary of active wallets + const newWallets: Record = Object.fromEntries( + walletList.map(wallet => [wallet, true]) + ); + for (const wallet of walletList) { + // don't overwrite existing wallets + if (!this.wallets[wallet]) { + this.wallets[wallet] = { name: wallet, addresses: {}, lastPoll: 0 }; + } + } + // remove wallets that are no longer active + for (const wallet of Object.keys(this.wallets)) { + if (!newWallets[wallet]) { + delete this.wallets[wallet]; + } + } + } + } catch (e) { + logger.err(`Error updating active wallets: ${(e instanceof Error ? e.message : e)}`); + } + } + for (const walletKey of Object.keys(this.wallets)) { const wallet = this.wallets[walletKey]; if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) { @@ -72,6 +104,7 @@ class WalletApi { } } } + this.syncing = false; } diff --git a/backend/src/config.ts b/backend/src/config.ts index a1050a7d5..3fe3db2ee 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -164,6 +164,7 @@ interface IConfig { }, WALLETS: { ENABLED: boolean; + AUTO: boolean; WALLETS: string[]; }, STRATUM: { @@ -334,6 +335,7 @@ const defaults: IConfig = { }, 'WALLETS': { 'ENABLED': false, + 'AUTO': false, 'WALLETS': [], }, 'STRATUM': { diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 9505601d2..87f58f916 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -159,6 +159,7 @@ }, "WALLETS": { "ENABLED": true, + "AUTO": true, "WALLETS": ["BITB", "3350"] }, "STRATUM": { From 072b83243e5202de0b2722855d8f66ca22031f18 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 10 Mar 2025 04:38:51 +0000 Subject: [PATCH 069/114] update custom dashboard config --- frontend/custom-sv-config.json | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/custom-sv-config.json b/frontend/custom-sv-config.json index dee3dab18..9a61704a2 100644 --- a/frontend/custom-sv-config.json +++ b/frontend/custom-sv-config.json @@ -16,10 +16,10 @@ "mobileOrder": 4 }, { - "component": "balance", + "component": "walletBalance", "mobileOrder": 1, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" + "wallet": "ONBTC" } }, { @@ -30,21 +30,22 @@ } }, { - "component": "address", + "component": "wallet", "mobileOrder": 2, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo", - "period": "1m" + "wallet": "ONBTC", + "period": "1m", + "label": "bitcoin.gob.sv" } }, { "component": "blocks" }, { - "component": "addressTransactions", + "component": "walletTransactions", "mobileOrder": 3, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" + "wallet": "ONBTC" } } ] From 3056454389ea80380920ef8e702089e9bb629f55 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 26 Feb 2025 04:48:22 +0000 Subject: [PATCH 070/114] Add configurable label to balance widget --- .../address-graph/address-graph.component.ts | 22 +++++++++++++++++++ .../custom-dashboard.component.html | 4 ++-- frontend/src/app/graphs/echarts.ts | 4 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/address-graph/address-graph.component.ts b/frontend/src/app/components/address-graph/address-graph.component.ts index 2bbfd5e34..005c64e9f 100644 --- a/frontend/src/app/components/address-graph/address-graph.component.ts +++ b/frontend/src/app/components/address-graph/address-graph.component.ts @@ -44,6 +44,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { @Input() right: number | string = 10; @Input() left: number | string = 70; @Input() widget: boolean = false; + @Input() label: string = ''; @Input() defaultFiat: boolean = false; @Input() showLegend: boolean = true; @Input() showYAxis: boolean = true; @@ -55,6 +56,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { hoverData: any[] = []; conversions: any; allowZoom: boolean = false; + labelGraphic: any; selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false }; @@ -85,6 +87,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { ngOnChanges(changes: SimpleChanges): void { this.isLoading = true; + this.labelGraphic = this.label ? { + type: 'text', + right: '36px', + bottom: '36px', + z: 100, + silent: true, + style: { + fill: '#fff', + text: this.label, + font: '24px sans-serif' + } + } : undefined; if (!this.addressSummary$ && (!this.address || !this.stats)) { return; } @@ -205,6 +219,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { right: this.adjustedRight, left: this.adjustedLeft, }, + graphic: this.labelGraphic ? [{ + ...this.labelGraphic, + right: this.adjustedRight + 22 + 'px', + }] : undefined, legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? { data: [ { @@ -443,6 +461,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { right: this.adjustedRight, left: this.adjustedLeft, }, + graphic: this.labelGraphic ? [{ + ...this.labelGraphic, + right: this.adjustedRight + 22 + 'px', + }] : undefined, legend: { selected: this.selected, }, diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html index 8ca1a5ac4..defb7c068 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html @@ -238,7 +238,7 @@   - +
    @@ -272,7 +272,7 @@   - + diff --git a/frontend/src/app/graphs/echarts.ts b/frontend/src/app/graphs/echarts.ts index 67ed7e3b8..36a0517e4 100644 --- a/frontend/src/app/graphs/echarts.ts +++ b/frontend/src/app/graphs/echarts.ts @@ -1,7 +1,7 @@ // Import tree-shakeable echarts import * as echarts from 'echarts/core'; import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts'; -import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components'; +import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent, GraphicComponent } from 'echarts/components'; import { SVGRenderer, CanvasRenderer } from 'echarts/renderers'; // Typescript interfaces import { EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption } from 'echarts'; @@ -13,6 +13,6 @@ echarts.use([ LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent, LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, - CustomChart, + CustomChart, GraphicComponent ]); export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption }; \ No newline at end of file From fb50ea7a6d06e3a4a28db3061d8b2ba74bedafee Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 4 Feb 2025 12:02:12 +0000 Subject: [PATCH 071/114] detect and warn about address poisoning attacks --- .../transactions-list.component.html | 21 ++- .../transactions-list.component.ts | 56 +++++++ frontend/src/app/shared/address-utils.ts | 145 ++++++++++++++++++ .../address-text/address-text.component.html | 17 ++ .../address-text/address-text.component.scss | 32 ++++ .../address-text/address-text.component.ts | 20 +++ frontend/src/app/shared/shared.module.ts | 6 +- 7 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/shared/components/address-text/address-text.component.html create mode 100644 frontend/src/app/shared/components/address-text/address-text.component.scss create mode 100644 frontend/src/app/shared/components/address-text/address-text.component.ts diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 6f1d76538..7721298fb 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -16,6 +16,11 @@
    {{ errorUnblinded }}
    + @if (similarityMatches.get(tx.txid)?.size) { + + }
    @@ -68,9 +73,11 @@ - - - + {{ vin.prevout.scriptpubkey_type?.toUpperCase() }} @@ -217,9 +224,11 @@ 'highlight': this.addresses.length && ((vout.scriptpubkey_type !== 'p2pk' && addresses.includes(vout.scriptpubkey_address)) || this.addresses.includes(vout.scriptpubkey.slice(2, -2))) }"> - +
    - - - + P2PK diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 20f11abd8..6f01a6e59 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -14,6 +14,7 @@ import { StorageService } from '@app/services/storage.service'; import { OrdApiService } from '@app/services/ord-api.service'; import { Inscription } from '@app/shared/ord/inscription.utils'; import { Etching, Runestone } from '@app/shared/ord/rune.utils'; +import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils'; @Component({ selector: 'app-transactions-list', @@ -55,6 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { showFullScript: { [vinIndex: number]: boolean } = {}; showFullWitness: { [vinIndex: number]: { [witnessIndex: number]: boolean } } = {}; showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {}; + similarityMatches: Map> = new Map(); constructor( public stateService: StateService, @@ -144,6 +146,8 @@ export class TransactionsListComponent implements OnInit, OnChanges { this.currency = currency; this.refreshPrice(); }); + + this.updateAddressSimilarities(); } refreshPrice(): void { @@ -183,6 +187,8 @@ export class TransactionsListComponent implements OnInit, OnChanges { } } if (changes.transactions || changes.addresses) { + this.similarityMatches.clear(); + this.updateAddressSimilarities(); if (!this.transactions || !this.transactions.length) { return; } @@ -296,6 +302,56 @@ export class TransactionsListComponent implements OnInit, OnChanges { } } + updateAddressSimilarities(): void { + if (!this.transactions || !this.transactions.length) { + return; + } + for (const tx of this.transactions) { + if (this.similarityMatches.get(tx.txid)) { + continue; + } + + const similarityGroups: Map = new Map(); + let lastGroup = 0; + + // Check for address poisoning similarity matches + this.similarityMatches.set(tx.txid, new Map()); + const comparableVouts = [ + ...tx.vout.slice(0, 20), + ...this.addresses.map(addr => ({ scriptpubkey_address: addr, scriptpubkey_type: detectAddressType(addr, this.stateService.network) })) + ].filter(v => ['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(v.scriptpubkey_type)); + const comparableVins = tx.vin.slice(0, 20).map(v => v.prevout).filter(v => ['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(v?.scriptpubkey_type)); + for (const vout of comparableVouts) { + const address = vout.scriptpubkey_address; + const addressType = vout.scriptpubkey_type; + if (this.similarityMatches.get(tx.txid)?.has(address)) { + continue; + } + for (const compareAddr of [ + ...comparableVouts.filter(v => v.scriptpubkey_type === addressType && v.scriptpubkey_address !== address), + ...comparableVins.filter(v => v.scriptpubkey_type === addressType && v.scriptpubkey_address !== address) + ]) { + const similarity = checkedCompareAddressStrings(address, compareAddr.scriptpubkey_address, addressType as AddressType, this.stateService.network); + if (similarity?.status === 'comparable' && similarity.score > ADDRESS_SIMILARITY_THRESHOLD) { + let group = similarityGroups.get(address) || lastGroup++; + similarityGroups.set(address, group); + const bestVout = this.similarityMatches.get(tx.txid)?.get(address); + if (!bestVout || bestVout.score < similarity.score) { + this.similarityMatches.get(tx.txid)?.set(address, { score: similarity.score, match: similarity.left, group }); + } + // opportunistically update the entry for the compared address + const bestCompare = this.similarityMatches.get(tx.txid)?.get(compareAddr.scriptpubkey_address); + if (!bestCompare || bestCompare.score < similarity.score) { + group = similarityGroups.get(compareAddr.scriptpubkey_address) || lastGroup++; + similarityGroups.set(compareAddr.scriptpubkey_address, group); + this.similarityMatches.get(tx.txid)?.set(compareAddr.scriptpubkey_address, { score: similarity.score, match: similarity.right, group }); + } + } + } + } + } + } + onScroll(): void { this.loadMore.emit(); } diff --git a/frontend/src/app/shared/address-utils.ts b/frontend/src/app/shared/address-utils.ts index 0a7f2df02..0ca4ec2e0 100644 --- a/frontend/src/app/shared/address-utils.ts +++ b/frontend/src/app/shared/address-utils.ts @@ -78,6 +78,7 @@ const p2trRegex = RegExp('^p' + BECH32_CHARS_LW + '{58}$'); const pubkeyRegex = RegExp('^' + `(04${HEX_CHARS}{128})|(0[23]${HEX_CHARS}{64})$`); export function detectAddressType(address: string, network: string): AddressType { + network = network || 'mainnet'; // normal address types const firstChar = address.substring(0, 1); if (ADDRESS_PREFIXES[network].base58.pubkey.includes(firstChar) && base58Regex.test(address.slice(1))) { @@ -211,6 +212,18 @@ export class AddressTypeInfo { } } + public compareTo(other: AddressTypeInfo): AddressSimilarityResult { + return compareAddresses(this.address, other.address, this.network); + } + + public compareToString(other: string): AddressSimilarityResult { + if (other === this.address) { + return { status: 'identical' }; + } + const otherInfo = new AddressTypeInfo(this.network, other); + return this.compareTo(otherInfo); + } + private processScript(script: ScriptInfo): void { this.scripts.set(script.key, script); if (script.template?.type === 'multisig') { @@ -218,3 +231,135 @@ export class AddressTypeInfo { } } } + +export interface AddressMatch { + prefix: string; + postfix: string; +} + +export interface AddressSimilarity { + status: 'comparable'; + score: number; + left: AddressMatch; + right: AddressMatch; +} +export type AddressSimilarityResult = + | { status: 'identical' } + | { status: 'incomparable' } + | AddressSimilarity; + +export const ADDRESS_SIMILARITY_THRESHOLD = 10_000_000; // 1 false positive per ~10 million comparisons + +function fuzzyPrefixMatch(a: string, b: string, rtl: boolean = false): { score: number, matchA: string, matchB: string } { + let score = 0; + let gap = false; + let done = false; + + let ai = 0; + let bi = 0; + let prefixA = ''; + let prefixB = ''; + if (rtl) { + a = a.split('').reverse().join(''); + b = b.split('').reverse().join(''); + } + + while (ai < a.length && bi < b.length && !done) { + if (a[ai] === b[bi]) { + // matching characters + prefixA += a[ai]; + prefixB += b[bi]; + score++; + ai++; + bi++; + } else if (!gap) { + // try looking ahead in both strings to find the best match + const nextMatchA = (ai + 1 < a.length && a[ai + 1] === b[bi]); + const nextMatchB = (bi + 1 < b.length && a[ai] === b[bi + 1]); + const nextMatchBoth = (ai + 1 < a.length && bi + 1 < b.length && a[ai + 1] === b[bi + 1]); + if (nextMatchBoth) { + // single differing character + prefixA += a[ai]; + prefixB += b[bi]; + ai++; + bi++; + } else if (nextMatchA) { + // character missing in b + prefixA += a[ai]; + ai++; + } else if (nextMatchB) { + // character missing in a + prefixB += b[bi]; + bi++; + } else { + ai++; + bi++; + } + gap = true; + } else { + done = true; + } + } + + if (rtl) { + prefixA = prefixA.split('').reverse().join(''); + prefixB = prefixB.split('').reverse().join(''); + } + + return { score, matchA: prefixA, matchB: prefixB }; +} + +export function compareAddressInfo(a: AddressTypeInfo, b: AddressTypeInfo): AddressSimilarityResult { + if (a.address === b.address) { + return { status: 'identical' }; + } + if (a.type !== b.type) { + return { status: 'incomparable' }; + } + if (!['p2pkh', 'p2sh', 'p2sh-p2wpkh', 'p2sh-p2wsh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(a.type)) { + return { status: 'incomparable' }; + } + const isBase58 = a.type === 'p2pkh' || a.type === 'p2sh'; + + const left = fuzzyPrefixMatch(a.address, b.address); + const right = fuzzyPrefixMatch(a.address, b.address, true); + // depending on address type, some number of matching prefix characters are guaranteed + const prefixScore = isBase58 ? 1 : ADDRESS_PREFIXES[a.network || 'mainnet'].bech32.length; + + // add the two scores together + const totalScore = left.score + right.score - prefixScore; + + // adjust for the size of the alphabet (58 vs 32) + const normalizedScore = Math.pow(isBase58 ? 58 : 32, totalScore); + + return { + status: 'comparable', + score: normalizedScore, + left: { + prefix: left.matchA, + postfix: right.matchA, + }, + right: { + prefix: left.matchB, + postfix: right.matchB, + }, + }; +} + +export function compareAddresses(a: string, b: string, network: string): AddressSimilarityResult { + if (a === b) { + return { status: 'identical' }; + } + const aInfo = new AddressTypeInfo(network, a); + return aInfo.compareToString(b); +} + +// avoids the overhead of creating AddressTypeInfo objects for each address, +// but a and b *MUST* be valid normalized addresses, of the same valid type +export function checkedCompareAddressStrings(a: string, b: string, type: AddressType, network: string): AddressSimilarityResult { + return compareAddressInfo( + { address: a, type: type, network: network } as AddressTypeInfo, + { address: b, type: type, network: network } as AddressTypeInfo, + ); +} + diff --git a/frontend/src/app/shared/components/address-text/address-text.component.html b/frontend/src/app/shared/components/address-text/address-text.component.html new file mode 100644 index 000000000..ddcd8d751 --- /dev/null +++ b/frontend/src/app/shared/components/address-text/address-text.component.html @@ -0,0 +1,17 @@ + +@if (similarity) { + +} @else { + + + +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/address-text/address-text.component.scss b/frontend/src/app/shared/components/address-text/address-text.component.scss new file mode 100644 index 000000000..3b5bf50b7 --- /dev/null +++ b/frontend/src/app/shared/components/address-text/address-text.component.scss @@ -0,0 +1,32 @@ +.address-text { + text-overflow: unset; + display: flex; + flex-direction: row; + align-items: start; + position: relative; + + font-family: monospace; + + .infix { + flex-grow: 0; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + user-select: none; + + text-decoration: underline 2px; + } + + .prefix, .postfix { + flex-shrink: 0; + flex-grow: 0; + user-select: none; + + text-decoration: underline var(--red) 2px; + } +} + +.poison-alert { + margin-left: .5em; + color: var(--yellow); +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/address-text/address-text.component.ts b/frontend/src/app/shared/components/address-text/address-text.component.ts new file mode 100644 index 000000000..f618428aa --- /dev/null +++ b/frontend/src/app/shared/components/address-text/address-text.component.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; +import { AddressMatch, AddressTypeInfo } from '@app/shared/address-utils'; + +@Component({ + selector: 'app-address-text', + templateUrl: './address-text.component.html', + styleUrls: ['./address-text.component.scss'] +}) +export class AddressTextComponent { + @Input() address: string; + @Input() info: AddressTypeInfo | null; + @Input() similarity: { score: number, match: AddressMatch, group: number } | null; + + groupColors: string[] = [ + 'var(--primary)', + 'var(--success)', + 'var(--info)', + 'white', + ]; +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index d937e6bbb..335b428ed 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, - faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope } from '@fortawesome/free-solid-svg-icons'; + faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '@components/menu/menu.component'; import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; @@ -94,6 +94,7 @@ import { SatsComponent } from '@app/shared/components/sats/sats.component'; import { BtcComponent } from '@app/shared/components/btc/btc.component'; import { FeeRateComponent } from '@app/shared/components/fee-rate/fee-rate.component'; import { AddressTypeComponent } from '@app/shared/components/address-type/address-type.component'; +import { AddressTextComponent } from '@app/shared/components/address-text/address-text.component'; import { TruncateComponent } from '@app/shared/components/truncate/truncate.component'; import { SearchResultsComponent } from '@components/search-form/search-results/search-results.component'; import { TimestampComponent } from '@app/shared/components/timestamp/timestamp.component'; @@ -214,6 +215,7 @@ import { GithubLogin } from '../components/github-login.component/github-login.c BtcComponent, FeeRateComponent, AddressTypeComponent, + AddressTextComponent, TruncateComponent, SearchResultsComponent, TimestampComponent, @@ -360,6 +362,7 @@ import { GithubLogin } from '../components/github-login.component/github-login.c BtcComponent, FeeRateComponent, AddressTypeComponent, + AddressTextComponent, TruncateComponent, SearchResultsComponent, TimestampComponent, @@ -465,5 +468,6 @@ export class SharedModule { library.addIcons(faShareNodes); library.addIcons(faCreditCard); library.addIcons(faMicroscope); + library.addIcons(faExclamationTriangle); } } From d805304d7958706a672fbc8734f9dd5f454cc67a Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Mon, 31 Mar 2025 16:14:46 +0900 Subject: [PATCH 072/114] Use github-hosted runners --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/docker_update_latest_tag.yml | 2 +- .github/workflows/get_backend_block_height.yml | 2 +- .github/workflows/get_backend_hash.yml | 2 +- .github/workflows/get_image_digest.yml | 2 +- .github/workflows/on-tag.yml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d2fc387f..9f99778e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: node: ["20", "21"] flavor: ["dev", "prod"] fail-fast: false - runs-on: "ubuntu-latest" + runs-on: mempool-ci name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} steps: @@ -66,7 +66,7 @@ jobs: cache: name: "Cache assets for builds" - runs-on: "ubuntu-latest" + runs-on: mempool-ci steps: - name: Checkout uses: actions/checkout@v3 @@ -163,7 +163,7 @@ jobs: node: ["20", "21"] flavor: ["dev", "prod"] fail-fast: false - runs-on: "ubuntu-latest" + runs-on: mempool-ci name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} steps: @@ -246,7 +246,7 @@ jobs: e2e: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" - runs-on: "ubuntu-latest" + runs-on: mempool-ci needs: frontend strategy: fail-fast: false @@ -378,7 +378,7 @@ jobs: validate_docker_json: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" - runs-on: "ubuntu-latest" + runs-on: mempool-ci name: Validate generated backend Docker JSON steps: diff --git a/.github/workflows/docker_update_latest_tag.yml b/.github/workflows/docker_update_latest_tag.yml index 5d21697d5..c5ba87f58 100644 --- a/.github/workflows/docker_update_latest_tag.yml +++ b/.github/workflows/docker_update_latest_tag.yml @@ -15,7 +15,7 @@ jobs: service: - frontend - backend - runs-on: ubuntu-latest + runs-on: mempool-ci steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/get_backend_block_height.yml b/.github/workflows/get_backend_block_height.yml index 52f3b038c..73db6107e 100644 --- a/.github/workflows/get_backend_block_height.yml +++ b/.github/workflows/get_backend_block_height.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch] jobs: print-backend-sha: - runs-on: 'ubuntu-latest' + runs-on: mempool-ci name: Get block height steps: - name: Checkout diff --git a/.github/workflows/get_backend_hash.yml b/.github/workflows/get_backend_hash.yml index 57950dee4..d63860c5e 100644 --- a/.github/workflows/get_backend_hash.yml +++ b/.github/workflows/get_backend_hash.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch] jobs: print-backend-sha: - runs-on: 'ubuntu-latest' + runs-on: mempool-ci name: Print backend hashes steps: - name: Checkout diff --git a/.github/workflows/get_image_digest.yml b/.github/workflows/get_image_digest.yml index 7414eeb08..3d86c860c 100644 --- a/.github/workflows/get_image_digest.yml +++ b/.github/workflows/get_image_digest.yml @@ -10,7 +10,7 @@ on: type: string jobs: print-images-sha: - runs-on: 'ubuntu-latest' + runs-on: mempool-ci name: Print digest for images steps: - name: Checkout diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 1447ec4ab..b02d9ff03 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -21,7 +21,7 @@ jobs: service: - frontend - backend - runs-on: ubuntu-latest + runs-on: mempool-ci timeout-minutes: 120 name: Build and push to DockerHub steps: From c3052285309da96251baef7159696567e17f5fbb Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Mon, 31 Mar 2025 22:02:52 +0900 Subject: [PATCH 073/114] Revert mempool-ci runners --- .github/workflows/ci.yml | 5 +++++ .github/workflows/docker_update_latest_tag.yml | 2 +- .github/workflows/get_backend_block_height.yml | 2 +- .github/workflows/get_backend_hash.yml | 2 +- .github/workflows/get_image_digest.yml | 2 +- .github/workflows/on-tag.yml | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f99778e2..08842cbe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ jobs: flavor: ["dev", "prod"] fail-fast: false runs-on: mempool-ci + runs-on: ubuntu-latest name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} steps: @@ -67,6 +68,7 @@ jobs: cache: name: "Cache assets for builds" runs-on: mempool-ci + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 @@ -164,6 +166,7 @@ jobs: flavor: ["dev", "prod"] fail-fast: false runs-on: mempool-ci + runs-on: ubuntu-latest name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} steps: @@ -247,6 +250,7 @@ jobs: e2e: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" runs-on: mempool-ci + runs-on: ubuntu-latest needs: frontend strategy: fail-fast: false @@ -379,6 +383,7 @@ jobs: validate_docker_json: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" runs-on: mempool-ci + runs-on: ubuntu-latest name: Validate generated backend Docker JSON steps: diff --git a/.github/workflows/docker_update_latest_tag.yml b/.github/workflows/docker_update_latest_tag.yml index c5ba87f58..5d21697d5 100644 --- a/.github/workflows/docker_update_latest_tag.yml +++ b/.github/workflows/docker_update_latest_tag.yml @@ -15,7 +15,7 @@ jobs: service: - frontend - backend - runs-on: mempool-ci + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/get_backend_block_height.yml b/.github/workflows/get_backend_block_height.yml index 73db6107e..ae30188e5 100644 --- a/.github/workflows/get_backend_block_height.yml +++ b/.github/workflows/get_backend_block_height.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch] jobs: print-backend-sha: - runs-on: mempool-ci + runs-on: ubuntu-latest name: Get block height steps: - name: Checkout diff --git a/.github/workflows/get_backend_hash.yml b/.github/workflows/get_backend_hash.yml index d63860c5e..0e31735b6 100644 --- a/.github/workflows/get_backend_hash.yml +++ b/.github/workflows/get_backend_hash.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch] jobs: print-backend-sha: - runs-on: mempool-ci + runs-on: ubuntu-latest name: Print backend hashes steps: - name: Checkout diff --git a/.github/workflows/get_image_digest.yml b/.github/workflows/get_image_digest.yml index 3d86c860c..18ad39fde 100644 --- a/.github/workflows/get_image_digest.yml +++ b/.github/workflows/get_image_digest.yml @@ -10,7 +10,7 @@ on: type: string jobs: print-images-sha: - runs-on: mempool-ci + runs-on: ubuntu-latest name: Print digest for images steps: - name: Checkout diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index b02d9ff03..1447ec4ab 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -21,7 +21,7 @@ jobs: service: - frontend - backend - runs-on: mempool-ci + runs-on: ubuntu-latest timeout-minutes: 120 name: Build and push to DockerHub steps: From 7c682d8be949b2ce583b7815a3a5933a9685166f Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Mon, 31 Mar 2025 22:05:52 +0900 Subject: [PATCH 074/114] Add parameterized proxy --- frontend/proxy.conf.parameterized.js | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/proxy.conf.parameterized.js diff --git a/frontend/proxy.conf.parameterized.js b/frontend/proxy.conf.parameterized.js new file mode 100644 index 000000000..eee9bf1f7 --- /dev/null +++ b/frontend/proxy.conf.parameterized.js @@ -0,0 +1,36 @@ +const fs = require('fs'); + +const PROXY_CONFIG = require('./proxy.conf'); + +const addApiKeyHeader = (proxyReq, req, res) => { + if (process.env.MEMPOOL_CI_API_KEY) { + proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY); + } +}; + +PROXY_CONFIG.forEach((entry) => { + const mempoolHostname = process.env.MEMPOOL_HOSTNAME + ? process.env.MEMPOOL_HOSTNAME + : 'mempool.space'; + + const liquidHostname = process.env.LIQUID_HOSTNAME + ? process.env.LIQUID_HOSTNAME + : 'liquid.network'; + + entry.target = entry.target.replace('mempool.space', mempoolHostname); + entry.target = entry.target.replace('liquid.network', liquidHostname); + + if (entry.onProxyReq) { + const originalProxyReq = entry.onProxyReq; + entry.onProxyReq = (proxyReq, req, res) => { + originalProxyReq(proxyReq, req, res); + if (process.env.MEMPOOL_CI_API_KEY) { + proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY); + } + }; + } else { + entry.onProxyReq = addApiKeyHeader; + } +}); + +module.exports = PROXY_CONFIG; From f56e748c0dcbee65aa34c082ef9ef50ceffa5627 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Mon, 31 Mar 2025 22:10:44 +0900 Subject: [PATCH 075/114] Replace staging with parameterized --- frontend/angular.json | 12 +++--------- frontend/package.json | 12 +++++------- frontend/proxy.conf.staging.js | 12 ------------ 3 files changed, 8 insertions(+), 28 deletions(-) delete mode 100644 frontend/proxy.conf.staging.js diff --git a/frontend/angular.json b/frontend/angular.json index 3aa1cb6a8..c5da6a09a 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -261,20 +261,14 @@ "proxyConfig": "proxy.conf.mixed.js", "verbose": true }, - "staging": { - "proxyConfig": "proxy.conf.js", - "disableHostCheck": true, - "host": "0.0.0.0", - "verbose": true - }, "local-prod": { "proxyConfig": "proxy.conf.js", "disableHostCheck": true, "host": "0.0.0.0", "verbose": false }, - "local-staging": { - "proxyConfig": "proxy.conf.staging.js", + "parameterized": { + "proxyConfig": "proxy.conf.parameterized.js", "disableHostCheck": true, "host": "0.0.0.0", "verbose": false @@ -371,4 +365,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index b50085a54..8d4ee6e27 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,14 +25,12 @@ "i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf", "i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force", "serve": "npm run generate-config && npm run ng -- serve -c local", - "serve:stg": "npm run generate-config && npm run ng -- serve -c staging", "serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod", - "serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging", + "serve:parameterized": "npm run generate-config && npm run ng -- serve -c parameterized", "start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local", + "start:parameterized": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c parameterized", "start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora", - "start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging", "start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod", - "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging", "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed", "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets && npm run build-mempool.js", "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'", @@ -58,8 +56,8 @@ "cypress:run:record": "cypress run --record", "cypress:open:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open", "cypress:run:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record", - "cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open", - "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record" + "cypress:open:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:open", + "cypress:run:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:run:record" }, "dependencies": { "@angular-devkit/build-angular": "^17.3.1", @@ -123,4 +121,4 @@ "scarfSettings": { "enabled": false } -} +} \ No newline at end of file diff --git a/frontend/proxy.conf.staging.js b/frontend/proxy.conf.staging.js deleted file mode 100644 index 0165bed96..000000000 --- a/frontend/proxy.conf.staging.js +++ /dev/null @@ -1,12 +0,0 @@ -const fs = require('fs'); - -let PROXY_CONFIG = require('./proxy.conf'); - -PROXY_CONFIG.forEach(entry => { - const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.va1.mempool.space'; - console.log(`e2e tests running against ${hostname}`); - entry.target = entry.target.replace("mempool.space", hostname); - entry.target = entry.target.replace("liquid.network", "liquid-staging.va1.mempool.space"); -}); - -module.exports = PROXY_CONFIG; From 57765e4ee64667c3fff48c7982ee0859c468d4de Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Mon, 31 Mar 2025 22:10:58 +0900 Subject: [PATCH 076/114] Update e2e task with the new target --- .github/workflows/ci.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08842cbe9..0c603c42a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ jobs: node: ["20", "21"] flavor: ["dev", "prod"] fail-fast: false - runs-on: mempool-ci runs-on: ubuntu-latest name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} @@ -67,7 +66,6 @@ jobs: cache: name: "Cache assets for builds" - runs-on: mempool-ci runs-on: ubuntu-latest steps: - name: Checkout @@ -165,7 +163,6 @@ jobs: node: ["20", "21"] flavor: ["dev", "prod"] fail-fast: false - runs-on: mempool-ci runs-on: ubuntu-latest name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} @@ -249,7 +246,6 @@ jobs: e2e: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" - runs-on: mempool-ci runs-on: ubuntu-latest needs: frontend strategy: @@ -313,7 +309,7 @@ jobs: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend build: npm run config:defaults:${{ matrix.module }} - start: npm run start:local-staging + start: npm run start:parameterized wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true @@ -338,7 +334,7 @@ jobs: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend build: npm run config:defaults:${{ matrix.module }} - start: npm run start:local-staging + start: npm run start:parameterized wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true @@ -363,7 +359,7 @@ jobs: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend build: npm run config:defaults:mempool - start: npm run start:local-staging + start: npm run start:parameterized wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true @@ -382,7 +378,6 @@ jobs: validate_docker_json: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" - runs-on: mempool-ci runs-on: ubuntu-latest name: Validate generated backend Docker JSON From 9f6d0a6dcb74ff61a7e695bd2ecdd8e7ac3a6083 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Mon, 31 Mar 2025 22:11:14 +0900 Subject: [PATCH 077/114] Add new parameterized e2e dispatch workflow --- .github/workflows/e2e_parameterized.yml | 154 ++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 .github/workflows/e2e_parameterized.yml diff --git a/.github/workflows/e2e_parameterized.yml b/.github/workflows/e2e_parameterized.yml new file mode 100644 index 000000000..d041a4b9f --- /dev/null +++ b/.github/workflows/e2e_parameterized.yml @@ -0,0 +1,154 @@ +name: 'Parameterized e2e tests' + +on: + workflow_dispatch: + inputs: + mempool_hostname: + description: 'Mempool Hostname' + required: true + default: 'mempool.space' + type: string + liquid_hostname: + description: 'Liquid Hostname' + required: true + default: 'liquid.network' + type: string + +jobs: + e2e: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + module: ["mempool", "liquid", "testnet4"] + + name: E2E tests for ${{ matrix.module }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + path: ${{ matrix.module }} + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 20 + cache: "npm" + cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json + + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore cached promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: mining-pool-assets + + - name: Unzip assets before building (src/resources) + run: unzip -o mining-pool-assets.zip -d ${{ matrix.module }}/frontend/src/resources/mining-pools + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: promo-video-assets + + - name: Unzip assets before building (src/resources) + run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video + + # mempool + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'mempool' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:${{ matrix.module }} + start: npm run start:ci-parameterized + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/mainnet/*.spec.ts + cypress/e2e/signet/*.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} + LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + + # liquid + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'liquid' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:${{ matrix.module }} + start: npm run start:ci-parameterized + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/liquid/liquid.spec.ts + cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} + LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + + # testnet + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'testnet4' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:mempool + start: npm run start:ci-parameterized + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/testnet4/*.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} + LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} From 860ca9698fbc7115200e7f15a2223ac0f0b4e003 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Mon, 31 Mar 2025 22:47:14 +0900 Subject: [PATCH 078/114] Fix failing RBF test on mobile --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 08a4741b3..70c3a1cb1 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -495,8 +495,8 @@ describe('Mainnet', () => { }); }); - describe('RBF transactions', () => { - it('shows RBF transactions properly (mobile)', () => { + describe.only('RBF transactions', () => { + it('shows RBF transactions properly (mobile - details)', () => { cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', { fixture: 'mainnet_tx_cached.json' }).as('cached_tx'); @@ -507,7 +507,7 @@ describe('Mainnet', () => { cy.viewport('iphone-xr'); cy.mockMempoolSocket(); - cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f'); + cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f?mode=details'); cy.waitForSkeletonGone(); @@ -525,7 +525,7 @@ describe('Mainnet', () => { } }); - cy.get('.alert-replaced').should('be.visible'); + cy.get('.alert-mempool').should('be.visible'); }); it('shows RBF transactions properly (desktop)', () => { From d57b9ac9a40e4a33b8b8cf1e446731a5632bf036 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Mon, 31 Mar 2025 22:56:33 +0900 Subject: [PATCH 079/114] Fix running only one test --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 70c3a1cb1..a011073bd 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -495,7 +495,7 @@ describe('Mainnet', () => { }); }); - describe.only('RBF transactions', () => { + describe('RBF transactions', () => { it('shows RBF transactions properly (mobile - details)', () => { cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', { fixture: 'mainnet_tx_cached.json' From 6533c19bd16979bbc814ce0ea4ad10ca1129d44b Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Tue, 1 Apr 2025 09:23:52 +0900 Subject: [PATCH 080/114] Add support for running tests against any PR or branch --- .github/workflows/e2e_parameterized.yml | 131 ++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e_parameterized.yml b/.github/workflows/e2e_parameterized.yml index d041a4b9f..da1814b84 100644 --- a/.github/workflows/e2e_parameterized.yml +++ b/.github/workflows/e2e_parameterized.yml @@ -3,6 +3,11 @@ name: 'Parameterized e2e tests' on: workflow_dispatch: inputs: + ref: + description: 'Branch name or Pull Request number (e.g., master or 6102)' + required: true + default: 'master' + type: string mempool_hostname: description: 'Mempool Hostname' required: true @@ -15,8 +20,111 @@ on: type: string jobs: + cache: + name: "Cache assets for builds" + runs-on: ubuntu-latest + steps: + - name: Determine checkout ref + id: determine-ref + run: | + REF_INPUT="${{ github.event.inputs.ref }}" + if [[ "$REF_INPUT" =~ ^[0-9]+$ ]]; then + echo "ref=refs/pull/$REF_INPUT/head" >> $GITHUB_OUTPUT + else + echo "ref=$REF_INPUT" >> $GITHUB_OUTPUT + fi + + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ steps.determine-ref.outputs.ref }} + path: assets + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + registry-url: "https://registry.npmjs.org" + + - name: Install (Prod dependencies only) + run: npm ci --omit=dev --omit=optional + working-directory: assets/frontend + + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + + - name: Unzip assets before building (src/resources) + continue-on-error: true + run: unzip -o mining-pool-assets.zip -d assets/frontend/src/resources/mining-pools + + - name: Unzip assets before building (src/resources) + continue-on-error: true + run: unzip -o promo-video-assets.zip -d assets/frontend/src/resources/promo-video + + # - name: Unzip assets before building (dist) + # continue-on-error: true + # run: unzip assets.zip -d assets/frontend/dist/mempool/browser/resources + + - name: Sync-assets + run: npm run sync-assets-dev + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MEMPOOL_CDN: 1 + VERBOSE: 1 + working-directory: assets/frontend + + - name: Zip mining-pool assets + run: zip -jrq mining-pool-assets.zip assets/frontend/src/resources/mining-pools/* + + - name: Zip promo-video assets + run: zip -jrq promo-video-assets.zip assets/frontend/src/resources/promo-video/* + + - name: Upload mining pool assets as artifact + uses: actions/upload-artifact@v4 + with: + name: mining-pool-assets + path: mining-pool-assets.zip + + - name: Upload promo video assets as artifact + uses: actions/upload-artifact@v4 + with: + name: promo-video-assets + path: promo-video-assets.zip + + - name: Save mining pool assets cache + id: cache-mining-pool-save + uses: actions/cache/save@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Save promo video assets cache + id: cache-promo-video-save + uses: actions/cache/save@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + e2e: runs-on: ubuntu-latest + needs: cache strategy: fail-fast: false matrix: @@ -24,9 +132,20 @@ jobs: name: E2E tests for ${{ matrix.module }} steps: + - name: Determine checkout ref + id: determine-ref + run: | + REF_INPUT="${{ github.event.inputs.ref }}" + if [[ "$REF_INPUT" =~ ^[0-9]+$ ]]; then + echo "ref=refs/pull/$REF_INPUT/head" >> $GITHUB_OUTPUT + else + echo "ref=$REF_INPUT" >> $GITHUB_OUTPUT + fi + - name: Checkout uses: actions/checkout@v3 with: + ref: ${{ steps.determine-ref.outputs.ref }} path: ${{ matrix.module }} - name: Setup node @@ -78,7 +197,7 @@ jobs: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend build: npm run config:defaults:${{ matrix.module }} - start: npm run start:ci-parameterized + start: npm run start:parameterized wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true @@ -90,7 +209,7 @@ jobs: browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" env: - COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} @@ -106,7 +225,7 @@ jobs: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend build: npm run config:defaults:${{ matrix.module }} - start: npm run start:ci-parameterized + start: npm run start:parameterized wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true @@ -118,7 +237,7 @@ jobs: browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" env: - COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} @@ -134,7 +253,7 @@ jobs: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend build: npm run config:defaults:mempool - start: npm run start:ci-parameterized + start: npm run start:parameterized wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true @@ -145,7 +264,7 @@ jobs: browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" env: - COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} From 3500a657a98187ca8a356e2278d7a170340af36d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 1 Apr 2025 06:23:36 +0000 Subject: [PATCH 081/114] misc changes --- .../blockchain/blockchain.component.html | 4 ++-- .../blockchain/blockchain.component.scss | 7 +++++++ .../blockchain/blockchain.component.ts | 19 +++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/blockchain/blockchain.component.html b/frontend/src/app/components/blockchain/blockchain.component.html index af3bf52b1..d56a115e5 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.html +++ b/frontend/src/app/components/blockchain/blockchain.component.html @@ -1,4 +1,4 @@ -
    +
    @@ -10,7 +10,7 @@
    - +
    diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index 32225598a..c428874ba 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -19,6 +19,13 @@ -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* IE10+/Edge */ user-select: none; /* Standard */ + + + transition: transform 2s; + + &.flipped { + transform: rotate(180deg); + } } .position-container { diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index 2e3224a9c..913fd29a9 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -29,6 +29,8 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { connected: boolean = true; blockDisplayMode: 'size' | 'fees'; + flipped: boolean = false; + dividerOffset: number | null = null; mempoolOffset: number | null = null; positionStyle = { @@ -90,10 +92,14 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { } toggleBlockDisplayMode(): void { - if (this.blockDisplayMode === 'size') this.blockDisplayMode = 'fees'; - else this.blockDisplayMode = 'size'; - this.StorageService.setValue('block-display-mode-preference', this.blockDisplayMode); - this.stateService.blockDisplayMode$.next(this.blockDisplayMode); + if (this.isA1()) { + this.flipped = !this.flipped; + } else { + if (this.blockDisplayMode === 'size') this.blockDisplayMode = 'fees'; + else this.blockDisplayMode = 'size'; + this.StorageService.setValue('block-display-mode-preference', this.blockDisplayMode); + this.stateService.blockDisplayMode$.next(this.blockDisplayMode); + } } onMempoolWidthChange(width): void { @@ -126,6 +132,11 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { } } + isA1(): boolean { + const now = new Date(); + return now.getMonth() === 3 && now.getDate() === 1; + } + onResize(): void { const width = this.containerWidth || window.innerWidth; if (width >= 768) { From 1c9b422db41cea2b095910612b65c55341bc96ee Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 1 Apr 2025 06:42:05 +0000 Subject: [PATCH 082/114] fix pool update retry delay --- backend/src/tasks/pools-updater.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index 38e816d74..1d015cb11 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -98,7 +98,8 @@ class PoolsUpdater { logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag); } catch (e) { - this.lastRun = now - 600; // Try again in 10 minutes + // fast-forward lastRun to 10 minutes before the next scheduled update + this.lastRun = now - (config.MEMPOOL.POOLS_UPDATE_DELAY - 600); logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag); } } From ab5c49ea7a7469b28b14d713a65f11cf4fd5a494 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 1 Apr 2025 07:34:49 +0000 Subject: [PATCH 083/114] Reset block cache after updating pools --- backend/src/api/pools-parser.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/src/api/pools-parser.ts b/backend/src/api/pools-parser.ts index 2fd55d6c5..2895da5a5 100644 --- a/backend/src/api/pools-parser.ts +++ b/backend/src/api/pools-parser.ts @@ -8,6 +8,7 @@ import mining from './mining/mining'; import transactionUtils from './transaction-utils'; import BlocksRepository from '../repositories/BlocksRepository'; import redisCache from './redis-cache'; +import blocks from './blocks'; class PoolsParser { miningPools: any[] = []; @@ -42,6 +43,8 @@ class PoolsParser { await this.$insertUnknownPool(); let reindexUnknown = false; + let clearCache = false; + for (const pool of this.miningPools) { if (!pool.id) { @@ -78,17 +81,20 @@ class PoolsParser { logger.debug(`Inserting new mining pool ${pool.name}`); await PoolsRepository.$insertNewMiningPool(pool, slug); reindexUnknown = true; + clearCache = true; } else { if (poolDB.name !== pool.name) { // Pool has been renamed const newSlug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); logger.warn(`Renaming ${poolDB.name} mining pool to ${pool.name}. Slug has been updated. Maybe you want to make a redirection from 'https://mempool.space/mining/pool/${poolDB.slug}' to 'https://mempool.space/mining/pool/${newSlug}`); await PoolsRepository.$renameMiningPool(poolDB.id, newSlug, pool.name); + clearCache = true; } if (poolDB.link !== pool.link) { // Pool link has changed logger.debug(`Updating link for ${pool.name} mining pool`); await PoolsRepository.$updateMiningPoolLink(poolDB.id, pool.link); + clearCache = true; } if (JSON.stringify(pool.addresses) !== poolDB.addresses || JSON.stringify(pool.regexes) !== poolDB.regexes) { @@ -96,6 +102,7 @@ class PoolsParser { logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`); await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes); reindexUnknown = true; + clearCache = true; await this.$reindexBlocksForPool(poolDB.id); } } @@ -111,6 +118,19 @@ class PoolsParser { } await this.$reindexBlocksForPool(unknownPool.id); } + + // refresh the in-memory block cache with the reindexed data + if (clearCache) { + for (const block of blocks.getBlocks()) { + const reindexedBlock = await blocks.$indexBlock(block.height); + if (reindexedBlock.id === block.id) { + block.extras.pool = reindexedBlock.extras.pool; + } + } + // update persistent cache with the reindexed data + diskCache.$saveCacheToDisk(); + redisCache.$updateBlocks(blocks.getBlocks()); + } } public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined { From a0946ab046f90fc9d1bf23e00ed87655849efbcd Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 1 Apr 2025 08:51:05 +0000 Subject: [PATCH 084/114] more misc changes --- .../blockchain/blockchain.component.html | 2 +- .../blockchain/blockchain.component.scss | 5 +++-- .../blockchain/blockchain.component.ts | 21 +++++++++++++++++-- frontend/src/app/services/state.service.ts | 4 ++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/blockchain/blockchain.component.html b/frontend/src/app/components/blockchain/blockchain.component.html index d56a115e5..6ff92f523 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.html +++ b/frontend/src/app/components/blockchain/blockchain.component.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index c428874ba..4f1d466a4 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -21,11 +21,12 @@ user-select: none; /* Standard */ - transition: transform 2s; - &.flipped { transform: rotate(180deg); } + &.ready-to-flip { + transition: transform 2s; + } } .position-container { diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index 913fd29a9..b24ef728d 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -29,7 +29,8 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { connected: boolean = true; blockDisplayMode: 'size' | 'fees'; - flipped: boolean = false; + flipped: boolean = true; + readyToFlip: boolean = false; dividerOffset: number | null = null; mempoolOffset: number | null = null; @@ -42,7 +43,22 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { public stateService: StateService, public StorageService: StorageService, private cd: ChangeDetectorRef, - ) {} + ) { + if (this.StorageService.getValue('ap-flipped') !== null) { + this.flipped = false; + } else { + this.flipped = this.stateService.apFlipped; + if (this.flipped) { + setTimeout(() => { + this.flipped = false; + this.stateService.apFlipped = false; + }, 5000); + } + setTimeout(() => { + this.readyToFlip = true; + }, 500); + } + } ngOnInit(): void { this.onResize(); @@ -94,6 +110,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { toggleBlockDisplayMode(): void { if (this.isA1()) { this.flipped = !this.flipped; + this.StorageService.setValue('ap-flipped', this.flipped ? 'true' : 'false'); } else { if (this.blockDisplayMode === 'size') this.blockDisplayMode = 'fees'; else this.blockDisplayMode = 'size'; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 7f8f81744..b1e8480c8 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -147,6 +147,8 @@ export class StateService { mempoolSequence: number; mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} }; + apFlipped = false; + backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora'); networkChanged$ = new ReplaySubject(1); lightningChanged$ = new ReplaySubject(1); @@ -254,6 +256,8 @@ export class StateService { } }); + this.apFlipped = true; + this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => { if (isMempoolState(change)) { const txMap = {}; From 9f5c654b52284b9b584e9b1a5af317b125237950 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 1 Apr 2025 11:31:37 +0000 Subject: [PATCH 085/114] clamp min pool update delay --- backend/src/tasks/pools-updater.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index 1d015cb11..8a1a779a1 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -99,7 +99,7 @@ class PoolsUpdater { } catch (e) { // fast-forward lastRun to 10 minutes before the next scheduled update - this.lastRun = now - (config.MEMPOOL.POOLS_UPDATE_DELAY - 600); + this.lastRun = now - Math.max(config.MEMPOOL.POOLS_UPDATE_DELAY - 600, 600); logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag); } } From 2235d6305fc10c332d501eac350fb3aefc0831db Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 2 Apr 2025 00:28:17 +0000 Subject: [PATCH 086/114] Fix loose screws on the blockchain bar --- .../blockchain/blockchain.component.html | 4 +- .../blockchain/blockchain.component.scss | 8 ---- .../blockchain/blockchain.component.ts | 38 +++---------------- frontend/src/app/services/state.service.ts | 4 -- 4 files changed, 7 insertions(+), 47 deletions(-) diff --git a/frontend/src/app/components/blockchain/blockchain.component.html b/frontend/src/app/components/blockchain/blockchain.component.html index 6ff92f523..af3bf52b1 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.html +++ b/frontend/src/app/components/blockchain/blockchain.component.html @@ -1,4 +1,4 @@ -
    +
    @@ -10,7 +10,7 @@
    - +
    diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index 4f1d466a4..32225598a 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -19,14 +19,6 @@ -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* IE10+/Edge */ user-select: none; /* Standard */ - - - &.flipped { - transform: rotate(180deg); - } - &.ready-to-flip { - transition: transform 2s; - } } .position-container { diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index b24ef728d..2e3224a9c 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -29,9 +29,6 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { connected: boolean = true; blockDisplayMode: 'size' | 'fees'; - flipped: boolean = true; - readyToFlip: boolean = false; - dividerOffset: number | null = null; mempoolOffset: number | null = null; positionStyle = { @@ -43,22 +40,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { public stateService: StateService, public StorageService: StorageService, private cd: ChangeDetectorRef, - ) { - if (this.StorageService.getValue('ap-flipped') !== null) { - this.flipped = false; - } else { - this.flipped = this.stateService.apFlipped; - if (this.flipped) { - setTimeout(() => { - this.flipped = false; - this.stateService.apFlipped = false; - }, 5000); - } - setTimeout(() => { - this.readyToFlip = true; - }, 500); - } - } + ) {} ngOnInit(): void { this.onResize(); @@ -108,15 +90,10 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { } toggleBlockDisplayMode(): void { - if (this.isA1()) { - this.flipped = !this.flipped; - this.StorageService.setValue('ap-flipped', this.flipped ? 'true' : 'false'); - } else { - if (this.blockDisplayMode === 'size') this.blockDisplayMode = 'fees'; - else this.blockDisplayMode = 'size'; - this.StorageService.setValue('block-display-mode-preference', this.blockDisplayMode); - this.stateService.blockDisplayMode$.next(this.blockDisplayMode); - } + if (this.blockDisplayMode === 'size') this.blockDisplayMode = 'fees'; + else this.blockDisplayMode = 'size'; + this.StorageService.setValue('block-display-mode-preference', this.blockDisplayMode); + this.stateService.blockDisplayMode$.next(this.blockDisplayMode); } onMempoolWidthChange(width): void { @@ -149,11 +126,6 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { } } - isA1(): boolean { - const now = new Date(); - return now.getMonth() === 3 && now.getDate() === 1; - } - onResize(): void { const width = this.containerWidth || window.innerWidth; if (width >= 768) { diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index b1e8480c8..7f8f81744 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -147,8 +147,6 @@ export class StateService { mempoolSequence: number; mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} }; - apFlipped = false; - backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora'); networkChanged$ = new ReplaySubject(1); lightningChanged$ = new ReplaySubject(1); @@ -256,8 +254,6 @@ export class StateService { } }); - this.apFlipped = true; - this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => { if (isMempoolState(change)) { const txMap = {}; From c43f436b04c1d7ad1680a149d94b271314e1b941 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 11:18:12 +0900 Subject: [PATCH 087/114] Add a new WS mocking utility to send fixtures or single messages --- frontend/cypress/support/commands.ts | 5 +++ frontend/cypress/support/index.d.ts | 1 + frontend/cypress/support/websocket.ts | 64 +++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts index 018f63569..2ce198241 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.ts @@ -44,6 +44,7 @@ import { PageIdleDetector } from './PageIdleDetector'; import { mockWebSocket } from './websocket'; +import { mockWebSocketV2 } from './websocket'; /* global Cypress */ const codes = { @@ -72,6 +73,10 @@ Cypress.Commands.add('mockMempoolSocket', () => { mockWebSocket(); }); +Cypress.Commands.add('mockMempoolSocketV2', () => { + mockWebSocketV2(); +}); + Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => { cy.get('.dropdown-toggle').click().then(() => { cy.get(`a.${network}`).click().then(() => { diff --git a/frontend/cypress/support/index.d.ts b/frontend/cypress/support/index.d.ts index 2c5328301..21ffe6a2d 100644 --- a/frontend/cypress/support/index.d.ts +++ b/frontend/cypress/support/index.d.ts @@ -5,6 +5,7 @@ declare namespace Cypress { waitForSkeletonGone(): Chainable waitForPageIdle(): Chainable mockMempoolSocket(): Chainable + mockMempoolSocketV2(): Chainable changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable } } \ No newline at end of file diff --git a/frontend/cypress/support/websocket.ts b/frontend/cypress/support/websocket.ts index 1356ccc76..b067cc6e8 100644 --- a/frontend/cypress/support/websocket.ts +++ b/frontend/cypress/support/websocket.ts @@ -27,6 +27,37 @@ const createMock = (url: string) => { return mocks[url]; }; +export const mockWebSocketV2 = () => { + cy.on('window:before:load', (win) => { + const winWebSocket = win.WebSocket; + cy.stub(win, 'WebSocket').callsFake((url) => { + console.log(url); + if ((new URL(url).pathname.indexOf('/sockjs-node/') !== 0)) { + const { server, websocket } = createMock(url); + + win.mockServer = server; + win.mockServer.on('connection', (socket) => { + win.mockSocket = socket; + }); + + win.mockServer.on('message', (message) => { + console.log(message); + }); + + return websocket; + } else { + return new winWebSocket(url); + } + }); + }); + + cy.on('window:before:unload', () => { + for (const url in mocks) { + cleanupMock(url); + } + }); +}; + export const mockWebSocket = () => { cy.on('window:before:load', (win) => { const winWebSocket = win.WebSocket; @@ -65,6 +96,27 @@ export const mockWebSocket = () => { }); }; +export const receiveWebSocketMessageFromServer = ({ + params +}: { params?: any } = {}) => { + cy.window().then((win) => { + if (params.message) { + console.log('sending message'); + win.mockSocket.send(params.message.contents); + } + + if (params.file) { + cy.readFile(`cypress/fixtures/${params.file.path}`, 'utf-8').then((fixture) => { + console.log('sending payload'); + win.mockSocket.send(JSON.stringify(fixture)); + }); + + } + }); + return; +}; + + export const emitMempoolInfo = ({ params }: { params?: any } = {}) => { @@ -82,16 +134,22 @@ export const emitMempoolInfo = ({ switch (params.command) { case "init": { win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}'); - cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => { + cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'utf-8').then((fixture) => { win.mockSocket.send(JSON.stringify(fixture)); }); - cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'ascii').then((fixture) => { + cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'utf-8').then((fixture) => { win.mockSocket.send(JSON.stringify(fixture)); }); break; } case "rbfTransaction": { - cy.readFile('cypress/fixtures/mainnet_rbf.json', 'ascii').then((fixture) => { + cy.readFile('cypress/fixtures/mainnet_rbf.json', 'utf-8').then((fixture) => { + win.mockSocket.send(JSON.stringify(fixture)); + }); + break; + } + case 'trackTx': { + cy.readFile('cypress/fixtures/track_tx.json', 'utf-8').then((fixture) => { win.mockSocket.send(JSON.stringify(fixture)); }); break; From 20ef5b328888a5eb858b80674094e047334c96df Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 11:19:06 +0900 Subject: [PATCH 088/114] Add fixtures for the RBF tx tracker test --- .../details_rbf/api_accelerator_estimate.json | 60 + .../details_rbf/api_block_history.json | 1 + .../details_rbf/api_mining_pools_1w.json | 260 ++++ .../fixtures/details_rbf/tx01_api_cached.json | 55 + .../fixtures/details_rbf/tx01_api_rbf.json | 34 + .../details_rbf/tx01_ws_blocks_01.json | 579 ++++++++ .../tx01_ws_mempool_blocks_01.json | 68 + .../details_rbf/tx01_ws_stratum_jobs.json | 1235 +++++++++++++++++ .../details_rbf/tx01_ws_tx_replaced.json | 5 + .../fixtures/details_rbf/tx02_api_cpfp.json | 9 + .../fixtures/details_rbf/tx02_api_rbf.json | 36 + .../fixtures/details_rbf/tx02_api_tx.json | 38 + .../details_rbf/tx02_api_tx_times.json | 3 + .../fixtures/details_rbf/tx02_ws_block.json | 116 ++ .../tx02_ws_block_confirmation.json | 116 ++ .../tx02_ws_mempool_blocks_01.json | 75 + .../details_rbf/tx02_ws_next_block.json | 75 + .../details_rbf/tx02_ws_tx_position.json | 9 + 18 files changed, 2774 insertions(+) create mode 100644 frontend/cypress/fixtures/details_rbf/api_accelerator_estimate.json create mode 100644 frontend/cypress/fixtures/details_rbf/api_block_history.json create mode 100644 frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx01_api_cached.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx01_ws_mempool_blocks_01.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx01_ws_tx_replaced.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_api_tx.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_api_tx_times.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_ws_block.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_ws_block_confirmation.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_ws_mempool_blocks_01.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_ws_next_block.json create mode 100644 frontend/cypress/fixtures/details_rbf/tx02_ws_tx_position.json diff --git a/frontend/cypress/fixtures/details_rbf/api_accelerator_estimate.json b/frontend/cypress/fixtures/details_rbf/api_accelerator_estimate.json new file mode 100644 index 000000000..889c5d763 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/api_accelerator_estimate.json @@ -0,0 +1,60 @@ +{ + "txSummary": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "effectiveVsize": 224, + "effectiveFee": 960, + "ancestorCount": 1 + }, + "cost": 1000, + "targetFeeRate": 3, + "nextBlockFee": 672, + "userBalance": 0, + "mempoolBaseFee": 50000, + "vsizeFee": 0, + "pools": [ + 36, + 102, + 112, + 44, + 4, + 2, + 6, + 94, + 143, + 43, + 105, + 115, + 142, + 111 + ], + "options": [ + { + "fee": 1000 + }, + { + "fee": 2000 + }, + { + "fee": 10000 + } + ], + "hasAccess": false, + "availablePaymentMethods": { + "bitcoin": { + "enabled": true, + "min": 1000, + "max": 10000000 + }, + "applePay": { + "enabled": true, + "min": 10, + "max": 1000 + }, + "googlePay": { + "enabled": true, + "min": 10, + "max": 1000 + } + }, + "unavailable": false +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/api_block_history.json b/frontend/cypress/fixtures/details_rbf/api_block_history.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/api_block_history.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json b/frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json new file mode 100644 index 000000000..3a678ca02 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json @@ -0,0 +1,260 @@ +{ + "pools": [ + { + "poolId": 112, + "name": "Foundry USA", + "link": "https://foundrydigital.com", + "blockCount": 323, + "rank": 1, + "emptyBlocks": 0, + "slug": "foundryusa", + "avgMatchRate": 99.96, + "avgFeeDelta": "-0.01971455", + "poolUniqueId": 111 + }, + { + "poolId": 45, + "name": "AntPool", + "link": "https://www.antpool.com", + "blockCount": 171, + "rank": 2, + "emptyBlocks": 0, + "slug": "antpool", + "avgMatchRate": 99.99, + "avgFeeDelta": "-0.04227368", + "poolUniqueId": 44 + }, + { + "poolId": 74, + "name": "ViaBTC", + "link": "https://viabtc.com", + "blockCount": 166, + "rank": 3, + "emptyBlocks": 0, + "slug": "viabtc", + "avgMatchRate": 99.99, + "avgFeeDelta": "-0.02530964", + "poolUniqueId": 73 + }, + { + "poolId": 37, + "name": "F2Pool", + "link": "https://www.f2pool.com", + "blockCount": 104, + "rank": 4, + "emptyBlocks": 0, + "slug": "f2pool", + "avgMatchRate": 99.99, + "avgFeeDelta": "-0.03299327", + "poolUniqueId": 36 + }, + { + "poolId": 116, + "name": "MARA Pool", + "link": "https://marapool.com", + "blockCount": 66, + "rank": 5, + "emptyBlocks": 0, + "slug": "marapool", + "avgMatchRate": 99.97, + "avgFeeDelta": "0.02366061", + "poolUniqueId": 115 + }, + { + "poolId": 103, + "name": "SpiderPool", + "link": "https://www.spiderpool.com", + "blockCount": 46, + "rank": 6, + "emptyBlocks": 1, + "slug": "spiderpool", + "avgMatchRate": 97.82, + "avgFeeDelta": "-0.07258913", + "poolUniqueId": 102 + }, + { + "poolId": 142, + "name": "SECPOOL", + "link": "https://www.secpool.com", + "blockCount": 30, + "rank": 7, + "emptyBlocks": 1, + "slug": "secpool", + "avgMatchRate": 96.67, + "avgFeeDelta": "-0.06596000", + "poolUniqueId": 141 + }, + { + "poolId": 106, + "name": "Binance Pool", + "link": "https://pool.binance.com", + "blockCount": 28, + "rank": 8, + "emptyBlocks": 0, + "slug": "binancepool", + "avgMatchRate": 99.99, + "avgFeeDelta": "-0.05834286", + "poolUniqueId": 105 + }, + { + "poolId": 5, + "name": "Luxor", + "link": "https://mining.luxor.tech", + "blockCount": 28, + "rank": 9, + "emptyBlocks": 0, + "slug": "luxor", + "avgMatchRate": 100, + "avgFeeDelta": "-0.05496071", + "poolUniqueId": 4 + }, + { + "poolId": 143, + "name": "OCEAN", + "link": "https://ocean.xyz/", + "blockCount": 12, + "rank": 10, + "emptyBlocks": 0, + "slug": "ocean", + "avgMatchRate": 91.9, + "avgFeeDelta": "-0.14650833", + "poolUniqueId": 142 + }, + { + "poolId": 44, + "name": "Braiins Pool", + "link": "https://braiins.com/pool", + "blockCount": 12, + "rank": 11, + "emptyBlocks": 0, + "slug": "braiinspool", + "avgMatchRate": 100, + "avgFeeDelta": "-0.03553333", + "poolUniqueId": 43 + }, + { + "poolId": 113, + "name": "SBI Crypto", + "link": "https://sbicrypto.com", + "blockCount": 8, + "rank": 12, + "emptyBlocks": 0, + "slug": "sbicrypto", + "avgMatchRate": 98.65, + "avgFeeDelta": "-0.04246250", + "poolUniqueId": 112 + }, + { + "poolId": 152, + "name": "Carbon Negative", + "link": "https://github.com/bitcoin-data/mining-pools/issues/48", + "blockCount": 7, + "rank": 13, + "emptyBlocks": 0, + "slug": "carbonnegative", + "avgMatchRate": 99.75, + "avgFeeDelta": "-0.04407143", + "poolUniqueId": 151 + }, + { + "poolId": 7, + "name": "BTC.com", + "link": "https://pool.btc.com", + "blockCount": 5, + "rank": 14, + "emptyBlocks": 0, + "slug": "btccom", + "avgMatchRate": 99.98, + "avgFeeDelta": "-0.02496000", + "poolUniqueId": 6 + }, + { + "poolId": 162, + "name": "Mining Squared", + "link": "https://pool.bsquared.network/", + "blockCount": 4, + "rank": 15, + "emptyBlocks": 0, + "slug": "miningsquared", + "avgMatchRate": 100, + "avgFeeDelta": "-0.00915000", + "poolUniqueId": 161 + }, + { + "poolId": 95, + "name": "Poolin", + "link": "https://www.poolin.com", + "blockCount": 4, + "rank": 16, + "emptyBlocks": 0, + "slug": "poolin", + "avgMatchRate": 100, + "avgFeeDelta": "-0.26485000", + "poolUniqueId": 94 + }, + { + "poolId": 1, + "name": "Unknown", + "link": "https://learnmeabitcoin.com/technical/coinbase-transaction", + "blockCount": 4, + "rank": 17, + "emptyBlocks": 0, + "slug": "unknown", + "avgMatchRate": 100, + "avgFeeDelta": "-0.06490000", + "poolUniqueId": 0 + }, + { + "poolId": 144, + "name": "WhitePool", + "link": "https://whitebit.com/mining-pool", + "blockCount": 3, + "rank": 18, + "emptyBlocks": 0, + "slug": "whitepool", + "avgMatchRate": 100, + "avgFeeDelta": "-0.01293333", + "poolUniqueId": 143 + }, + { + "poolId": 3, + "name": "ULTIMUSPOOL", + "link": "https://www.ultimuspool.com", + "blockCount": 1, + "rank": 19, + "emptyBlocks": 0, + "slug": "ultimuspool", + "avgMatchRate": 100, + "avgFeeDelta": "-0.16130000", + "poolUniqueId": 2 + }, + { + "poolId": 50, + "name": "Solo CK", + "link": "https://solo.ckpool.org", + "blockCount": 1, + "rank": 20, + "emptyBlocks": 0, + "slug": "solock", + "avgMatchRate": 100, + "avgFeeDelta": "-0.01510000", + "poolUniqueId": 49 + }, + { + "poolId": 158, + "name": "BitFuFuPool", + "link": "https://www.bitfufu.com/pool", + "blockCount": 1, + "rank": 21, + "emptyBlocks": 0, + "slug": "bitfufupool", + "avgMatchRate": 100, + "avgFeeDelta": "-0.01630000", + "poolUniqueId": 157 + } + ], + "blockCount": 1024, + "lastEstimatedHashrate": 786391245138648900000, + "lastEstimatedHashrate3d": 797683179385121300000, + "lastEstimatedHashrate1w": 827836055441520300000 +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_api_cached.json b/frontend/cypress/fixtures/details_rbf/tx01_api_cached.json new file mode 100644 index 000000000..62184cc9d --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_api_cached.json @@ -0,0 +1,55 @@ +{ + "txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "fb16c141d22a3a7af5e44e7478a8663866c35d554cd85107ec8a99b97e5a72e9", + "vout": 0, + "prevout": { + "scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj", + "value": 50000 + }, + "scriptsig": "483045022100f44a50e894c30b28400933da120b872ff15bd2e66dd034ffbbb925fd054dd60f02206ba477a9da3fae68a9f420f53bc25493270bd987d13b75fef39cf514aea9cdb3014104ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df", + "scriptsig_asm": "OP_PUSHBYTES_72 3045022100f44a50e894c30b28400933da120b872ff15bd2e66dd034ffbbb925fd054dd60f02206ba477a9da3fae68a9f420f53bc25493270bd987d13b75fef39cf514aea9cdb301 OP_PUSHBYTES_65 04ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df", + "is_coinbase": false, + "sequence": 4294967293 + } + ], + "vout": [ + { + "scriptpubkey": "5120a2fc5cb445755389eed295ab9d594b0facd3e00840a3e477fa7c025412c53795", + "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 a2fc5cb445755389eed295ab9d594b0facd3e00840a3e477fa7c025412c53795", + "scriptpubkey_type": "v1_p2tr", + "scriptpubkey_address": "bc1p5t79edz9w4fcnmkjjk4e6k2tp7kd8cqggz37gal60sp9gyk9x72sk4mk0f", + "value": 49394 + } + ], + "size": 233, + "weight": 932, + "sigops": 0, + "fee": 606, + "status": { + "confirmed": false + }, + "order": 701313494, + "vsize": 233, + "adjustedVsize": 233, + "feePerVsize": 2.6008583690987126, + "adjustedFeePerVsize": 2.6008583690987126, + "effectiveFeePerVsize": 2.6008583690987126, + "firstSeen": 1743541407, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 318595.5 + }, + "flags": 1099511645193 +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json b/frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json new file mode 100644 index 000000000..5278f9ffe --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json @@ -0,0 +1,34 @@ +{ + "replacements": { + "tx": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "fee": 960, + "vsize": 224, + "value": 49040, + "rate": 4.285714285714286, + "time": 1743541726, + "rbf": true, + "fullRbf": false + }, + "time": 1743541726, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29", + "fee": 606, + "vsize": 233, + "value": 49394, + "rate": 2.6008583690987126, + "time": 1743541407, + "rbf": true + }, + "time": 1743541407, + "interval": 319, + "fullRbf": false, + "replaces": [] + } + ] + }, + "replaces": null +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json b/frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json new file mode 100644 index 000000000..32b6578fc --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json @@ -0,0 +1,579 @@ +{ + "blocks": [ + { + "id": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4", + "height": 890440, + "version": 559235072, + "timestamp": 1743535677, + "bits": 386038124, + "nonce": 2920325684, + "difficulty": 113757508810854, + "merkle_root": "c793d5fdbfb1ebe99e14a13a6d65370057d311774d33c71da166663b18722474", + "tx_count": 3823, + "size": 1578209, + "weight": 3993461, + "previousblockhash": "000000000000000000020fb2e24425793e17e60e188205dc1694d221790348b2", + "mediantime": 1743532406, + "stale": false, + "extras": { + "reward": 319838750, + "coinbaseRaw": "0348960d082f5669614254432f2cfabe6d6d294719da11c017243828bf32c405341db7f19387fee92c25413c45e114907f9810000000000000001058bf9601429f9fa7a6c160d10d00000000000000", + "orphans": [], + "medianFee": 4, + "feeRange": [ + 3, + 3, + 3.0191082802547773, + 3.980952380952381, + 5, + 10, + 427.748502994012 + ], + "totalFees": 7338750, + "avgFee": 1920, + "avgFeeRate": 7, + "utxoSetChange": 4093, + "avgTxSize": 412.71000000000004, + "totalInputs": 7430, + "totalOutputs": 11523, + "totalOutputAmt": 547553568373, + "segwitTotalTxs": 3432, + "segwitTotalSize": 1467920, + "segwitTotalWeight": 3552413, + "feePercentiles": null, + "virtualSize": 998365.25, + "coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4", + "coinbaseAddresses": [ + "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4" + ], + "coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG", + "coinbaseSignatureAscii": "\u0003H–\r\b/ViaBTC/,ú¾mm)G\u0019Ú\u0011À\u0017$8(¿2Ä\u00054\u001d·ñ“‡þé,%Aìg/Foundry USA Pool #dropgold/\u0001S\nEaç\u0002\u0000\u0000\u0000\u0000\u0000", + "header": "00a03220b46014bdbeb557d1d87065e0ecb1b0ab33654bb7f579000000000000000000003ed60f06cec16df4399b5dafa7077036c2eb58cc6a16e6cdca559b9e2f7e4525bb3eec676c790217b7b3c9cb", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 111, + "name": "Foundry USA", + "slug": "foundryusa", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2792968, + "expectedWeight": 3991959, + "similarity": 0.9951416839808291 + } + }, + { + "id": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce", + "height": 890442, + "version": 557981696, + "timestamp": 1743536834, + "bits": 386038124, + "nonce": 470697326, + "difficulty": 113757508810854, + "merkle_root": "5e92e681c1db2797a5b3e5016729059f8b60a256cafb51d835dac2b3964c0db4", + "tx_count": 3566, + "size": 1628328, + "weight": 3993552, + "previousblockhash": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c", + "mediantime": 1743532867, + "stale": false, + "extras": { + "reward": 318057766, + "coinbaseRaw": "034a960d194d696e656420627920416e74506f6f6c204d000201e15e2989fabe6d6dd599e9dfa40be51f1517c8f512c5c3d51c7656182f1df335d34b98ee02c527db080000000000000000004f92b702000000000000", + "orphans": [], + "medianFee": 3.00860164711668, + "feeRange": [ + 1.5174418604651163, + 2.0140845070422535, + 2.492354740061162, + 3, + 4.020942408376963, + 7, + 200 + ], + "totalFees": 5557766, + "avgFee": 1558, + "avgFeeRate": 5, + "utxoSetChange": 1971, + "avgTxSize": 456.48, + "totalInputs": 7938, + "totalOutputs": 9909, + "totalOutputAmt": 900044492230, + "segwitTotalTxs": 3214, + "segwitTotalSize": 1526463, + "segwitTotalWeight": 3586200, + "feePercentiles": null, + "virtualSize": 998388, + "coinbaseAddress": "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z", + "coinbaseAddresses": [ + "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z", + "39C7fxSzEACPjM78Z7xdPxhf7mKxJwvfMJ" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 42402a28dd61f2718a4b27ae72a4791d5bbdade7 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003J–\r\u0019Mined by AntPool M\u0000\u0002\u0001á^)‰ú¾mmՙéߤ\u000bå\u001f\u0015\u0017Èõ\u0012ÅÃÕ\u001cvV\u0018/\u001dó5ÓK˜î\u0002Å'Û\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000O’·\u0002\u0000\u0000\u0000\u0000\u0000\u0000", + "header": "002042219cd10cfc6daf06d3faad645fe0914cea859745275aef00000000000000000000b40d4c96b3c2da35d851fbca56a2608b9f05296701e5b3a59727dbc181e6925ec242ec676c7902176e450e1c", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 44, + "name": "AntPool", + "slug": "antpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 5764747, + "expectedWeight": 3991786, + "similarity": 0.9029319155137951 + } + }, + { + "id": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6", + "height": 890443, + "version": 706666496, + "timestamp": 1743537197, + "bits": 386038124, + "nonce": 321696065, + "difficulty": 113757508810854, + "merkle_root": "3d7574f7eca741fa94b4690868a242e5b286f8a0417ad0275d4ab05893e96350", + "tx_count": 2155, + "size": 1700002, + "weight": 3993715, + "previousblockhash": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce", + "mediantime": 1743533789, + "stale": false, + "extras": { + "reward": 315112344, + "coinbaseRaw": "034b960d21202020204d696e656420627920536563706f6f6c2020202070000b05e388958c01fabe6d6db7ae4bfa7b1294e16e800b4563f1f5ddeb5c0740319eba45600f3f05d2d7272910000000000000000000c2cb7e020000", + "orphans": [], + "medianFee": 1.4360674424569184, + "feeRange": [ + 1, + 1.0135135135135136, + 1.09717868338558, + 2.142857142857143, + 3.009584664536741, + 4.831858407079646, + 196.07843137254903 + ], + "totalFees": 2612344, + "avgFee": 1212, + "avgFeeRate": 2, + "utxoSetChange": -2880, + "avgTxSize": 788.64, + "totalInputs": 9773, + "totalOutputs": 6893, + "totalOutputAmt": 264603969671, + "segwitTotalTxs": 1933, + "segwitTotalSize": 1556223, + "segwitTotalWeight": 3418707, + "feePercentiles": null, + "virtualSize": 998428.75, + "coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "coinbaseAddresses": [ + "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003K–\r! Mined by Secpool p\u0000\u000b\u0005㈕Œ\u0001ú¾mm·®Kú{\u0012”án€\u000bEcñõÝë\\\u0007@1žºE`\u000f?\u0005Ò×')\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÂË~\u0002\u0000\u0000", + "header": "00e01e2acedf6db0523987887ed8b989d4f58b3d6b878a974548010000000000000000005063e99358b04a5d27d07a41a0f886b2e542a2680869b494fa41a7ecf774753d2d44ec676c79021741b12c13", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 141, + "name": "SECPOOL", + "slug": "secpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2623934, + "expectedWeight": 3991917, + "similarity": 0.9951244468050102 + } + }, + { + "id": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d", + "height": 890444, + "version": 671080448, + "timestamp": 1743539347, + "bits": 386038124, + "nonce": 994357124, + "difficulty": 113757508810854, + "merkle_root": "c891d4bf68e22916274b667eb3287d50da2ddd63f8dad892da045cc2ad4a7b21", + "tx_count": 3797, + "size": 1500309, + "weight": 3993525, + "previousblockhash": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6", + "mediantime": 1743533986, + "stale": false, + "extras": { + "reward": 318708524, + "coinbaseRaw": "034c960d082f5669614254432f2cfabe6d6d45b7fd7ab53a0914da7dcc9d21fe44f0936f5354169a56df9d5139f07afbc2b41000000000000000106fc0eb03f0ac2e851d18d8d9f85ad70000000000", + "orphans": [], + "medianFee": 4.064775540157046, + "feeRange": [ + 3.014354066985646, + 3.18368700265252, + 3.602836879432624, + 4.231825525040388, + 5.581730769230769, + 10, + 697.7151162790698 + ], + "totalFees": 6208524, + "avgFee": 1635, + "avgFeeRate": 6, + "utxoSetChange": 5755, + "avgTxSize": 395.02, + "totalInputs": 6681, + "totalOutputs": 12436, + "totalOutputAmt": 835839828101, + "segwitTotalTxs": 3351, + "segwitTotalSize": 1354446, + "segwitTotalWeight": 3410181, + "feePercentiles": null, + "virtualSize": 998381.25, + "coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4", + "coinbaseAddresses": [ + "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4" + ], + "coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG", + "coinbaseSignatureAscii": "\u0003L–\r\b/ViaBTC/,ú¾mmE·ýzµ:\t\u0014Ú}̝!þDð“oST\u0016šVߝQ9ðzû´\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010oÀë\u0003ð¬.…\u001d\u0018ØÙøZ×\u0000\u0000\u0000\u0000\u0000", + "header": "00e0ff27f6f596dc1a210647d530ed3b351b5173428370b2086e02000000000000000000217b4aadc25c04da92d8daf863dd2dda507d28b37e664b271629e268bfd491c8934cec676c79021784af443b", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 73, + "name": "ViaBTC", + "slug": "viabtc", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 6253024, + "expectedWeight": 3991868, + "similarity": 0.9862862477811569 + } + }, + { + "id": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a", + "height": 890445, + "version": 601202688, + "timestamp": 1743539574, + "bits": 386038124, + "nonce": 1647397133, + "difficulty": 113757508810854, + "merkle_root": "61d8294afa8f6bafa4d979a77d187dee5f75a6392f957ea647d96eefbbbc5e9b", + "tx_count": 3579, + "size": 1659862, + "weight": 3993406, + "previousblockhash": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d", + "mediantime": 1743535677, + "stale": false, + "extras": { + "reward": 315617086, + "coinbaseRaw": "034d960d04764dec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f4fac7c451540000000000000", + "orphans": [], + "medianFee": 2.5565329189526835, + "feeRange": [ + 1.521613832853026, + 2, + 2.2411347517730498, + 3, + 3, + 3.954954954954955, + 162.78343949044586 + ], + "totalFees": 3117086, + "avgFee": 871, + "avgFeeRate": 3, + "utxoSetChange": 1881, + "avgTxSize": 463.65000000000003, + "totalInputs": 7893, + "totalOutputs": 9774, + "totalOutputAmt": 324878597485, + "segwitTotalTxs": 3189, + "segwitTotalSize": 1538741, + "segwitTotalWeight": 3509030, + "feePercentiles": null, + "virtualSize": 998351.5, + "coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63", + "coinbaseAddresses": [ + "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63", + "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38" + ], + "coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3", + "coinbaseSignatureAscii": "\u0003M–\r\u0004vMìg/Foundry USA Pool #dropgold/O¬|E\u0015@\u0000\u0000\u0000\u0000\u0000\u0000", + "header": "00a0d5230d2965fa5bd3e9406f5d665f975d9fc34eae70c46eb3010000000000000000009b5ebcbbef6ed947a67e952f39a6755fee7d187da779d9a4af6b8ffa4a29d861764dec676c7902170d493162", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 111, + "name": "Foundry USA", + "slug": "foundryusa", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 3145370, + "expectedWeight": 3991903, + "similarity": 0.9903353189076812 + } + }, + { + "id": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b", + "height": 890446, + "version": 537722880, + "timestamp": 1743541107, + "bits": 386038124, + "nonce": 826569764, + "difficulty": 113757508810854, + "merkle_root": "d9b320d7cb5aace80ca20b934b13b4a272121fbdd59f3aaba690e0326ca2c144", + "tx_count": 3998, + "size": 1541360, + "weight": 3993545, + "previousblockhash": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a", + "mediantime": 1743535803, + "stale": false, + "extras": { + "reward": 317976882, + "coinbaseRaw": "034e960d20202020204d696e656420627920536563706f6f6c2020202070001b04fad5fdfefabe6d6d59dd8ebce6e5aab8fb943bbdcede474b6f2d00a395a717970104a6958c17f1ca100000000000000000008089c9350200", + "orphans": [], + "medianFee": 3.3750830641948864, + "feeRange": [ + 2.397163120567376, + 3, + 3, + 3.463647199046484, + 4.49438202247191, + 7.213930348258707, + 476.1904761904762 + ], + "totalFees": 5476882, + "avgFee": 1370, + "avgFeeRate": 5, + "utxoSetChange": 4951, + "avgTxSize": 385.41, + "totalInputs": 7054, + "totalOutputs": 12005, + "totalOutputAmt": 983289729453, + "segwitTotalTxs": 3538, + "segwitTotalSize": 1396505, + "segwitTotalWeight": 3414233, + "feePercentiles": null, + "virtualSize": 998386.25, + "coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "coinbaseAddresses": [ + "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003N–\r Mined by Secpool p\u0000\u001b\u0004úÕýþú¾mmYݎ¼æåª¸û”;½ÎÞGKo-\u0000£•§\u0017—\u0001\u0004¦•Œ\u0017ñÊ\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000€‰É5\u0002\u0000", + "header": "00000d208a33fa2f65c6d3662ac962c9bd595147b940f96520400100000000000000000044c1a26c32e090a6ab3a9fd5bd1f1272a2b4134b930ba20ce8ac5acbd720b3d97353ec676c79021724744431", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 141, + "name": "SECPOOL", + "slug": "secpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 5601814, + "expectedWeight": 3991928, + "similarity": 0.9537877497871488 + } + }, + { + "id": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "height": 890447, + "version": 568860672, + "timestamp": 1743541240, + "bits": 386038124, + "nonce": 4008077709, + "difficulty": 113757508810854, + "merkle_root": "8c3b098e4e50b67075a4fc52bf4cd603aaa450c240c18a865c9ddc0f27104f5f", + "tx_count": 1919, + "size": 1747789, + "weight": 3993172, + "previousblockhash": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b", + "mediantime": 1743536834, + "stale": false, + "extras": { + "reward": 314435106, + "coinbaseRaw": "034f960d0f2f736c7573682f65000002fba05ef1fabe6d6df8d29032ea6f9ab1debd223651f30887df779c6195869e70a7b787b3a15f4b1710000000000000000000ee5f0c00b20200000000", + "orphans": [], + "medianFee": 1.4653828213500366, + "feeRange": [ + 1.0845070422535212, + 1.2, + 1.51, + 2.0141129032258065, + 2.3893805309734515, + 4.025477707006369, + 300.0065359477124 + ], + "totalFees": 1935106, + "avgFee": 1008, + "avgFeeRate": 1, + "utxoSetChange": -4244, + "avgTxSize": 910.58, + "totalInputs": 9909, + "totalOutputs": 5665, + "totalOutputAmt": 210763861504, + "segwitTotalTxs": 1720, + "segwitTotalSize": 1629450, + "segwitTotalWeight": 3519924, + "feePercentiles": null, + "virtualSize": 998293, + "coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11", + "coinbaseAddresses": [ + "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL", + "coinbaseSignatureAscii": "\u0003O–\r\u000f/slush/e\u0000\u0000\u0002û ^ñú¾mmøÒ2êoš±Þ½\"6Qó\b‡ßwœa•†žp§·‡³¡_K\u0017\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000î_\f\u0000²\u0002\u0000\u0000\u0000\u0000", + "header": "0020e8216b30f547b3d47824c1cb06db9221fcd04621b0263dfe010000000000000000005f4f10270fdc9d5c868ac140c250a4aa03d64cbf52fca47570b6504e8e093b8cf853ec676c7902178d69e6ee", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 43, + "name": "Braiins Pool", + "slug": "braiinspool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2059571, + "expectedWeight": 3991720, + "similarity": 0.9149852183486826 + } + } + ], + "mempool-blocks": [ + { + "blockSize": 1779311, + "blockVSize": 997968.5, + "nTx": 2132, + "totalFees": 2902870, + "medianFee": 2.0479263387949875, + "feeRange": [ + 1.0721153846153846, + 1.9980563654033041, + 2.2195704057279237, + 3.009493670886076, + 3.4955223880597015, + 6.0246913580246915, + 218.1818181818182 + ] + }, + { + "blockSize": 1959636, + "blockVSize": 997903.5, + "nTx": 497, + "totalFees": 1093076, + "medianFee": 1.102049424602265, + "feeRange": [ + 1.0401794819498267, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0761096766260911, + 1.1021605957228275 + ] + }, + { + "blockSize": 1477260, + "blockVSize": 997997.25, + "nTx": 720, + "totalFees": 1016195, + "medianFee": 1.007409072434199, + "feeRange": [ + 1, + 1.0019120458891013, + 1.0040863981319323, + 1.0081019768823594, + 1.018450184501845, + 1.0203327171903882, + 1.0485018498190837 + ] + }, + { + "blockSize": 1021308, + "blockVSize": 431071.5, + "nTx": 823, + "totalFees": 432342, + "medianFee": 0, + "feeRange": [ + 1, + 1, + 1, + 1.0028011204481793, + 1.0042075736325387, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ] +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_ws_mempool_blocks_01.json b/frontend/cypress/fixtures/details_rbf/tx01_ws_mempool_blocks_01.json new file mode 100644 index 000000000..47a685757 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_ws_mempool_blocks_01.json @@ -0,0 +1,68 @@ +{ + "mempool-blocks": [ + { + "blockSize": 1780038, + "blockVSize": 997989.75, + "nTx": 2134, + "totalFees": 2919589, + "medianFee": 2.0479263387949875, + "feeRange": [ + 1.0101010101010102, + 2, + 2.235576923076923, + 3.010452961672474, + 3.5240274599542336, + 6.032085561497326, + 218.1818181818182 + ] + }, + { + "blockSize": 1958446, + "blockVSize": 997996, + "nTx": 503, + "totalFees": 1093277, + "medianFee": 1.102049424602265, + "feeRange": [ + 1.0101010101010102, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.067677314564158, + 1.0761096766260911, + 1.1021605957228275 + ] + }, + { + "blockSize": 1477611, + "blockVSize": 997927.5, + "nTx": 725, + "totalFees": 1016311, + "medianFee": 1.0075971559364956, + "feeRange": [ + 1, + 1.0019334049409236, + 1.0042075736325387, + 1.0081019768823594, + 1.018450184501845, + 1.0203327171903882, + 1.0548148148148149 + ] + }, + { + "blockSize": 1028219, + "blockVSize": 435137, + "nTx": 833, + "totalFees": 436414, + "medianFee": 0, + "feeRange": [ + 1, + 1, + 1, + 1.0028011204481793, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ] +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json b/frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json new file mode 100644 index 000000000..48d2fe01e --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json @@ -0,0 +1,1235 @@ +{ + "stratumJobs": { + "2": { + "jobId": "1743541734_2517178", + "extraNonce": "00004237", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff510350960d142f756c74696d75732f373833e5005202fe934b78fabe6d6d4bec519f2060f58bc5ba10289fbedb1f9456cc864a15f153cbfed624324382181000000000000000", + "coinbase2": "ffffffff05220200000000000017a914332c82217820c32fb9c107b67356cfdc8f8da6a6876271cc120000000017a91472c52c9c71c5f644cf6e712661710503e65e69ad870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f524501ebbaf365b0d5fa072e2b2429db23696291f2c0383d81f61600af32dd72a5ac21aaf1274a3862af6d00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541734950, + "pool": 2, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff510350960d142f756c74696d75732f373833e5005202fe934b78fabe6d6d4bec519f2060f58bc5ba10289fbedb1f9456cc864a15f153cbfed624324382181000000000000000000042370000000000000000ffffffff05220200000000000017a914332c82217820c32fb9c107b67356cfdc8f8da6a6876271cc120000000017a91472c52c9c71c5f644cf6e712661710503e65e69ad870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f524501ebbaf365b0d5fa072e2b2429db23696291f2c0383d81f61600af32dd72a5ac21aaf1274a3862af6d00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315388804, + "scriptsig": "0350960d142f756c74696d75732f373833e5005202fe934b78fabe6d6d4bec519f2060f58bc5ba10289fbedb1f9456cc864a15f153cbfed624324382181000000000000000000042370000000000000000" + }, + "4": { + "jobId": "450915", + "extraNonce": "00", + "extraNonce2Size": 7, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff580350960d1b4d696e6564206279204c75786f72205465636868005702fe933c3bfabe6d6d8ae1255df7029c6f68cd8ea446d227ef3dc39ef1ac7a55085969307c7bfaa736100000000000000000005813", + "coinbase2": "ffffffff05220200000000000017a914bf73ad4cf3a107812bad3deb310611bee49a3c79875173cc120000000017a914056adde53ebc396a1b3b678bb0d3a5c116ff430c870000000000000000266a24aa21a9ed0844cc9ee7cc307219c6002e18930cdde972a552bc8d1eabad7976208a0e1b8000000000000000002f6a2d434f524501a37cf4faa0758b26dca666f3e36d42fa15cc0106f459cc4ca322d298304ff163b2a360d756c5db8400000000000000002b6a2952534b424c4f434b3a1357f52232e7585b34c3e0cd5d433a50aac01937aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "71b686070fe7bdfaf6a8812ab487fd91d15e21c90fcf5d6bafca745e9b474a19", + "802911f2f657223b6cf06eec9b523b005b6bc2bec56b8fb35d777ef275398ba7", + "7a2626e535df0e88542c11e2c939de4d49e29a654da437bdbbf9d4b89ad8cea1", + "c49488289a1335c2bcf24b8661ce01ace0e6c35e0105a116fb55ccf981e46c59", + "8f8908b51423dc8ea13a97524f4159d629754f89c8ddfab140b8c695442215f3", + "48cc791e2e2fd0d16c9bf774967162a333ee2eeeedbace83f9ac5368bbc1dc97", + "8e25b97a71a9121cbec116aae71825cc1f8679687c44ef7dd94d2841244e16d9", + "e5328ff6cc72161b5c5e97152a4544f3900ac0a63abde9564919ffa30e28505d", + "3f12570807f882825eaa97b598308bd27e7f36d8ba79e039d81dcfe6cbf1fdd6", + "6afcf4b7a3fd41e8b6290459722bf83f0aff5a36b2b54e9f4210f9cb8bef51b9", + "1a9bdc30e04b9705106827938046b8485384b584254c2f93afdc50ed42a189b2" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541735146, + "pool": 4, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff580350960d1b4d696e6564206279204c75786f72205465636868005702fe933c3bfabe6d6d8ae1255df7029c6f68cd8ea446d227ef3dc39ef1ac7a55085969307c7bfaa7361000000000000000000058130000000000000000ffffffff05220200000000000017a914bf73ad4cf3a107812bad3deb310611bee49a3c79875173cc120000000017a914056adde53ebc396a1b3b678bb0d3a5c116ff430c870000000000000000266a24aa21a9ed0844cc9ee7cc307219c6002e18930cdde972a552bc8d1eabad7976208a0e1b8000000000000000002f6a2d434f524501a37cf4faa0758b26dca666f3e36d42fa15cc0106f459cc4ca322d298304ff163b2a360d756c5db8400000000000000002b6a2952534b424c4f434b3a1357f52232e7585b34c3e0cd5d433a50aac01937aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315389299, + "scriptsig": "0350960d1b4d696e6564206279204c75786f72205465636868005702fe933c3bfabe6d6d8ae1255df7029c6f68cd8ea446d227ef3dc39ef1ac7a55085969307c7bfaa7361000000000000000000058130000000000000000" + }, + "36": { + "jobId": "B75cp1aWq", + "extraNonce": "00", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff640350960d2cfabe6d6da35ece7974edab96759db21f0953f4eaa2200241771a0976d18c5c6e27c5340a10000000f09f909f092f4632506f6f6c2f6b000000000000000000000000000000000000000000000000000000000000000000000005", + "coinbase2": "0722020000000000001976a914c6740a12d0a7d556f89782bf5faf0e12cf25a63988ac3a4acb12000000001976a91469f2a01f4ff9e6ac24df9062e9828753474b348088ac0000000000000000266a24aa21a9ed534cb114f7fd20c22a47d45e6bb6c5460bf3a9cce404265e0961cfd6c881190700000000000000002f6a2d434f5245017c706ca44a28fdd25761250961276bd61d5aa87be7ec323813c943336c579e238228a8ebd096a7e50000000000000000126a10455853415401051b0f0e0e0b1f1200130000000000000000266a2448617468241c617362765014f47be645944e22747340fba7c5a705548dbae9b4a84e62ff00000000000000002c6a4c2952534b424c4f434b3a81cc66e44e74bd510bfde3b83b286662e6a2e597aa8a53c435370b130070fc91dc608334", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "723847223fbee3db7918859bb384999905cba259070859ec77ebfa329b05065a", + "cd7913bf8e499b13ad83c84b2dcc5de216823ff59e45cc76af8f47570114476e", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "6c30f7579f6b6734d6ce96b86e33d318513bafeee287870a960c5a7081244a0c", + "aac144dd768fb69fb813f0eebdd02afb8b55e00d0a61101cf167ecda07a1bc2b", + "ed81f05edaf48584c328a7d4c1f2de3720c2eaba0fa7aa37e4f0792603c60b67", + "98ce77b30480722596a787892493dfdac342b5c717aa5d2a3a4ca1a4b461321c", + "d1372489dd7304c4c44214da63b071c987ee084440861140fb56d5ea03ba8e39", + "efeeb673f474c3d4724bf2503a0903ff73ff01afabc37ffa694f8c79022d7078" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55cb", + "cleanJobs": false, + "received": 1743541719279, + "pool": 36, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff640350960d2cfabe6d6da35ece7974edab96759db21f0953f4eaa2200241771a0976d18c5c6e27c5340a10000000f09f909f092f4632506f6f6c2f6b0000000000000000000000000000000000000000000000000000000000000000000000050000000000000000000722020000000000001976a914c6740a12d0a7d556f89782bf5faf0e12cf25a63988ac3a4acb12000000001976a91469f2a01f4ff9e6ac24df9062e9828753474b348088ac0000000000000000266a24aa21a9ed534cb114f7fd20c22a47d45e6bb6c5460bf3a9cce404265e0961cfd6c881190700000000000000002f6a2d434f5245017c706ca44a28fdd25761250961276bd61d5aa87be7ec323813c943336c579e238228a8ebd096a7e50000000000000000126a10455853415401051b0f0e0e0b1f1200130000000000000000266a2448617468241c617362765014f47be645944e22747340fba7c5a705548dbae9b4a84e62ff00000000000000002c6a4c2952534b424c4f434b3a81cc66e44e74bd510bfde3b83b286662e6a2e597aa8a53c435370b130070fc91dc608334", + "height": 890448, + "timestamp": 1743541707, + "reward": 315313244, + "scriptsig": "0350960d2cfabe6d6da35ece7974edab96759db21f0953f4eaa2200241771a0976d18c5c6e27c5340a10000000f09f909f092f4632506f6f6c2f6b0000000000000000000000000000000000000000000000000000000000000000000000050000000000" + }, + "43": { + "jobId": "131d", + "extraNonce": "00", + "extraNonce2Size": 6, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4c0350960d0f2f736c7573682f65000303fe9334c4fabe6d6dbb9c929fe497398b9a628aa161b72ed26b10e8ca5f29c485f42b468355043a15100000000000000000004e802e", + "coinbase2": "ffffffff038473cc120000000017a9141f0cbbec8bc4c945e4e16249b11eee911eded55f870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541734908, + "pool": 43, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4c0350960d0f2f736c7573682f65000303fe9334c4fabe6d6dbb9c929fe497398b9a628aa161b72ed26b10e8ca5f29c485f42b468355043a15100000000000000000004e802e00000000000000ffffffff038473cc120000000017a9141f0cbbec8bc4c945e4e16249b11eee911eded55f870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315388804, + "scriptsig": "0350960d0f2f736c7573682f65000303fe9334c4fabe6d6dbb9c929fe497398b9a628aa161b72ed26b10e8ca5f29c485f42b468355043a15100000000000000000004e802e00000000000000" + }, + "44": { + "jobId": "3750742", + "extraNonce": "00002c10", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff580350960d1b4d696e656420627920416e74506f6f6c3830369c001700fe93b694fabe6d6d433177d6fe558d8cee3035bb1b481156f8b411c21bddef7fb8aec740e4617c501000000000000000", + "coinbase2": "ffffffff06220200000000000017a91442402a28dd61f2718a4b27ae72a4791d5bbdade7876271cc120000000017a9145249bdf2c131d43995cff42e8feee293f79297a8870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f52450164db24a662e20bbdf72d1cc6e973dbb2d12897d54e3ecda72cb7961caa4b541b1e322bcfe0b5a0300000000000000000146a12455853415401000d130f0e0e0b041f12001300000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e7", + "cleanJobs": false, + "received": 1743541735750, + "pool": 44, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff580350960d1b4d696e656420627920416e74506f6f6c3830369c001700fe93b694fabe6d6d433177d6fe558d8cee3035bb1b481156f8b411c21bddef7fb8aec740e4617c50100000000000000000002c100000000000000000ffffffff06220200000000000017a91442402a28dd61f2718a4b27ae72a4791d5bbdade7876271cc120000000017a9145249bdf2c131d43995cff42e8feee293f79297a8870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f52450164db24a662e20bbdf72d1cc6e973dbb2d12897d54e3ecda72cb7961caa4b541b1e322bcfe0b5a0300000000000000000146a12455853415401000d130f0e0e0b041f12001300000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541735, + "reward": 315388804, + "scriptsig": "0350960d1b4d696e656420627920416e74506f6f6c3830369c001700fe93b694fabe6d6d433177d6fe558d8cee3035bb1b481156f8b411c21bddef7fb8aec740e4617c50100000000000000000002c100000000000000000" + }, + "49": { + "jobId": "67444bb00005e280", + "extraNonce": "1eecec72", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff350350960d0004d155ec67047a5741090c", + "coinbase2": "0a636b706f6f6c112f736f6c6f2e636b706f6f6c2e6f72672fffffffff03dd276b12000000001976a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac483a60000000000016001451ed61d2f6aa260cc72cdf743e4e436a82c010270000000000000000266a24aa21a9ed6c1f16b33ae5c1c628b53c6011f38866955c8b59448e5aee14f9967c8cb7ab1c00000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "723847223fbee3db7918859bb384999905cba259070859ec77ebfa329b05065a", + "cd7913bf8e499b13ad83c84b2dcc5de216823ff59e45cc76af8f47570114476e", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "6c30f7579f6b6734d6ce96b86e33d318513bafeee287870a960c5a7081244a0c", + "717d5dac15b3253aeec8e1079141760337bebe0101d0b4e4c43384416ca132ad", + "9eba7f7def1ad83bb3d7df151f880fa76c4b4ec3b531a4e0856a1e7437d3d060", + "0558088354e8bf625f19e28e6ec02231d61a31c26f215d2e1253baf0452064bb", + "6c25959fb0244719945408c5da48055f0d9ad30240ebc520c2d81b5df1b0f3d7", + "150163eb46086007fafbef5c2c71faf3f8b4690c338e2d74a966ff2650df63c3" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55d1", + "cleanJobs": false, + "received": 1743541713511, + "pool": 49, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff350350960d0004d155ec67047a5741090c1eecec7200000000000000000a636b706f6f6c112f736f6c6f2e636b706f6f6c2e6f72672fffffffff03dd276b12000000001976a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac483a60000000000016001451ed61d2f6aa260cc72cdf743e4e436a82c010270000000000000000266a24aa21a9ed6c1f16b33ae5c1c628b53c6011f38866955c8b59448e5aee14f9967c8cb7ab1c00000000", + "height": 890448, + "timestamp": 1743541713, + "reward": 315318821, + "scriptsig": "0350960d0004d155ec67047a5741090c1eecec7200000000000000000a636b706f6f6c112f736f6c6f2e636b706f6f6c2e6f72672f" + }, + "73": { + "jobId": "8711", + "extraNonce": "41fade6a", + "extraNonce2Size": 4, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff610350960d1e2f5669614254432f4d696e6564206279206d6f6e6f6e6175746963616c2f2cfabe6d6d0d586d106170cc5c7380e81f3e79c40726b2313bdb6263eedba507d9cdc53bca1000000000000000101187c609caa65f88", + "coinbase2": "ffffffff040388cb12000000001976a914fb37342f6275b13936799def06f2eb4c0f20151588ac00000000000000002b6a2952534b424c4f434b3a280a7381d0ccac30cd6dcfd0beae78f22cb58d6caa8a53c435370b120070fc920000000000000000146a124558534154011508000113021b1a1f1200130000000000000000266a24aa21a9ed10c31ffb387781fdab75b9b2e4994223622049c78f1f81d05a4a421ae178626000000000", + "merkleBranches": [ + "b4efa4f5b9d41f9fc63480038ba7586dac490d08fd738aaa691d49bcbee1dd54", + "97c694c3a659a43a7ed8f7e2ae4d06927a152b348f33e7a51c1b5dd242631e72", + "09350f4bc26ad644fadeec9bea500daadb35bf43f8f39d82a8adb708661a8f18", + "e5f3663e21ccc3f4565529a73b11811dd68abcc9ac701aa494b7b18782eefa23", + "bcdfb143b322e8677b8edb90417a1ec739a6d9eb8efc84e5ba6424e7f3adfc0f", + "b90794158874321306985105e45aa872a9646a5bf683a070d7ca6602450ad8e5", + "26e4fb36ec030a6510c9047120575dca2a8b8760674af4284f5c27e53f03f46b", + "e6a15e522b650f9cebfdd003c0d29f5ca571bf704d63b9aa627ce915acb229f7", + "392d3e593f80e9180a3bce84ebaa044d5406cad39897c78e58e66ca59e7499c8", + "ce8409542b60ed5ca9c567b89fa9a7c2e5e8e2bbf2b42b0ce8edb0836a7a2e82", + "0e8201e0367364ed40263747547d3a0c26926d2b94799b414c22a25c6fc04d43", + "33e95cd9da59d9e33e239b9b4e647a2da98f36ef6a137f40ba428782bb1c4021" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55d7", + "cleanJobs": false, + "received": 1743541719896, + "pool": 73, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff610350960d1e2f5669614254432f4d696e6564206279206d6f6e6f6e6175746963616c2f2cfabe6d6d0d586d106170cc5c7380e81f3e79c40726b2313bdb6263eedba507d9cdc53bca1000000000000000101187c609caa65f8841fade6a00000000ffffffff040388cb12000000001976a914fb37342f6275b13936799def06f2eb4c0f20151588ac00000000000000002b6a2952534b424c4f434b3a280a7381d0ccac30cd6dcfd0beae78f22cb58d6caa8a53c435370b120070fc920000000000000000146a124558534154011508000113021b1a1f1200130000000000000000266a24aa21a9ed10c31ffb387781fdab75b9b2e4994223622049c78f1f81d05a4a421ae178626000000000", + "height": 890448, + "timestamp": 1743541719, + "reward": 315328515, + "scriptsig": "0350960d1e2f5669614254432f4d696e6564206279206d6f6e6f6e6175746963616c2f2cfabe6d6d0d586d106170cc5c7380e81f3e79c40726b2313bdb6263eedba507d9cdc53bca1000000000000000101187c609caa65f8841fade6a00000000" + }, + "94": { + "jobId": "1186343", + "extraNonce": "0000776f", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff570350960d1a2f706f6f6c696e2e636f6d2f66702f3936369c01b704fe93ba97fabe6d6d89255a8f9c17d3a3dc753a3e2b2eb55a116c1d7280bcf4b4f76a0af1f2e12ce31000000000000000", + "coinbase2": "ffffffff038473cc120000000017a9141366dca425687186b9b54e27e4ed48163b5beb80870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e7", + "cleanJobs": false, + "received": 1743541735127, + "pool": 94, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff570350960d1a2f706f6f6c696e2e636f6d2f66702f3936369c01b704fe93ba97fabe6d6d89255a8f9c17d3a3dc753a3e2b2eb55a116c1d7280bcf4b4f76a0af1f2e12ce310000000000000000000776f0000000000000000ffffffff038473cc120000000017a9141366dca425687186b9b54e27e4ed48163b5beb80870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541735, + "reward": 315388804, + "scriptsig": "0350960d1a2f706f6f6c696e2e636f6d2f66702f3936369c01b704fe93ba97fabe6d6d89255a8f9c17d3a3dc753a3e2b2eb55a116c1d7280bcf4b4f76a0af1f2e12ce310000000000000000000776f0000000000000000" + }, + "102": { + "jobId": "33", + "extraNonce": "04264b21", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff570350960d04d655ec67537069646572506f6f6c2f7573383838382ffabe6d6d013135a66fcdf604440c12e28ffb6f448d1d7b633d16737217081646c41912270100000000000000b286b3fc", + "coinbase2": "ffffffff04b55dcb12000000001976a914717a4c9074577a05af94271c32b249d298a22d9888ac0000000000000000266a24aa21a9eded8df9e053ff66fe09268a9df4e369d60a48437a012a1b157e143bb967379cab00000000000000002f6a2d434f52450164db24a662e20bbdf72d1cc6e973dbb2d12897d596a6689031f48a857d344e1a42fdb272bb15d6210000000000000000126a10455853415401120f080304111f12001300000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "723847223fbee3db7918859bb384999905cba259070859ec77ebfa329b05065a", + "cd7913bf8e499b13ad83c84b2dcc5de216823ff59e45cc76af8f47570114476e", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "6c30f7579f6b6734d6ce96b86e33d318513bafeee287870a960c5a7081244a0c", + "717d5dac15b3253aeec8e1079141760337bebe0101d0b4e4c43384416ca132ad", + "9eba7f7def1ad83bb3d7df151f880fa76c4b4ec3b531a4e0856a1e7437d3d060", + "5fb573157a34ed041274de444f9dd86e210189cf325bdbde44a92edded8f1b90", + "1e77c2e67bf442dd78a2e078fe9b7bb1bece599bf7e77e2086e5d0659afedf9e", + "e6c75e3d1859a1d8d6a10ac5fe990969202fa37d0715c7ca50c2159e3b3a463e" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55ce", + "cleanJobs": false, + "received": 1743541719031, + "pool": 102, + "coinbase": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff570350960d04d655ec67537069646572506f6f6c2f7573383838382ffabe6d6d013135a66fcdf604440c12e28ffb6f448d1d7b633d16737217081646c41912270100000000000000b286b3fc04264b210000000000000000ffffffff04b55dcb12000000001976a914717a4c9074577a05af94271c32b249d298a22d9888ac0000000000000000266a24aa21a9eded8df9e053ff66fe09268a9df4e369d60a48437a012a1b157e143bb967379cab00000000000000002f6a2d434f52450164db24a662e20bbdf72d1cc6e973dbb2d12897d596a6689031f48a857d344e1a42fdb272bb15d6210000000000000000126a10455853415401120f080304111f12001300000000", + "height": 890448, + "timestamp": 1743541710, + "reward": 315317685, + "scriptsig": "0350960d04d655ec67537069646572506f6f6c2f7573383838382ffabe6d6d013135a66fcdf604440c12e28ffb6f448d1d7b633d16737217081646c41912270100000000000000b286b3fc04264b210000000000000000" + }, + "105": { + "jobId": "4287955", + "extraNonce": "0000e9a5", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff500350960d1362696e616e63652f3832319c001606fe93bd58fabe6d6d2cbf0bfed0a1d41f829160053963a92e7a2c44942646e992955ce00fd53f62991000000000000000", + "coinbase2": "ffffffff05220200000000000017a914265ae1340f5d442d099ffc27b443ffdba4bc0346876271cc120000000017a9149e3e8a50d9acb3ac7649625432e2207c25e0faf8870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f5245012d058b58dcf4b0db11168c62d3109f6e02710b02221456a6c24c9891680d295eb6bc57c6fb68e8f100000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e7", + "cleanJobs": false, + "received": 1743541735177, + "pool": 105, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff500350960d1362696e616e63652f3832319c001606fe93bd58fabe6d6d2cbf0bfed0a1d41f829160053963a92e7a2c44942646e992955ce00fd53f629910000000000000000000e9a50000000000000000ffffffff05220200000000000017a914265ae1340f5d442d099ffc27b443ffdba4bc0346876271cc120000000017a9149e3e8a50d9acb3ac7649625432e2207c25e0faf8870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f5245012d058b58dcf4b0db11168c62d3109f6e02710b02221456a6c24c9891680d295eb6bc57c6fb68e8f100000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541735, + "reward": 315388804, + "scriptsig": "0350960d1362696e616e63652f3832319c001606fe93bd58fabe6d6d2cbf0bfed0a1d41f829160053963a92e7a2c44942646e992955ce00fd53f629910000000000000000000e9a50000000000000000" + }, + "111": { + "jobId": "7", + "extraNonce": "31a044e7", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff310350960d04e155ec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f", + "coinbase2": "ffffffff0522020000000000002251200f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a37f28cc12000000002200207086320071974eef5e72eaa01dd9096e10c0383483855ea6b344259c244f73c20000000000000000266a24aa21a9eda9d49baa733153f684d3e885e731818571f650adfef974d99fe1e11b69af0bc600000000000000002f6a2d434f524501359b5fbc5b294e953dbec5cbb769e2186bb30e56e6d18fda214e5b9f350ffc7b6cf3058b9026e76500000000000000002b6a2952534b424c4f434b3aaadde8fc40282f7f5a2c02a16700e42ad9f6bc5aaa8a53c435370b120070fc9200000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "470ea829e1e3085254952441894afcb97582e5675fbb35c226edf79a226890d5", + "09a4875a0b37d78e9643154473bb974b2eba5c800e3cd58c99adfb196f383358", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "2af55ecd2164a88f5df8c3de727633690dabb2de4b61827ac86adcd927cd3e24", + "8601c35a421ac857f22a1ea05b53f003771aaaaf1e65bea339bdc80a9986e671", + "230d09f9ee198e35e1b1be1de2f87acf1a1e73690d680b85ba96bd21790e8766", + "8f4cee6b0fc9bca8608134c4a93116cf4de4ab753863219201cd20ff06b18d1e", + "4de04f813d0c4fbd5b4fb79e490f76ba4da31391d25f0447f1e9eccc935c9fe4", + "c3de6f84d58eeb5277be653e241331dddd19d056ce00ff7d3d9ecdefadfa9067" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e1", + "cleanJobs": false, + "received": 1743541729815, + "pool": 111, + "coinbase": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff310350960d04e155ec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f31a044e70000000000000000ffffffff0522020000000000002251200f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a37f28cc12000000002200207086320071974eef5e72eaa01dd9096e10c0383483855ea6b344259c244f73c20000000000000000266a24aa21a9eda9d49baa733153f684d3e885e731818571f650adfef974d99fe1e11b69af0bc600000000000000002f6a2d434f524501359b5fbc5b294e953dbec5cbb769e2186bb30e56e6d18fda214e5b9f350ffc7b6cf3058b9026e76500000000000000002b6a2952534b424c4f434b3aaadde8fc40282f7f5a2c02a16700e42ad9f6bc5aaa8a53c435370b120070fc9200000000", + "height": 890448, + "timestamp": 1743541729, + "reward": 315370145, + "scriptsig": "0350960d04e155ec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f31a044e70000000000000000" + }, + "112": { + "jobId": "12", + "extraNonce": "050300c0", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff290350960d04db55ec672f53424943727970746f2e636f6d20506f6f6c2f", + "coinbase2": "ffffffff02ffc8cb1200000000160014cab30e9b367d646f326ace7fcaf3c9ce4afc37530000000000000000266a24aa21a9edd0dd8d14ec5a97c6fd3de15f02c86d889e932e0262807795fa7c127e5a8fa0be00000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "b41e3afa7672a36d6a462612f3bdc9ee8000388471b29ba1d187135b435f3efa", + "4236249ae2883af9d68c5570f893aa808a2a15d61ea654444cebfb9b3ba2c598", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "4b5b7ee5fe7813f0e1ae5d1265215062d449d5c50b5edede2816274216e88a8d", + "6624f27663e2330c010fe68f5f397ebef70a1f6188ccb104e9eff97a9e48bb02", + "b94d8d0e4cc4b8b62bd4f4d70fc4497f85b05280b43e0b5dc399cc8778a1074a", + "a97d374c628f07cc5283b577558c3c0e3f2f3c8dac8df6ccdea7cf26b6776fa5", + "190f5f1bc8535d731fd095dd8e97ec3c33facdbce8614ca654017f3baf84a3fe", + "102b7769fe8b53ca163151eb461c767d331c465d95056f556e38a07298e6046a" + ], + "version": "20000004", + "bits": "1702796c", + "time": "67ec55db", + "cleanJobs": false, + "received": 1743541728640, + "pool": 112, + "coinbase": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff290350960d04db55ec672f53424943727970746f2e636f6d20506f6f6c2f050300c00000000000000000ffffffff02ffc8cb1200000000160014cab30e9b367d646f326ace7fcaf3c9ce4afc37530000000000000000266a24aa21a9edd0dd8d14ec5a97c6fd3de15f02c86d889e932e0262807795fa7c127e5a8fa0be00000000", + "height": 890448, + "timestamp": 1743541723, + "reward": 315345151, + "scriptsig": "0350960d04db55ec672f53424943727970746f2e636f6d20506f6f6c2f050300c00000000000000000" + }, + "141": { + "jobId": "1832214", + "extraNonce": "0000ac40", + "extraNonce2Size": 4, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff510350960d184d696e656420627920536563506f6f6c58007904fe939454fabe6d6d2e07daf9ae89d5bfcdb87dd64455e7357ca9fb3708a2cf08ca8f30a38142a68e1000000000000000", + "coinbase2": "ffffffff05220200000000000017a9148ee90177614ecde53314fd67c46162f315852a07875173cc120000000017a9146582f2551e2a47e1ae8b03fb666401ed7c4552ef870000000000000000266a24aa21a9ed0844cc9ee7cc307219c6002e18930cdde972a552bc8d1eabad7976208a0e1b8000000000000000002f6a2d434f524501a21cbd3caa4fe89bccd1d716c92ce4533e4d4733942c01262fa046fdb8ba0dfce3753405c74aed0e00000000000000002b6a2952534b424c4f434b3a0374e206447243d32da96205942c62a298fddbd7aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "71b686070fe7bdfaf6a8812ab487fd91d15e21c90fcf5d6bafca745e9b474a19", + "802911f2f657223b6cf06eec9b523b005b6bc2bec56b8fb35d777ef275398ba7", + "7a2626e535df0e88542c11e2c939de4d49e29a654da437bdbbf9d4b89ad8cea1", + "c49488289a1335c2bcf24b8661ce01ace0e6c35e0105a116fb55ccf981e46c59", + "8f8908b51423dc8ea13a97524f4159d629754f89c8ddfab140b8c695442215f3", + "48cc791e2e2fd0d16c9bf774967162a333ee2eeeedbace83f9ac5368bbc1dc97", + "8e25b97a71a9121cbec116aae71825cc1f8679687c44ef7dd94d2841244e16d9", + "e5328ff6cc72161b5c5e97152a4544f3900ac0a63abde9564919ffa30e28505d", + "3f12570807f882825eaa97b598308bd27e7f36d8ba79e039d81dcfe6cbf1fdd6", + "6afcf4b7a3fd41e8b6290459722bf83f0aff5a36b2b54e9f4210f9cb8bef51b9", + "1a9bdc30e04b9705106827938046b8485384b584254c2f93afdc50ed42a189b2" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541735015, + "pool": 141, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff510350960d184d696e656420627920536563506f6f6c58007904fe939454fabe6d6d2e07daf9ae89d5bfcdb87dd64455e7357ca9fb3708a2cf08ca8f30a38142a68e10000000000000000000ac4000000000ffffffff05220200000000000017a9148ee90177614ecde53314fd67c46162f315852a07875173cc120000000017a9146582f2551e2a47e1ae8b03fb666401ed7c4552ef870000000000000000266a24aa21a9ed0844cc9ee7cc307219c6002e18930cdde972a552bc8d1eabad7976208a0e1b8000000000000000002f6a2d434f524501a21cbd3caa4fe89bccd1d716c92ce4533e4d4733942c01262fa046fdb8ba0dfce3753405c74aed0e00000000000000002b6a2952534b424c4f434b3a0374e206447243d32da96205942c62a298fddbd7aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315389299, + "scriptsig": "0350960d184d696e656420627920536563506f6f6c58007904fe939454fabe6d6d2e07daf9ae89d5bfcdb87dd64455e7357ca9fb3708a2cf08ca8f30a38142a68e10000000000000000000ac4000000000" + }, + "142": { + "jobId": "67ec55c62cc0f502", + "extraNonce": "b10cf017", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1e0350960d163c204f4345414e2e58595a203e0f0f30304f43423400024087ffffffff140000000000000000166a144f4342313fd6fdce13000000290000002c029c30562b26000000000016001472fb714ecb210311655a3da22729ff8626bdea590000000000000000106a0e1a27", + "coinbase2": "f65ccf020000000017a9142b636a84e5b12177cd49c687b5a1ebd07fb83fbf8783c76e0200000000220020ac11a29a5721e9602e396acd7e5216c9196b7dbb14cf573921c46e6fea38ac09d6632e020000000017a91449b58fb476449dab3a2e175fe2c1f0d753a33e92878b8ad9010000000017a914cc888b5846746a5a7f449eea90082feaa09b9bf387833cbe01000000001600146983f7fe983e96dc5b43677fb14822a075596364b136ae0100000000220020dc406857cac1b9d3f53c6986acb78e7774c604281076d10d3fc8e873e18df6d0663e6e0100000000160014d192719c5be868068f023b9383b3cbecb6afbb1639205300000000001976a91412c00ad1271bd4c91e4f8ff11e738c285181399d88ac49844e000000000017a9141ef31e9bc9ccb532b8c01950727990cca190656a877aa148000000000016001404afd9f6e36ba7cadb62133c458a5318f41a33de731b42000000000016001415c0899a887e36f6620619b621855d40b41cbf838b5a3b0000000000220020e65791d3c340710f7aebf117d1f38e1c5383c1df0345a2082e8ebff945e9994646d83a00000000001600140345e587cb42c201c30e505ea9f6f5fb935876ac5df731000000000017a914278dadd16d1314122e4dde67ff9152a61c3a9f4787254f3000000000001600146d095474bec41ccc8ad40f94c74dd25b79b83579e85b7e020000000017a914413b5901fe4e591c95405fd446b5b002da575bf0870000000000000000266a24aa21a9ed2a3f54b40a557c2884545b770a050154a46b5dc132eec3604f01714f413b32a300000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "81890975bdb859a5c5527a40a7354d0d60b133fa4eaef69f1738ece447714e2c", + "a7fe518c62a02f13cc9e18d4622af75b5fcd4ebad10f129ea0e9eee165cae6b5", + "626773fea3f749684748ec54e167fceb58e3520cbdcf7926717ab7d3b481a2f1", + "de27b0f244aeed874d5b8e03730e7c386905016563058919a476a7efa0414921", + "52903b24bb6d22b7df633c6a426d014cab6c592069043d51ec4b9a910bb98c3e", + "0834ec8e29e9cea5ff2b05cea6f5ee6ce71088569a44fac069c94f777366e806", + "f792f333b3687ca868e80449265720091bcc29a8bd986b5e5d35a297986f9ea2", + "6ea6eca3a29cf603146043e1e5d6bb20e0460dbc72157bedfe585bf38c7e6c56", + "36fe42124e6a5e5250af65cc12e6d7b6370793e559b5fc76b7a46760a83396d1", + "f1c2b5b11af512e743b9c9f219192dc82b8826760b4678683ea16303e2b65508" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55c5", + "cleanJobs": false, + "received": 1743541714701, + "pool": 142, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1e0350960d163c204f4345414e2e58595a203e0f0f30304f43423400024087ffffffff140000000000000000166a144f4342313fd6fdce13000000290000002c029c30562b26000000000016001472fb714ecb210311655a3da22729ff8626bdea590000000000000000106a0e1a27b10cf0170000000000000000f65ccf020000000017a9142b636a84e5b12177cd49c687b5a1ebd07fb83fbf8783c76e0200000000220020ac11a29a5721e9602e396acd7e5216c9196b7dbb14cf573921c46e6fea38ac09d6632e020000000017a91449b58fb476449dab3a2e175fe2c1f0d753a33e92878b8ad9010000000017a914cc888b5846746a5a7f449eea90082feaa09b9bf387833cbe01000000001600146983f7fe983e96dc5b43677fb14822a075596364b136ae0100000000220020dc406857cac1b9d3f53c6986acb78e7774c604281076d10d3fc8e873e18df6d0663e6e0100000000160014d192719c5be868068f023b9383b3cbecb6afbb1639205300000000001976a91412c00ad1271bd4c91e4f8ff11e738c285181399d88ac49844e000000000017a9141ef31e9bc9ccb532b8c01950727990cca190656a877aa148000000000016001404afd9f6e36ba7cadb62133c458a5318f41a33de731b42000000000016001415c0899a887e36f6620619b621855d40b41cbf838b5a3b0000000000220020e65791d3c340710f7aebf117d1f38e1c5383c1df0345a2082e8ebff945e9994646d83a00000000001600140345e587cb42c201c30e505ea9f6f5fb935876ac5df731000000000017a914278dadd16d1314122e4dde67ff9152a61c3a9f4787254f3000000000001600146d095474bec41ccc8ad40f94c74dd25b79b83579e85b7e020000000017a914413b5901fe4e591c95405fd446b5b002da575bf0870000000000000000266a24aa21a9ed2a3f54b40a557c2884545b770a050154a46b5dc132eec3604f01714f413b32a300000000", + "height": 890448, + "timestamp": 1743541701, + "reward": 315238004, + "scriptsig": "0350960d163c204f4345414e2e58595a203e0f0f30304f43423400024087" + }, + "161": { + "jobId": "1611979", + "extraNonce": "000002dc", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4f0350960d122f62737175617265642fe5006405fe934bb9fabe6d6d2db144ad7b2eed39c89a2cde9f88f06d9082613911584463126673a700303e881000000000000000", + "coinbase2": "ffffffff05220200000000000017a9141bd79ae4d7811daea94bd25db93761b10fecccd7876271cc120000000017a9146547a37cae8db12fd4c8f191f69d4a52cfa886fa870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f524501307f36ff0aff7000ebd4eea1e8e9bbbfa0e1134cb203d7822f404faca98985eb083e8fd9e16ad64700000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541734876, + "pool": 161, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4f0350960d122f62737175617265642fe5006405fe934bb9fabe6d6d2db144ad7b2eed39c89a2cde9f88f06d9082613911584463126673a700303e881000000000000000000002dc0000000000000000ffffffff05220200000000000017a9141bd79ae4d7811daea94bd25db93761b10fecccd7876271cc120000000017a9146547a37cae8db12fd4c8f191f69d4a52cfa886fa870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f524501307f36ff0aff7000ebd4eea1e8e9bbbfa0e1134cb203d7822f404faca98985eb083e8fd9e16ad64700000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315388804, + "scriptsig": "0350960d122f62737175617265642fe5006405fe934bb9fabe6d6d2db144ad7b2eed39c89a2cde9f88f06d9082613911584463126673a700303e881000000000000000000002dc0000000000000000" + } + }, + "mempoolInfo": { + "loaded": true, + "size": 4173, + "bytes": 3427792, + "usage": 16974288, + "total_fee": 0.05445324, + "maxmempool": 300000000, + "mempoolminfee": 0.00001, + "minrelaytxfee": 0.00001, + "incrementalrelayfee": 0.00001, + "unbroadcastcount": 0, + "fullrbf": true + }, + "vBytesPerSecond": 1415, + "mempool-blocks": [ + { + "blockSize": 1779311, + "blockVSize": 997968.5, + "nTx": 2132, + "totalFees": 2902870, + "medianFee": 2.0479263387949875, + "feeRange": [ + 1.0721153846153846, + 1.9980563654033041, + 2.2195704057279237, + 3.009493670886076, + 3.4955223880597015, + 6.0246913580246915, + 218.1818181818182 + ] + }, + { + "blockSize": 1959636, + "blockVSize": 997903.5, + "nTx": 497, + "totalFees": 1093076, + "medianFee": 1.102049424602265, + "feeRange": [ + 1.0401794819498267, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0761096766260911, + 1.1021605957228275 + ] + }, + { + "blockSize": 1477260, + "blockVSize": 997997.25, + "nTx": 720, + "totalFees": 1016195, + "medianFee": 1.007409072434199, + "feeRange": [ + 1, + 1.0019120458891013, + 1.0040863981319323, + 1.0081019768823594, + 1.018450184501845, + 1.0203327171903882, + 1.0485018498190837 + ] + }, + { + "blockSize": 1021308, + "blockVSize": 431071.5, + "nTx": 823, + "totalFees": 432342, + "medianFee": 0, + "feeRange": [ + 1, + 1, + 1, + 1.0028011204481793, + 1.0042075736325387, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "transactions": [ + { + "txid": "bccfbff8d129df7dcfb65b7587186a807ab2c7e4b8494188ac27733705d9e482", + "fee": 564, + "vsize": 140.25, + "value": 160404, + "rate": 4.021390374331551, + "time": 1743541739 + }, + { + "txid": "94c81b3fc40369520fd7607ed54f18416057aab46bc6da328c0588e5967a2ebd", + "fee": 1044, + "vsize": 315.75, + "value": 64814, + "rate": 3.3064133016627077, + "time": 1743541739 + }, + { + "txid": "a16276de4ce8b2eba0013428af73255060ad04e685bee98731504bf973d49bf7", + "fee": 426, + "vsize": 141.25, + "value": 349522, + "rate": 3.015929203539823, + "time": 1743541739 + }, + { + "txid": "bb25deab784cb213dc4cba234f5a41a6c8950c432a05b5858053b39ceda6d1a3", + "fee": 356, + "vsize": 152.25, + "value": 1562308, + "rate": 2.3382594417077174, + "time": 1743541738 + }, + { + "txid": "17a9e5194345d8da287d49c0bf595474e08fc81434646925d6640964929aa7f9", + "fee": 11400, + "vsize": 189.5, + "value": 3352669, + "rate": 60.15831134564644, + "time": 1743541738 + }, + { + "txid": "ced722a2f9007182b8268240af19fee486e8af07da83efccea6e2374aa484d07", + "fee": 534, + "vsize": 177, + "value": 302121849, + "rate": 3.016949152542373, + "time": 1743541736 + } + ], + "loadingIndicators": {}, + "fees": { + "fastestFee": 3, + "halfHourFee": 3, + "hourFee": 3, + "economyFee": 2, + "minimumFee": 1 + }, + "rbfSummary": [ + { + "txid": "51f468b28c4ee790bdfa3152a3add656f29e908828b55fb295cf55dc6a33a855", + "mined": false, + "fullRbf": false, + "oldFee": 19437, + "oldVsize": 166.5, + "newFee": 19770, + "newVsize": 166.5 + }, + { + "txid": "8fb448ef9a26ca3cee8341e3627adf57ae9374c37b83082ad5d90491955cc38c", + "mined": false, + "fullRbf": false, + "oldFee": 19270, + "oldVsize": 166.25, + "newFee": 19603, + "newVsize": 166.5 + }, + { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "mined": false, + "fullRbf": false, + "oldFee": 606, + "oldVsize": 233, + "newFee": 960, + "newVsize": 224 + }, + { + "txid": "a2b7c8a1e8d0f17108c5089198ccbf6c50341c99abb91959a25a41f9bb44fa60", + "mined": false, + "fullRbf": false, + "oldFee": 504, + "oldVsize": 225, + "newFee": 958, + "newVsize": 226 + }, + { + "txid": "798fb7eb84e69c58a596478436f48d3a620a825624ff88d5f15a0b1e17fa8fdf", + "mined": false, + "fullRbf": false, + "oldFee": 1680, + "oldVsize": 381.25, + "newFee": 2240, + "newVsize": 381.5 + }, + { + "txid": "86dd928e524e12fdfbb6d2ba51e1a0e4d8557d05552184bb20c0427c3e0d19f8", + "mined": false, + "fullRbf": false, + "oldFee": 1476, + "oldVsize": 328.5, + "newFee": 1968, + "newVsize": 328.5 + } + ], + "backend": "esplora", + "blocks": [ + { + "id": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4", + "height": 890440, + "version": 559235072, + "timestamp": 1743535677, + "bits": 386038124, + "nonce": 2920325684, + "difficulty": 113757508810854, + "merkle_root": "c793d5fdbfb1ebe99e14a13a6d65370057d311774d33c71da166663b18722474", + "tx_count": 3823, + "size": 1578209, + "weight": 3993461, + "previousblockhash": "000000000000000000020fb2e24425793e17e60e188205dc1694d221790348b2", + "mediantime": 1743532406, + "stale": false, + "extras": { + "reward": 319838750, + "coinbaseRaw": "0348960d082f5669614254432f2cfabe6d6d294719da11c017243828bf32c405341db7f19387fee92c25413c45e114907f9810000000000000001058bf9601429f9fa7a6c160d10d00000000000000", + "orphans": [], + "medianFee": 4, + "feeRange": [ + 3, + 3, + 3.0191082802547773, + 3.980952380952381, + 5, + 10, + 427.748502994012 + ], + "totalFees": 7338750, + "avgFee": 1920, + "avgFeeRate": 7, + "utxoSetChange": 4093, + "avgTxSize": 412.71000000000004, + "totalInputs": 7430, + "totalOutputs": 11523, + "totalOutputAmt": 547553568373, + "segwitTotalTxs": 3432, + "segwitTotalSize": 1467920, + "segwitTotalWeight": 3552413, + "feePercentiles": null, + "virtualSize": 998365.25, + "coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4", + "coinbaseAddresses": [ + "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4" + ], + "coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG", + "coinbaseSignatureAscii": "\u0003H–\r\b/ViaBTC/,ú¾mm)G\u0019Ú\u0011À\u0017$8(¿2Ä\u00054\u001d·ñ“‡þé,%Aìg/Foundry USA Pool #dropgold/\u0001S\nEaç\u0002\u0000\u0000\u0000\u0000\u0000", + "header": "00a03220b46014bdbeb557d1d87065e0ecb1b0ab33654bb7f579000000000000000000003ed60f06cec16df4399b5dafa7077036c2eb58cc6a16e6cdca559b9e2f7e4525bb3eec676c790217b7b3c9cb", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 111, + "name": "Foundry USA", + "slug": "foundryusa", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2792968, + "expectedWeight": 3991959, + "similarity": 0.9951416839808291 + } + }, + { + "id": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce", + "height": 890442, + "version": 557981696, + "timestamp": 1743536834, + "bits": 386038124, + "nonce": 470697326, + "difficulty": 113757508810854, + "merkle_root": "5e92e681c1db2797a5b3e5016729059f8b60a256cafb51d835dac2b3964c0db4", + "tx_count": 3566, + "size": 1628328, + "weight": 3993552, + "previousblockhash": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c", + "mediantime": 1743532867, + "stale": false, + "extras": { + "reward": 318057766, + "coinbaseRaw": "034a960d194d696e656420627920416e74506f6f6c204d000201e15e2989fabe6d6dd599e9dfa40be51f1517c8f512c5c3d51c7656182f1df335d34b98ee02c527db080000000000000000004f92b702000000000000", + "orphans": [], + "medianFee": 3.00860164711668, + "feeRange": [ + 1.5174418604651163, + 2.0140845070422535, + 2.492354740061162, + 3, + 4.020942408376963, + 7, + 200 + ], + "totalFees": 5557766, + "avgFee": 1558, + "avgFeeRate": 5, + "utxoSetChange": 1971, + "avgTxSize": 456.48, + "totalInputs": 7938, + "totalOutputs": 9909, + "totalOutputAmt": 900044492230, + "segwitTotalTxs": 3214, + "segwitTotalSize": 1526463, + "segwitTotalWeight": 3586200, + "feePercentiles": null, + "virtualSize": 998388, + "coinbaseAddress": "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z", + "coinbaseAddresses": [ + "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z", + "39C7fxSzEACPjM78Z7xdPxhf7mKxJwvfMJ" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 42402a28dd61f2718a4b27ae72a4791d5bbdade7 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003J–\r\u0019Mined by AntPool M\u0000\u0002\u0001á^)‰ú¾mmՙéߤ\u000bå\u001f\u0015\u0017Èõ\u0012ÅÃÕ\u001cvV\u0018/\u001dó5ÓK˜î\u0002Å'Û\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000O’·\u0002\u0000\u0000\u0000\u0000\u0000\u0000", + "header": "002042219cd10cfc6daf06d3faad645fe0914cea859745275aef00000000000000000000b40d4c96b3c2da35d851fbca56a2608b9f05296701e5b3a59727dbc181e6925ec242ec676c7902176e450e1c", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 44, + "name": "AntPool", + "slug": "antpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 5764747, + "expectedWeight": 3991786, + "similarity": 0.9029319155137951 + } + }, + { + "id": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6", + "height": 890443, + "version": 706666496, + "timestamp": 1743537197, + "bits": 386038124, + "nonce": 321696065, + "difficulty": 113757508810854, + "merkle_root": "3d7574f7eca741fa94b4690868a242e5b286f8a0417ad0275d4ab05893e96350", + "tx_count": 2155, + "size": 1700002, + "weight": 3993715, + "previousblockhash": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce", + "mediantime": 1743533789, + "stale": false, + "extras": { + "reward": 315112344, + "coinbaseRaw": "034b960d21202020204d696e656420627920536563706f6f6c2020202070000b05e388958c01fabe6d6db7ae4bfa7b1294e16e800b4563f1f5ddeb5c0740319eba45600f3f05d2d7272910000000000000000000c2cb7e020000", + "orphans": [], + "medianFee": 1.4360674424569184, + "feeRange": [ + 1, + 1.0135135135135136, + 1.09717868338558, + 2.142857142857143, + 3.009584664536741, + 4.831858407079646, + 196.07843137254903 + ], + "totalFees": 2612344, + "avgFee": 1212, + "avgFeeRate": 2, + "utxoSetChange": -2880, + "avgTxSize": 788.64, + "totalInputs": 9773, + "totalOutputs": 6893, + "totalOutputAmt": 264603969671, + "segwitTotalTxs": 1933, + "segwitTotalSize": 1556223, + "segwitTotalWeight": 3418707, + "feePercentiles": null, + "virtualSize": 998428.75, + "coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "coinbaseAddresses": [ + "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003K–\r! Mined by Secpool p\u0000\u000b\u0005㈕Œ\u0001ú¾mm·®Kú{\u0012”án€\u000bEcñõÝë\\\u0007@1žºE`\u000f?\u0005Ò×')\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÂË~\u0002\u0000\u0000", + "header": "00e01e2acedf6db0523987887ed8b989d4f58b3d6b878a974548010000000000000000005063e99358b04a5d27d07a41a0f886b2e542a2680869b494fa41a7ecf774753d2d44ec676c79021741b12c13", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 141, + "name": "SECPOOL", + "slug": "secpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2623934, + "expectedWeight": 3991917, + "similarity": 0.9951244468050102 + } + }, + { + "id": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d", + "height": 890444, + "version": 671080448, + "timestamp": 1743539347, + "bits": 386038124, + "nonce": 994357124, + "difficulty": 113757508810854, + "merkle_root": "c891d4bf68e22916274b667eb3287d50da2ddd63f8dad892da045cc2ad4a7b21", + "tx_count": 3797, + "size": 1500309, + "weight": 3993525, + "previousblockhash": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6", + "mediantime": 1743533986, + "stale": false, + "extras": { + "reward": 318708524, + "coinbaseRaw": "034c960d082f5669614254432f2cfabe6d6d45b7fd7ab53a0914da7dcc9d21fe44f0936f5354169a56df9d5139f07afbc2b41000000000000000106fc0eb03f0ac2e851d18d8d9f85ad70000000000", + "orphans": [], + "medianFee": 4.064775540157046, + "feeRange": [ + 3.014354066985646, + 3.18368700265252, + 3.602836879432624, + 4.231825525040388, + 5.581730769230769, + 10, + 697.7151162790698 + ], + "totalFees": 6208524, + "avgFee": 1635, + "avgFeeRate": 6, + "utxoSetChange": 5755, + "avgTxSize": 395.02, + "totalInputs": 6681, + "totalOutputs": 12436, + "totalOutputAmt": 835839828101, + "segwitTotalTxs": 3351, + "segwitTotalSize": 1354446, + "segwitTotalWeight": 3410181, + "feePercentiles": null, + "virtualSize": 998381.25, + "coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4", + "coinbaseAddresses": [ + "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4" + ], + "coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG", + "coinbaseSignatureAscii": "\u0003L–\r\b/ViaBTC/,ú¾mmE·ýzµ:\t\u0014Ú}̝!þDð“oST\u0016šVߝQ9ðzû´\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010oÀë\u0003ð¬.…\u001d\u0018ØÙøZ×\u0000\u0000\u0000\u0000\u0000", + "header": "00e0ff27f6f596dc1a210647d530ed3b351b5173428370b2086e02000000000000000000217b4aadc25c04da92d8daf863dd2dda507d28b37e664b271629e268bfd491c8934cec676c79021784af443b", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 73, + "name": "ViaBTC", + "slug": "viabtc", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 6253024, + "expectedWeight": 3991868, + "similarity": 0.9862862477811569 + } + }, + { + "id": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a", + "height": 890445, + "version": 601202688, + "timestamp": 1743539574, + "bits": 386038124, + "nonce": 1647397133, + "difficulty": 113757508810854, + "merkle_root": "61d8294afa8f6bafa4d979a77d187dee5f75a6392f957ea647d96eefbbbc5e9b", + "tx_count": 3579, + "size": 1659862, + "weight": 3993406, + "previousblockhash": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d", + "mediantime": 1743535677, + "stale": false, + "extras": { + "reward": 315617086, + "coinbaseRaw": "034d960d04764dec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f4fac7c451540000000000000", + "orphans": [], + "medianFee": 2.5565329189526835, + "feeRange": [ + 1.521613832853026, + 2, + 2.2411347517730498, + 3, + 3, + 3.954954954954955, + 162.78343949044586 + ], + "totalFees": 3117086, + "avgFee": 871, + "avgFeeRate": 3, + "utxoSetChange": 1881, + "avgTxSize": 463.65000000000003, + "totalInputs": 7893, + "totalOutputs": 9774, + "totalOutputAmt": 324878597485, + "segwitTotalTxs": 3189, + "segwitTotalSize": 1538741, + "segwitTotalWeight": 3509030, + "feePercentiles": null, + "virtualSize": 998351.5, + "coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63", + "coinbaseAddresses": [ + "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63", + "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38" + ], + "coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3", + "coinbaseSignatureAscii": "\u0003M–\r\u0004vMìg/Foundry USA Pool #dropgold/O¬|E\u0015@\u0000\u0000\u0000\u0000\u0000\u0000", + "header": "00a0d5230d2965fa5bd3e9406f5d665f975d9fc34eae70c46eb3010000000000000000009b5ebcbbef6ed947a67e952f39a6755fee7d187da779d9a4af6b8ffa4a29d861764dec676c7902170d493162", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 111, + "name": "Foundry USA", + "slug": "foundryusa", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 3145370, + "expectedWeight": 3991903, + "similarity": 0.9903353189076812 + } + }, + { + "id": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b", + "height": 890446, + "version": 537722880, + "timestamp": 1743541107, + "bits": 386038124, + "nonce": 826569764, + "difficulty": 113757508810854, + "merkle_root": "d9b320d7cb5aace80ca20b934b13b4a272121fbdd59f3aaba690e0326ca2c144", + "tx_count": 3998, + "size": 1541360, + "weight": 3993545, + "previousblockhash": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a", + "mediantime": 1743535803, + "stale": false, + "extras": { + "reward": 317976882, + "coinbaseRaw": "034e960d20202020204d696e656420627920536563706f6f6c2020202070001b04fad5fdfefabe6d6d59dd8ebce6e5aab8fb943bbdcede474b6f2d00a395a717970104a6958c17f1ca100000000000000000008089c9350200", + "orphans": [], + "medianFee": 3.3750830641948864, + "feeRange": [ + 2.397163120567376, + 3, + 3, + 3.463647199046484, + 4.49438202247191, + 7.213930348258707, + 476.1904761904762 + ], + "totalFees": 5476882, + "avgFee": 1370, + "avgFeeRate": 5, + "utxoSetChange": 4951, + "avgTxSize": 385.41, + "totalInputs": 7054, + "totalOutputs": 12005, + "totalOutputAmt": 983289729453, + "segwitTotalTxs": 3538, + "segwitTotalSize": 1396505, + "segwitTotalWeight": 3414233, + "feePercentiles": null, + "virtualSize": 998386.25, + "coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "coinbaseAddresses": [ + "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003N–\r Mined by Secpool p\u0000\u001b\u0004úÕýþú¾mmYݎ¼æåª¸û”;½ÎÞGKo-\u0000£•§\u0017—\u0001\u0004¦•Œ\u0017ñÊ\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000€‰É5\u0002\u0000", + "header": "00000d208a33fa2f65c6d3662ac962c9bd595147b940f96520400100000000000000000044c1a26c32e090a6ab3a9fd5bd1f1272a2b4134b930ba20ce8ac5acbd720b3d97353ec676c79021724744431", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 141, + "name": "SECPOOL", + "slug": "secpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 5601814, + "expectedWeight": 3991928, + "similarity": 0.9537877497871488 + } + }, + { + "id": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "height": 890447, + "version": 568860672, + "timestamp": 1743541240, + "bits": 386038124, + "nonce": 4008077709, + "difficulty": 113757508810854, + "merkle_root": "8c3b098e4e50b67075a4fc52bf4cd603aaa450c240c18a865c9ddc0f27104f5f", + "tx_count": 1919, + "size": 1747789, + "weight": 3993172, + "previousblockhash": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b", + "mediantime": 1743536834, + "stale": false, + "extras": { + "reward": 314435106, + "coinbaseRaw": "034f960d0f2f736c7573682f65000002fba05ef1fabe6d6df8d29032ea6f9ab1debd223651f30887df779c6195869e70a7b787b3a15f4b1710000000000000000000ee5f0c00b20200000000", + "orphans": [], + "medianFee": 1.4653828213500366, + "feeRange": [ + 1.0845070422535212, + 1.2, + 1.51, + 2.0141129032258065, + 2.3893805309734515, + 4.025477707006369, + 300.0065359477124 + ], + "totalFees": 1935106, + "avgFee": 1008, + "avgFeeRate": 1, + "utxoSetChange": -4244, + "avgTxSize": 910.58, + "totalInputs": 9909, + "totalOutputs": 5665, + "totalOutputAmt": 210763861504, + "segwitTotalTxs": 1720, + "segwitTotalSize": 1629450, + "segwitTotalWeight": 3519924, + "feePercentiles": null, + "virtualSize": 998293, + "coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11", + "coinbaseAddresses": [ + "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL", + "coinbaseSignatureAscii": "\u0003O–\r\u000f/slush/e\u0000\u0000\u0002û ^ñú¾mmøÒ2êoš±Þ½\"6Qó\b‡ßwœa•†žp§·‡³¡_K\u0017\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000î_\f\u0000²\u0002\u0000\u0000\u0000\u0000", + "header": "0020e8216b30f547b3d47824c1cb06db9221fcd04621b0263dfe010000000000000000005f4f10270fdc9d5c868ac140c250a4aa03d64cbf52fca47570b6504e8e093b8cf853ec676c7902178d69e6ee", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 43, + "name": "Braiins Pool", + "slug": "braiinspool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2059571, + "expectedWeight": 3991720, + "similarity": 0.9149852183486826 + } + } + ], + "conversions": { + "time": 1743541504, + "USD": 85181, + "EUR": 78939, + "GBP": 65914, + "CAD": 121813, + "CHF": 75399, + "AUD": 135492, + "JPY": 12753000 + }, + "backendInfo": { + "hostname": "node205.tk7.mempool.space", + "version": "3.1.0-dev", + "gitCommit": "3c08b5c72", + "lightning": false, + "backend": "esplora" + }, + "da": { + "progressPercent": 68.99801587301587, + "difficultyChange": 2.7058667283164084, + "estimatedRetargetDate": 1743907120500, + "remainingBlocks": 625, + "remainingTime": 365382500, + "previousRetarget": 1.433804484570416, + "previousTime": 1742728542, + "nextRetargetHeight": 891072, + "timeAvg": 584612, + "adjustedTimeAvg": 584612, + "timeOffset": 0, + "expectedBlocks": 1355.3266666666666 + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_ws_tx_replaced.json b/frontend/cypress/fixtures/details_rbf/tx01_ws_tx_replaced.json new file mode 100644 index 000000000..7af723546 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_ws_tx_replaced.json @@ -0,0 +1,5 @@ +{ + "txReplaced": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698" + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json b/frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json new file mode 100644 index 000000000..478eecfbc --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json @@ -0,0 +1,9 @@ +{ + "ancestors": [], + "bestDescendant": null, + "descendants": [], + "effectiveFeePerVsize": 4.285714285714286, + "sigops": 4, + "fee": 960, + "adjustedVsize": 224 +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json b/frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json new file mode 100644 index 000000000..96bd1efdd --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json @@ -0,0 +1,36 @@ +{ + "replacements": { + "tx": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "fee": 960, + "vsize": 224, + "value": 49040, + "rate": 4.285714285714286, + "time": 1743541726, + "rbf": true, + "fullRbf": false + }, + "time": 1743541726, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29", + "fee": 606, + "vsize": 233, + "value": 49394, + "rate": 2.6008583690987126, + "time": 1743541407, + "rbf": true + }, + "time": 1743541407, + "interval": 319, + "fullRbf": false, + "replaces": [] + } + ] + }, + "replaces": [ + "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29" + ] +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_api_tx.json b/frontend/cypress/fixtures/details_rbf/tx02_api_tx.json new file mode 100644 index 000000000..cbcb4fe8c --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_api_tx.json @@ -0,0 +1,38 @@ +{ + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "fb16c141d22a3a7af5e44e7478a8663866c35d554cd85107ec8a99b97e5a72e9", + "vout": 0, + "prevout": { + "scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj", + "value": 50000 + }, + "scriptsig": "483045022100ab59cf33b33eab1f76286a0fa6962c12171f2133931fec3616f96883551e6c9d02205cacbae788359f2a32ff629e732be0d95843440a3d1c222a63095f83fb69f438014104ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df", + "scriptsig_asm": "OP_PUSHBYTES_72 3045022100ab59cf33b33eab1f76286a0fa6962c12171f2133931fec3616f96883551e6c9d02205cacbae788359f2a32ff629e732be0d95843440a3d1c222a63095f83fb69f43801 OP_PUSHBYTES_65 04ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df", + "is_coinbase": false, + "sequence": 4294967293 + } + ], + "vout": [ + { + "scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj", + "value": 49040 + } + ], + "size": 224, + "weight": 896, + "sigops": 4, + "fee": 960, + "status": { + "confirmed": false + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_api_tx_times.json b/frontend/cypress/fixtures/details_rbf/tx02_api_tx_times.json new file mode 100644 index 000000000..365c3885e --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_api_tx_times.json @@ -0,0 +1,3 @@ +[ + 1743541726 +] \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_block.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_block.json new file mode 100644 index 000000000..9fc358795 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_block.json @@ -0,0 +1,116 @@ +{ + "block": { + "id": "000000000000000000019bfe551da67e0d93dda4b355d16f1fdb4031ba7e5d61", + "height": 890448, + "version": 626941952, + "timestamp": 1743541850, + "bits": 386038124, + "nonce": 1177284424, + "difficulty": 113757508810854, + "merkle_root": "563d862ef4ec95ea97735ca5561e347ca6e216cb56bd451e8bedb1014078d6c3", + "tx_count": 2229, + "size": 1763153, + "weight": 3993275, + "previousblockhash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "mediantime": 1743537197, + "stale": false, + "extras": { + "reward": 315498786, + "coinbaseRaw": "0350960d0f2f736c7573682fe500c702ff43d142fabe6d6def42d9effd800b9839c832dcfdc6899e137547f15c8a598dbe3a1363f999a0a310000000000000000000e9a2050064dc00000000", + "orphans": [], + "medianFee": 2.144206217858874, + "feeRange": [ + 1.0845921450151057, + 2, + 2.2448979591836733, + 3, + 3.5985915492957745, + 6, + 217.0212765957447 + ], + "totalFees": 2998786, + "avgFee": 1345, + "avgFeeRate": 3, + "utxoSetChange": -3073, + "avgTxSize": 790.84, + "totalInputs": 9558, + "totalOutputs": 6485, + "totalOutputAmt": 442206797883, + "segwitTotalTxs": 1986, + "segwitTotalSize": 1676431, + "segwitTotalWeight": 3646495, + "feePercentiles": null, + "virtualSize": 998318.75, + "coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11", + "coinbaseAddresses": [ + "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL", + "coinbaseSignatureAscii": "\u0003P–\r\u000f/slush/å\u0000Ç\u0002ÿCÑBú¾mmïBÙïý€\u000b˜9È2ÜýƉž\u0013uGñ\\ŠY¾:\u0013cù™ £\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000é¢\u0005\u0000dÜ\u0000\u0000\u0000\u0000", + "header": "00605e25abe2e310c10e724cfb641859658fee6570ec6062cc0400000000000000000000c3d6784001b1ed8b1e45bd56cb16e2a67c341e56a55c7397ea95ecf42e863d565a56ec676c79021748ef2b46", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 43, + "name": "Braiins Pool", + "slug": "braiinspool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 3287140, + "expectedWeight": 3991809, + "similarity": 0.9079894021278392 + } + }, + "mempool-blocks": [ + { + "blockSize": 1979907, + "blockVSize": 997974.75, + "nTx": 461, + "totalFees": 1429178, + "medianFee": 1.1020797417745662, + "feeRange": [ + 1.0666666666666667, + 1.0746847720659554, + 1.102059530141031, + 3, + 3.4233409610983982, + 5.017605633802817, + 148.4084084084084 + ] + }, + { + "blockSize": 1363691, + "blockVSize": 997986.5, + "nTx": 1080, + "totalFees": 1023741, + "medianFee": 1.014827018121911, + "feeRange": [ + 1, + 1.0036011703803736, + 1.0054683365672958, + 1.0186757215619695, + 1.0548148148148149, + 1.0548148148148149, + 1.068146618482189 + ] + }, + { + "blockSize": 1337253, + "blockVSize": 563516.25, + "nTx": 901, + "totalFees": 564834, + "medianFee": 1.0028011204481793, + "feeRange": [ + 1, + 1, + 1, + 1.0025062656641603, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "txConfirmed": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698" +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_block_confirmation.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_block_confirmation.json new file mode 100644 index 000000000..9fc358795 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_block_confirmation.json @@ -0,0 +1,116 @@ +{ + "block": { + "id": "000000000000000000019bfe551da67e0d93dda4b355d16f1fdb4031ba7e5d61", + "height": 890448, + "version": 626941952, + "timestamp": 1743541850, + "bits": 386038124, + "nonce": 1177284424, + "difficulty": 113757508810854, + "merkle_root": "563d862ef4ec95ea97735ca5561e347ca6e216cb56bd451e8bedb1014078d6c3", + "tx_count": 2229, + "size": 1763153, + "weight": 3993275, + "previousblockhash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "mediantime": 1743537197, + "stale": false, + "extras": { + "reward": 315498786, + "coinbaseRaw": "0350960d0f2f736c7573682fe500c702ff43d142fabe6d6def42d9effd800b9839c832dcfdc6899e137547f15c8a598dbe3a1363f999a0a310000000000000000000e9a2050064dc00000000", + "orphans": [], + "medianFee": 2.144206217858874, + "feeRange": [ + 1.0845921450151057, + 2, + 2.2448979591836733, + 3, + 3.5985915492957745, + 6, + 217.0212765957447 + ], + "totalFees": 2998786, + "avgFee": 1345, + "avgFeeRate": 3, + "utxoSetChange": -3073, + "avgTxSize": 790.84, + "totalInputs": 9558, + "totalOutputs": 6485, + "totalOutputAmt": 442206797883, + "segwitTotalTxs": 1986, + "segwitTotalSize": 1676431, + "segwitTotalWeight": 3646495, + "feePercentiles": null, + "virtualSize": 998318.75, + "coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11", + "coinbaseAddresses": [ + "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL", + "coinbaseSignatureAscii": "\u0003P–\r\u000f/slush/å\u0000Ç\u0002ÿCÑBú¾mmïBÙïý€\u000b˜9È2ÜýƉž\u0013uGñ\\ŠY¾:\u0013cù™ £\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000é¢\u0005\u0000dÜ\u0000\u0000\u0000\u0000", + "header": "00605e25abe2e310c10e724cfb641859658fee6570ec6062cc0400000000000000000000c3d6784001b1ed8b1e45bd56cb16e2a67c341e56a55c7397ea95ecf42e863d565a56ec676c79021748ef2b46", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 43, + "name": "Braiins Pool", + "slug": "braiinspool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 3287140, + "expectedWeight": 3991809, + "similarity": 0.9079894021278392 + } + }, + "mempool-blocks": [ + { + "blockSize": 1979907, + "blockVSize": 997974.75, + "nTx": 461, + "totalFees": 1429178, + "medianFee": 1.1020797417745662, + "feeRange": [ + 1.0666666666666667, + 1.0746847720659554, + 1.102059530141031, + 3, + 3.4233409610983982, + 5.017605633802817, + 148.4084084084084 + ] + }, + { + "blockSize": 1363691, + "blockVSize": 997986.5, + "nTx": 1080, + "totalFees": 1023741, + "medianFee": 1.014827018121911, + "feeRange": [ + 1, + 1.0036011703803736, + 1.0054683365672958, + 1.0186757215619695, + 1.0548148148148149, + 1.0548148148148149, + 1.068146618482189 + ] + }, + { + "blockSize": 1337253, + "blockVSize": 563516.25, + "nTx": 901, + "totalFees": 564834, + "medianFee": 1.0028011204481793, + "feeRange": [ + 1, + 1, + 1, + 1.0025062656641603, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "txConfirmed": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698" +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_mempool_blocks_01.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_mempool_blocks_01.json new file mode 100644 index 000000000..4c83cf797 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_mempool_blocks_01.json @@ -0,0 +1,75 @@ +{ + "mempool-blocks": [ + { + "blockSize": 1779823, + "blockVSize": 997995.25, + "nTx": 2133, + "totalFees": 2922926, + "medianFee": 2.0479263387949875, + "feeRange": [ + 1.0825892857142858, + 2, + 2.2439024390243905, + 3.010452961672474, + 3.554973821989529, + 6.032085561497326, + 218.1818181818182 + ] + }, + { + "blockSize": 1957833, + "blockVSize": 997953, + "nTx": 500, + "totalFees": 1093270, + "medianFee": 1.102049424602265, + "feeRange": [ + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.067677314564158, + 1.0766488413547237, + 1.1021605957228275 + ] + }, + { + "blockSize": 1477864, + "blockVSize": 997999, + "nTx": 730, + "totalFees": 1016458, + "medianFee": 1.0075971559364956, + "feeRange": [ + 1, + 1.0019552465783186, + 1.004255319148936, + 1.0081019768823594, + 1.018450184501845, + 1.0203327171903882, + 1.0548148148148149 + ] + }, + { + "blockSize": 1030954, + "blockVSize": 436613.5, + "nTx": 838, + "totalFees": 437891, + "medianFee": 0, + "feeRange": [ + 1, + 1, + 1, + 1.0026525198938991, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "txPosition": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "position": { + "block": 0, + "vsize": 111102 + } + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_next_block.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_next_block.json new file mode 100644 index 000000000..4245d4611 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_next_block.json @@ -0,0 +1,75 @@ +{ + "mempool-blocks": [ + { + "blockSize": 1719945, + "blockVSize": 997952.25, + "nTx": 2558, + "totalFees": 3287140, + "medianFee": 2.4046448299072485, + "feeRange": [ + 1.073446327683616, + 2, + 2.2567567567567566, + 3.0106761565836297, + 3.6169014084507043, + 6.015037593984962, + 218.1818181818182 + ] + }, + { + "blockSize": 2022898, + "blockVSize": 997983.25, + "nTx": 131, + "totalFees": 1098129, + "medianFee": 1.1020797417745662, + "feeRange": [ + 1.0625, + 1.0691217722793642, + 1.073436083408885, + 1.0761096766260911, + 1.080091533180778, + 1.102110739151618, + 1.1021909190121146 + ] + }, + { + "blockSize": 1363844, + "blockVSize": 997998.5, + "nTx": 1073, + "totalFees": 1023651, + "medianFee": 1.014827018121911, + "feeRange": [ + 1, + 1.003584229390681, + 1.0054683365672958, + 1.0186757215619695, + 1.0548148148148149, + 1.0548148148148149, + 1.068146618482189 + ] + }, + { + "blockSize": 1335390, + "blockVSize": 562453.5, + "nTx": 902, + "totalFees": 563772, + "medianFee": 1.0028011204481793, + "feeRange": [ + 1, + 1, + 1, + 1.0025402201524132, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "txPosition": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "position": { + "block": 0, + "vsize": 128920 + } + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_tx_position.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_tx_position.json new file mode 100644 index 000000000..c4a2db35a --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_tx_position.json @@ -0,0 +1,9 @@ +{ + "txPosition": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "position": { + "block": 0, + "vsize": 110880 + } + } +} \ No newline at end of file From b6ad17b995d3b3de6df42e802dc0a5fcb99307b9 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 11:19:32 +0900 Subject: [PATCH 089/114] Add accelerator fixture --- .../cypress/fixtures/details_rbf/api_accelerator_version.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 frontend/cypress/fixtures/details_rbf/api_accelerator_version.json diff --git a/frontend/cypress/fixtures/details_rbf/api_accelerator_version.json b/frontend/cypress/fixtures/details_rbf/api_accelerator_version.json new file mode 100644 index 000000000..a01c899b8 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/api_accelerator_version.json @@ -0,0 +1,3 @@ +{ + "gitCommit": "62f80296" +} \ No newline at end of file From a0e41b31e02a46203426211332b8e3acd84b0241 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 11:24:59 +0900 Subject: [PATCH 090/114] Add a RBF tx tracker test --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index a011073bd..4e7535858 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -528,6 +528,119 @@ describe('Mainnet', () => { cy.get('.alert-mempool').should('be.visible'); }); + it('shows RBF transactions properly (mobile - tracker)', () => { + cy.mockMempoolSocketV2(); + cy.viewport('iphone-xr'); + + // API Mocks + cy.intercept('/api/v1/mining/pools/1w', { + fixture: 'details_rbf/api_mining_pools_1w.json' + }).as('api_mining_1w'); + + cy.intercept('/api/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29', { + statusCode: 404, + body: 'Transaction not found' + }).as('api_tx01_404'); + + cy.intercept('/api/v1/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29/cached', { + fixture: 'details_rbf/tx01_api_cached.json' + }).as('api_tx01_cached'); + + cy.intercept('/api/v1/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29/rbf', { + fixture: 'details_rbf/tx01_api_rbf.json' + }).as('api_tx01_rbf'); + + cy.visit('/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29?mode=tracker'); + cy.wait('@api_tx01_rbf'); + + // Start sending mocked WS messages + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx01_ws_stratum_jobs.json' + } + } + }); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx01_ws_blocks_01.json' + } + } + }); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx01_ws_tx_replaced.json' + } + } + }); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx01_ws_mempool_blocks_01.json' + } + } + }); + + cy.get('.alert-replaced').should('be.visible'); + cy.get('.explainer').should('be.visible'); + cy.get('svg[data-icon=timeline]').should('be.visible'); + + // Second TX setup + cy.intercept('/api/tx/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', { + fixture: 'details_rbf/tx02_api_tx.json' + }).as('tx02_api'); + + cy.intercept('/api/v1/transaction-times?txId%5B%5D=b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', { + fixture: 'details_rbf/tx02_api_tx_times.json' + }).as('tx02_api_tx_times'); + + cy.intercept('/api/v1/tx/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698/rbf', { + fixture: 'details_rbf/tx02_api_rbf.json' + }).as('tx02_api_rbf'); + + cy.intercept('/api/v1/cpfp/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', { + fixture: 'details_rbf/tx02_api_cpfp.json' + }).as('tx02_api_cpfp'); + + // Go to the replacement tx + cy.get('.alert-replaced a').click(); + + cy.wait('@tx02_api_cpfp'); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx02_ws_tx_position.json' + } + } + }); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx02_ws_mempool_blocks_01.json' + } + } + }); + + cy.get('svg[data-icon=hourglass-half]').should('be.visible'); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx02_ws_block.json' + } + } + }); + cy.get('app-confirmations'); + cy.get('svg[data-icon=circle-check]').should('be.visible'); + }); + it('shows RBF transactions properly (desktop)', () => { cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', { fixture: 'mainnet_tx_cached.json' From 1d2d9c0be6518071167a6b5ea3238109a95a7974 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 11:25:14 +0900 Subject: [PATCH 091/114] Add an address poisoning attack test --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 4e7535858..6a10d2215 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -216,6 +216,31 @@ describe('Mainnet', () => { cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').its('length').should('equal', 2); cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').invoke('text').should('contain', `${address}`); }); + + it('highlights potential address poisoning attacks', () => { + const txid = '152a5dea805f95d6f83e50a9fd082630f542a52a076ebabdb295723eaf53fa30'; + const prefix = '1DonatePLease'; + const infix1 = 'SenderAddressXVXCmAY'; + const infix2 = '5btcToSenderXXWBoKhB'; + + cy.visit(`/tx/${txid}`); + cy.waitForSkeletonGone(); + cy.get('.alert-mempool').should('exist'); + cy.get('.poison-alert').its('length').should('equal', 2); + + cy.get('.prefix') + .should('have.length', 2) + .each(($el) => { + cy.wrap($el).should('have.text', prefix); + }); + + cy.get('.infix') + .should('have.length', 2) + .then(($infixes) => { + cy.wrap($infixes[0]).should('have.text', infix1); + cy.wrap($infixes[1]).should('have.text', infix2); + }); + }); }); describe('blocks navigation', () => { From 35d7b1c71d4a3418e40eefa03478dac7e9f26fe5 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 11:25:40 +0900 Subject: [PATCH 092/114] Import the missing ws function --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 6a10d2215..aae76f829 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -1,4 +1,5 @@ import { emitMempoolInfo, dropWebSocket } from '../../support/websocket'; +import { emitMempoolInfo, dropWebSocket, receiveWebSocketMessageFromServer } from '../../support/websocket'; const baseModule = Cypress.env('BASE_MODULE'); From 7712034c847f72f82903051a065a285be5db070d Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 12:19:00 +0900 Subject: [PATCH 093/114] Add another test for address poisioning --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 81 +++++++++++++++----- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index aae76f829..0d3b0a72b 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -1,4 +1,3 @@ -import { emitMempoolInfo, dropWebSocket } from '../../support/websocket'; import { emitMempoolInfo, dropWebSocket, receiveWebSocketMessageFromServer } from '../../support/websocket'; const baseModule = Cypress.env('BASE_MODULE'); @@ -218,30 +217,68 @@ describe('Mainnet', () => { cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').invoke('text').should('contain', `${address}`); }); - it('highlights potential address poisoning attacks', () => { - const txid = '152a5dea805f95d6f83e50a9fd082630f542a52a076ebabdb295723eaf53fa30'; - const prefix = '1DonatePLease'; - const infix1 = 'SenderAddressXVXCmAY'; - const infix2 = '5btcToSenderXXWBoKhB'; + describe('address poisoning', () => { + it('highlights potential address poisoning attacks on outputs, prefix and infix', () => { + const txid = '152a5dea805f95d6f83e50a9fd082630f542a52a076ebabdb295723eaf53fa30'; + const prefix = '1DonatePLease'; + const infix1 = 'SenderAddressXVXCmAY'; + const infix2 = '5btcToSenderXXWBoKhB'; - cy.visit(`/tx/${txid}`); - cy.waitForSkeletonGone(); - cy.get('.alert-mempool').should('exist'); - cy.get('.poison-alert').its('length').should('equal', 2); + cy.visit(`/tx/${txid}`); + cy.waitForSkeletonGone(); + cy.get('.alert-mempool').should('exist'); + cy.get('.poison-alert').its('length').should('equal', 2); - cy.get('.prefix') - .should('have.length', 2) - .each(($el) => { - cy.wrap($el).should('have.text', prefix); - }); + cy.get('.prefix') + .should('have.length', 2) + .each(($el) => { + cy.wrap($el).should('have.text', prefix); + }); - cy.get('.infix') - .should('have.length', 2) - .then(($infixes) => { - cy.wrap($infixes[0]).should('have.text', infix1); - cy.wrap($infixes[1]).should('have.text', infix2); - }); + cy.get('.infix') + .should('have.length', 2) + .then(($infixes) => { + cy.wrap($infixes[0]).should('have.text', infix1); + cy.wrap($infixes[1]).should('have.text', infix2); + }); + }); + + it('highlights potential address poisoning attacks on inputs and outputs, prefix, infix and postfix', () => { + const txid = '44544516084ea916ff1eb69c675c693e252addbbaf77102ffff86e3979ac6132'; + const prefix = 'bc1qge8'; + const infix1 = '6gqjnk8aqs3nvv7ejrvcd4zq6qur3'; + const infix2 = 'xyxjex6zzzx5g8hh65vsel4e548p2'; + const postfix1 = '6p6e3r'; + const postfix2 = '6p6e3r'; + + cy.visit(`/tx/${txid}`); + cy.waitForSkeletonGone(); + cy.get('.alert-mempool').should('exist'); + cy.get('.poison-alert').its('length').should('equal', 2); + + cy.get('.prefix') + .should('have.length', 2) + .each(($el) => { + cy.wrap($el).should('have.text', prefix); + }); + + cy.get('.infix') + .should('have.length', 2) + .then(($infixes) => { + cy.wrap($infixes[0]).should('have.text', infix1); + cy.wrap($infixes[1]).should('have.text', infix2); + }); + + cy.get('.postfix') + .should('have.length', 2) + .then(($postfixes) => { + cy.wrap($postfixes[0]).should('include.text', postfix1); + cy.wrap($postfixes[1]).should('include.text', postfix2); + }); + + }); }); + }); describe('blocks navigation', () => { @@ -423,6 +460,7 @@ describe('Mainnet', () => { cy.get('#dropdownFees').should('be.visible'); cy.get('.btn-group').should('be.visible'); }); + it('check buttons - tablet', () => { cy.viewport('ipad-2'); cy.visit('/graphs'); @@ -431,6 +469,7 @@ describe('Mainnet', () => { cy.get('#dropdownFees').should('be.visible'); cy.get('.btn-group').should('be.visible'); }); + it('check buttons - desktop', () => { cy.viewport('macbook-16'); cy.visit('/graphs'); From 42d4cb3cc5015c195e9ebb277481261f3cc1ed91 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 13:29:39 +0900 Subject: [PATCH 094/114] Run CI workflow after merging too --- .github/workflows/ci.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c603c42a..864151951 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,13 @@ name: CI Pipeline for the Backend and Frontend on: pull_request: types: [opened, review_requested, synchronize] + push: + branches: + - master jobs: backend: - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" + if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" strategy: matrix: node: ["20", "21"] @@ -157,7 +160,7 @@ jobs: frontend: needs: cache - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" + if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" strategy: matrix: node: ["20", "21"] @@ -245,7 +248,7 @@ jobs: VERBOSE: 1 e2e: - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" + if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" runs-on: ubuntu-latest needs: frontend strategy: @@ -375,9 +378,8 @@ jobs: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} - validate_docker_json: - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" + if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" runs-on: ubuntu-latest name: Validate generated backend Docker JSON From f0224b0bf02ca536d2fba2f90fe45d225c290bf7 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 13:42:25 +0900 Subject: [PATCH 095/114] Bump node version to v22 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/e2e_parameterized.yml | 2 +- .nvmrc | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 864151951..842fe56f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" strategy: matrix: - node: ["20", "21"] + node: ["22"] flavor: ["dev", "prod"] fail-fast: false runs-on: ubuntu-latest @@ -266,7 +266,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 22 cache: "npm" cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json diff --git a/.github/workflows/e2e_parameterized.yml b/.github/workflows/e2e_parameterized.yml index da1814b84..a53677a80 100644 --- a/.github/workflows/e2e_parameterized.yml +++ b/.github/workflows/e2e_parameterized.yml @@ -151,7 +151,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 22 cache: "npm" cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json diff --git a/.nvmrc b/.nvmrc index a9b234d51..53d1c14db 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.8.0 +v22 From f41c9a0e57707cbc95f6eada02568e235f435979 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 15:45:21 +0900 Subject: [PATCH 096/114] Update missing node matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 842fe56f0..68ffa840a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,7 +163,7 @@ jobs: if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" strategy: matrix: - node: ["20", "21"] + node: ["22"] flavor: ["dev", "prod"] fail-fast: false runs-on: ubuntu-latest From 05e407ff0f185d475cda545fa3c32af1de0f1d54 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 2 Apr 2025 19:26:46 +0900 Subject: [PATCH 097/114] Add a test for the RBF page updates --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 37 ++++++++++ .../cypress/fixtures/rbf_page/rbf_01.json | 37 ++++++++++ .../cypress/fixtures/rbf_page/rbf_02.json | 68 +++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 frontend/cypress/fixtures/rbf_page/rbf_01.json create mode 100644 frontend/cypress/fixtures/rbf_page/rbf_02.json diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 0d3b0a72b..a664f333c 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -561,6 +561,43 @@ describe('Mainnet', () => { }); describe('RBF transactions', () => { + it('RBF page gets updated over websockets', () => { + cy.intercept('/api/v1/replacements', { + statusCode: 200, + body: [] + }); + + cy.intercept('/api/v1/fullrbf/replacements', { + statusCode: 200, + body: [] + }); + + cy.mockMempoolSocketV2(); + + cy.visit('/rbf'); + cy.get('.no-replacements'); + cy.get('.tree').should('have.length', 0); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'rbf_page/rbf_01.json' + } + } + }); + + cy.get('.tree').should('have.length', 1); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'rbf_page/rbf_02.json' + } + } + }); + cy.get('.tree').should('have.length', 2); + }); + it('shows RBF transactions properly (mobile - details)', () => { cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', { fixture: 'mainnet_tx_cached.json' diff --git a/frontend/cypress/fixtures/rbf_page/rbf_01.json b/frontend/cypress/fixtures/rbf_page/rbf_01.json new file mode 100644 index 000000000..2c79f142a --- /dev/null +++ b/frontend/cypress/fixtures/rbf_page/rbf_01.json @@ -0,0 +1,37 @@ +{ + "rbfLatest": [ + { + "tx": { + "txid": "f4bae4f626036250fd00d68490e572f65f66417452003a0f4c4d76f17a9fde68", + "fee": 1185, + "vsize": 223, + "value": 41729, + "rate": 5.313901345291479, + "time": 1743587177, + "rbf": true, + "fullRbf": false, + "mined": true + }, + "time": 1743587177, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "12945412dfc455e0ed6049dc2ee8737756c8d9e2d9a2eb26f366cd5019a0369f", + "fee": 504, + "vsize": 222, + "value": 42410, + "rate": 2.27027027027027, + "time": 1743586081, + "rbf": true + }, + "time": 1743586081, + "interval": 1096, + "fullRbf": false, + "replaces": [] + } + ], + "mined": true + } + ] +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/rbf_page/rbf_02.json b/frontend/cypress/fixtures/rbf_page/rbf_02.json new file mode 100644 index 000000000..3b5011002 --- /dev/null +++ b/frontend/cypress/fixtures/rbf_page/rbf_02.json @@ -0,0 +1,68 @@ +{ + "rbfLatest": [ + { + "tx": { + "txid": "d313b479acfbae719afb488a078e0fe0e052a67b9f65f73f7c75d3d95fd36acc", + "fee": 672, + "vsize": 167.25, + "value": 29996328, + "rate": 4.017937219730942, + "time": 1743587365, + "rbf": true, + "fullRbf": false + }, + "time": 1743587365, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "eb5aa786cabda307cc9642cfb9c41a3b405ac20a391eefbe54be7930bea61865", + "fee": 336, + "vsize": 167.5, + "value": 29996664, + "rate": 2.005970149253731, + "time": 1743586424, + "rbf": true + }, + "time": 1743586424, + "interval": 941, + "fullRbf": false, + "replaces": [] + } + ] + }, + { + "tx": { + "txid": "f4bae4f626036250fd00d68490e572f65f66417452003a0f4c4d76f17a9fde68", + "fee": 1185, + "vsize": 223, + "value": 41729, + "rate": 5.313901345291479, + "time": 1743587177, + "rbf": true, + "fullRbf": false, + "mined": true + }, + "time": 1743587177, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "12945412dfc455e0ed6049dc2ee8737756c8d9e2d9a2eb26f366cd5019a0369f", + "fee": 504, + "vsize": 222, + "value": 42410, + "rate": 2.27027027027027, + "time": 1743586081, + "rbf": true + }, + "time": 1743586081, + "interval": 1096, + "fullRbf": false, + "replaces": [] + } + ], + "mined": true + } + ] +} \ No newline at end of file From 1790c83babd9b3d4d643dc5d4bd3274767b77c71 Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 2 Apr 2025 20:08:09 +0900 Subject: [PATCH 098/114] ops: Bump NodeJS to v22 for install and start scripts --- production/README.md | 4 ++-- production/install | 6 +++--- production/mempool-start-all | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/production/README.md b/production/README.md index 2805cde81..bf281fb1b 100644 --- a/production/README.md +++ b/production/README.md @@ -84,11 +84,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh- ### Node.js + npm -Build Node.js v20.17.0 and npm v9 from source using `nvm`: +Build Node.js v22.14.0 and npm v9 from source using `nvm`: ``` curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | zsh source $HOME/.zshrc -nvm install v20.17.0 --shared-zlib +nvm install v22.14.0 --shared-zlib nvm alias default node ``` diff --git a/production/install b/production/install index f3bebb4e7..95b38369b 100755 --- a/production/install +++ b/production/install @@ -1116,8 +1116,8 @@ echo "[*] Installing nvm.sh from GitHub" osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh' echo "[*] Building NodeJS via nvm.sh" -osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v20.12.0 --shared-zlib' -osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm alias default 20.12.0' +osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v22.14.0 --shared-zlib' +osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm alias default 22.14.0' #################### # Tor installation # @@ -1565,7 +1565,7 @@ EOF osSudo "${UNFURL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh' echo "[*] Building NodeJS via nvm.sh" - osSudo "${UNFURL_USER}" zsh -c 'source ~/.zshrc ; nvm install v20.12.0 --shared-zlib' + osSudo "${UNFURL_USER}" zsh -c 'source ~/.zshrc ; nvm install v22.14.0 --shared-zlib' ;; esac diff --git a/production/mempool-start-all b/production/mempool-start-all index 9d4c6ee58..f6c3c76c2 100755 --- a/production/mempool-start-all +++ b/production/mempool-start-all @@ -1,7 +1,7 @@ #!/usr/bin/env zsh export NVM_DIR="$HOME/.nvm" source "$NVM_DIR/nvm.sh" -nvm use v20.12.0 +nvm use v22.14.0 # start all mempool backends that exist for site in mainnet mainnet-lightning testnet testnet-lightning testnet4 signet signet-lightning liquid liquidtestnet;do From 1baae6474be831ab783cbf82699120bf84927700 Mon Sep 17 00:00:00 2001 From: wiz Date: Thu, 3 Apr 2025 19:19:55 +0900 Subject: [PATCH 099/114] ops: Bump elements to v23.2.7 --- production/install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/production/install b/production/install index 95b38369b..aadb8005e 100755 --- a/production/install +++ b/production/install @@ -378,7 +378,7 @@ ELEMENTS_REPO_URL=https://github.com/ElementsProject/elements ELEMENTS_REPO_NAME=elements ELEMENTS_REPO_BRANCH=master #ELEMENTS_LATEST_RELEASE=$(curl -s https://api.github.com/repos/ElementsProject/elements/releases/latest|grep tag_name|head -1|cut -d '"' -f4) -ELEMENTS_LATEST_RELEASE=elements-23.2.6 +ELEMENTS_LATEST_RELEASE=elements-23.2.7 echo -n '.' BITCOIN_ELECTRS_REPO_URL=https://github.com/mempool/electrs From d3d7829fee3a456863c17a2b33c835f228cac310 Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 3 Apr 2025 18:00:45 +0200 Subject: [PATCH 100/114] FIx pool page CSS --- .../app/components/pool/pool.component.scss | 117 +++++++++--------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index 26e6008b6..f09ac0d65 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -84,74 +84,79 @@ div.scrollable { text-align: right; } } +} - .progress { - background-color: var(--secondary); +.progress { + background-color: var(--secondary); +} + +.coinbase { + width: 20%; + @media (max-width: 875px) { + display: none; } +} - .coinbase { - width: 20%; - @media (max-width: 875px) { - display: none; - } +.health { + @media (max-width: 1150px) { + display: none; } +} - .height { - width: 10%; +.height { + width: 10%; +} + +.timestamp { + @media (max-width: 875px) { + padding-left: 50px; } - - .timestamp { - @media (max-width: 875px) { - padding-left: 50px; - } - @media (max-width: 685px) { - display: none; - } + @media (max-width: 625px) { + display: none; } +} - .mined { - width: 13%; - @media (max-width: 1100px) { - display: none; - } +.mined { + width: 13%; +} + +.txs { + @media (max-width: 938px) { + display: none; } - - .txs { - padding-right: 40px; - @media (max-width: 1100px) { - padding-right: 10px; - } - @media (max-width: 875px) { - padding-right: 20px; - } - @media (max-width: 567px) { - padding-right: 10px; - } + padding-right: 40px; + @media (max-width: 1100px) { + padding-right: 10px; } - - .size { - width: 12%; - @media (max-width: 1000px) { - width: 15%; - } - @media (max-width: 875px) { - width: 20%; - } - @media (max-width: 650px) { - width: 20%; - } - @media (max-width: 450px) { - display: none; - } + @media (max-width: 875px) { + padding-right: 20px; } + @media (max-width: 567px) { + padding-right: 10px; + } +} - .scriptmessage { - overflow: hidden; - display: inline-block; - text-overflow: ellipsis; - vertical-align: middle; - width: auto; - text-align: left; +.size { + min-width: 80px; + width: 12%; + @media (max-width: 1000px) { + width: 15%; + } +} + +.scriptmessage { + max-width: 340px; + overflow: hidden; + display: inline-block; + text-overflow: ellipsis; + vertical-align: middle; + width: auto; + text-align: left; +} + +.reward { + @media (max-width: 1035px) { + display: none; } } From 22af7de5bd8ae2affd096fed97d644eac930fbc3 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sat, 5 Apr 2025 20:03:14 +0900 Subject: [PATCH 101/114] Bump package.json versions ahead of the official release --- backend/package-lock.json | 4 ++-- backend/package.json | 4 ++-- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- unfurler/package-lock.json | 4 ++-- unfurler/package.json | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index a4963d6f0..7138c59e1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-backend", - "version": "3.1.0-dev", + "version": "3.2.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-backend", - "version": "3.1.0-dev", + "version": "3.2.0-dev", "hasInstallScript": true, "license": "GNU Affero General Public License v3.0", "dependencies": { diff --git a/backend/package.json b/backend/package.json index bcbc0f256..f039ccad2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-backend", - "version": "3.1.0-dev", + "version": "3.2.0-dev", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", @@ -68,4 +68,4 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.1" } -} +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 932979e2b..148c08751 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-frontend", - "version": "3.1.0-dev", + "version": "3.2.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-frontend", - "version": "3.1.0-dev", + "version": "3.2.0-dev", "license": "GNU Affero General Public License v3.0", "dependencies": { "@angular-devkit/build-angular": "^17.3.1", diff --git a/frontend/package.json b/frontend/package.json index 8d4ee6e27..a8abf1d4b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-frontend", - "version": "3.1.0-dev", + "version": "3.2.0-dev", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", diff --git a/unfurler/package-lock.json b/unfurler/package-lock.json index 799148486..8f8373a36 100644 --- a/unfurler/package-lock.json +++ b/unfurler/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-unfurl", - "version": "3.1.0-dev", + "version": "3.2.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-unfurl", - "version": "3.0.0", + "version": "3.2.0-dev", "dependencies": { "@types/node": "^16.11.41", "ejs": "^3.1.10", diff --git a/unfurler/package.json b/unfurler/package.json index bf3dad55b..968334f22 100644 --- a/unfurler/package.json +++ b/unfurler/package.json @@ -1,6 +1,6 @@ { "name": "mempool-unfurl", - "version": "3.1.0-dev", + "version": "3.2.0-dev", "description": "Renderer for mempool open graph link preview images", "repository": { "type": "git", @@ -32,4 +32,4 @@ "eslint-config-prettier": "^8.5.0", "prettier": "^2.7.1" } -} +} \ No newline at end of file From 05d2aa73f85e7c9994f88a873a8cc912bc11c4f8 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 6 Apr 2025 06:55:27 +0000 Subject: [PATCH 102/114] pump up monitoring frequency --- backend/src/api/bitcoin/esplora-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 8035d92c0..30a6822d5 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -36,7 +36,7 @@ class FailoverRouter { maxHeight: number = 0; hosts: FailoverHost[]; multihost: boolean; - gitHashInterval: number = 600000; // 10 minutes + gitHashInterval: number = 60000; // 1 minute pollInterval: number = 60000; // 1 minute pollTimer: NodeJS.Timeout | null = null; pollConnection = axios.create(); From b73f93e1cde277c6c7d3f87af880fe61251bac1f Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 6 Apr 2025 17:51:14 +0900 Subject: [PATCH 103/114] ops: Use gcc to build NodeJS v22 on FreeBSD 14 --- production/install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/production/install b/production/install index aadb8005e..f4b2c10ed 100755 --- a/production/install +++ b/production/install @@ -1113,10 +1113,10 @@ echo "[*] Installing Mempool crontab" osSudo "${ROOT_USER}" crontab -u "${MEMPOOL_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/mempool.crontab" echo "[*] Installing nvm.sh from GitHub" -osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh' +osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | zsh' echo "[*] Building NodeJS via nvm.sh" -osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v22.14.0 --shared-zlib' +osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; CC=gcc CXX=g++ nvm install v22.14.0 --shared-zlib' osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm alias default 22.14.0' #################### From 849eebe5837ab8d8018c7ca0f2db704f740329e4 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 6 Apr 2025 08:53:35 +0000 Subject: [PATCH 104/114] Fix axios unix sockets --- backend/src/api/bitcoin/esplora-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 30a6822d5..6ee51fda4 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -111,7 +111,7 @@ class FailoverRouter { for (const host of this.hosts) { try { const result = await (host.socket - ? this.pollConnection.get('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }) + ? this.pollConnection.get('http://api/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }) : this.pollConnection.get(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT }) ); if (result) { @@ -288,7 +288,7 @@ class FailoverRouter { let url; if (host.socket) { axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType }; - url = path; + url = 'http://api' + path; } else { axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType }; url = host.host + path; From 3df953c0a8c478951d859acaba7fbcb7c10c0653 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 6 Apr 2025 18:02:22 +0900 Subject: [PATCH 105/114] Pin GitHub actions to node 22.14.0 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/e2e_parameterized.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68ffa840a..976fe5a7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" strategy: matrix: - node: ["22"] + node: ["22.14.0"] flavor: ["dev", "prod"] fail-fast: false runs-on: ubuntu-latest @@ -163,7 +163,7 @@ jobs: if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" strategy: matrix: - node: ["22"] + node: ["22.14.0"] flavor: ["dev", "prod"] fail-fast: false runs-on: ubuntu-latest diff --git a/.github/workflows/e2e_parameterized.yml b/.github/workflows/e2e_parameterized.yml index a53677a80..8b07ffe82 100644 --- a/.github/workflows/e2e_parameterized.yml +++ b/.github/workflows/e2e_parameterized.yml @@ -43,7 +43,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node }} + node-version: 22.14.0 registry-url: "https://registry.npmjs.org" - name: Install (Prod dependencies only) @@ -151,7 +151,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 22 + node-version: 22.14.0 cache: "npm" cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json From 84c9a01b6df64d7b78faca98448e078ec882c4b6 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 6 Apr 2025 18:03:03 +0900 Subject: [PATCH 106/114] Pin Node versions to 22.14.0 on the Docker images --- docker/backend/Dockerfile | 4 ++-- docker/frontend/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index e56b07da3..942a8f9c8 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /build RUN apt-get update && \ apt-get install -y curl ca-certificates && \ curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ - apt-get install -y nodejs build-essential python3 pkg-config && \ + apt-get install -y nodejs=22.14.0-1nodesource1 build-essential python3 pkg-config && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -29,7 +29,7 @@ FROM rust:1.84-bookworm AS runtime RUN apt-get update && \ apt-get install -y curl ca-certificates && \ curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ - apt-get install -y nodejs && \ + apt-get install -y nodejs=22.14.0-1nodesource1 && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index 8d97c9dc6..23c1da3d4 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim AS builder +FROM node:22.14.0-bookworm-slim AS builder ARG commitHash ENV DOCKER_COMMIT_HASH=${commitHash} From c5f7d561131f20be27e408972f34c147f7f6c68a Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 6 Apr 2025 23:33:53 +0900 Subject: [PATCH 107/114] Set automatic pool updates on by default --- docker/backend/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/backend/start.sh b/docker/backend/start.sh index 8adb631da..ae0bc616f 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -26,7 +26,7 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1} __MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0} __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool} __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} -__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false} +__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=true} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} __MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800} From 7dab8f19a2e11da7244ac4ac34fb3ce95f72c2fd Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 6 Apr 2025 23:44:06 +0900 Subject: [PATCH 108/114] docker: Add missing nginx route for /api/v1/services --- nginx-mempool.conf | 3 +++ production/install | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nginx-mempool.conf b/nginx-mempool.conf index 2532a7082..4b12ed714 100644 --- a/nginx-mempool.conf +++ b/nginx-mempool.conf @@ -58,6 +58,9 @@ proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } + location /api/v1/services { + proxy_pass https://mempool.space; + } location /api/v1 { proxy_pass http://127.0.0.1:8999/api/v1; } diff --git a/production/install b/production/install index f4b2c10ed..1d8faa9f8 100755 --- a/production/install +++ b/production/install @@ -418,7 +418,7 @@ DEBIAN_UNFURL_PKG+=(libxdamage-dev libxrandr-dev libgbm-dev libpango1.0-dev liba FREEBSD_PKG=() FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim) FREEBSD_PKG+=(openssh-portable py311-pip rust llvm17 jq base64 libzmq4) -FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf) +FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc13 libevent libtool pkgconf) FREEBSD_PKG+=(nginx rsync py311-certbot-nginx mariadb1011-server) FREEBSD_PKG+=(geoipupdate redis) From 74dde851128295a5402d46e02af7ca45059c9984 Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 7 Apr 2025 14:17:34 +0900 Subject: [PATCH 109/114] Release v3.2.0 --- backend/package-lock.json | 4 ++-- backend/package.json | 4 ++-- frontend/package-lock.json | 4 ++-- frontend/package.json | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 7138c59e1..e24af1a0a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-backend", - "version": "3.2.0-dev", + "version": "3.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-backend", - "version": "3.2.0-dev", + "version": "3.2.0", "hasInstallScript": true, "license": "GNU Affero General Public License v3.0", "dependencies": { diff --git a/backend/package.json b/backend/package.json index f039ccad2..53825449d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-backend", - "version": "3.2.0-dev", + "version": "3.2.0", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", @@ -68,4 +68,4 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.1" } -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 148c08751..4a3f7a4a2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-frontend", - "version": "3.2.0-dev", + "version": "3.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-frontend", - "version": "3.2.0-dev", + "version": "3.2.0", "license": "GNU Affero General Public License v3.0", "dependencies": { "@angular-devkit/build-angular": "^17.3.1", diff --git a/frontend/package.json b/frontend/package.json index a8abf1d4b..3651f7a43 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-frontend", - "version": "3.2.0-dev", + "version": "3.2.0", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", @@ -121,4 +121,4 @@ "scarfSettings": { "enabled": false } -} \ No newline at end of file +} From 2b56b9016910324791b7ea84925fd7c0b5212d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Haf?= Date: Wed, 5 Feb 2025 12:45:41 +0100 Subject: [PATCH 110/114] reworked health audit --- backend/src/api/audit.ts | 24 +++++++++++++++---- backend/src/api/common.ts | 19 ++++++++++++++- backend/src/mempool.interfaces.ts | 1 + .../app/components/block/block.component.html | 2 +- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index e09234cdc..17fefa3d5 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -1,3 +1,4 @@ +import { couldStartTrivia } from 'typescript'; import config from '../config'; import logger from '../logger'; import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; @@ -28,6 +29,9 @@ class Audit { let matchedWeight = 0; let projectedWeight = 0; + let spamWeight = 0; + let blkWeight = 0; + const inBlock = {}; const inTemplate = {}; @@ -72,6 +76,19 @@ class Audit { matchedWeight += transactions[0].weight; } + + for (const tx of transactions){ + blkWeight += tx.weight; + } + + for (const tx of transactions){ + if(tx.spam !== undefined){ + if (tx.spam == true){ + spamWeight += tx.weight; + } + } + } + // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs // these displaced transactions should occupy the first N weight units of the next projected block let displacedWeightRemaining = displacedWeight + 4000; @@ -169,11 +186,8 @@ class Audit { const numCensored = Object.keys(isCensored).length; const numMatches = matches.length - 1; // adjust for coinbase tx let score = 0; - if (numMatches <= 0 && numCensored <= 0) { - score = 1; - } else if (numMatches > 0) { - score = (numMatches / (numMatches + numCensored)); - } + + score = (Math.abs((spamWeight/blkWeight)-1)); const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; return { diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index f3569c44c..8ac21ce5e 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -462,6 +462,20 @@ export class Common { return flags; } + static isInscription2(vin, tx): void { + // 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 || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]); + // inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope + if (asm?.includes('OP_0 OP_IF')) { + tx.spam = true; + } + } + } + static getTransactionFlags(tx: TransactionExtended, height?: number): number { let flags = tx.flags ? BigInt(tx.flags) : 0n; @@ -513,6 +527,7 @@ export class Common { flags |= TransactionFlags.p2tr; if (vin.witness?.length) { flags = Common.isInscription(vin, flags); + Common.isInscription2(vin, tx); } } break; } @@ -521,6 +536,7 @@ export class Common { if (vin.witness?.length >= 2) { try { flags = Common.isInscription(vin, flags); + Common.isInscription2(vin, tx); } catch { // witness script parsing will fail if this isn't really a taproot output } @@ -571,7 +587,7 @@ export class Common { 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; + case 'op_return': flags |= TransactionFlags.op_return; tx.spam = true; break; } if (vout.scriptpubkey_address) { reusedOutputAddresses[vout.scriptpubkey_address] = (reusedOutputAddresses[vout.scriptpubkey_address] || 0) + 1; @@ -594,6 +610,7 @@ export class Common { } if (hasFakePubkey) { flags |= TransactionFlags.fake_pubkey; + tx.spam = true; } // fast but bad heuristic to detect possible coinjoins diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index e1da4be6f..1b028e5f5 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -132,6 +132,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction { replacement?: boolean; uid?: number; flags?: number; + spam?: boolean; } export interface MempoolTransactionExtended extends TransactionExtended { diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 105cdf31a..d7216f675 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -59,7 +59,7 @@
    HealthHealth Date: Wed, 5 Feb 2025 12:59:18 +0100 Subject: [PATCH 111/114] remove trade mark --- .../app/components/about/about.component.html | 1 - .../master-page-preview.component.html | 1 - .../master-page/master-page.component.html | 5 ----- .../search-form/search-form.component.html | 2 +- .../src/app/dashboard/dashboard.component.html | 2 +- .../global-footer/global-footer.component.html | 16 ++++------------ frontend/src/index.mempool.html | 14 +++++++------- 7 files changed, 13 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 8ac931d7a..552f8f6bb 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -2,7 +2,6 @@
    ® -
    v{{ packetJsonVersion }} [{{ frontendGitCommitHash }}] [{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}] diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.html b/frontend/src/app/components/master-page-preview/master-page-preview.component.html index 01995906f..0202c5a94 100644 --- a/frontend/src/app/components/master-page-preview/master-page-preview.component.html +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.html @@ -8,7 +8,6 @@ @if (enterpriseInfo?.header_img) { } @else { - } diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 436cf3c67..eee0a5cf6 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -26,7 +26,6 @@
    - }
    @@ -50,7 +49,6 @@ @if (enterpriseInfo?.header_img) { } @else { - }
    @@ -105,9 +103,6 @@ -
    diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html index 73e9d0501..963446dac 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -1,7 +1,7 @@
    - +
    diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 7c84351ca..d3d61ae0d 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -18,7 +18,7 @@
    -
    Mempool Goggles™ : {{ goggleCycle[goggleIndex].name }}
    +
    mempool glass™ : {{ goggleCycle[goggleIndex].name }}
     
    diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index 0dcea9245..3af212caa 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -8,7 +8,6 @@ } @else { - }
    @if (!enterpriseInfo?.footer_img) { @@ -17,8 +16,7 @@ Explore the full Bitcoin ecosystem ® } @else { - Be your own explorer - + Explore Bitcoin }

    } @@ -61,8 +59,7 @@ Explore the full Bitcoin ecosystem ® } @else { - Be your own explorer - + Explore Bitcoin }

    } @@ -118,13 +115,8 @@
    diff --git a/frontend/src/index.mempool.html b/frontend/src/index.mempool.html index ed5f7e0b4..efc30c178 100644 --- a/frontend/src/index.mempool.html +++ b/frontend/src/index.mempool.html @@ -8,18 +8,18 @@ - - + + - + - - - + + + @@ -27,7 +27,7 @@ - + From 14dba2ef49ee5c4b889588f2793f641a64b507f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Haf?= Date: Wed, 5 Feb 2025 12:59:57 +0100 Subject: [PATCH 112/114] rename data to spam --- frontend/src/app/dashboard/dashboard.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index c5c35dce5..7fb9bc289 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -78,7 +78,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { { index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' }, { index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' }, { index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' }, - { index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' }, + { index: 3, name: $localize`Spam`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' }, ]; goggleFlags = 0n; goggleMode: FilterMode = 'and'; From ba291cd697c9fc2476be4f022aba114c4a55bbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Haf?= Date: Wed, 5 Feb 2025 13:04:45 +0100 Subject: [PATCH 113/114] remove shitcoin explorer --- .../transactions-list/transactions-list.component.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 7721298fb..9c6f661b4 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -89,7 +89,6 @@
    -
    @@ -256,11 +255,7 @@ OP_RETURN  - @if (vout.isRunestone) { - - } @else { {{ vout.scriptpubkey_asm | hex2ascii }} - } {{ vout.scriptpubkey_type | scriptpubkeyType }} From 37bb76eedb33d191d20b1a10e54e2a4c030aa5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=CC=81o=20Haf?= Date: Mon, 17 Mar 2025 14:43:54 +0100 Subject: [PATCH 114/114] fix coinbase audit --- backend/src/api/audit.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index 17fefa3d5..f20ebd6c6 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -29,6 +29,7 @@ class Audit { let matchedWeight = 0; let projectedWeight = 0; + let countCb = 0; let spamWeight = 0; let blkWeight = 0; @@ -82,11 +83,14 @@ class Audit { } for (const tx of transactions){ - if(tx.spam !== undefined){ - if (tx.spam == true){ - spamWeight += tx.weight; + if (countCb !== 0){ + if(tx.spam !== undefined){ + if (tx.spam == true){ + spamWeight += tx.weight; + } } } + countCb += 1; } // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs