Merge branch 'master' into mononaut/document-backend-config
This commit is contained in:
commit
3b8802d39e
@ -2,6 +2,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { IBackendInfo } from '../mempool.interfaces';
|
import { IBackendInfo } from '../mempool.interfaces';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
class BackendInfo {
|
class BackendInfo {
|
||||||
private backendInfo: IBackendInfo;
|
private backendInfo: IBackendInfo;
|
||||||
@ -22,7 +23,8 @@ class BackendInfo {
|
|||||||
this.backendInfo = {
|
this.backendInfo = {
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
version: versionInfo.version,
|
version: versionInfo.version,
|
||||||
gitCommit: versionInfo.gitCommit
|
gitCommit: versionInfo.gitCommit,
|
||||||
|
lightning: config.LIGHTNING.ENABLED
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,12 +22,10 @@ import poolsParser from './pools-parser';
|
|||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
import cpfpRepository from '../repositories/CpfpRepository';
|
import cpfpRepository from '../repositories/CpfpRepository';
|
||||||
import transactionRepository from '../repositories/TransactionRepository';
|
|
||||||
import mining from './mining/mining';
|
import mining from './mining/mining';
|
||||||
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import { Block } from 'bitcoinjs-lib';
|
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@ -340,9 +338,10 @@ class Blocks {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all indexed block hash
|
// Get all indexed block hash
|
||||||
const unindexedBlocks = await blocksRepository.$getCPFPUnindexedBlocks();
|
const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks();
|
||||||
|
logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`);
|
||||||
|
|
||||||
if (!unindexedBlocks?.length) {
|
if (!unindexedBlockHeights?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,30 +350,26 @@ class Blocks {
|
|||||||
let countThisRun = 0;
|
let countThisRun = 0;
|
||||||
let timer = new Date().getTime() / 1000;
|
let timer = new Date().getTime() / 1000;
|
||||||
const startedAt = new Date().getTime() / 1000;
|
const startedAt = new Date().getTime() / 1000;
|
||||||
|
for (const height of unindexedBlockHeights) {
|
||||||
for (const block of unindexedBlocks) {
|
|
||||||
// Logging
|
// Logging
|
||||||
|
const hash = await bitcoinApi.$getBlockHash(height);
|
||||||
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||||
if (elapsedSeconds > 5) {
|
if (elapsedSeconds > 5) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const blockPerSeconds = Math.max(1, countThisRun / elapsedSeconds);
|
const blockPerSeconds = (countThisRun / elapsedSeconds);
|
||||||
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
|
const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100;
|
||||||
logger.debug(`Indexing cpfp clusters for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
countThisRun = 0;
|
countThisRun = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$indexCPFP(block.hash, block.height); // Calculate and save CPFP data for transactions in this block
|
await this.$indexCPFP(hash, height); // Calculate and save CPFP data for transactions in this block
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
count++;
|
count++;
|
||||||
countThisRun++;
|
countThisRun++;
|
||||||
}
|
}
|
||||||
if (count > 0) {
|
logger.notice(`CPFP indexing completed: indexed ${count} blocks`);
|
||||||
logger.notice(`CPFP indexing completed: indexed ${count} blocks`);
|
|
||||||
} else {
|
|
||||||
logger.debug(`CPFP indexing completed: indexed ${count} blocks`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
@ -752,34 +747,15 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
||||||
let transactions;
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
if (Common.blocksSummariesIndexingEnabled()) {
|
const transactions = block.tx.map(tx => {
|
||||||
transactions = await this.$getStrippedBlockTransactions(hash);
|
tx.vsize = tx.weight / 4;
|
||||||
const rawBlock = await bitcoinApi.$getRawBlock(hash);
|
tx.fee *= 100_000_000;
|
||||||
const block = Block.fromBuffer(rawBlock);
|
return tx;
|
||||||
const txMap = {};
|
});
|
||||||
for (const tx of block.transactions || []) {
|
|
||||||
txMap[tx.getId()] = tx;
|
const clusters: any[] = [];
|
||||||
}
|
|
||||||
for (const tx of transactions) {
|
|
||||||
// convert from bitcoinjs to esplora vin format
|
|
||||||
if (txMap[tx.txid]?.ins) {
|
|
||||||
tx.vin = txMap[tx.txid].ins.map(vin => {
|
|
||||||
return {
|
|
||||||
txid: vin.hash.slice().reverse().toString('hex')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const block = await bitcoinClient.getBlock(hash, 2);
|
|
||||||
transactions = block.tx.map(tx => {
|
|
||||||
tx.vsize = tx.weight / 4;
|
|
||||||
tx.fee *= 100_000_000;
|
|
||||||
return tx;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let cluster: TransactionStripped[] = [];
|
let cluster: TransactionStripped[] = [];
|
||||||
let ancestors: { [txid: string]: boolean } = {};
|
let ancestors: { [txid: string]: boolean } = {};
|
||||||
for (let i = transactions.length - 1; i >= 0; i--) {
|
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||||
@ -793,10 +769,12 @@ class Blocks {
|
|||||||
});
|
});
|
||||||
const effectiveFeePerVsize = totalFee / totalVSize;
|
const effectiveFeePerVsize = totalFee / totalVSize;
|
||||||
if (cluster.length > 1) {
|
if (cluster.length > 1) {
|
||||||
await cpfpRepository.$saveCluster(height, cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }), effectiveFeePerVsize);
|
clusters.push({
|
||||||
for (const tx of cluster) {
|
root: cluster[0].txid,
|
||||||
await transactionRepository.$setCluster(tx.txid, cluster[0].txid);
|
height,
|
||||||
}
|
txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }),
|
||||||
|
effectiveFeePerVsize,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cluster = [];
|
cluster = [];
|
||||||
ancestors = {};
|
ancestors = {};
|
||||||
@ -806,7 +784,10 @@ class Blocks {
|
|||||||
ancestors[vin.txid] = true;
|
ancestors[vin.txid] = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await blocksRepository.$setCPFPIndexed(hash);
|
const result = await cpfpRepository.$batchSaveClusters(clusters);
|
||||||
|
if (!result) {
|
||||||
|
await cpfpRepository.$insertProgressMarker(height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,12 @@ import config from '../config';
|
|||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
import blocksRepository from '../repositories/BlocksRepository';
|
||||||
|
import cpfpRepository from '../repositories/CpfpRepository';
|
||||||
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 49;
|
private static currentVersion = 52;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -442,6 +445,29 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
|
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
|
||||||
await this.updateToSchemaVersion(49);
|
await this.updateToSchemaVersion(49);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 50) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks` DROP COLUMN `cpfp_indexed`');
|
||||||
|
await this.updateToSchemaVersion(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 51) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `cpfp_clusters` ADD INDEX `height` (`height`)');
|
||||||
|
await this.updateToSchemaVersion(51);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 52) {
|
||||||
|
await this.$executeQuery(this.getCreateCompactCPFPTableQuery(), await this.$checkIfTableExists('compact_cpfp_clusters'));
|
||||||
|
await this.$executeQuery(this.getCreateCompactTransactionsTableQuery(), await this.$checkIfTableExists('compact_transactions'));
|
||||||
|
try {
|
||||||
|
await this.$convertCompactCpfpTables();
|
||||||
|
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
|
||||||
|
await this.$executeQuery('DROP TABLE IF EXISTS `transactions`');
|
||||||
|
await this.updateToSchemaVersion(52);
|
||||||
|
} catch(e) {
|
||||||
|
logger.warn('' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -913,6 +939,25 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateCompactCPFPTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS compact_cpfp_clusters (
|
||||||
|
root binary(32) NOT NULL,
|
||||||
|
height int(10) NOT NULL,
|
||||||
|
txs BLOB DEFAULT NULL,
|
||||||
|
fee_rate float unsigned,
|
||||||
|
PRIMARY KEY (root),
|
||||||
|
INDEX (height)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateCompactTransactionsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS compact_transactions (
|
||||||
|
txid binary(32) NOT NULL,
|
||||||
|
cluster binary(32) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (txid)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
public async $truncateIndexedData(tables: string[]) {
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
||||||
|
|
||||||
@ -933,6 +978,49 @@ class DatabaseMigration {
|
|||||||
logger.warn(`Unable to erase indexed data`);
|
logger.warn(`Unable to erase indexed data`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $convertCompactCpfpTables(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const batchSize = 250;
|
||||||
|
const maxHeight = await blocksRepository.$mostRecentBlockHeight() || 0;
|
||||||
|
const [minHeightRows]: any = await DB.query(`SELECT MIN(height) AS minHeight from cpfp_clusters`);
|
||||||
|
const minHeight = (minHeightRows.length && minHeightRows[0].minHeight != null) ? minHeightRows[0].minHeight : maxHeight;
|
||||||
|
let height = maxHeight;
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
let timer = new Date().getTime() / 1000;
|
||||||
|
const startedAt = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
while (height > minHeight) {
|
||||||
|
const [rows] = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT * from cpfp_clusters
|
||||||
|
WHERE height <= ? AND height > ?
|
||||||
|
ORDER BY height
|
||||||
|
`,
|
||||||
|
[height, height - batchSize]
|
||||||
|
) as RowDataPacket[][];
|
||||||
|
if (rows?.length) {
|
||||||
|
await cpfpRepository.$batchSaveClusters(rows.map(row => {
|
||||||
|
return {
|
||||||
|
root: row.root,
|
||||||
|
height: row.height,
|
||||||
|
txs: JSON.parse(row.txs),
|
||||||
|
effectiveFeePerVsize: row.fee_rate,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = new Date().getTime() / 1000 - timer;
|
||||||
|
const runningFor = new Date().getTime() / 1000 - startedAt;
|
||||||
|
logger.debug(`Migrated cpfp data from block ${height} to ${height - batchSize} in ${elapsed.toFixed(2)} seconds | total elapsed: ${runningFor.toFixed(2)} seconds`);
|
||||||
|
timer = new Date().getTime() / 1000;
|
||||||
|
height -= batchSize;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DatabaseMigration();
|
export default new DatabaseMigration();
|
||||||
|
@ -41,13 +41,70 @@ class NodesRoutes {
|
|||||||
let nodes: any[] = [];
|
let nodes: any[] = [];
|
||||||
switch (config.MEMPOOL.NETWORK) {
|
switch (config.MEMPOOL.NETWORK) {
|
||||||
case 'testnet':
|
case 'testnet':
|
||||||
nodesList = ['032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', '032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0', '029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3', '0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af', '03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab', '032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8', '02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605', '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584'];
|
nodesList = [
|
||||||
|
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
|
||||||
|
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
|
||||||
|
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
|
||||||
|
'032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0',
|
||||||
|
'029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3',
|
||||||
|
'0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af',
|
||||||
|
'03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab',
|
||||||
|
'032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8',
|
||||||
|
'02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605',
|
||||||
|
'039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205',
|
||||||
|
'033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18',
|
||||||
|
'029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584',
|
||||||
|
'0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c',
|
||||||
|
'029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5',
|
||||||
|
'02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075',
|
||||||
|
'030b0ca1ea7b1075716d2a555630e6fd47ef11bc7391fe68963ec06cf370a5e382',
|
||||||
|
'031adb9eb2d66693f85fa31a4adca0319ba68219f3ad5f9a2ef9b34a6b40755fa1',
|
||||||
|
'02ccd07faa47eda810ecf5591ccf5ca50f6c1034d0d175052898d32a00b9bae24f',
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
case 'signet':
|
case 'signet':
|
||||||
nodesList = ['03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', '025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029', '027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe', '03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43', '02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991', '02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34', '02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86', '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7'];
|
nodesList = [
|
||||||
|
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
|
||||||
|
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
|
||||||
|
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
|
||||||
|
'025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029',
|
||||||
|
'027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe',
|
||||||
|
'03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43',
|
||||||
|
'02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991',
|
||||||
|
'02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34',
|
||||||
|
'02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86',
|
||||||
|
'038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7',
|
||||||
|
'03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761',
|
||||||
|
'028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7',
|
||||||
|
'02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070',
|
||||||
|
'02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45',
|
||||||
|
'038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097',
|
||||||
|
'0242c7f7d315095f37ad1421ae0a2fc967d4cbe65b61b079c5395a769436959853',
|
||||||
|
'02a909e70eb03742f12666ebb1f56ac42a5fbaab0c0e8b5b1df4aa9f10f8a09240',
|
||||||
|
'03a26efa12489803c07f3ac2f1dba63812e38f0f6e866ce3ebb34df7de1f458cd2',
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
nodesList = ['03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', '0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108', '03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c', '03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5', '021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a', '032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9', '02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34', '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43'];
|
nodesList = [
|
||||||
|
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
|
||||||
|
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
|
||||||
|
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',
|
||||||
|
'0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108',
|
||||||
|
'03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c',
|
||||||
|
'03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5',
|
||||||
|
'021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a',
|
||||||
|
'032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9',
|
||||||
|
'02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34',
|
||||||
|
'02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf',
|
||||||
|
'03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c',
|
||||||
|
'0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43',
|
||||||
|
'02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06',
|
||||||
|
'0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e',
|
||||||
|
'03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7',
|
||||||
|
'038d118996b3eaa15dcd317b32a539c9ecfdd7698f204acf8a087336af655a9192',
|
||||||
|
'02a928903d93d78877dacc3642b696128a3636e9566dd42d2d132325b2c8891c09',
|
||||||
|
'0328cd17f3a9d3d90b532ade0d1a67e05eb8a51835b3dce0a2e38eac04b5a62a57',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let pubKey of nodesList) {
|
for (let pubKey of nodesList) {
|
||||||
|
@ -274,6 +274,7 @@ export interface IBackendInfo {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
gitCommit: string;
|
gitCommit: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
lightning: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDifficultyAdjustment {
|
export interface IDifficultyAdjustment {
|
||||||
@ -337,4 +338,4 @@ export interface IOldestNodes {
|
|||||||
updatedAt?: number,
|
updatedAt?: number,
|
||||||
city?: any,
|
city?: any,
|
||||||
country?: any,
|
country?: any,
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import HashratesRepository from './HashratesRepository';
|
|||||||
import { escape } from 'mysql2';
|
import { escape } from 'mysql2';
|
||||||
import BlocksSummariesRepository from './BlocksSummariesRepository';
|
import BlocksSummariesRepository from './BlocksSummariesRepository';
|
||||||
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
|
||||||
|
import bitcoinClient from '../api/bitcoin/bitcoin-client';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
class BlocksRepository {
|
class BlocksRepository {
|
||||||
/**
|
/**
|
||||||
@ -667,16 +669,32 @@ class BlocksRepository {
|
|||||||
*/
|
*/
|
||||||
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
|
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks WHERE cpfp_indexed = 0 ORDER BY height DESC`);
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
return rows;
|
const currentBlockHeight = blockchainInfo.blocks;
|
||||||
|
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
|
||||||
|
if (indexingBlockAmount <= -1) {
|
||||||
|
indexingBlockAmount = currentBlockHeight + 1;
|
||||||
|
}
|
||||||
|
const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
|
||||||
|
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT height
|
||||||
|
FROM compact_cpfp_clusters
|
||||||
|
WHERE height <= ? AND height >= ?
|
||||||
|
ORDER BY height DESC;
|
||||||
|
`, [currentBlockHeight, minHeight]);
|
||||||
|
|
||||||
|
const indexedHeights = {};
|
||||||
|
rows.forEach((row) => { indexedHeights[row.height] = true; });
|
||||||
|
const allHeights: number[] = Array.from(Array(currentBlockHeight - minHeight + 1).keys(), n => n + minHeight).reverse();
|
||||||
|
const unindexedHeights = allHeights.filter(x => !indexedHeights[x]);
|
||||||
|
|
||||||
|
return unindexedHeights;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
return [];
|
||||||
|
|
||||||
public async $setCPFPIndexed(hash: string): Promise<void> {
|
|
||||||
await DB.query(`UPDATE blocks SET cpfp_indexed = 1 WHERE hash = ?`, [hash]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,34 +1,151 @@
|
|||||||
|
import cluster, { Cluster } from 'cluster';
|
||||||
|
import { RowDataPacket } from 'mysql2';
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Ancestor } from '../mempool.interfaces';
|
import { Ancestor } from '../mempool.interfaces';
|
||||||
|
import transactionRepository from '../repositories/TransactionRepository';
|
||||||
|
|
||||||
class CpfpRepository {
|
class CpfpRepository {
|
||||||
public async $saveCluster(height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<void> {
|
public async $saveCluster(clusterRoot: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<boolean> {
|
||||||
|
if (!txs[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// skip clusters of transactions with the same fees
|
||||||
|
const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100;
|
||||||
|
const equalFee = txs.reduce((acc, tx) => {
|
||||||
|
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
|
||||||
|
}, true);
|
||||||
|
if (equalFee) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const txsJson = JSON.stringify(txs);
|
const packedTxs = Buffer.from(this.pack(txs));
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`
|
`
|
||||||
INSERT INTO cpfp_clusters(root, height, txs, fee_rate)
|
INSERT INTO compact_cpfp_clusters(root, height, txs, fee_rate)
|
||||||
VALUE (?, ?, ?, ?)
|
VALUE (UNHEX(?), ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
height = ?,
|
height = ?,
|
||||||
txs = ?,
|
txs = ?,
|
||||||
fee_rate = ?
|
fee_rate = ?
|
||||||
`,
|
`,
|
||||||
[txs[0].txid, height, txsJson, effectiveFeePerVsize, height, txsJson, effectiveFeePerVsize, height]
|
[clusterRoot, height, packedTxs, effectiveFeePerVsize, height, packedTxs, effectiveFeePerVsize]
|
||||||
);
|
);
|
||||||
|
const maxChunk = 10;
|
||||||
|
let chunkIndex = 0;
|
||||||
|
while (chunkIndex < txs.length) {
|
||||||
|
const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk).map(tx => {
|
||||||
|
return { txid: tx.txid, cluster: clusterRoot };
|
||||||
|
});
|
||||||
|
await transactionRepository.$batchSetCluster(chunk);
|
||||||
|
chunkIndex += maxChunk;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $batchSaveClusters(clusters: { root: string, height: number, txs: any, effectiveFeePerVsize: number}[]): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const clusterValues: any[] = [];
|
||||||
|
const txs: any[] = [];
|
||||||
|
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
if (cluster.txs?.length > 1) {
|
||||||
|
const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100;
|
||||||
|
const equalFee = cluster.txs.reduce((acc, tx) => {
|
||||||
|
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
|
||||||
|
}, true);
|
||||||
|
if (!equalFee) {
|
||||||
|
clusterValues.push([
|
||||||
|
cluster.root,
|
||||||
|
cluster.height,
|
||||||
|
Buffer.from(this.pack(cluster.txs)),
|
||||||
|
cluster.effectiveFeePerVsize
|
||||||
|
]);
|
||||||
|
for (const tx of cluster.txs) {
|
||||||
|
txs.push({ txid: tx.txid, cluster: cluster.root });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clusterValues.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxChunk = 100;
|
||||||
|
let chunkIndex = 0;
|
||||||
|
// insert transactions in batches of up to 100 rows
|
||||||
|
while (chunkIndex < txs.length) {
|
||||||
|
const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk);
|
||||||
|
await transactionRepository.$batchSetCluster(chunk);
|
||||||
|
chunkIndex += maxChunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkIndex = 0;
|
||||||
|
// insert clusters in batches of up to 100 rows
|
||||||
|
while (chunkIndex < clusterValues.length) {
|
||||||
|
const chunk = clusterValues.slice(chunkIndex, chunkIndex + maxChunk);
|
||||||
|
let query = `
|
||||||
|
INSERT IGNORE INTO compact_cpfp_clusters(root, height, txs, fee_rate)
|
||||||
|
VALUES
|
||||||
|
`;
|
||||||
|
query += chunk.map(chunk => {
|
||||||
|
return (' (UNHEX(?), ?, ?, ?)');
|
||||||
|
}) + ';';
|
||||||
|
const values = chunk.flat();
|
||||||
|
await DB.query(
|
||||||
|
query,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
chunkIndex += maxChunk;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getCluster(clusterRoot: string): Promise<Cluster> {
|
||||||
|
const [clusterRows]: any = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM compact_cpfp_clusters
|
||||||
|
WHERE root = UNHEX(?)
|
||||||
|
`,
|
||||||
|
[clusterRoot]
|
||||||
|
);
|
||||||
|
const cluster = clusterRows[0];
|
||||||
|
cluster.txs = this.unpack(cluster.txs);
|
||||||
|
return cluster;
|
||||||
|
}
|
||||||
|
|
||||||
public async $deleteClustersFrom(height: number): Promise<void> {
|
public async $deleteClustersFrom(height: number): Promise<void> {
|
||||||
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
||||||
try {
|
try {
|
||||||
|
const [rows] = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT txs, height, root from compact_cpfp_clusters
|
||||||
|
WHERE height >= ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
) as RowDataPacket[][];
|
||||||
|
if (rows?.length) {
|
||||||
|
for (let clusterToDelete of rows) {
|
||||||
|
const txs = this.unpack(clusterToDelete.txs);
|
||||||
|
for (let tx of txs) {
|
||||||
|
await transactionRepository.$removeTransaction(tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`
|
`
|
||||||
DELETE from cpfp_clusters
|
DELETE from compact_cpfp_clusters
|
||||||
WHERE height >= ?
|
WHERE height >= ?
|
||||||
`,
|
`,
|
||||||
[height]
|
[height]
|
||||||
@ -38,6 +155,70 @@ class CpfpRepository {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insert a dummy row to mark that we've indexed as far as this block
|
||||||
|
public async $insertProgressMarker(height: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const [rows]: any = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT root
|
||||||
|
FROM compact_cpfp_clusters
|
||||||
|
WHERE height = ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
);
|
||||||
|
if (!rows?.length) {
|
||||||
|
const rootBuffer = Buffer.alloc(32);
|
||||||
|
rootBuffer.writeInt32LE(height);
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
INSERT INTO compact_cpfp_clusters(root, height, fee_rate)
|
||||||
|
VALUE (?, ?, ?)
|
||||||
|
`,
|
||||||
|
[rootBuffer, height, 0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot insert cpfp progress marker. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public pack(txs: Ancestor[]): ArrayBuffer {
|
||||||
|
const buf = new ArrayBuffer(44 * txs.length);
|
||||||
|
const view = new DataView(buf);
|
||||||
|
txs.forEach((tx, i) => {
|
||||||
|
const offset = i * 44;
|
||||||
|
for (let x = 0; x < 32; x++) {
|
||||||
|
// store txid in little-endian
|
||||||
|
view.setUint8(offset + (31 - x), parseInt(tx.txid.slice(x * 2, (x * 2) + 2), 16));
|
||||||
|
}
|
||||||
|
view.setUint32(offset + 32, tx.weight);
|
||||||
|
view.setBigUint64(offset + 36, BigInt(Math.round(tx.fee)));
|
||||||
|
});
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unpack(buf: Buffer): Ancestor[] {
|
||||||
|
if (!buf) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||||
|
const txs: Ancestor[] = [];
|
||||||
|
const view = new DataView(arrayBuffer);
|
||||||
|
for (let offset = 0; offset < arrayBuffer.byteLength; offset += 44) {
|
||||||
|
const txid = Array.from(new Uint8Array(arrayBuffer, offset, 32)).reverse().map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
const weight = view.getUint32(offset + 32);
|
||||||
|
const fee = Number(view.getBigUint64(offset + 36));
|
||||||
|
txs.push({
|
||||||
|
txid,
|
||||||
|
weight,
|
||||||
|
fee
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return txs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new CpfpRepository();
|
export default new CpfpRepository();
|
@ -1,6 +1,7 @@
|
|||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
|
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
|
||||||
|
import cpfpRepository from './CpfpRepository';
|
||||||
|
|
||||||
interface CpfpSummary {
|
interface CpfpSummary {
|
||||||
txid: string;
|
txid: string;
|
||||||
@ -12,20 +13,20 @@ interface CpfpSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TransactionRepository {
|
class TransactionRepository {
|
||||||
public async $setCluster(txid: string, cluster: string): Promise<void> {
|
public async $setCluster(txid: string, clusterRoot: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`
|
`
|
||||||
INSERT INTO transactions
|
INSERT INTO compact_transactions
|
||||||
(
|
(
|
||||||
txid,
|
txid,
|
||||||
cluster
|
cluster
|
||||||
)
|
)
|
||||||
VALUE (?, ?)
|
VALUE (UNHEX(?), UNHEX(?))
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
cluster = ?
|
cluster = UNHEX(?)
|
||||||
;`,
|
;`,
|
||||||
[txid, cluster, cluster]
|
[txid, clusterRoot, clusterRoot]
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
@ -33,20 +34,45 @@ class TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
|
public async $batchSetCluster(txs): Promise<void> {
|
||||||
try {
|
try {
|
||||||
let query = `
|
let query = `
|
||||||
SELECT *
|
INSERT IGNORE INTO compact_transactions
|
||||||
FROM transactions
|
(
|
||||||
LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster
|
txid,
|
||||||
WHERE transactions.txid = ?
|
cluster
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
`;
|
`;
|
||||||
const [rows]: any = await DB.query(query, [txid]);
|
query += txs.map(tx => {
|
||||||
if (rows.length) {
|
return (' (UNHEX(?), UNHEX(?))');
|
||||||
rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[];
|
}) + ';';
|
||||||
if (rows[0]?.txs?.length) {
|
const values = txs.map(tx => [tx.txid, tx.cluster]).flat();
|
||||||
return this.convertCpfp(rows[0]);
|
await DB.query(
|
||||||
}
|
query,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
|
||||||
|
try {
|
||||||
|
const [txRows]: any = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT HEX(txid) as id, HEX(cluster) as root
|
||||||
|
FROM compact_transactions
|
||||||
|
WHERE txid = UNHEX(?)
|
||||||
|
`,
|
||||||
|
[txid]
|
||||||
|
);
|
||||||
|
if (txRows.length && txRows[0].root != null) {
|
||||||
|
const txid = txRows[0].id.toLowerCase();
|
||||||
|
const clusterId = txRows[0].root.toLowerCase();
|
||||||
|
const cluster = await cpfpRepository.$getCluster(clusterId);
|
||||||
|
return this.convertCpfp(txid, cluster);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -54,12 +80,23 @@ class TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertCpfp(cpfp: CpfpSummary): CpfpInfo {
|
public async $removeTransaction(txid: string): Promise<void> {
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
DELETE FROM compact_transactions
|
||||||
|
WHERE txid = UNHEX(?)
|
||||||
|
`,
|
||||||
|
[txid]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertCpfp(txid, cluster): CpfpInfo {
|
||||||
const descendants: Ancestor[] = [];
|
const descendants: Ancestor[] = [];
|
||||||
const ancestors: Ancestor[] = [];
|
const ancestors: Ancestor[] = [];
|
||||||
let matched = false;
|
let matched = false;
|
||||||
for (const tx of cpfp.txs) {
|
|
||||||
if (tx.txid === cpfp.txid) {
|
for (const tx of cluster.txs) {
|
||||||
|
if (tx.txid === txid) {
|
||||||
matched = true;
|
matched = true;
|
||||||
} else if (!matched) {
|
} else if (!matched) {
|
||||||
descendants.push(tx);
|
descendants.push(tx);
|
||||||
@ -70,7 +107,6 @@ class TransactionRepository {
|
|||||||
return {
|
return {
|
||||||
descendants,
|
descendants,
|
||||||
ancestors,
|
ancestors,
|
||||||
effectiveFeePerVsize: cpfp.fee_rate
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,9 @@ export class AddressLabelsComponent implements OnChanges {
|
|||||||
|
|
||||||
handleChannel() {
|
handleChannel() {
|
||||||
const type = this.vout ? 'open' : 'close';
|
const type = this.vout ? 'open' : 'close';
|
||||||
this.label = `Channel ${type}: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`;
|
const leftNodeName = this.channel.node_left.alias || this.channel.node_left.public_key.substring(0, 10);
|
||||||
|
const rightNodeName = this.channel.node_right.alias || this.channel.node_right.public_key.substring(0, 10);
|
||||||
|
this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleVin() {
|
handleVin() {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary">
|
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-purple">
|
||||||
<fa-icon *ngIf="!(isTypeaheading$ | async) else searchLoading" [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon>
|
<fa-icon *ngIf="!(isTypeaheading$ | async) else searchLoading" [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,9 +43,6 @@ form {
|
|||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
input {
|
|
||||||
border: 0px;
|
|
||||||
}
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
@ -131,26 +131,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.cpfpInfo = null;
|
this.cpfpInfo = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (cpfpInfo.effectiveFeePerVsize) {
|
// merge ancestors/descendants
|
||||||
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
|
||||||
} else {
|
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
||||||
const lowerFeeParents = cpfpInfo.ancestors.filter(
|
relatives.push(cpfpInfo.bestDescendant);
|
||||||
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
|
|
||||||
);
|
|
||||||
let totalWeight =
|
|
||||||
this.tx.weight +
|
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
|
||||||
let totalFees =
|
|
||||||
this.tx.fee +
|
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
|
||||||
|
|
||||||
if (cpfpInfo?.bestDescendant) {
|
|
||||||
totalWeight += cpfpInfo?.bestDescendant.weight;
|
|
||||||
totalFees += cpfpInfo?.bestDescendant.fee;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
|
||||||
}
|
}
|
||||||
|
let totalWeight =
|
||||||
|
this.tx.weight +
|
||||||
|
relatives.reduce((prev, val) => prev + val.weight, 0);
|
||||||
|
let totalFees =
|
||||||
|
this.tx.fee +
|
||||||
|
relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||||
|
|
||||||
|
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||||
|
|
||||||
if (!this.tx.status.confirmed) {
|
if (!this.tx.status.confirmed) {
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
||||||
|
@ -24,7 +24,6 @@ export interface CpfpInfo {
|
|||||||
ancestors: Ancestor[];
|
ancestors: Ancestor[];
|
||||||
descendants?: Ancestor[];
|
descendants?: Ancestor[];
|
||||||
bestDescendant?: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
effectiveFeePerVsize?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DifficultyAdjustment {
|
export interface DifficultyAdjustment {
|
||||||
|
@ -115,10 +115,38 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
color: #495057;
|
color: #fff;
|
||||||
|
background-color: #2d3348;
|
||||||
|
border: 1px solid rgba(17, 19, 31, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
color: #000;
|
color: #fff;
|
||||||
|
background-color: #2d3348;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-purple {
|
||||||
|
background-color: #653b9c;
|
||||||
|
border-color: #653b9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-purple:not(:disabled):not(.disabled):active, .btn-purple:not(:disabled):not(.disabled).active, .show > .btn-purple.dropdown-toggle {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #4d2d77;
|
||||||
|
border-color: #472a6e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-purple:focus, .btn-purple.focus {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #533180;
|
||||||
|
border-color: #4d2d77;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgb(124 88 171 / 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-purple:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #533180;
|
||||||
|
border-color: #4d2d77;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control.form-control-secondary {
|
.form-control.form-control-secondary {
|
||||||
|
@ -54,9 +54,13 @@ function downloadMiningPoolLogos() {
|
|||||||
|
|
||||||
response.on('end', () => {
|
response.on('end', () => {
|
||||||
let response_body = Buffer.concat(chunks_of_data);
|
let response_body = Buffer.concat(chunks_of_data);
|
||||||
const poolLogos = JSON.parse(response_body.toString());
|
try {
|
||||||
for (const poolLogo of poolLogos) {
|
const poolLogos = JSON.parse(response_body.toString());
|
||||||
download(`${PATH}/mining-pools/${poolLogo.name}`, poolLogo.download_url);
|
for (const poolLogo of poolLogos) {
|
||||||
|
download(`${PATH}/mining-pools/${poolLogo.name}`, poolLogo.download_url);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Unable to download mining pool logos. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user