Merge branch 'master' into fix-liquid-frontend-crash

This commit is contained in:
Felipe Knorr Kuhn 2024-02-03 09:17:53 -08:00 committed by GitHub
commit e478fb2279
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 87 additions and 43 deletions

6
backend/.gitignore vendored
View File

@ -7,6 +7,12 @@ mempool-config.json
pools.json
icons.json
# docker
Dockerfile
GeoIP
start.sh
wait-for-it.sh
# compiled output
/dist
/tmp

View File

@ -646,7 +646,7 @@ class BisqMarketsApi {
case 'year':
return strtotime('midnight first day of january', ts);
default:
throw new Error('Unsupported interval: ' + interval);
throw new Error('Unsupported interval');
}
}

View File

@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified } from '../mempool.interfaces';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
@ -451,7 +451,9 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
}
} else {
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
}
@ -995,11 +997,11 @@ class Blocks {
return state;
}
private updateTimerProgress(state, msg) {
private updateTimerProgress(state, msg): void {
state.progress = msg;
}
private clearTimer(state) {
private clearTimer(state): void {
if (state.timer) {
clearTimeout(state.timer);
}
@ -1088,13 +1090,19 @@ class Blocks {
summary = {
id: hash,
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = tx.flags || Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
return {
txid: tx.txid,
fee: tx.fee || 0,
vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
rate: tx.effectiveFeePerVsize,
flags: tx.flags || Common.getTransactionFlags(tx),
flags: flags,
};
}),
};
@ -1284,7 +1292,7 @@ class Blocks {
return blocks;
}
public async $getBlockAuditSummary(hash: string): Promise<any> {
public async $getBlockAuditSummary(hash: string): Promise<BlockAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
return BlocksAuditsRepository.$getBlockAudit(hash);
} else {
@ -1304,7 +1312,7 @@ class Blocks {
return this.currentBlockHeight;
}
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
let transactions = txs;
if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') {
@ -1319,14 +1327,19 @@ class Blocks {
}
}
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
if (transactions?.length != null) {
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
await this.$saveCpfp(hash, height, summary);
await this.$saveCpfp(hash, height, summary);
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
return summary;
return summary;
} else {
logger.err(`Cannot index CPFP for block ${height} - missing transaction data`);
return null;
}
}
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {

View File

@ -6,6 +6,7 @@ import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
import transactionUtils from './transaction-utils';
import { isPoint } from '../utils/secp256k1';
import logger from '../logger';
export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
@ -261,6 +262,9 @@ export class Common {
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': {
if (!vin.witness?.length) {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr;
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
@ -301,7 +305,7 @@ export class Common {
case 'p2pk': {
flags |= TransactionFlags.p2pk;
// detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve)
hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey.slice(2, -2));
hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey?.slice(2, -2));
} break;
case 'multisig': {
flags |= TransactionFlags.p2ms;
@ -348,7 +352,12 @@ export class Common {
}
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
const flags = Common.getTransactionFlags(tx);
let flags = 0;
try {
flags = Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
tx.flags = flags;
return {
...Common.stripTransaction(tx),

View File

@ -142,7 +142,7 @@ class Mining {
public async $getPoolStat(slug: string): Promise<object> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
throw new Error('This mining pool does not exist');
}
const blockCount: number = await BlocksRepository.$blockCount(pool.id);

View File

@ -59,7 +59,7 @@ class BlocksAuditRepositories {
}
}
public async $getBlockAudit(hash: string): Promise<any> {
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
try {
const [rows]: any[] = await DB.query(
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
@ -75,8 +75,8 @@ class BlocksAuditRepositories {
expected_weight as expectedWeight
FROM blocks_audits
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.hash = "${hash}"
`);
WHERE blocks_audits.hash = ?
`, [hash]);
if (rows.length) {
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
@ -101,8 +101,8 @@ class BlocksAuditRepositories {
const [rows]: any[] = await DB.query(
`SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
FROM blocks_audits
WHERE blocks_audits.hash = "${hash}"
`);
WHERE blocks_audits.hash = ?
`, [hash]);
return rows[0];
} catch (e: any) {
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));

View File

@ -5,7 +5,7 @@ import logger from '../logger';
import { Common } from '../api/common';
import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository';
import { escape } from 'mysql2';
import { RowDataPacket, escape } from 'mysql2';
import BlocksSummariesRepository from './BlocksSummariesRepository';
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
import bitcoinClient from '../api/bitcoin/bitcoin-client';
@ -478,7 +478,7 @@ class BlocksRepository {
public async $getBlocksByPool(slug: string, startHeight?: number): Promise<BlockExtended[]> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
throw new Error('This mining pool does not exist');
}
const params: any[] = [];
@ -802,10 +802,10 @@ class BlocksRepository {
/**
* Get a list of blocks that have been indexed
*/
public async $getIndexedBlocks(): Promise<any[]> {
public async $getIndexedBlocks(): Promise<{ height: number, hash: string }[]> {
try {
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`);
return rows;
const [rows] = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`) as RowDataPacket[][];
return rows as { height: number, hash: string }[];
} catch (e) {
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
@ -815,7 +815,7 @@ class BlocksRepository {
/**
* Get a list of blocks that have not had CPFP data indexed
*/
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
public async $getCPFPUnindexedBlocks(): Promise<number[]> {
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
@ -825,13 +825,13 @@ class BlocksRepository {
}
const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
const [rows]: any[] = await DB.query(`
const [rows] = await DB.query(`
SELECT height
FROM compact_cpfp_clusters
WHERE height <= ? AND height >= ?
GROUP BY height
ORDER BY height DESC;
`, [currentBlockHeight, minHeight]);
`, [currentBlockHeight, minHeight]) as RowDataPacket[][];
const indexedHeights = {};
rows.forEach((row) => { indexedHeights[row.height] = true; });

View File

@ -1,3 +1,4 @@
import { RowDataPacket } from 'mysql2';
import DB from '../database';
import logger from '../logger';
import { BlockSummary, TransactionClassified } from '../mempool.interfaces';
@ -69,7 +70,7 @@ class BlocksSummariesRepository {
public async $getIndexedSummariesId(): Promise<string[]> {
try {
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
const [rows] = await DB.query(`SELECT id from blocks_summaries`) as RowDataPacket[][];
return rows.map(row => row.id);
} catch (e) {
logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e));

View File

@ -139,7 +139,7 @@ class HashratesRepository {
public async $getPoolWeeklyHashrate(slug: string): Promise<any[]> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
throw new Error('This mining pool does not exist');
}
// Find hashrate boundaries

View File

@ -31,6 +31,9 @@ const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
* @returns {boolean} true if the point is on the SECP256K1 curve
*/
export function isPoint(pointHex: string): boolean {
if (!pointHex?.length) {
return false;
}
if (
!(
// is uncompressed

View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 20, 2023.
Signed: jamesblacklock

View File

@ -55,7 +55,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=""}
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}

7
frontend/.gitignore vendored
View File

@ -6,6 +6,13 @@
/out-tsc
server.run.js
# docker
Dockerfile
entrypoint.sh
nginx-mempool.conf
nginx.conf
wait-for
# Only exists if Bazel was run
/bazel-out

View File

@ -45,28 +45,30 @@ export class AcceleratorDashboardComponent implements OnInit {
this.pendingAccelerations$ = interval(30000).pipe(
startWith(true),
switchMap(() => {
return this.apiService.getAccelerations$();
}),
catchError((e) => {
return of([]);
return this.apiService.getAccelerations$().pipe(
catchError(() => {
return of([]);
}),
);
}),
share(),
);
this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
switchMap((chainTip) => {
return this.apiService.getAccelerationHistory$({ timeframe: '1m' });
}),
catchError((e) => {
return of([]);
switchMap(() => {
return this.apiService.getAccelerationHistory$({ timeframe: '1m' }).pipe(
catchError(() => {
return of([]);
}),
);
}),
share(),
);
this.minedAccelerations$ = this.accelerations$.pipe(
map(accelerations => {
return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status))
return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status));
})
);

View File

@ -540,7 +540,7 @@
</ng-container>
</ng-template>
</div>
<button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
<button *ngIf="cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</td>
</tr>
</tbody>