Compare commits

..

4 Commits

Author SHA1 Message Date
Mononaut
79e2883ebe
update unfurler and build config 2024-07-26 14:17:55 +00:00
Mononaut
fdbca80920
check in new resources 2024-07-26 14:17:12 +00:00
Mononaut
64baade3b3
custom dashboard wallet widgets 2024-07-26 14:17:12 +00:00
Mononaut
4d06636d83
wallet tracking backend support 2024-07-26 14:17:07 +00:00
524 changed files with 14748 additions and 31741 deletions

View File

@ -251,7 +251,17 @@ jobs:
strategy:
fail-fast: false
matrix:
module: ["mempool", "liquid", "testnet4"]
module: ["mempool", "liquid"]
include:
- module: "mempool"
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet4/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
name: E2E tests for ${{ matrix.module }}
steps:
@ -300,10 +310,8 @@ jobs:
- 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 }}
@ -314,9 +322,7 @@ jobs:
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
spec: ${{ matrix.spec }}
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@ -326,56 +332,6 @@ jobs:
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:local-staging
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.pull_request.title }}
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:local-staging
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:
CYPRESS_REROUTE_TESTNET: true
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
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/')"
runs-on: "ubuntu-latest"
@ -403,4 +359,4 @@ jobs:
- name: Validate JSON syntax
run: |
cat mempool-config.json | jq
working-directory: docker/docker/backend
working-directory: docker/docker/backend

12
LICENSE
View File

@ -1,5 +1,5 @@
The Mempool Open Source Project®
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
@ -12,12 +12,10 @@ or any other contributor to The Mempool Open Source Project.
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
registered trademarks or trademarks of Mempool Space K.K in Japan,
the United States, and/or other countries.
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, and the mempool.space Horizontal logo are registered trademarks or trademarks
of Mempool Space K.K in Japan, the United States, and/or other countries.
See our full Trademark Policy and Guidelines for more details, published on
<https://mempool.space/trademark-policy>.

View File

@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build
_Make sure to use Node.js 20.x and npm 9.x or newer_
_Make sure to use Node.js 16.10 and npm 7._
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._

View File

@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
automock: false,
collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"],
coverageProvider: "v8",
coverageProvider: "babel",
coverageThreshold: {
global: {
lines: 1

View File

@ -27,9 +27,8 @@
"AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": false,
"RUST_GBT": true,
"RUST_GBT": false,
"LIMIT_GBT": false,
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6,
@ -46,8 +45,7 @@
"PASSWORD": "mempool",
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie",
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
@ -155,10 +153,6 @@
"API": "https://mempool.space/api/v1/services",
"ACCELERATIONS": false
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
},
"FIAT_PRICE": {
"ENABLED": true,
"PAID": false,

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "3.1.0-dev",
"version": "3.0.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@ -39,21 +39,24 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
},
"dependencies": {
"@babel/core": "^7.24.0",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
"axios": "~1.7.2",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.12.0",
"mysql2": "~3.10.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.7.0",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.24.0",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",

View File

@ -28,7 +28,6 @@
"INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": true,
"RUST_GBT": false,
"LIMIT_GBT": false,
@ -47,8 +46,7 @@
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000,
"COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@ -151,9 +149,5 @@
"ENABLED": true,
"PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
}
}

View File

@ -1,5 +1,5 @@
import { Common } from '../../api/common';
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json');
@ -10,14 +10,14 @@ describe('Common', () => {
describe('RBF', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
test('should detect RBF transactions with fast method', () => {
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
test('should detect RBF transactions with scalable method', () => {
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');

View File

@ -41,9 +41,8 @@ describe('Mempool Backend Config', () => {
STDOUT_LOG_MIN_PRIORITY: 'debug',
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
POOLS_UPDATE_DELAY: 604800,
AUDIT: false,
RUST_GBT: true,
RUST_GBT: false,
LIMIT_GBT: false,
CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0,
@ -74,8 +73,7 @@ describe('Mempool Backend Config', () => {
PASSWORD: 'mempool',
TIMEOUT: 60000,
COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie',
DEBUG_LOG_PATH: '',
COOKIE_PATH: '/bitcoin/.cookie'
});
expect(config.SECOND_CORE_RPC).toStrictEqual({
@ -159,11 +157,6 @@ describe('Mempool Backend Config', () => {
PAID: false,
API_KEY: '',
});
expect(config.STRATUM).toStrictEqual({
ENABLED: false,
API: 'http://localhost:1234',
});
});
});

View File

@ -70,7 +70,7 @@ class AboutRoutes {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => {
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });

View File

@ -2,28 +2,24 @@ import config from '../config';
import logger from '../logger';
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache';
import transactionUtils from './transaction-utils';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
class Audit {
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
: { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
}
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const unseen: string[] = []; // present in the mined block, not in our mempool
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
const isAccelerated = {};
let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
@ -36,7 +32,6 @@ class Audit {
inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
isAccelerated[tx.txid] = true;
}
}
// coinbase is always expected
@ -118,16 +113,11 @@ class Audit {
} else {
if (rbfCache.has(tx.txid)) {
rbf.push(tx.txid);
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) {
unseen.push(tx.txid);
}
} else {
} else if (!isDisplaced[tx.txid]) {
if (mempool[tx.txid]) {
if (isDisplaced[tx.txid]) {
added.push(tx.txid);
}
prioritized.push(tx.txid);
} else {
unseen.push(tx.txid);
added.push(tx.txid);
}
}
overflowWeight += tx.weight;
@ -135,8 +125,6 @@ class Audit {
totalWeight += tx.weight;
}
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
// transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
let maxOverflowRate = 0;
@ -177,7 +165,6 @@ class Audit {
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return {
unseen,
censored: Object.keys(isCensored),
added,
prioritized,

View File

@ -1,4 +1,4 @@
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
@ -23,7 +23,6 @@ export interface AbstractBitcoinApi {
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;

View File

@ -218,21 +218,3 @@ export interface TestMempoolAcceptResult {
},
['reject-reason']?: string,
}
export interface SubmitPackageResult {
package_msg: string;
"tx-results": { [wtxid: string]: TxResult };
"replaced-transactions"?: string[];
}
export interface TxResult {
txid: string;
"other-wtxid"?: string;
vsize?: number;
fees?: {
base: number;
"effective-feerate"?: number;
"effective-includes"?: string[];
};
error?: string;
}

View File

@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
@ -196,10 +196,6 @@ class BitcoinApi implements AbstractBitcoinApi {
}
}
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult> {
return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined);
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return {
@ -331,7 +327,6 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return'
};

View File

@ -1,11 +1,6 @@
import { Application, NextFunction, Request, Response } from 'express';
import logger from '../../logger';
import bitcoinClient from './bitcoin-client';
import config from '../../config';
const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i;
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
/**
* Define a set of routes used by the accelerator server
@ -14,26 +9,26 @@ const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
class BitcoinBackendRoutes {
private static tag = 'BitcoinBackendRoutes';
public initRoutes(app: Application): void {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
.get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
;
}
/**
* Disable caching for bitcoin core routes
*
* @param req
* @param res
* @param next
*
* @param req
* @param res
* @param next
*/
private disableCache(req: Request, res: Response, next: NextFunction): void {
res.setHeader('Pragma', 'no-cache');
@ -44,16 +39,16 @@ class BitcoinBackendRoutes {
/**
* Exeption handler to return proper details to the accelerator server
*
* @param e
* @param fnName
* @param res
*
* @param e
* @param fnName
* @param res
*/
private static handleException(e: any, fnName: string, res: Response): void {
if (typeof(e.code) === 'number') {
res.status(400).send(JSON.stringify(e, ['code']));
} else {
const err = `unknown exception in ${fnName}`;
res.status(400).send(JSON.stringify(e, ['code', 'message']));
} else {
const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`;
logger.err(err, BitcoinBackendRoutes.tag);
res.status(500).send(err);
}
@ -62,13 +57,13 @@ class BitcoinBackendRoutes {
private async $getMempoolEntry(req: Request, res: Response): Promise<void> {
const txid = req.query.txid;
try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return;
}
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
if (!mempoolEntry) {
res.status(404).send();
res.status(404).send(`no mempool entry found for txid ${txid}`);
return;
}
res.status(200).send(mempoolEntry);
@ -80,13 +75,13 @@ class BitcoinBackendRoutes {
private async $decodeRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx;
try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return;
}
const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!decodedTx) {
res.status(400).send(`unable to decode rawTx`);
res.status(400).send(`unable to decode rawTx ${rawTx}`);
return;
}
res.status(200).send(decodedTx);
@ -99,23 +94,23 @@ class BitcoinBackendRoutes {
const txid = req.query.txid;
const verbose = req.query.verbose;
try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return;
}
if (typeof(verbose) !== 'string') {
res.status(400).send(`invalid param verbose. must be a string representing an integer`);
res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`);
return;
}
const verboseNumber = parseInt(verbose, 10);
if (typeof(verboseNumber) !== 'number') {
res.status(400).send(`invalid param verbose. must be a valid integer`);
res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`);
return;
}
const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber);
if (!decodedTx) {
res.status(400).send(`unable to get raw transaction`);
res.status(400).send(`unable to get raw transaction for txid ${txid}`);
return;
}
res.status(200).send(decodedTx);
@ -127,13 +122,13 @@ class BitcoinBackendRoutes {
private async $sendRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx;
try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return;
}
const txHex = await bitcoinClient.sendRawTransaction(rawTx);
if (!txHex) {
res.status(400).send(`unable to send rawTx`);
res.status(400).send(`unable to send rawTx ${rawTx}`);
return;
}
res.status(200).send(txHex);
@ -145,13 +140,13 @@ class BitcoinBackendRoutes {
private async $testMempoolAccept(req: Request, res: Response): Promise<void> {
const rawTxs = req.body.rawTxs;
try {
if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) {
res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`);
if (typeof(rawTxs) !== 'object') {
res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`);
return;
}
const txHex = await bitcoinClient.testMempoolAccept(rawTxs);
if (typeof(txHex) !== 'object' || txHex.length === 0) {
res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`);
res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`);
return;
}
res.status(200).send(txHex);
@ -164,18 +159,18 @@ class BitcoinBackendRoutes {
const txid = req.query.txid;
const verbose = req.query.verbose;
try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return;
}
if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) {
res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`);
res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`);
return;
}
const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false);
if (!ancestors) {
res.status(400).send(`unable to get mempool ancestors`);
res.status(400).send(`unable to get mempool ancestors for txid ${txid}`);
return;
}
res.status(200).send(ancestors);
@ -188,23 +183,23 @@ class BitcoinBackendRoutes {
const blockHash = req.query.hash;
const verbosity = req.query.verbosity;
try {
if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) {
res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`);
if (typeof(blockHash) !== 'string' || blockHash.length !== 64) {
res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`);
return;
}
if (typeof(verbosity) !== 'string') {
res.status(400).send(`invalid param verbosity. must be a string representing an integer`);
res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`);
return;
}
const verbosityNumber = parseInt(verbosity, 10);
if (typeof(verbosityNumber) !== 'number') {
res.status(400).send(`invalid param verbosity. must be a valid integer`);
res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`);
return;
}
const block = await bitcoinClient.getBlock(blockHash, verbosityNumber);
if (!block) {
res.status(400).send(`unable to get block`);
res.status(400).send(`unable to get block for block hash ${blockHash}`);
return;
}
res.status(200).send(block);
@ -217,18 +212,18 @@ class BitcoinBackendRoutes {
const blockHeight = req.query.height;
try {
if (typeof(blockHeight) !== 'string') {
res.status(400).send(`invalid param blockHeight, must be a string representing an integer`);
res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`);
return;
}
const blockHeightNumber = parseInt(blockHeight, 10);
if (typeof(blockHeightNumber) !== 'number') {
res.status(400).send(`invalid param blockHeight. must be a valid integer`);
res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`);
return;
}
const block = await bitcoinClient.getBlockHash(blockHeightNumber);
if (!block) {
res.status(400).send(`unable to get block hash`);
res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`);
return;
}
res.status(200).send(block);
@ -251,4 +246,4 @@ class BitcoinBackendRoutes {
}
}
export default new BitcoinBackendRoutes;
export default new BitcoinBackendRoutes

View File

@ -20,13 +20,6 @@ import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
import poolsUpdater from '../../tasks/pools-updater';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i;
const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i;
class BitcoinRoutes {
public initRoutes(app: Application) {
@ -48,19 +41,12 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.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))
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
// Internal routes
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/list', this.getBlockDefinitionHashes)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/current', this.getCurrentBlockDefinitionHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/:definitionHash', this.getBlocksByDefinitionHash)
;
if (config.MEMPOOL.BACKEND !== 'esplora') {
@ -100,7 +86,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json');
res.send(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get init data');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -119,22 +105,19 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks();
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get mempool blocks');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) {
handleError(req, res, 500, 'Not an array');
res.status(500).send('Not an array');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
txIds.push(req.query.txId[_txId].toString());
}
}
@ -145,16 +128,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') {
handleError(req, res, 500, 'Invalid txids format');
res.status(500).send('Invalid txids format');
return;
}
const txids = txids_csv.split(',');
if (txids.length > 50) {
handleError(req, res, 400, 'Too many txids requested');
return;
}
if (txids.some((txid) => !TXID_REGEX.test(txid))) {
handleError(req, res, 400, 'Invalid txids format');
res.status(400).send('Too many txids requested');
return;
}
@ -162,13 +141,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends);
} catch (e) {
handleError(req, res, 500, 'Failed to get batched outspends');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getCpfpInfo(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`);
return;
}
@ -186,7 +165,6 @@ class BitcoinRoutes {
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined,
feeDelta: tx.feeDelta || undefined,
});
return;
}
@ -201,7 +179,7 @@ class BitcoinRoutes {
try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) {
handleError(req, res, 500, 'Failed to get CPFP info');
res.status(500).send('failed to get CPFP info');
return;
}
}
@ -222,10 +200,6 @@ class BitcoinRoutes {
}
private async getTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction);
@ -233,18 +207,12 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
handleError(req, res, statusCode, 'Failed to get transaction');
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
private async getRawTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain');
@ -253,10 +221,8 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
handleError(req, res, statusCode, 'Failed to get raw transaction');
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
@ -317,22 +283,18 @@ class BitcoinRoutes {
// Not modified
// 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
}
} catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
handleError(req, res, 404, notFoundError);
res.status(404).send(e.message);
} else {
handleError(req, res, 500, 'Failed to process PSBT');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
private async getTransactionStatus(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status);
@ -340,54 +302,22 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
handleError(req, res, statusCode, 'Failed to get transaction status');
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
private async getStrippedBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
} catch (e) {
handleError(req, res, 500, 'Failed to get block summary');
}
}
private async getStrippedBlockTransaction(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) {
handleError(req, res, 404, `Transaction not found in summary`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction);
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction from summary');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const block = await blocks.$getBlock(req.params.hash);
@ -399,69 +329,51 @@ class BitcoinRoutes {
} else if (blockAge > 30 * day) {
cacheDuration = 10 * day;
} else {
cacheDuration = 600;
cacheDuration = 600
}
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} catch (e) {
handleError(req, res, 500, 'Failed to get block');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockHeader(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain');
res.send(blockHeader);
} catch (e) {
handleError(req, res, 500, 'Failed to get block header');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `Audit not available`);
return;
return res.status(404).send(`audit not available`);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get block audit summary');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getBlockTxAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `Transaction audit not available`);
return;
return res.status(404).send(`transaction audit not available`);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction audit summary');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -475,49 +387,42 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get blocks');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlocksByBulk(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
return;
return res.status(404).send(`This API is only available for Bitcoin networks`);
}
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
return;
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
}
if (!Common.indexingEnabled()) {
handleError(req, res, 404, `Indexing is required for this API`);
return;
return res.status(404).send(`Indexing is required for this API`);
}
const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) {
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
return;
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
}
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) {
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
return;
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
}
if (from > to) {
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
return;
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
}
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
return;
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) {
handleError(req, res, 500, 'Failed to get blocks');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -552,15 +457,11 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} catch (e) {
handleError(req, res, 500, 'Failed to get blocks');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@ -581,7 +482,7 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
handleError(req, res, 500, 'Failed to get block transactions');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -590,17 +491,13 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash);
} catch (e) {
handleError(req, res, 500, 'Failed to get block at height');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@ -609,20 +506,15 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message);
return;
return res.status(413).send(e instanceof Error ? e.message : e);
}
handleError(req, res, 500, 'Failed to get address');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@ -635,27 +527,23 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message);
res.status(413).send(e instanceof Error ? e.message : e);
return;
}
handleError(req, res, 500, 'Failed to get address transactions');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') {
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
res.status(405).send('Address summary lookups require mempool/electrs backend.');
return;
}
}
private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@ -666,20 +554,15 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message);
return;
return res.status(413).send(e instanceof Error ? e.message : e);
}
handleError(req, res, 500, 'Failed to get script hash');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@ -694,26 +577,26 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message);
res.status(413).send(e instanceof Error ? e.message : e);
return;
}
handleError(req, res, 500, 'Failed to get script hash transactions');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') {
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
return;
}
}
private async getAddressPrefix(req: Request, res: Response) {
try {
const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(addressPrefix);
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash);
} catch (e) {
handleError(req, res, 500, 'Failed to get address prefix');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -740,53 +623,7 @@ class BitcoinRoutes {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlockDefinitionHashes(req: Request, res: Response): Promise<void> {
try {
const result = await blocks.$getBlockDefinitionHashes();
if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'application/json');
res.send(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getCurrentBlockDefinitionHash(req: Request, res: Response): Promise<void> {
try {
const currentSha = await poolsUpdater.getShaFromDb();
if (!currentSha) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'text/plain');
res.send(currentSha);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlocksByDefinitionHash(req: Request, res: Response): Promise<void> {
try {
if (typeof(req.params.definitionHash) !== 'string') {
res.status(400).send('Parameter "hash" must be a valid string');
return;
}
const blocksHash = await blocks.$getBlocksByDefinitionHash(req.params.definitionHash as string);
if (!blocksHash) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'application/json');
res.send(blocksHash);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -794,13 +631,12 @@ class BitcoinRoutes {
try {
const result = blocks.getCurrentBlockHeight();
if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
return res.status(503).send(`Service Temporarily Unavailable`);
}
res.setHeader('content-type', 'text/plain');
res.send(result.toString());
} catch (e) {
handleError(req, res, 500, 'Failed to get height at tip');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -810,55 +646,39 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get hash at tip');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getRawBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream');
res.send(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get raw block');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getTxIdsForBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get txids for block');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async validateAddress(req: Request, res: Response) {
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try {
const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to validate address');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getRbfHistory(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null;
const replaces = rbfCache.getReplaces(req.params.txId) || null;
@ -867,7 +687,7 @@ class BitcoinRoutes {
replaces
});
} catch (e) {
handleError(req, res, 500, 'Failed to get rbf history');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -876,7 +696,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get rbf trees');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -885,15 +705,11 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get full rbf replacements');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getCachedTx(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = rbfCache.getTx(req.params.txId);
if (result) {
@ -902,20 +718,16 @@ class BitcoinRoutes {
res.status(204).send();
}
} catch (e) {
handleError(req, res, 500, 'Failed to get cached tx');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getTransactionOutspends(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction outspends');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -925,10 +737,10 @@ class BitcoinRoutes {
if (da) {
res.json(da);
} else {
handleError(req, res, 503, `Service Temporarily Unavailable`);
res.status(503).send(`Service Temporarily Unavailable`);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get difficulty change');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -939,8 +751,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to send raw transaction');
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@ -951,8 +763,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to send raw transaction');
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@ -963,21 +775,9 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to test transactions');
}
}
private async $submitPackage(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const maxburnamount = parseFloat(req.query.maxburnamount as string);
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to submit package');
res.setHeader('content-type', 'text/plain');
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}

View File

@ -1,12 +1,12 @@
import config from '../../config';
import axios, { isAxiosError } from 'axios';
import axios, { AxiosResponse, isAxiosError } from 'axios';
import http from 'http';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
import { Common } from '../common';
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import os from 'os';
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
interface FailoverHost {
host: string,
rtts: number[],
@ -20,13 +20,6 @@ interface FailoverHost {
preferred?: boolean,
checked: boolean,
lastChecked?: number,
publicDomain: string,
hashes: {
frontend?: string,
backend?: string,
electrs?: string,
lastUpdated: number,
}
}
class FailoverRouter {
@ -36,21 +29,14 @@ class FailoverRouter {
maxHeight: number = 0;
hosts: FailoverHost[];
multihost: boolean;
gitHashInterval: number = 600000; // 10 minutes
pollInterval: number = 60000; // 1 minute
pollInterval: number = 60000;
pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create();
localHostname: string = 'localhost';
requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true })
});
constructor() {
try {
this.localHostname = os.hostname();
} catch (e) {
logger.warn('Failed to set local hostname, using "localhost"');
}
// setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return {
@ -59,10 +45,6 @@ class FailoverRouter {
rtts: [],
rtt: Infinity,
failures: 0,
publicDomain: 'https://' + this.extractPublicDomain(domain),
hashes: {
lastUpdated: 0,
},
};
});
this.activeHost = {
@ -73,10 +55,6 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true,
checked: false,
publicDomain: `http://${this.localHostname}`,
hashes: {
lastUpdated: 0,
},
};
this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost);
@ -128,24 +106,6 @@ class FailoverRouter {
host.outOfSync = false;
}
host.unreachable = false;
// update esplora git hash using the x-powered-by header from the height check
const poweredBy = result.headers['x-powered-by'];
if (poweredBy) {
const match = poweredBy.match(/([a-fA-F0-9]{5,40})/);
if (match && match[1]?.length) {
host.hashes.electrs = match[1];
}
}
// Check front and backend git hashes less often
if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
await Promise.all([
this.$updateFrontendGitHash(host),
this.$updateBackendGitHash(host)
]);
host.hashes.lastUpdated = Date.now();
}
} else {
host.outOfSync = true;
host.unreachable = true;
@ -242,47 +202,6 @@ class FailoverRouter {
}
}
// methods for retrieving git hashes by host
private async $updateFrontendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/resources/config.js`;
const response = await this.pollConnection.get<string>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/);
if (match && match[1]?.length) {
host.hashes.frontend = match[1];
}
} catch (e) {
// failed to get frontend build hash - do nothing
}
}
private async $updateBackendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/api/v1/backend-info`;
const response = await this.pollConnection.get<any>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
if (response.data?.gitCommit) {
host.hashes.backend = response.data.gitCommit;
}
} catch (e) {
// failed to get backend build hash - do nothing
}
}
// returns the public mempool domain corresponding to an esplora server url
// (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server)
private extractPublicDomain(url: string): string {
// force the url to start with a valid protocol
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
// parse as URL and extract the hostname
try {
const parsed = new URL(urlWithProtocol);
return parsed.hostname;
} catch (e) {
// fallback to the original url
return url;
}
}
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
let axiosConfig;
let url;
@ -386,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$getAddress(address: string): Promise<IEsploraApi.Address> {
return this.failoverRouter.$get<IEsploraApi.Address>('/address/' + address);
throw new Error('Method getAddress not implemented.');
}
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
@ -413,10 +332,6 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.');
}
$submitPackage(rawTransactions: string[]): Promise<SubmitPackageResult> {
throw new Error('Method not implemented.');
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
}
@ -462,7 +377,6 @@ class ElectrsApi implements AbstractBitcoinApi {
unreachable: !!host.unreachable,
checked: !!host.checked,
lastChecked: host.lastChecked || 0,
hashes: host.hashes,
}));
} else {
return [];

View File

@ -33,8 +33,6 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import database from '../database';
class Blocks {
private blocks: BlockExtended[] = [];
@ -220,10 +218,10 @@ class Blocks {
};
}
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
return {
id: hash,
transactions: Common.classifyTransactions(transactions, height),
transactions: Common.classifyTransactions(transactions),
};
}
@ -343,12 +341,7 @@ class Blocks {
id: pool.uniqueId,
name: pool.name,
slug: pool.slug,
minerNames: null,
};
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
}
extras.matchRate = null;
@ -412,16 +405,8 @@ class Blocks {
}
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
if (indexingBlockAmount <= -1) {
indexingBlockAmount = currentBlockHeight + 1;
}
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
// Get all indexed block hash
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
@ -454,7 +439,7 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
@ -630,7 +615,7 @@ class Blocks {
// add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@ -667,7 +652,7 @@ class Blocks {
}
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx;
@ -919,14 +904,9 @@ class Blocks {
}
}
let accelerations = Object.values(mempool.getAccelerations());
if (accelerations?.length > 0) {
const pool = await this.$findBlockMiner(transactionUtils.stripCoinbaseTransaction(transactions[0]));
accelerations = accelerations.filter(a => a.pools.includes(pool.uniqueId));
}
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) {
@ -947,12 +927,12 @@ class Blocks {
const newBlock = await this.$indexBlock(lastBlock.height - i);
this.blocks.push(newBlock);
this.updateTimerProgress(timer, `reindexed block`);
let newCpfpSummary;
let cpfpSummary;
if (config.MEMPOOL.CPFP_INDEXING) {
newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block cpfp`);
}
await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height);
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
this.updateTimerProgress(timer, `reindexed block summary`);
}
await mining.$indexDifficultyAdjustments();
@ -1001,7 +981,7 @@ class Blocks {
// start async callbacks
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
if (block.height % 2016 === 0) {
if (Common.indexingEnabled()) {
@ -1183,7 +1163,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = Common.getTransactionFlags(tx, height);
flags = Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
@ -1198,11 +1178,11 @@ class Blocks {
};
}),
};
summaryVersion = cpfpSummary.version;
summaryVersion = 1;
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
summary = this.summarizeBlockTransactions(hash, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@ -1224,11 +1204,6 @@ class Blocks {
return summary.transactions;
}
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
const txs = await this.$getStrippedBlockTransactions(hash);
return txs.find(tx => tx.txid === txid) || null;
}
/**
* Get 15 blocks
*
@ -1343,7 +1318,7 @@ class Blocks {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@ -1422,11 +1397,11 @@ class Blocks {
return this.currentBlockHeight;
}
public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> {
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
let transactions = txs;
if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') {
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
}
if (!transactions) {
const block = await bitcoinClient.getBlock(hash, 2);
@ -1438,7 +1413,7 @@ class Blocks {
}
if (transactions?.length != null) {
const summary = calculateFastBlockCpfp(height, transactions);
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
await this.$saveCpfp(hash, height, summary);
@ -1462,36 +1437,6 @@ class Blocks {
// not a fatal error, we'll try again next time the indexer runs
}
}
public async $getBlockDefinitionHashes(): Promise<string[] | null> {
try {
const [rows]: any = await database.query(`SELECT DISTINCT(definition_hash) FROM 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)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`);
return null;
}
}
public async $getBlocksByDefinitionHash(definitionHash: string): Promise<string[] | null> {
try {
const [rows]: any = await database.query(`SELECT hash FROM blocks WHERE definition_hash = ?`, [definitionHash]);
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)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
return null;
}
}
}
export default new Blocks();

View File

@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express';
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
@ -10,6 +10,7 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@ -79,8 +80,8 @@ export class Common {
return arr;
}
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
@ -95,7 +96,7 @@ export class Common {
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
});
if (foundMatches?.length) {
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
matches[addedTx.txid] = [...new Set(foundMatches)];
}
});
} else {
@ -123,7 +124,7 @@ export class Common {
foundMatches.add(deletedTx);
}
if (foundMatches.size) {
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
matches[addedTx.txid] = [...foundMatches];
}
}
}
@ -138,17 +139,17 @@ export class Common {
const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i];
const key = `${vin.txid}:${vin.vout}`;
const match = spendMap.get(key);
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
if (match && match.txid !== tx.txid) {
replaced.add(match);
// remove this tx from the spendMap
// prevents the same tx being replaced more than once
for (const replacedVin of match.vin) {
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(replacedKey);
const key = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(key);
}
}
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key);
}
if (replaced.size) {
@ -199,13 +200,10 @@ export class Common {
*
* returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
static isNonStandard(tx: TransactionExtended): boolean {
// version
if (this.isNonStandardVersion(tx, height)) {
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
@ -252,8 +250,6 @@ export class Common {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
}
// TODO: bad-witness-nonstandard
}
@ -339,49 +335,6 @@ export class Common {
return false;
}
// Individual versioned standardness rules
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
if (
height != null
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;
@ -462,7 +415,7 @@ export class Common {
return flags;
}
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
static getTransactionFlags(tx: TransactionExtended): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
@ -595,7 +548,7 @@ export class Common {
if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey;
}
// fast but bad heuristic to detect possible coinjoins
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
@ -611,17 +564,17 @@ export class Common {
flags |= TransactionFlags.batch_payout;
}
if (this.isNonStandard(tx, height)) {
if (this.isNonStandard(tx)) {
flags |= TransactionFlags.nonstandard;
}
return Number(flags);
}
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
let flags = 0;
try {
flags = Common.getTransactionFlags(tx, height);
flags = Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
@ -632,8 +585,8 @@ export class Common {
};
}
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
return txs.map(tx => Common.classifyTransaction(tx, height));
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
return txs.map(Common.classifyTransaction);
}
static stripTransaction(tx: TransactionExtended): TransactionStripped {

View File

@ -6,7 +6,7 @@ import { Acceleration } from './acceleration/acceleration';
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
const MAX_CLUSTER_ITERATIONS = 100;
export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
@ -93,7 +93,6 @@ export function calculateFastBlockCpfp(height: number, transactions: MempoolTran
return {
transactions,
clusters,
version: 1,
};
}
@ -160,7 +159,6 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
return {
transactions: transactions.map(tx => txMap[tx.txid]),
clusters: clusterArray,
version: 2,
};
}

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 95;
private static currentVersion = 80;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@ -653,11 +653,9 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
if (isBitcoin === true) {
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
}
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
await this.updateToSchemaVersion(75);
}
@ -693,443 +691,6 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
await this.updateToSchemaVersion(80);
}
if (databaseSchemaVersion < 81 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81);
}
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
await this.$fixBadV1AuditBlocks();
await this.updateToSchemaVersion(82);
}
if (databaseSchemaVersion < 83 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
await this.updateToSchemaVersion(83);
}
// add new pools indexes
if (databaseSchemaVersion < 84 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
await this.updateToSchemaVersion(84);
}
// lightning channels indexes
if (databaseSchemaVersion < 85 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
await this.updateToSchemaVersion(85);
}
// lightning nodes indexes
if (databaseSchemaVersion < 86 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
await this.updateToSchemaVersion(86);
}
// lightning node sockets indexes
if (databaseSchemaVersion < 87 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
}
// lightning stats indexes
if (databaseSchemaVersion < 88 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
await this.updateToSchemaVersion(88);
}
// geo names indexes
if (databaseSchemaVersion < 89 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
await this.updateToSchemaVersion(89);
}
// hashrates indexes
if (databaseSchemaVersion < 90 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(90);
}
// block audits indexes
if (databaseSchemaVersion < 91 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
await this.updateToSchemaVersion(91);
}
// elements_pegs indexes
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
await this.updateToSchemaVersion(92);
}
// federation_txos indexes
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
await this.updateToSchemaVersion(93);
}
// Unify database schema for all mempool netwoks
// versions above 94 should not use network-specific flags
if (databaseSchemaVersion < 94) {
if (!isBitcoin) {
// Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!)
// Version 5
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
// Version 6
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
// Version 7
await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
// Version 8
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
// Version 9
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
// Version 10
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
// Version 11
await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL
`);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
// Version 12
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 13
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 14
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 17
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
// Version 18
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
// Version 20
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
// Version 22
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
// Version 24
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
// Version 25
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
// Version 26
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
// Version 27
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
// Version 28
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
// Version 29
await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names'));
await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
// Version 30
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
// Version 31
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
// Version 32
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
// Version 33
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
// Version 34
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
// Version 35
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
// Version 36
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
// Version 37
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
// Version 38
await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
await this.updateToSchemaVersion(38);
// Version 39
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
// Version 40
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
// Version 41
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
// Version 42
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
// Version 43
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
// Version 44
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
// Version 45
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
// Version 48
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
// Version 57
await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
// Version 60
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
// Version 61
if (! await this.$checkIfTableExists('blocks_templates')) {
await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
}
await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
// Version 62
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
// Version 63
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
// Version 64
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
// Version 65
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
// Version 67
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
// Version 76
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
// Version 81
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
// Version 83
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
// Version 84
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
// Version 85
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
// Version 86
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
// Version 87
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
// Version 88
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
// Version 89
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
// Version 90
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
// Version 91
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
}
if (config.MEMPOOL.NETWORK !== 'liquid') {
// Apply all the liquid specific migrations to all other networks
// Version 68
await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
// Version 71
await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0');
// Version 92
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
// Version 93
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
}
if (config.MEMPOOL.NETWORK !== 'mainnet') {
// Apply all the mainnet specific migrations to all other networks
// Version 69
await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
// Version 70
await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
// Version 77
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
}
await this.updateToSchemaVersion(94);
}
// blocks pools-v2.json hash
if (databaseSchemaVersion < 95) {
let poolJsonSha = 'f737d86571d190cf1a1a3cf5fd86b33ba9624254';
const [poolJsonShaDb]: any[] = await DB.query(`SELECT string FROM state WHERE name = 'pools_json_sha'`);
if (poolJsonShaDb?.length > 0) {
poolJsonSha = poolJsonShaDb[0].string;
}
await this.$executeQuery(`ALTER TABLE blocks ADD definition_hash varchar(255) NOT NULL DEFAULT "${poolJsonSha}"`);
await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
await this.updateToSchemaVersion(95);
}
}
/**
@ -1744,28 +1305,6 @@ class DatabaseMigration {
logger.warn(`Failed to migrate cpfp transaction data`);
}
}
private async $fixBadV1AuditBlocks(): Promise<void> {
const badBlocks = [
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
];
for (const hash of badBlocks) {
try {
await this.$executeQuery(`
UPDATE blocks_audits
SET prioritized_txs = '[]'
WHERE hash = '${hash}'
`, true);
} catch (e) {
continue;
}
}
}
}
export default new DatabaseMigration();

View File

@ -257,7 +257,6 @@ class DiskCache {
trees: rbfData.rbf.trees,
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
});
}
} catch (e) {

View File

@ -1,9 +1,6 @@
import config from '../../config';
import { Application, Request, Response } from 'express';
import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
class ChannelsRoutes {
constructor() { }
@ -25,7 +22,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels);
} catch (e) {
handleError(req, res, 500, 'Failed to search channels by id');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -41,7 +38,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel);
} catch (e) {
handleError(req, res, 500, 'Failed to get channel');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -56,11 +53,11 @@ class ChannelsRoutes {
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) {
handleError(req, res, 400, 'Invalid index');
res.status(400).send('Invalid index');
return;
}
if (['open', 'active', 'closed'].includes(status) === false) {
handleError(req, res, 400, 'Invalid status');
res.status(400).send('Invalid status');
return;
}
@ -72,23 +69,20 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, 'Failed to get channels for node');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try {
if (!Array.isArray(req.query.txId)) {
handleError(req, res, 400, 'Not an array');
res.status(400).send('Not an array');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
txIds.push(req.query.txId[_txId].toString());
}
}
const channels = await channelsApi.$getChannelsByTransactionId(txIds);
@ -113,7 +107,7 @@ class ChannelsRoutes {
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get channels by transaction ids');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -125,7 +119,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, 'Failed to get penalty closed channels');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -138,7 +132,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, 'Failed to get channel geodata');
res.status(500).send(e instanceof Error ? e.message : e);
}
}

View File

@ -3,8 +3,6 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import channelsApi from './channels.api';
import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes {
constructor() { }
@ -29,7 +27,7 @@ class GeneralLightningRoutes {
channels: channels,
});
} catch (e) {
handleError(req, res, 500, 'Failed to search for nodes and channels');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -43,7 +41,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
handleError(req, res, 500, 'Failed to get lightning statistics');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -52,7 +50,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics);
} catch (e) {
handleError(req, res, 500, 'Failed to get lightning statistics');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@ -3,7 +3,6 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes {
constructor() { }
@ -32,7 +31,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes);
} catch (e) {
handleError(req, res, 500, 'Failed to search for node');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -182,13 +181,13 @@ class NodesRoutes {
}
} catch (e) {}
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get node group');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -196,7 +195,7 @@ class NodesRoutes {
try {
const node = await nodesApi.$getNode(req.params.public_key);
if (!node) {
handleError(req, res, 404, 'Node not found');
res.status(404).send('Node not found');
return;
}
res.header('Pragma', 'public');
@ -204,7 +203,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
handleError(req, res, 500, 'Failed to get node');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -216,7 +215,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical node stats');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -224,7 +223,7 @@ class NodesRoutes {
try {
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) {
handleError(req, res, 404, 'Node not found');
res.status(404).send('Node not found');
return;
}
res.header('Pragma', 'public');
@ -232,7 +231,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
handleError(req, res, 500, 'Failed to get fee histogram');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -248,7 +247,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes,
});
} catch (e) {
handleError(req, res, 500, 'Failed to get nodes ranking');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -260,7 +259,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get top nodes by capacity');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -272,7 +271,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get top nodes by channels');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -284,7 +283,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get oldest nodes');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -296,7 +295,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
handleError(req, res, 500, 'Failed to get ISP ranking');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -308,7 +307,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get world nodes');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -323,7 +322,7 @@ class NodesRoutes {
);
if (country.length === 0) {
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
return;
}
@ -336,7 +335,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
handleError(req, res, 500, 'Failed to get nodes per country');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -350,7 +349,7 @@ class NodesRoutes {
);
if (isp.length === 0) {
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
return;
}
@ -363,7 +362,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
handleError(req, res, 500, 'Failed to get nodes per ISP');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -375,7 +374,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
handleError(req, res, 500, 'Failed to get nodes per country');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@ -3,7 +3,6 @@ import { Application, Request, Response } from 'express';
import config from '../../config';
import elementsParser from './elements-parser';
import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes {
public initRoutes(app: Application) {
@ -43,7 +42,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length);
res.send(result);
} else {
handleError(req, res, 404, 'Asset icon not found');
res.status(404).send('Asset icon not found');
}
}
@ -52,7 +51,7 @@ class LiquidRoutes {
if (result) {
res.json(result);
} else {
handleError(req, res, 404, 'Asset icons not found');
res.status(404).send('Asset icons not found');
}
}
@ -83,7 +82,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs by month');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -95,7 +94,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves);
} catch (e) {
handleError(req, res, 500, 'Failed to get reserves by month');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -107,7 +106,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -119,7 +118,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves);
} catch (e) {
handleError(req, res, 500, 'Failed to get reserves');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -131,7 +130,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation audit status');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -143,7 +142,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation addresses');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -155,7 +154,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation addresses');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -167,7 +166,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation utxos');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -179,7 +178,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get expired utxos');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -191,7 +190,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation utxos number');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -203,7 +202,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get emergency spent utxos');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -215,7 +214,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get emergency spent utxos stats');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -227,7 +226,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs list');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -239,7 +238,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs volume daily');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -251,7 +250,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs count');
res.status(500).send(e instanceof Error ? e.message : e);
}
}

View File

@ -369,7 +369,7 @@ class MempoolBlocks {
const lastBlockIndex = blocks.length - 1;
let hasBlockStack = blocks.length >= 8;
let stackWeight;
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
if (hasBlockStack) {
if (blockWeights && blockWeights[7] !== null) {
stackWeight = blockWeights[7];
@ -380,36 +380,28 @@ class MempoolBlocks {
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
}
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended;
for (const cluster of clusters) {
for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid];
if (mempoolTx) {
// ugly micro-optimization to avoid allocating new arrays
ancestors.length = 0;
descendants.length = 0;
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(txid => {
ancestor = mempool[txid];
if (txid === memberTxid) {
matched = true;
} else {
if (!ancestor) {
if (!mempool[txid]) {
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
return;
}
const relative = {
txid: txid,
fee: ancestor.fee,
weight: (ancestor.adjustedVsize * 4),
fee: mempool[txid].fee,
weight: (mempool[txid].adjustedVsize * 4),
};
if (matched) {
descendants.push(relative);
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
mempoolTx.lastBoosted = ancestor.firstSeen;
}
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
} else {
ancestors.push(relative);
}
@ -418,20 +410,7 @@ class MempoolBlocks {
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
// ugly micro-optimization to avoid allocating new arrays or objects
if (mempoolTx.ancestors) {
mempoolTx.ancestors.length = 0;
} else {
mempoolTx.ancestors = [];
}
if (mempoolTx.descendants) {
mempoolTx.descendants.length = 0;
} else {
mempoolTx.descendants = [];
}
mempoolTx.ancestors.push(...ancestors);
mempoolTx.descendants.push(...descendants);
mempoolTx.cpfpChecked = true;
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
}
}
}
@ -441,10 +420,7 @@ class MempoolBlocks {
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended;
let acceleration: Acceleration;
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block = blocks[blockIndex];
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
let totalSize = 0;
let totalVsize = 0;
let totalWeight = 0;
@ -460,9 +436,8 @@ class MempoolBlocks {
}
}
for (let i = 0; i < block.length; i++) {
const txid = block[i];
if (txid in mempool) {
for (const txid of block) {
if (txid) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
@ -470,40 +445,28 @@ class MempoolBlocks {
vsize: totalVsize + (mempoolTx.vsize / 2),
};
if (txid in accelerations) {
acceleration = accelerations[txid];
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!(ancestor.txid in mempool)) {
continue;
}
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
const acceleration = accelerations[txid];
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
}
delete mempoolTx.acceleration;
}
// online calculation of stack-of-blocks fee stats
@ -521,7 +484,7 @@ class MempoolBlocks {
}
}
}
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
return this.dataToMempoolBlocks(
block,
transactions,
totalSize,
@ -529,7 +492,7 @@ class MempoolBlocks {
totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
);
};
});
if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
@ -691,7 +654,7 @@ class MempoolBlocks {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
} = {};
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4);

View File

@ -10,7 +10,6 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import redisCache from './redis-cache';
import blocks from './blocks';
@ -20,13 +19,12 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {};
private spendMap = new Map<string, MempoolTransactionExtended>();
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
@ -76,12 +74,12 @@ class Mempool {
}
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn;
}
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn;
}
@ -208,7 +206,7 @@ class Mempool {
return txTimes;
}
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes
@ -355,7 +353,7 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
if (accelerationDelta.length) {
hasChange = true;
}
@ -364,15 +362,12 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
this.recentlyDeleted.unshift(deletedTransactions);
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
}
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
this.updateTimerProgress(timer, 'completed async mempool callback');
}
@ -400,11 +395,58 @@ class Mempool {
return this.accelerations;
}
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
try {
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
this.accelerations[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
this.accelerations = newAccelerationMap;
return accelerationDelta;
return changed;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];
@ -499,7 +541,16 @@ class Mempool {
}
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
// Store replaced transactions
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
}
}
}
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions

View File

@ -337,7 +337,7 @@ export function makeBlockTemplate(candidates: MempoolTransactionExtended[], acce
let failures = 0;
while (mempoolArray.length || modified.length) {
// skip invalid transactions
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
while (mempoolArray[0].used || mempoolArray[0].modified) {
mempoolArray.shift();
}

View File

@ -10,7 +10,6 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes {
public initRoutes(app: Application) {
@ -54,12 +53,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Prices are not available on testnets.');
res.status(400).send('Prices are not available on testnets.');
return;
}
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string;
let response;
if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@ -72,7 +71,7 @@ class MiningRoutes {
}
res.status(200).send(response);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical prices');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -85,9 +84,9 @@ class MiningRoutes {
res.json(stats);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
res.status(404).send(e.message);
} else {
handleError(req, res, 500, 'Failed to get pool');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
@ -104,9 +103,9 @@ class MiningRoutes {
res.json(poolBlocks);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
res.status(404).send(e.message);
} else {
handleError(req, res, 500, 'Failed to get blocks for pool');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
@ -130,7 +129,7 @@ class MiningRoutes {
res.json(pools);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get pools');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -144,7 +143,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
handleError(req, res, 500, 'Failed to get pools');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -158,7 +157,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
handleError(req, res, 500, 'Failed to get pools historical hashrate');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -173,9 +172,9 @@ class MiningRoutes {
res.json(hashrates);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
res.status(404).send(e.message);
} else {
handleError(req, res, 500, 'Failed to get pool historical hashrate');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
@ -183,7 +182,7 @@ class MiningRoutes {
private async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0;
try {
currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
currentHashrate = await bitcoinClient.getNetworkHashPs();
currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
@ -204,7 +203,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty,
});
} catch (e) {
handleError(req, res, 500, 'Failed to get historical hashrate');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -218,7 +217,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block fees');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -236,7 +235,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block fees');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -250,7 +249,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block rewards');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -264,7 +263,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block fee rates');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -282,7 +281,7 @@ class MiningRoutes {
weights: blockWeights
});
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block size and weight');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -294,7 +293,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) {
handleError(req, res, 500, 'Failed to get historical difficulty adjustments');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -304,7 +303,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response);
} catch (e) {
handleError(req, res, 500, 'Failed to get reward stats');
res.status(500).end();
}
}
@ -318,7 +317,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) {
handleError(req, res, 500, 'Failed to get historical blocks health');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -327,7 +326,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) {
handleError(req, res, 204, `This block has not been audited.`);
res.status(204).send(`This block has not been audited.`);
return;
}
@ -336,7 +335,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit);
} catch (e) {
handleError(req, res, 500, 'Failed to get block audit');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -359,7 +358,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get height from timestamp');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -372,7 +371,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) {
handleError(req, res, 500, 'Failed to get block audit scores');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -385,7 +384,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null');
} catch (e) {
handleError(req, res, 500, 'Failed to get block audit score');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -395,12 +394,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) {
handleError(req, res, 500, 'Failed to get accelerations by pool');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -410,13 +409,13 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) {
handleError(req, res, 500, 'Failed to get accelerations by height');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -426,12 +425,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) {
handleError(req, res, 500, 'Failed to get recent accelerations');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -441,12 +440,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) {
handleError(req, res, 500, 'Failed to get acceleration totals');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@ -456,24 +455,28 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
res.status(200).send(accelerationApi.accelerations || []);
} catch (e) {
handleError(req, res, 500, 'Failed to get active accelerations');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS || config.MEMPOOL.OFFICIAL) {
res.status(405).send('not available.');
return;
}
res.setHeader('Pragma', 'no-cache');
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
res.setHeader('expires', -1);
try {
accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send();
res.status(200).send('ok');
} catch (e) {
handleError(req, res, 500, 'Failed to request acceleration');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@ -136,13 +136,9 @@ class Mining {
poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
} catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);

View File

@ -19,6 +19,15 @@ class PoolsParser {
'addresses': '[]',
'slug': 'unknown'
};
private uniqueLogs: string[] = [];
private uniqueLog(loggerFunction: any, msg: string): void {
if (this.uniqueLogs.includes(msg)) {
return;
}
this.uniqueLogs.push(msg);
loggerFunction(msg);
}
public setMiningPools(pools): void {
for (const pool of pools) {

View File

@ -1,15 +1,10 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes {
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this))
;
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
}
private $getCurrentPrices(req: Request, res: Response): void {
@ -19,23 +14,6 @@ class PricesRoutes {
res.json(pricesUpdater.getLatestPrices());
}
private async $getAllPrices(req: Request, res: Response): Promise<void> {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
try {
const usdPriceHistory = await PricesRepository.$getPricesTimesAndId();
const responseData = usdPriceHistory.map(p => {
return { time: p.time, USD: p.USD };
});
res.status(200).json(responseData);
} catch (e: any) {
logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`);
res.status(403).send();
}
}
}
export default new PricesRoutes();

View File

@ -44,22 +44,6 @@ interface CacheEvent {
value?: any,
}
/**
* Singleton for tracking RBF trees
*
* Maintains a set of RBF trees, where each tree represents a sequence of
* consecutive RBF replacements.
*
* Trees are identified by the txid of the root transaction.
*
* To maintain consistency, the following invariants must be upheld:
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
* - Existence: X in treeMap => treeMap(X) in rbfTrees
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
*/
class RbfCache {
private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map();
@ -77,10 +61,6 @@ class RbfCache {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
/**
* Low level cache operations
*/
private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
@ -112,18 +92,8 @@ class RbfCache {
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
}
/**
* Basic data structure operations
* must uphold tree invariants
*/
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if ( !newTxExtended
|| !replaced?.length
|| this.txs.has(newTxExtended.txid)
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
) {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
return;
}
@ -144,10 +114,6 @@ class RbfCache {
if (!replacedTx.rbf) {
txFullRbf = true;
}
if (this.replacedBy.has(replacedTx.txid)) {
// should never happen
continue;
}
this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid);
@ -174,47 +140,18 @@ class RbfCache {
}
}
newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid;
const newTree = {
tx: newTx,
time: newTime,
fullRbf: treeFullRbf,
replaces: replacedTrees
};
this.addTree(newTree.tx.txid, newTree);
this.updateTreeMap(newTree.tx.txid, newTree);
this.addTree(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
/**
* Read-only public interface
*/
public has(txId: string): boolean {
return this.txs.has(txId);
}
@ -295,6 +232,32 @@ class RbfCache {
return changes;
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
// is the transaction involved in a full rbf replacement?
public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid);
@ -308,10 +271,6 @@ class RbfCache {
return tree?.fullRbf;
}
/**
* Cache maintenance & utility functions
*/
private cleanup(): void {
const now = Date.now();
for (const txid of this.expiring.keys()) {
@ -340,6 +299,10 @@ class RbfCache {
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache
this.replacedBy.delete(tx);
// if this is the id of a tree, remove that too
if (this.treeMap.get(tx) === tx) {
this.removeTree(tx);
}
this.remove(tx);
}
}
@ -407,21 +370,14 @@ class RbfCache {
};
}
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
public async load({ txs, trees, expiring, mempool }): Promise<void> {
try {
txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value);
});
this.staleCount = 0;
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
if (tree) {
this.addTree(tree.tx.txid, tree);
this.updateTreeMap(tree.tx.txid, tree);
if (tree.mined) {
this.evict(tree.tx.txid);
}
}
for (const deflatedTree of trees) {
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
@ -429,31 +385,6 @@ class RbfCache {
}
});
this.staleCount = 0;
// connect cached trees to current mempool transactions
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
for (const tree of this.rbfTrees.values()) {
const tx = this.getTx(tree.tx.txid);
if (!tx || tree.mined) {
continue;
}
for (const vin of tx.vin) {
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
if (conflict && conflict.txid !== tx.txid) {
if (!conflicts[conflict.txid]) {
conflicts[conflict.txid] = {
replacedBy: conflict,
replaces: new Set(),
};
}
conflicts[conflict.txid].replaces.add(tx);
}
}
}
for (const { replacedBy, replaces } of Object.values(conflicts)) {
this.add([...replaces.values()], replacedBy);
}
await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup();
@ -495,12 +426,6 @@ class RbfCache {
return;
}
// if this tx is already in the cache, return early
if (this.treeMap.has(txid)) {
this.removeTree(deflated.key);
return;
}
// recursively reconstruct child trees
for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
@ -532,6 +457,10 @@ class RbfCache {
fullRbf: treeInfo.fullRbf,
replaces,
};
this.treeMap.set(txid, root);
if (root === txid) {
this.addTree(root, tree);
}
return tree;
}
@ -582,7 +511,6 @@ class RbfCache {
processTxs(txs);
}
// evict missing transactions
for (const txid of txids) {
if (!found[txid]) {
this.evict(txid, false);

View File

@ -365,7 +365,6 @@ class RedisCache {
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
expiring: rbfExpirations,
mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
});
}

View File

@ -1,10 +1,7 @@
import { WebSocket } from 'ws';
import config from '../../config';
import logger from '../../logger';
import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios';
import mempool from '../mempool';
import websocketHandler from '../websocket-handler';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
@ -40,23 +37,13 @@ export interface AccelerationHistory {
};
class AccelerationApi {
private ws: WebSocket | null = null;
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
private _accelerations: Record<string, Acceleration> = {};
private _accelerations: Acceleration[] | null = null;
private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public constructor() {}
public getAccelerations(): Record<string, Acceleration> {
public get accelerations(): Acceleration[] | null {
return this._accelerations;
}
@ -65,9 +52,7 @@ class AccelerationApi {
}
public accelerationRequested(txid: string): void {
if (this.onDemandPollingEnabled) {
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
}
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
}
public accelerationConfirmed(): void {
@ -84,18 +69,11 @@ class AccelerationApi {
}
}
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
if (this.useWebsocket && this.websocketConnected) {
return this._accelerations;
}
if (!this.onDemandPollingEnabled) {
public async $updateAccelerations(): Promise<Acceleration[] | null> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
const accelerations = await this.$fetchAccelerations();
if (accelerations) {
const latestAccelerations = {};
for (const acc of accelerations) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
this._accelerations = accelerations;
return this._accelerations;
}
} else {
@ -104,7 +82,7 @@ class AccelerationApi {
return null;
}
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
@ -139,11 +117,7 @@ class AccelerationApi {
}
}
const latestAccelerations = {};
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
return this._accelerations;
}
@ -175,148 +149,6 @@ class AccelerationApi {
}
return anyAccelerated;
}
// get a list of accelerations that have changed between two sets of accelerations
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
const changed: string[] = [];
const mempoolCache = mempool.getMempool();
for (const acceleration of Object.values(newAccelerationMap)) {
// skip transactions we don't know about
if (!mempoolCache[acceleration.txid]) {
continue;
}
if (oldAccelerationMap[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(oldAccelerationMap)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
return changed;
}
private handleWebsocketMessage(msg: any): void {
if (msg?.accelerations !== null) {
const latestAccelerations = {};
for (const acc of msg?.accelerations || []) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
websocketHandler.handleAccelerationsChanged(this._accelerations);
}
}
public async connectWebsocket(): Promise<void> {
if (this.startedWebsocketLoop) {
return;
}
while (this.useWebsocket) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(this.websocketPath);
this.lastPing = 0;
this.ws.on('open', () => {
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
this.websocketConnected = true;
this.ws?.send(JSON.stringify({
'watch-accelerations': true
}));
});
this.ws.on('error', (error) => {
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
if (error['errors']) {
errMsg += ' - ' + error['errors'].join(' - ');
}
logger.err(errMsg);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Acceleration websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const msg = (isBinary ? data : data.toString()) as string;
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
}
});
this.ws.on('ping', () => {
logger.debug('received ping from acceleration websocket server');
});
this.ws.on('pong', () => {
logger.debug('received pong from acceleration websocket server');
this.lastPong = Date.now();
});
} else if (this.websocketConnected) {
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
logger.warn('No pong received within 10 seconds, terminating connection');
try {
this.ws?.terminate();
} catch (e) {
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
} finally {
this.ws = null;
this.websocketConnected = false;
this.lastPing = 0;
}
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
logger.debug('sending ping to acceleration websocket server');
if (this.ws?.readyState === WebSocket.OPEN) {
try {
this.ws?.ping();
this.lastPing = Date.now();
} catch (e) {
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
}
}
}
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new AccelerationApi();

View File

@ -1,7 +1,6 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import WalletApi from './wallets';
import { handleError } from '../../utils/api';
class ServicesRoutes {
public initRoutes(app: Application): void {
@ -19,7 +18,7 @@ class ServicesRoutes {
const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet);
} catch (e) {
handleError(req, res, 500, 'Failed to get wallet');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@ -1,105 +0,0 @@
import { WebSocket } from 'ws';
import logger from '../../logger';
import config from '../../config';
import websocketHandler from '../websocket-handler';
export interface StratumJob {
pool: number;
height: number;
coinbase: string;
scriptsig: string;
reward: number;
jobId: string;
extraNonce: string;
extraNonce2Size: number;
prevHash: string;
coinbase1: string;
coinbase2: string;
merkleBranches: string[];
version: string;
bits: string;
time: string;
timestamp: number;
cleanJobs: boolean;
received: number;
}
function isStratumJob(obj: any): obj is StratumJob {
return obj
&& typeof obj === 'object'
&& 'pool' in obj
&& 'prevHash' in obj
&& 'height' in obj
&& 'received' in obj
&& 'version' in obj
&& 'timestamp' in obj
&& 'bits' in obj
&& 'merkleBranches' in obj
&& 'cleanJobs' in obj;
}
class StratumApi {
private ws: WebSocket | null = null;
private runWebsocketLoop: boolean = false;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private jobs: Record<string, StratumJob> = {};
public constructor() {}
public getJobs(): Record<string, StratumJob> {
return this.jobs;
}
private handleWebsocketMessage(msg: any): void {
if (isStratumJob(msg)) {
this.jobs[msg.pool] = msg;
websocketHandler.handleNewStratumJob(this.jobs[msg.pool]);
}
}
public async connectWebsocket(): Promise<void> {
if (!config.STRATUM.ENABLED) {
return;
}
this.runWebsocketLoop = true;
if (this.startedWebsocketLoop) {
return;
}
while (this.runWebsocketLoop) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(`${config.STRATUM.API}`);
this.websocketConnected = true;
this.ws.on('open', () => {
logger.info('Stratum websocket opened');
});
this.ws.on('error', (error) => {
logger.err('Stratum websocket error: ' + error);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Stratum websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e));
}
});
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new StratumApi();

View File

@ -8,60 +8,56 @@ import { TransactionExtended } from '../../mempool.interfaces';
interface WalletAddress {
address: string;
active: boolean;
stats: {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
};
transactions: IEsploraApi.AddressTxSummary[];
lastSync: number;
transactions?: IEsploraApi.AddressTxSummary[];
}
interface Wallet {
interface WalletConfig {
url: string;
name: string;
apiKey: string;
}
interface Wallet extends WalletConfig {
addresses: Record<string, WalletAddress>;
lastPoll: number;
}
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
const POLL_FREQUENCY = 60 * 60 * 1000; // 1 hour
class WalletApi {
private wallets: Record<string, Wallet> = {};
private syncing = false;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
this.wallets = (config.WALLETS.WALLETS as WalletConfig[]).reduce((acc, wallet) => {
acc[wallet.name] = { ...wallet, addresses: {}, lastPoll: 0 };
return acc;
}, {} as Record<string, Wallet>) : {};
}, {} as Record<string, Wallet>);
}
public getWallet(wallet: string): Record<string, WalletAddress> {
return this.wallets?.[wallet]?.addresses || {};
}
// resync wallet addresses from the services backend
// resync wallet addresses from the provided API
async $syncWallets(): Promise<void> {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true;
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`);
const addresses: Record<string, WalletAddress> = response.data;
const addressList: WalletAddress[] = Object.values(addresses);
const response = await axios.get(`${wallet.url}/${wallet.name}`, { headers: { 'Authorization': `${wallet.apiKey}` } });
const data: { walletBalances: WalletAddress[] } = response.data;
const addresses = data.walletBalances;
const newAddresses: Record<string, boolean> = {};
// sync all current addresses
for (const address of addressList) {
for (const address of addresses) {
await this.$syncWalletAddress(wallet, address);
newAddresses[address.address] = true;
}
// remove old addresses
for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) {
if (!newAddresses[address]) {
delete wallet.addresses[address];
}
}
@ -77,19 +73,16 @@ class WalletApi {
// resync address transactions from esplora
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> {
// fetch full transaction data if the address is new or still active and hasn't been synced in the last hour
const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000);
// fetch full transaction data if the address is new or still active
const refreshTransactions = !wallet.addresses[address.address] || address.active;
if (refreshTransactions) {
try {
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
const addressInfo = await bitcoinApi.$getAddress(address.address);
const walletAddress: WalletAddress = {
address: address.address,
active: address.active,
transactions: summary,
stats: addressInfo.chain_stats,
lastSync: Date.now(),
transactions: await bitcoinApi.$getAddressTransactionSummary(address.address),
};
logger.debug(`Synced ${walletAddress.transactions?.length || 0} transactions for wallet ${wallet.name} address ${address.address}`);
wallet.addresses[address.address] = walletAddress;
} catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
@ -98,51 +91,36 @@ class WalletApi {
}
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> {
const walletTransactions: Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> = {};
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
walletTransactions[walletKey] = [];
walletTransactions[walletKey] = {};
for (const tx of blockTxs) {
const funded: Record<string, number> = {};
const spent: Record<string, number> = {};
const fundedCount: Record<string, number> = {};
const spentCount: Record<string, number> = {};
let anyMatch = false;
for (const vin of tx.vin) {
const address = vin.prevout?.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
spentCount[address] = (spentCount[address] ?? 0) + 1;
}
}
for (const vout of tx.vout) {
const address = vout.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
}
}
for (const address of Object.keys({ ...funded, ...spent })) {
// update address stats
wallet.addresses[address].stats.tx_count++;
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0;
wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0;
wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0;
// add tx to summary
const txSummary: IEsploraApi.AddressTxSummary = {
if (!walletTransactions[walletKey][address]) {
walletTransactions[walletKey][address] = [];
}
walletTransactions[walletKey][address].push({
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height,
time: block.timestamp,
};
wallet.addresses[address].transactions?.push(txSummary);
}
if (anyMatch) {
walletTransactions[walletKey].push(tx);
});
}
}
}

View File

@ -1,7 +1,7 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import statisticsApi from './statistics-api';
import { handleError } from '../../utils/api';
class StatisticsRoutes {
public initRoutes(app: Application) {
app
@ -65,7 +65,7 @@ class StatisticsRoutes {
}
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get statistics');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@ -121,7 +121,6 @@ class TransactionUtils {
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid),
vsize,
@ -129,7 +128,7 @@ class TransactionUtils {
sigops,
feePerVsize: feePerVbytes,
adjustedFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: effectiveFeePerVsize,
effectiveFeePerVsize: adjustedFeePerVsize,
});
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
@ -339,87 +338,6 @@ class TransactionUtils {
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
// calculate the most parsimonious set of prioritizations given a list of block transactions
// (i.e. the most likely prioritizations and deprioritizations)
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
// find the longest increasing subsequence of transactions
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
// should be O(n log n)
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
if (X.length < 2) {
return { prioritized: [], deprioritized: [] };
}
const N = X.length;
const P: number[] = new Array(N);
const M: number[] = new Array(N + 1);
M[0] = -1; // undefined so can be set to any value
let L = 0;
for (let i = 0; i < N; i++) {
// Binary search for the smallest positive l ≤ L
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
let lo = 1;
let hi = L + 1;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
if (X[M[mid]].rate > X[i].rate) {
hi = mid;
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
lo = mid + 1;
}
}
// After searching, lo == hi is 1 greater than the
// length of the longest prefix of X[i]
const newL = lo;
// The predecessor of X[i] is the last index of
// the subsequence of length newL-1
P[i] = M[newL - 1];
M[newL] = i;
if (newL > L) {
// If we found a subsequence longer than any we've
// found yet, update L
L = newL;
}
}
// Reconstruct the longest increasing subsequence
// It consists of the values of X at the L indices:
// ..., P[P[M[L]]], P[M[L]], M[L]
const LIS: any[] = new Array(L);
let k = M[L];
for (let j = L - 1; j >= 0; j--) {
LIS[j] = X[k];
k = P[k];
}
const lisMap = new Map<string, number>();
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
const prioritized: string[] = [];
const deprioritized: string[] = [];
let lastRate = X[0].rate;
for (const tx of X) {
if (lisMap.has(tx.txid)) {
lastRate = tx.rate;
} else {
if (Math.abs(tx.rate - lastRate) < 0.1) {
// skip if the rate is almost the same as the previous transaction
} else if (tx.rate <= lastRate) {
prioritized.push(tx.txid);
} else {
deprioritized.push(tx.txid);
}
}
}
return { prioritized, deprioritized };
}
}
export default new TransactionUtils();

View File

@ -3,7 +3,7 @@ import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolDelta, MempoolDeltaTxids
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@ -16,13 +16,11 @@ import transactionUtils from './transaction-utils';
import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api';
import BlocksRepository from '../repositories/BlocksRepository';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import Audit from './audit';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
import statistics from './statistics/statistics';
@ -37,8 +35,6 @@ interface AddressTransactions {
}
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
import stratumApi, { StratumJob } from './services/stratum';
// valid 'want' subscriptions
const wantable = [
@ -62,8 +58,6 @@ class WebsocketHandler {
private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0;
private accelerations: Record<string, Acceleration> = {};
constructor() { }
addWebsocketServer(wss: WebSocket.Server) {
@ -404,16 +398,6 @@ class WebsocketHandler {
delete client['track-mempool'];
}
if (parsedMessage && parsedMessage['track-stratum'] != null) {
if (parsedMessage['track-stratum']) {
const sub = parsedMessage['track-stratum'];
client['track-stratum'] = sub;
response['stratumJobs'] = this.socketData['stratumJobs'];
} else {
client['track-stratum'] = false;
}
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
@ -509,42 +493,6 @@ class WebsocketHandler {
}
}
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server has been set');
}
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
if (!websocketAccelerationDelta.length) {
return;
}
// pre-compute acceleration delta
const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
try {
const response = JSON.stringify({
accelerations: accelerationUpdate,
});
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
} catch (e) {
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
}
}
handleReorg(): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@ -581,17 +529,8 @@ class WebsocketHandler {
}
}
/**
*
* @param newMempool
* @param mempoolSize
* @param newTransactions array of transactions added this mempool update.
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
* @param accelerationDelta
* @param candidates
*/
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@ -599,8 +538,6 @@ class WebsocketHandler {
this.printLogs();
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
let added = newTransactions;
let removed = deletedTransactions;
@ -619,9 +556,9 @@ class WebsocketHandler {
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = accelerationApi.getAccelerations();
const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
@ -650,7 +587,7 @@ class WebsocketHandler {
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid].replaced) {
for (const replaced of rbfTransactions[tx.txid]) {
replacedTransactions.push({ replaced: replaced.txid, by: tx });
}
}
@ -729,13 +666,10 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
// pre-compute acceleration delta
const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged
@ -898,7 +832,6 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
@ -940,7 +873,6 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
if (!mempoolTx.cpfpChecked) {
calculateMempoolTxCpfp(mempoolTx, newMempool);
@ -1008,8 +940,6 @@ class WebsocketHandler {
throw new Error('No WebSocket.Server have been set');
}
const blockTransactions = structuredClone(transactions);
this.printLogs();
await statistics.runStatistics();
@ -1019,10 +949,10 @@ class WebsocketHandler {
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));
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions);
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleRbfTransactions(rbfTransactions);
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
@ -1039,7 +969,7 @@ class WebsocketHandler {
}
if (Common.indexingEnabled()) {
const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@ -1061,11 +991,9 @@ class WebsocketHandler {
});
BlocksAuditsRepository.$saveAudit({
version: 1,
time: block.timestamp,
height: block.height,
hash: block.id,
unseenTxs: unseen,
addedTxs: added,
prioritizedTxs: prioritized,
missingTxs: censored,
@ -1092,14 +1020,6 @@ class WebsocketHandler {
}
}
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
const firstSeen = getRecentFirstSeen(block.id);
if (firstSeen) {
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
block.extras.firstSeen = firstSeen;
}
}
const confirmedTxids: { [txid: string]: boolean } = {};
// Update mempool to remove transactions included in the new block
@ -1230,7 +1150,6 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
@ -1253,7 +1172,6 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
}
}
@ -1395,23 +1313,6 @@ class WebsocketHandler {
await statistics.runStatistics();
}
public handleNewStratumJob(job: StratumJob): void {
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
client.send(JSON.stringify({
'stratumJob': job
}));
}
});
}
}
// takes a dictionary of JSON serialized values
// and zips it together into a valid JSON object
private serializeResponse(response): string {

View File

@ -32,7 +32,6 @@ interface IConfig {
AUTOMATIC_POOLS_UPDATE: boolean;
POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string,
POOLS_UPDATE_DELAY: number,
AUDIT: boolean;
RUST_GBT: boolean;
LIMIT_GBT: boolean;
@ -86,7 +85,6 @@ interface IConfig {
TIMEOUT: number;
COOKIE: boolean;
COOKIE_PATH: string;
DEBUG_LOG_PATH: string;
};
SECOND_CORE_RPC: {
HOST: string;
@ -164,11 +162,11 @@ interface IConfig {
},
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
},
STRATUM: {
ENABLED: boolean;
API: string;
WALLETS: {
url: string;
name: string;
apiKey: string;
}[];
}
}
@ -202,9 +200,8 @@ const defaults: IConfig = {
'AUTOMATIC_POOLS_UPDATE': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
'AUDIT': false,
'RUST_GBT': true,
'RUST_GBT': false,
'LIMIT_GBT': false,
'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0,
@ -236,8 +233,7 @@ const defaults: IConfig = {
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie',
'DEBUG_LOG_PATH': '',
'COOKIE_PATH': '/bitcoin/.cookie'
},
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',
@ -336,10 +332,6 @@ const defaults: IConfig = {
'ENABLED': false,
'WALLETS': [],
},
'STRATUM': {
'ENABLED': false,
'API': 'http://localhost:1234',
}
};
class Config implements IConfig {
@ -362,7 +354,6 @@ class Config implements IConfig {
REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
STRATUM: IConfig['STRATUM'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@ -385,7 +376,6 @@ class Config implements IConfig {
this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
this.STRATUM = configs.STRATUM;
}
merge = (...objects: object[]): IConfig => {

View File

@ -48,7 +48,6 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
import stratumApi from './api/services/stratum';
class Server {
private wss: WebSocket.Server | undefined;
@ -214,8 +213,6 @@ class Server {
}
});
}
poolsUpdater.$startService();
}
async runMainUpdateLoop(): Promise<void> {
@ -234,11 +231,11 @@ class Server {
const newMempool = await bitcoinApi.$getRawMempool();
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
const latestAccelerations = await accelerationApi.$updateAccelerations();
const newAccelerations = await accelerationApi.$updateAccelerations();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
}
indexer.$run();
if (config.WALLETS.ENABLED) {
@ -319,18 +316,11 @@ class Server {
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
}
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
if (config.STRATUM.ENABLED) {
stratumApi.connectWebsocket();
}
}
setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app);
if (config.MEMPOOL.OFFICIAL) {
bitcoinCoreRoutes.initRoutes(this.app);
}
bitcoinCoreRoutes.initRoutes(this.app);
pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app);

View File

@ -10,7 +10,6 @@ import config from './config';
import auditReplicator from './replication/AuditReplication';
import statisticsReplicator from './replication/StatisticsReplication';
import AccelerationRepository from './repositories/AccelerationRepository';
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
export interface CoreIndex {
name: string;
@ -193,7 +192,6 @@ class Indexer {
await auditReplicator.$sync();
await statisticsReplicator.$sync();
await AccelerationRepository.$indexPastAccelerations();
await BlocksAuditsRepository.$migrateAuditsV0toV1();
// do not wait for classify blocks to finish
blocks.$classifyBlocks();
} catch (e) {

View File

@ -29,11 +29,9 @@ export interface PoolStats extends PoolInfo {
}
export interface BlockAudit {
version: number,
time: number,
height: number,
hash: string,
unseenTxs: string[],
missingTxs: string[],
freshTxs: string[],
sigopTxs: string[],
@ -128,7 +126,6 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
replacement?: boolean;
uid?: number;
flags?: number;
@ -299,7 +296,6 @@ export interface BlockExtension {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
name: string;
slug: string;
minerNames: string[] | null;
};
avgFee: number;
avgFeeRate: number;
@ -320,13 +316,10 @@ export interface BlockExtension {
segwitTotalSize: number;
segwitTotalWeight: number;
header: string;
firstSeen: number | null;
utxoSetChange: number;
// Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null;
totalInputAmt: number | null;
// pools-v2.json git hash
definitionHash: string | undefined;
}
/**
@ -389,9 +382,8 @@ export interface CpfpCluster {
}
export interface CpfpSummary {
transactions: MempoolTransactionExtended[];
transactions: TransactionExtended[];
clusters: CpfpCluster[];
version: number;
}
export interface Statistic {
@ -457,7 +449,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo {
replacedBy?: string,
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
cpfp?: {
ancestors?: Ancestor[],
bestDescendant?: Ancestor | null,
@ -470,7 +462,6 @@ export interface TxTrackingInfo {
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
feeDelta?: number,
confirmed?: boolean
}

View File

@ -31,11 +31,11 @@ class AuditReplication {
const missingAudits = await this.$getMissingAuditBlocks();
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
let totalSynced = 0;
let totalMissed = 0;
let loggerTimer = Date.now();
// process missing audits in batches of BATCH_SIZE
// process missing audits in batches of
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
const slice = missingAudits.slice(i, i + BATCH_SIZE);
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
@ -109,11 +109,9 @@ class AuditReplication {
version: 1,
});
await blocksAuditsRepository.$saveAudit({
version: auditSummary.version || 0,
hash: blockHash,
height: auditSummary.height,
time: auditSummary.timestamp || auditSummary.time,
unseenTxs: auditSummary.unseenTxs || [],
missingTxs: auditSummary.missingTxs || [],
addedTxs: auditSummary.addedTxs || [],
prioritizedTxs: auditSummary.prioritizedTxs || [],

View File

@ -192,7 +192,6 @@ class AccelerationRepository {
}
}
// modifies block transactions
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of transactions) {

View File

@ -1,24 +1,13 @@
import blocks from '../api/blocks';
import DB from '../database';
import logger from '../logger';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import { BlockAudit, AuditScore, TransactionAudit, TransactionStripped } from '../mempool.interfaces';
interface MigrationAudit {
version: number,
height: number,
id: string,
timestamp: number,
prioritizedTxs: string[],
acceleratedTxs: string[],
template: TransactionStripped[],
transactions: TransactionStripped[],
}
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> {
try {
await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs),
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
@ -73,30 +62,24 @@ class BlocksAuditRepositories {
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
try {
const [rows]: any[] = await DB.query(
`SELECT
blocks_audits.version,
blocks_audits.height,
blocks_audits.hash as id,
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
template,
unseen_txs as unseenTxs,
missing_txs as missingTxs,
added_txs as addedTxs,
prioritized_txs as prioritizedTxs,
fresh_txs as freshTxs,
sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs,
accelerated_txs as acceleratedTxs,
match_rate as matchRate,
expected_fees as expectedFees,
expected_weight as expectedWeight
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
template,
missing_txs as missingTxs,
added_txs as addedTxs,
prioritized_txs as prioritizedTxs,
fresh_txs as freshTxs,
sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs,
accelerated_txs as acceleratedTxs,
match_rate as matchRate,
expected_fees as expectedFees,
expected_weight as expectedWeight
FROM blocks_audits
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.hash = ?
`, [hash]);
if (rows.length) {
rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
@ -118,7 +101,7 @@ class BlocksAuditRepositories {
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
try {
const blockAudit = await this.$getBlockAudit(hash);
if (blockAudit) {
const isAdded = blockAudit.addedTxs.includes(txid);
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
@ -132,17 +115,16 @@ class BlocksAuditRepositories {
firstSeen = tx.time;
}
});
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return {
seen: wasSeen,
seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected,
added: isAdded && (blockAudit.version === 0 || !wasSeen),
added: isAdded,
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
firstSeen,
};
}
}
return null;
} catch (e: any) {
@ -204,96 +186,6 @@ class BlocksAuditRepositories {
throw e;
}
}
/**
* [INDEXING] Migrate audits from v0 to v1
*/
public async $migrateAuditsV0toV1(): Promise<void> {
try {
let done = false;
let processed = 0;
let lastHeight;
while (!done) {
const [toMigrate]: MigrationAudit[][] = await DB.query(
`SELECT
blocks_audits.height as height,
blocks_audits.hash as id,
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
blocks_summaries.transactions as transactions,
blocks_templates.template as template,
blocks_audits.prioritized_txs as prioritizedTxs,
blocks_audits.accelerated_txs as acceleratedTxs
FROM blocks_audits
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.version = 0
AND blocks_summaries.version = 2
ORDER BY blocks_audits.height DESC
LIMIT 100
`) as any[];
if (toMigrate.length <= 0 || lastHeight === toMigrate[0].height) {
done = true;
break;
}
lastHeight = toMigrate[0].height;
logger.info(`migrating ${toMigrate.length} audits to version 1`);
for (const audit of toMigrate) {
// unpack JSON-serialized transaction lists
audit.transactions = JSON.parse((audit.transactions as any as string) || '[]');
audit.template = JSON.parse((audit.template as any as string) || '[]');
// we know transactions in the template, or marked "prioritized" or "accelerated"
// were seen in our mempool before the block was mined.
const isSeen = new Set<string>();
for (const tx of audit.template) {
isSeen.add(tx.txid);
}
for (const txid of audit.prioritizedTxs) {
isSeen.add(txid);
}
for (const txid of audit.acceleratedTxs) {
isSeen.add(txid);
}
const unseenTxs = audit.transactions.slice(0).map(tx => tx.txid).filter(txid => !isSeen.has(txid));
// identify "prioritized" transactions
const prioritizedTxs: string[] = [];
let lastEffectiveRate = 0;
// Iterate over the mined template from bottom to top (excluding the coinbase)
// Transactions should appear in ascending order of mining priority.
for (let i = audit.transactions.length - 1; i > 0; i--) {
const blockTx = audit.transactions[i];
// If a tx has a lower in-band effective fee rate than the previous tx,
// it must have been prioritized out-of-band (in order to have a higher mining priority)
// so exclude from the analysis.
if ((blockTx.rate || 0) < lastEffectiveRate) {
prioritizedTxs.push(blockTx.txid);
} else {
lastEffectiveRate = blockTx.rate || 0;
}
}
// Update audit in the database
await DB.query(`
UPDATE blocks_audits SET
version = ?,
unseen_txs = ?,
prioritized_txs = ?
WHERE hash = ?
`, [1, JSON.stringify(unseenTxs), JSON.stringify(prioritizedTxs), audit.id]);
}
processed += toMigrate.length;
}
logger.info(`migrated ${processed} audits to version 1`);
} catch (e: any) {
logger.err(`Error while migrating audits from v0 to v1. Will try again later. Reason: ` + (e instanceof Error ? e.message : e));
}
}
}
export default new BlocksAuditRepositories();

View File

@ -14,8 +14,6 @@ import chainTips from '../api/chain-tips';
import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import poolsUpdater from '../tasks/pools-updater';
interface DatabaseBlock {
id: string;
@ -58,7 +56,6 @@ interface DatabaseBlock {
utxoSetChange: number;
utxoSetSize: number;
totalInputAmt: number;
firstSeen: number;
}
const BLOCK_DB_FIELDS = `
@ -101,8 +98,7 @@ const BLOCK_DB_FIELDS = `
blocks.header,
blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt,
UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen
blocks.total_input_amt AS totalInputAmt
`;
class BlocksRepository {
@ -115,16 +111,16 @@ class BlocksRepository {
try {
const query = `INSERT INTO blocks(
height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii, definition_hash
height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii
) VALUE (
?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?,
@ -135,7 +131,7 @@ class BlocksRepository {
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?
?, ?
)`;
const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id);
@ -182,7 +178,6 @@ class BlocksRepository {
block.extras.segwitTotalWeight,
block.extras.medianFeeAmt,
truncatedCoinbaseSignatureAscii,
poolsUpdater.currentSha
];
await DB.query(query, params);
@ -503,7 +498,7 @@ class BlocksRepository {
}
query += ` ORDER BY height DESC
LIMIT 100`;
LIMIT 10`;
try {
const [rows]: any[] = await DB.query(query, params);
@ -1015,9 +1010,9 @@ class BlocksRepository {
public async $savePool(id: string, poolId: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET pool_id = ?, definition_hash = ?
UPDATE blocks SET pool_id = ?
WHERE hash = ?`,
[poolId, poolsUpdater.currentSha, id]
[poolId, id]
);
} catch (e) {
logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));
@ -1025,24 +1020,6 @@ class BlocksRepository {
}
}
/**
* Save block first seen time
*
* @param id
*/
public async $saveFirstSeenTime(id: string, firstSeen: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET first_seen = FROM_UNIXTIME(?)
WHERE hash = ?`,
[firstSeen, id]
);
} catch (e) {
logger.err(`Cannot update block first seen time. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Convert a mysql row block into a BlockExtended. Note that you
* must provide the correct field into dbBlk object param
@ -1077,7 +1054,6 @@ class BlocksRepository {
id: dbBlk.poolId,
name: dbBlk.poolName,
slug: dbBlk.poolSlug,
minerNames: null,
};
extras.avgFee = dbBlk.avgFee;
extras.avgFeeRate = dbBlk.avgFeeRate;
@ -1100,7 +1076,6 @@ class BlocksRepository {
extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0;
extras.firstSeen = dbBlk.firstSeen;
// Re-org can happen after indexing so we need to always get the
// latest state from core
@ -1131,7 +1106,7 @@ class BlocksRepository {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@ -1148,10 +1123,6 @@ class BlocksRepository {
}
}
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
blk.extras = <BlockExtension>extras;
return <BlockExtended>blk;
}

View File

@ -83,7 +83,6 @@ module.exports = {
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
stop: 'stop',
submitBlock: 'submitblock', // bitcoind v0.7.0+
submitPackage: 'submitpackage',
validateAddress: 'validateaddress',
verifyChain: 'verifychain', // bitcoind v0.9.0+
verifyMessage: 'verifymessage',

View File

@ -6,30 +6,16 @@ import backendInfo from '../api/backend-info';
import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https';
import { Common } from '../api/common';
/**
* Maintain the most recent version of pools-v2.json
*/
class PoolsUpdater {
tag = 'PoolsUpdater';
lastRun: number = 0;
currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async $startService(): Promise<void> {
while ('Bitcoin is still alive') {
try {
await this.updatePoolsJson();
} catch (e: any) {
logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag);
}
await Common.sleep$(10000);
}
}
public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false
@ -37,8 +23,11 @@ class PoolsUpdater {
return;
}
const oneWeek = 604800;
const oneDay = 86400;
const now = new Date().getTime() / 1000;
if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart
if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart
return;
}
@ -54,7 +43,7 @@ class PoolsUpdater {
this.currentSha = await this.getShaFromDb();
}
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag);
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
if (this.currentSha !== null && this.currentSha === githubSha) {
return;
}
@ -64,16 +53,16 @@ class PoolsUpdater {
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool
) {
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag);
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag);
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`);
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
return;
}
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === null) {
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag);
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} else {
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag);
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
}
const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) {
@ -82,24 +71,24 @@ class PoolsUpdater {
poolsParser.setMiningPools(poolsJson);
if (config.DATABASE.ENABLED === false) { // Don't run db operations
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag);
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`);
return;
}
try {
await DB.query('START TRANSACTION;');
await this.updateDBSha(githubSha);
await poolsParser.migratePoolsJson();
await this.updateDBSha(githubSha);
await DB.query('COMMIT;');
} catch (e) {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag);
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
await DB.query('ROLLBACK;');
}
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
} catch (e) {
this.lastRun = now - 600; // Try again in 10 minutes
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag);
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
}
}
@ -113,7 +102,7 @@ class PoolsUpdater {
await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) {
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
}
}
}
@ -121,12 +110,12 @@ class PoolsUpdater {
/**
* Fetch our latest pools-v2.json sha from the db
*/
public async getShaFromDb(): Promise<string | null> {
private async getShaFromDb(): Promise<string | null> {
try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null);
} catch (e) {
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
return null;
}
}
@ -145,7 +134,7 @@ class PoolsUpdater {
}
}
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag);
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
return null;
}
@ -197,7 +186,7 @@ class PoolsUpdater {
}
return data.data;
} catch (e) {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
retry++;
}
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);

View File

@ -1,9 +0,0 @@
import { Request, Response } from 'express';
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
if (req.accepts('json')) {
res.status(statusCode).json({ error: errorMessage });
} else {
res.status(statusCode).send(errorMessage);
}
}

View File

@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opN) {
return;
}
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
if (!opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opM) {
return;
}
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
if (!opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
@ -200,28 +200,4 @@ export function getVarIntLength(n: number): number {
} else {
return 9;
}
}
/** Extracts miner names from a DATUM coinbase transaction */
export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null {
let bytes: number[] = [];
for (let c = 0; c < coinbaseRaw.length; c += 2) {
bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16));
}
// Skip block height
let tagLengthByte = 1 + bytes[0];
let tagsLength = bytes[tagLengthByte];
if (tagsLength == 0x4c) {
tagLengthByte += 1;
tagsLength = bytes[tagLengthByte];
}
const tagStart = tagLengthByte + 1;
const tags = bytes.slice(tagStart, tagStart + tagsLength);
let tagString = String.fromCharCode(...tags);
tagString = tagString.replace('\x00', '');
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
}

View File

@ -1,58 +0,0 @@
import * as fs from 'fs';
import logger from '../logger';
import config from '../config';
function readFile(filePath: string, bufferSize?: number): string[] {
const fileSize = fs.statSync(filePath).size;
const chunkSize = bufferSize || fileSize;
const fileDescriptor = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(chunkSize);
fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize);
fs.closeSync(fileDescriptor);
const lines = buffer.toString('utf8', 0, chunkSize).split('\n');
return lines;
}
function extractDateFromLogLine(line: string): number | undefined {
// Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z"
const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{6})?Z/);
if (!dateMatch) {
return undefined;
}
const dateStr = dateMatch[0];
const date = new Date(dateStr);
let timestamp = Math.floor(date.getTime() / 1000); // Remove decimal (microseconds are added later)
const timePart = dateStr.split('T')[1];
const microseconds = timePart.split('.')[1] || '';
if (!microseconds) {
return timestamp;
}
return parseFloat(timestamp + '.' + microseconds);
}
export function getRecentFirstSeen(hash: string): number | undefined {
const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH;
if (debugLogPath) {
try {
// Read the last few lines of debug.log
const lines = readFile(debugLogPath, 2048);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (line && line.includes(`Saw new header hash=${hash}`)) {
return extractDateFromLogLine(line);
}
}
} catch (e) {
logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e));
}
}
return undefined;
}

View File

@ -109,7 +109,6 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6,
@ -141,7 +140,6 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
MEMPOOL_POOLS_JSON_URL: ""
MEMPOOL_POOLS_JSON_TREE_URL: ""
MEMPOOL_POOLS_UPDATE_DELAY: ""
MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""

View File

@ -36,7 +36,6 @@
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
},
@ -47,8 +46,7 @@
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@ -148,10 +146,6 @@
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
},
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",

View File

@ -29,9 +29,8 @@ __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
__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}
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
@ -49,7 +48,6 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
# ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@ -146,13 +144,9 @@ __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=14819
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
@ -193,7 +187,6 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json
@ -212,7 +205,6 @@ sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__CORE_RPC_DEBUG_LOG_PATH__!${__CORE_RPC_DEBUG_LOG_PATH__}!g" mempool-config.json
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
@ -304,10 +296,6 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
# STRATUM
sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json
# REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json

View File

@ -11,14 +11,10 @@ services:
stop_grace_period: 1m
command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports:
- 8080:8080
- 80:8080
api:
environment:
MEMPOOL_BACKEND: "electrum"
ELECTRUM_HOST: "172.27.0.1"
ELECTRUM_PORT: "50001"
ELECTRUM_TLS_ENABLED: "false"
MEMPOOL_BACKEND: "none"
CORE_RPC_HOST: "172.27.0.1"
CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "mempool"

View File

@ -41,11 +41,10 @@ __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
__ACCELERATOR__=${ACCELERATOR:=false}
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__SERVICES_API__=${SERVICES_API:=false}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
# Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__
@ -77,7 +76,6 @@ export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__
export __STRATUM_ENABLED__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder}

View File

@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend
cp ./nginx.conf ./frontend/
cp ./nginx-mempool.conf ./frontend/
# sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
# sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
sed -i"" -e "s/user nobody;//g" ./frontend/nginx.conf
sed -i"" -e "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf

View File

@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
### 3. Run the Frontend
_Make sure to use Node.js 20.x and npm 9.x or newer._
_Make sure to use Node.js 16.10 and npm 7._
Install project dependencies and run the frontend server:
@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
### 1. Build the Frontend
_Make sure to use Node.js 20.x and npm 9.x or newer._
_Make sure to use Node.js 16.10 and npm 7._
Build the frontend:

View File

@ -54,10 +54,6 @@
"translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/"
},
"hr": {
"translation": "src/locale/messages.hr.xlf",
"baseHref": "/hr/"
},
"ja": {
"translation": "src/locale/messages.ja.xlf",
"baseHref": "/ja/"

View File

@ -1,51 +0,0 @@
{
"theme": "contrast",
"enterprise": "meta",
"branding": {
"name": "metaplanet",
"title": "Metaplanet",
"site_id": 21,
"header_img": "/resources/metalogo.svg",
"footer_img": "/resources/metalogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "3350"
}
},
{
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "Metaplanet_JP"
}
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "3350",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "3350"
}
}
]
}
}

View File

@ -344,9 +344,7 @@ describe('Mainnet', () => {
cy.visit('/');
cy.waitForSkeletonGone();
//TODO(knorrium): add a check for the proxied server
// cy.changeNetwork('testnet4');
cy.changeNetwork('testnet4');
cy.changeNetwork('signet');
cy.changeNetwork('mainnet');
});
@ -545,7 +543,16 @@ describe('Mainnet', () => {
}
});
cy.get('.alert-replaced').should('be.visible');
cy.get('.alert').should('be.visible');
cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
});
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
cy.get('.alert').then(getRectangle).then((rectB) => {
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
});
});
});
it('shows RBF transactions properly (desktop)', () => {

View File

@ -750,7 +750,7 @@
},
"backendInfo": {
"hostname": "node205.tk7.mempool.space",
"version": "3.1.0-dev",
"version": "3.0.0-dev",
"gitCommit": "abbc8a134",
"lightning": false
},

View File

@ -27,6 +27,5 @@
"ACCELERATOR": false,
"ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false,
"STRATUM_ENABLED": false,
"SERVICES_API": "https://mempool.space/api/v1/services"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "3.1.0-dev",
"version": "3.0.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@ -76,9 +76,9 @@
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.7.2",
"@fortawesome/fontawesome-svg-core": "~6.7.2",
"@fortawesome/free-solid-svg-icons": "~6.7.2",
"@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
@ -86,15 +86,16 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.6.0",
"echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.24.0",
"esbuild": "^0.23.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.8.0",
"tslib": "~2.6.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
@ -104,7 +105,7 @@
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0",
"browser-sync": "^3.0.3",
"browser-sync": "^3.0.0",
"http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
@ -114,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.17.0",
"cypress": "^13.13.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@ -3,10 +3,8 @@ 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';
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("mempool.space", "mempool-staging.fra.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
});
module.exports = PROXY_CONFIG;

View File

@ -1,15 +1,14 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy'
import { BlockViewComponent } from '@components/block-view/block-view.component';
import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from '@components/clock/clock.component';
import { StatusViewComponent } from '@components/status-view/status-view.component';
import { AddressGroupComponent } from '@components/address-group/address-group.component';
import { TrackerComponent } from '@components/tracker/tracker.component';
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from '@app/route-guards';
import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { AddressGroupComponent } from './components/address-group/address-group.component';
import { TrackerComponent } from './components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
const browserWindow = window || {};
// @ts-ignore
@ -22,50 +21,12 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
component: StatusViewComponent
},
{
path: '',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: '/testnet'
},
]
},
{
path: 'testnet4',
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
@ -83,7 +44,45 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: '/testnet'
},
]
},
{
path: 'testnet4',
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
component: StatusViewComponent
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
@ -103,16 +102,16 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'widget/wallet',
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
@ -126,7 +125,7 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
@ -138,22 +137,21 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: 'tx',
canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always',
loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: '',
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'widget/wallet',
path: 'tracker',
data: { networkSpecific: true },
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
@ -165,19 +163,19 @@ let routes: Routes = [
children: [
{
path: '',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet4',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{
path: 'signet',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
],
},
@ -212,9 +210,13 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: ''
},
];
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
@ -225,16 +227,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: '',
pathMatch: 'full',
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'widget/wallet',
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
@ -248,7 +250,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
},
{
path: '',
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
@ -260,16 +262,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: '',
pathMatch: 'full',
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'widget/wallet',
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
@ -281,11 +283,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
children: [
{
path: '',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet',
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
],
},
@ -296,19 +298,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
},
{
path: '',
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: ''
},
];
}
if (!window['isMempoolSpaceBuild']) {
routes.push({
path: '**',
redirectTo: ''
});
}
@NgModule({
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking',

View File

@ -151,7 +151,7 @@ export const languages: Language[] = [
{ code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean
{ code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese
@ -439,39 +439,4 @@ export const fiatCurrencies = {
code: 'ZAR',
indexed: true,
},
};
export interface Timezone {
offset: string;
name: string;
}
export const timezones: Timezone[] = [
{ offset: '-12', name: 'Anywhere on Earth (AoE)' },
{ offset: '-11', name: 'Samoa Standard Time (SST)' },
{ offset: '-10', name: 'Hawaii Standard Time (HST)' },
{ offset: '-9', name: 'Alaska Standard Time (AKST)' },
{ offset: '-8', name: 'Pacific Standard Time (PST)' },
{ offset: '-7', name: 'Mountain Standard Time (MST)' },
{ offset: '-6', name: 'Central Standard Time (CST)' },
{ offset: '-5', name: 'Eastern Standard Time (EST)' },
{ offset: '-4', name: 'Atlantic Standard Time (AST)' },
{ offset: '-3', name: 'Argentina Time (ART)' },
{ offset: '-2', name: 'Fernando de Noronha Time (FNT)' },
{ offset: '-1', name: 'Azores Time (AZOT)' },
{ offset: '+0', name: 'Greenwich Mean Time (GMT)' },
{ offset: '+1', name: 'Central European Time (CET)' },
{ offset: '+2', name: 'Eastern European Time (EET)' },
{ offset: '+3', name: 'Moscow Standard Time (MSK)' },
{ offset: '+4', name: 'Armenia Time (AMT)' },
{ offset: '+5', name: 'Pakistan Standard Time (PKT)' },
{ offset: '+6', name: 'Xinjiang Time (XJT)' },
{ offset: '+7', name: 'Indochina Time (ICT)' },
{ offset: '+8', name: 'Hong Kong Time (HKT)' },
{ offset: '+9', name: 'Japan Standard Time (JST)' },
{ offset: '+10', name: 'Australian Eastern Standard Time (AEST)' },
{ offset: '+11', name: 'Norfolk Time (NFT)' },
{ offset: '+12', name: 'New Zealand Standard Time (NZST)' },
{ offset: '+13', name: 'Tonga Time (TOT)' },
{ offset: '+14', name: 'Line Islands Time (LINT)' }
];
};

View File

@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { ZONE_SERVICE } from './injection-tokens';
import { AppModule } from './app.module';
import { AppComponent } from '@components/app/app.component';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { ZoneService } from '@app/services/zone.service';
import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { ZoneService } from './services/zone.service';
@NgModule({
@ -20,4 +20,4 @@ import { ZoneService } from '@app/services/zone.service';
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
export class AppServerModule {}

View File

@ -2,38 +2,35 @@ import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { ZONE_SERVICE } from './injection-tokens';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from '@components/app/app.component';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { OrdApiService } from '@app/services/ord-api.service';
import { StateService } from '@app/services/state.service';
import { CacheService } from '@app/services/cache.service';
import { PriceService } from '@app/services/price.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { WebsocketService } from '@app/services/websocket.service';
import { AudioService } from '@app/services/audio.service';
import { PreloadService } from '@app/services/preload.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { ZoneService } from '@app/services/zone-shim.service';
import { SharedModule } from '@app/shared/shared.module';
import { StorageService } from '@app/services/storage.service';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { LanguageService } from '@app/services/language.service';
import { ThemeService } from '@app/services/theme.service';
import { TimeService } from '@app/services/time.service';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
import { ServicesApiServices } from '@app/services/services-api.service';
import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service';
import { StateService } from './services/state.service';
import { CacheService } from './services/cache.service';
import { PriceService } from './services/price.service';
import { EnterpriseService } from './services/enterprise.service';
import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service';
import { PreloadService } from './services/preload.service';
import { SeoService } from './services/seo.service';
import { OpenGraphService } from './services/opengraph.service';
import { ZoneService } from './services/zone-shim.service';
import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service';
import { ThemeService } from './services/theme.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service';
import { DatePipe } from '@angular/common';
const providers = [
ElectrsApiService,
OrdApiService,
StateService,
CacheService,
PriceService,
@ -45,7 +42,6 @@ const providers = [
EnterpriseService,
LanguageService,
ThemeService,
TimeService,
ShortenStringPipe,
FiatShortenerPipe,
FiatCurrencyPipe,

View File

@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { MasterPageComponent } from '@components/master-page/master-page.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
const routes: Routes = [
{
path: '',
component: MasterPageComponent,
loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true },
}
];

View File

@ -1,5 +1,5 @@
import { Transaction, Vin } from '@interfaces/electrs.interface';
import { Hash } from '@app/shared/sha256';
import { Transaction, Vin } from './interfaces/electrs.interface';
import { Hash } from './shared/sha256';
const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return;
}
const opN = ops.pop();
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
if (!opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
}
}
const opM = ops.pop();
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
if (!opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
return hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
}
}

View File

@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core';
import { EnterpriseService } from '@app/services/enterprise.service';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({
selector: 'app-about-sponsors',

View File

@ -53,7 +53,7 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
<defs>
<style>
.d {
@ -125,14 +125,17 @@
<span>Blockstream</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
</svg>
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
<span>Unchained</span>
</a>
<a href="https://bitkey.world/" target="_blank" title="Bitkey">
<img class="image" src="/resources/profile/bitkey.svg" />
<span>Bitkey</span>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
@ -147,7 +150,7 @@
<span>Bull Bitcoin</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
@ -188,36 +191,18 @@
</svg>
<span>Exodus</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://leather.io/" target="_blank" title="Leather">
<img class="image" src="/resources/profile/leather.svg" />
<span>Leather</span>
</a>
<a href="https://taprootwizards.com/" target="_blank" title="Taproot Wizards">
<img class="image" src="/resources/profile/wizardhat.png" />
<span>Taproot Wizards</span>
</a>
</div>
</div>
<ng-container>
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
<div class="community-sponsor whale-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper">
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@ -229,7 +214,7 @@
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@ -450,7 +435,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, 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&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
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 <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@ -13,6 +13,8 @@
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
@ -92,13 +94,6 @@
}
}
.whale-sponsor {
img {
width: 70px;
height: 70px;
}
}
.alliances {
margin-bottom: 100px;
a {
@ -163,8 +158,9 @@
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
height: auto;
margin: 20px 29px 20px;
width: auto;
max-height: 50px;
margin: 40px 29px 14px 29px;
}
}
}
@ -258,12 +254,3 @@
width: 64px;
height: 64px;
}
.enterprise-sponsor {
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
}
}

View File

@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { StateService } from '../../services/state.service';
import { Observable } from 'rxjs';
import { ApiService } from '@app/services/api.service';
import { IBackendInfo } from '@interfaces/websocket.interface';
import { ApiService } from '../../services/api.service';
import { IBackendInfo } from '../../interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators';
import { ITranslators } from '@interfaces/node-api.interface';
import { ITranslators } from '../../interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common';
import { EnterpriseService } from '@app/services/enterprise.service';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({
selector: 'app-about',

View File

@ -1,9 +1,9 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from '@components/about/about.component';
import { AboutSponsorsComponent } from '@components/about/about-sponsors.component';
import { SharedModule } from '@app/shared/shared.module';
import { AboutComponent } from './about.component';
import { AboutSponsorsComponent } from './about-sponsors.component';
import { SharedModule } from '../../shared/shared.module';
const routes: Routes = [
{

View File

@ -1,18 +1,10 @@
<div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
@if (accelerateError) {
@if (accelerateError.includes('Payment declined')) {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">{{ accelerateError }}</h1>
</div>
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div>
} @else {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div>
</div>
}
</div>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
@ -365,11 +357,11 @@
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
</div>
</div>
<div class="payment-area" style="font-size: 14px;">
<div class="payment-area mt-2 p-2" style="font-size: 14px;">
<div class="row text-center justify-content-center mx-2">
<span i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></span>
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
</div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
<div class="row">
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p>
@ -386,12 +378,9 @@
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span></p>
<app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
} @else if (btcpayInvoiceFailed) {
<div class="btcpay-invoice">
<fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
<span i18n="accelerator.failed-to-load-invoice">Failed to load invoice</span>
@if (!loadingBtcpayInvoice) {
<button class="btn btn-sm btn-secondary mt-0 mt-md-1" (click)="requestBTCPayInvoice()">Retry ↻</button>
}
<p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p>
<div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;">
<fa-icon style="font-size: 24px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
</div>
} @else {
<p i18n="accelerator.loading-invoice">Loading invoice...</p>
@ -400,40 +389,21 @@
</div>
}
</div>
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay) {
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
<p class="text-nowrap">&mdash;&mdash;<span i18n="or"> OR </span>&mdash;&mdash;</p>
<p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p>
</div>
}
}
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay) {
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
@if (canPayWithCashapp) {
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
}
@if (canPayWithApplePay) {
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('applepay')">
<img src="/resources/apple-pay.png" height=37>
</div>
}
@if (canPayWithGooglePay) {
@if (canPayWithCashapp || canPayWithApplePay) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('googlepay')">
<img src="/resources/google-pay.png" height=37>
</div>
}
@if (canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center" style="width: 200px; height: 55px" (click)="moveToStep('cardonfile')">
@if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
<app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
} @else {
<app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
}
<span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
@if (canPayWithCashapp) { <hr class="w-25 mt-2 mb-2"> }
<img style="cursor: pointer;" src="/resources/apple-pay.svg" height=55 (click)="moveToStep('applepay')">
}
</div>
}
@ -457,7 +427,7 @@
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
</div>
</div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
} @else if (step === 'cashapp' || step === 'applepay') {
<!-- Show checkout page -->
<div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title">
@ -473,7 +443,7 @@
</div>
</div>
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay) {
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
@ -493,29 +463,17 @@
<div class="col-sm">
<div class="form-group w-100">
@if (step === 'applepay') {
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-black" style="height: 50px" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-white" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'cashapp') {
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'googlepay') {
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'cardonfile') {
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center ml-auto mr-auto" style="width: 200px; height: 55px" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
}
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
@if (loadingCashapp || loadingApplePay) {
<div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div>
@if (isTokenizing > 0) {
<div class="d-flex flex-row justify-content-center">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div>
</div>
@ -557,7 +515,7 @@
<div class="col-sm">
<div class="d-flex flex-row flex-column justify-content-center align-items-center">
<span i18n="accelerator.confirming-acceleration-with-miners">Confirming your acceleration with our mining pool partners...</span>
@if (timeSincePaid > 30000) {
@if (timeSincePaid > 20000) {
<span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span>
}
<div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>

View File

@ -8,13 +8,6 @@
color: var(--green)
}
.accelerate-checkout-inner {
&.input-disabled {
pointer-events: none;
opacity: 0.75;
}
}
.paymentMethod {
padding: 10px;
background-color: var(--secondary);
@ -153,11 +146,6 @@
.payment-area {
background: var(--bg);
margin-top: 0.5rem;
padding: 0.5rem;
@media (max-width: 575px) {
padding-bottom: 1.25rem;
}
}
.col.pie {
@ -184,6 +172,10 @@
background-color: var(--tertiary);
}
.btn-small-height {
line-height: 1;
}
.summary-row {
display: flex;
flex-direction: row;
@ -224,17 +216,4 @@
}
.apple-pay-button-white-with-line {
-apple-pay-button-style: white-outline;
}
.btcpay-invoice {
display: flex;
height: 292px;
flex-direction: column;
justify-content: center;
align-items: center;
@media (max-width: 575px) {
height: 75px;
flex-direction: row;
gap: 5px;
}
}

View File

@ -12,7 +12,7 @@
</p>
</div>
<div class="spacer"></div>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<div class="spacer"></div>
<div class="spacer"></div>
</div>

View File

@ -1,6 +1,6 @@
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '@interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component';
import { Transaction } from '../../interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
interface GraphBar {
rate: number;

View File

@ -1,63 +0,0 @@
<div
#tooltip
*ngIf="accelerationInfo && tooltipPosition !== null"
class="acceleration-tooltip"
[style.left]="tooltipPosition.x + 'px'"
[style.top]="tooltipPosition.y + 'px'"
>
<table>
<tbody>
<tr>
<td class="label" i18n="transaction.status|Transaction Status">Status</td>
<td class="value">
@if (accelerationInfo.status === 'seen') {
<span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span>
} @else if (accelerationInfo.status === 'accelerated') {
<span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
} @else if (accelerationInfo.status === 'mined') {
<span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
}
</td>
</tr>
<tr *ngIf="accelerationInfo.fee">
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
</tr>
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
@if (accelerationInfo.status === 'accelerated') {
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
} @else {
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
}
</tr>
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
@if (accelerationInfo.status === 'seen') {
<td class="label" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee" [weight]="accelerationInfo.weight"></app-fee-rate></td>
} @else if (accelerationInfo.status === 'accelerated' || accelerationInfo.status === 'mined') {
<td class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
@if (accelerationInfo.status === 'accelerated') {
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
} @else {
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
}
}
</tr>
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="value" *ngIf="accelerationInfo.pools">
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
<img *ngIf="accelerationInfo.poolsData[pool]"
class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'">
<br *ngIf="i % 6 === 5">
</ng-container>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,41 +0,0 @@
.acceleration-tooltip {
position: fixed;
z-index: 3;
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: var(--tooltip-grey);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px 15px;
text-align: left;
pointer-events: none;
.badge.badge-accelerated {
background-color: var(--tertiary);
color: white;
}
.value {
text-align: end;
}
.label {
padding-right: 30px;
vertical-align: top;
}
.pool-logo {
width: 22px;
height: 22px;
position: relative;
top: -1px;
margin-right: 4px;
margin-bottom: 4px;
}
.oobFees {
color: #905cf4;
}
}

View File

@ -1,38 +0,0 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-acceleration-timeline-tooltip',
templateUrl: './acceleration-timeline-tooltip.component.html',
styleUrls: ['./acceleration-timeline-tooltip.component.scss'],
})
export class AccelerationTimelineTooltipComponent implements OnChanges {
@Input() accelerationInfo: any;
@Input() cursorPosition: { x: number, y: number };
tooltipPosition: any = null;
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor() {}
ngOnChanges(changes): void {
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
let y = changes.cursorPosition.currentValue.y + 20;
if (this.tooltipElement) {
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
if ((x + elementBounds.width) > (window.innerWidth - 10)) {
x = Math.max(0, window.innerWidth - elementBounds.width - 10);
}
if (y + elementBounds.height > (window.innerHeight - 20)) {
y = y - elementBounds.height - 20;
}
}
this.tooltipPosition = { x, y };
}
}
hasPoolsData(): boolean {
return Object.keys(this.accelerationInfo.poolsData).length > 0;
}
}

View File

@ -1,6 +1,6 @@
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
<div class="timeline-wrapper">
@if (!tx.status.confirmed || canceled) {
@if (!tx.status.confirmed) {
<div class="timeline">
<div class="intervals">
<div class="node-spacer"></div>
@ -8,8 +8,8 @@
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
@if (eta && !canceled) {
~<app-time [time]="eta?.wait / 1000"></app-time>
@if (eta) {
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
}
</div>
</div>
@ -19,20 +19,16 @@
<div class="node-spacer"></div>
<div class="interval-spacer"></div>
<div class="node">
<div class="acc-to-confirmed right go-faster" [class.no-animation]="canceled"></div>
<div class="acc-to-confirmed right go-faster"></div>
</div>
<div class="interval-spacer">
</div>
<div class="node" [id]="'confirmed'">
<div class="acc-to-confirmed left go-faster" [class.no-animation]="canceled"></div>
<div class="acc-to-confirmed left go-faster"></div>
<div class="shape-border waiting">
<div class="shape"></div>
<div class="shape animate"></div>
</div>
@if (canceled) {
<div class="status"><span class="badge badge-danger" i18n="accelerator.canceled">Canceled</span></div>
} @else {
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
}
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
</div>
</div>
</div>
@ -42,16 +38,18 @@
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
<app-time [time]="firstSeenToAccelerated"></app-time>
<app-time [time]="acceleratedAt - transactionTime"></app-time>
</div>
</div>
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
@if (tx.status.confirmed) {
<app-time [time]="acceleratedToMined"></app-time>
} @else if (eta && canceled) {
~<app-time [time]="eta?.wait / 1000"></app-time>
<div class="interval-time">
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
</div>
} @else if (standardETA && !tx.status.confirmed) {
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
}
</div>
</div>
@ -60,7 +58,7 @@
<div class="nodes">
<div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div>
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
<div class="shape-border">
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
@ -75,50 +73,47 @@
<div class="interval-spacer">
<div class="seen-to-acc"></div>
</div>
<div class="node" [class.accelerated]="!tx.status.confirmed && !canceled" [id]="'accelerated'">
<div class="node" [class.accelerated]="!tx.status.confirmed" [id]="'accelerated'">
<div class="seen-to-acc left"></div>
@if (tx.status.confirmed && !canceled) {
@if (tx.status.confirmed) {
<div class="acc-to-confirmed right"></div>
} @else {
<div class="seen-to-acc right"></div>
}
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
<div class="shape-border">
<div class="shape"></div>
@if (!tx.status.confirmed || canceled) {
<div class="connector down" [class.loading]="!canceled"></div>
@if (!tx.status.confirmed) {
<div class="connector down loading"></div>
}
</div>
@if (tx.status.confirmed && !canceled) {
@if (tx.status.confirmed) {
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
}
<div class="time" [class.no-margin]="!tx.status.confirmed || canceled" [class.offset-left]="!tx.status.confirmed || canceled">
<div class="time offset-left" [class.no-margin]="!tx.status.confirmed">
@if (!tx.status.confirmed) {
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
}
@if (useAbsoluteTime) {
<span>{{ acceleratedAt * 1000 | date }}</span>
} @else {
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed || canceled"></app-time>
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="true"></app-time>
}
</div>
</div>
<div class="interval-spacer">
@if (tx.status.confirmed && !canceled) {
@if (tx.status.confirmed) {
<div class="acc-to-confirmed"></div>
} @else {
<div class="seen-to-acc"></div>
}
</div>
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
@if (tx.status.confirmed && !canceled) {
@if (tx.status.confirmed) {
<div class="acc-to-confirmed left"></div>
} @else {
<div class="seen-to-acc left"></div>
}
<div class="shape-border"
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
(pointerout)="onBlur($event);">
<div class="shape-border" [class.waiting]="!tx.status.confirmed">
<div class="shape"></div>
</div>
@if (tx.status.confirmed) {
@ -135,10 +130,4 @@
</div>
</div>
</div>
<app-acceleration-timeline-tooltip
[accelerationInfo]="hoverInfo"
[cursorPosition]="tooltipPosition"
></app-acceleration-timeline-tooltip>
</div>

View File

@ -129,9 +129,6 @@
margin-left: calc(-4em + 5px);
animation: goFasterLeft 0.8s infinite linear;
}
&.no-animation {
animation: none;
}
}
&.left {
@ -155,16 +152,9 @@
margin-bottom: -8px;
transform: translateY(-50%);
border-radius: 50%;
cursor: pointer;
padding: 4px;
background: transparent;
transition: background-color 300ms, padding 300ms;
&.hovering {
cursor: pointer;
&:hover {
padding: 0px;
}
}
.shape {
position: relative;

View File

@ -1,8 +1,6 @@
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '@app/services/eta.service';
import { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { MiningService } from '@app/services/mining.service';
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
@Component({
selector: 'app-acceleration-timeline',
@ -11,82 +9,47 @@ import { MiningService } from '@app/services/mining.service';
})
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() acceleratedAt: number;
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() eta: ETA;
@Input() canceled: boolean;
// A mined transaction has standard ETA and accelerated ETA undefined
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
@Input() standardETA: number;
@Input() acceleratedETA: number;
acceleratedAt: number;
now: number;
accelerateRatio: number;
useAbsoluteTime: boolean = false;
firstSeenToAccelerated: number;
acceleratedToMined: number;
interval: number;
tooltipPosition = null;
hoverInfo: any = null;
poolsData: { [id: number]: SinglePoolStats } = {};
constructor(
private miningService: MiningService,
) {}
constructor() {}
ngOnInit(): void {
this.updateTimes();
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.poolsData[pool.unique_id] = pool;
}
});
this.interval = window.setInterval(() => {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
}, 60000);
}
ngOnChanges(changes): void {
this.updateTimes();
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
// if (changes?.eta?.currentValue) {
// if (changes?.acceleratedETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
// } else if (changes?.standardETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
// }
// }
// }
}
updateTimes(): void {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime);
this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt);
}
onHover(event, status: string): void {
if (status === 'seen') {
this.hoverInfo = {
status,
fee: this.tx.fee,
weight: this.tx.weight
};
} else if (status === 'accelerated') {
this.hoverInfo = {
status,
fee: this.accelerationInfo?.effectiveFee || this.tx.fee,
weight: this.tx.weight,
feeDelta: this.accelerationInfo?.feeDelta || this.tx.feeDelta,
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
poolsData: this.poolsData
};
} else if (status === 'mined') {
this.hoverInfo = {
status,
fee: this.accelerationInfo?.effectiveFee,
weight: this.tx.weight,
bidBoost: this.accelerationInfo?.bidBoost,
minedByPoolUniqueId: this.accelerationInfo?.minedByPoolUniqueId,
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
poolsData: this.poolsData
};
}
}
onBlur(event): void {
this.hoverInfo = null;
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
ngOnDestroy(): void {
clearInterval(this.interval);
}
}

View File

@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { EChartsOption } from '@app/graphs/echarts';
import { EChartsOption } from '../../../graphs/echarts';
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '@app/services/seo.service';
import { SeoService } from '../../../services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
import { StorageService } from '@app/services/storage.service';
import { MiningService } from '@app/services/mining.service';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
import { StorageService } from '../../../services/storage.service';
import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute, Router } from '@angular/router';
import { Acceleration } from '@interfaces/node-api.interface';
import { ServicesApiServices } from '@app/services/services-api.service';
import { StateService } from '@app/services/state.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '../../../services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-acceleration-fees-graph',
@ -23,7 +23,7 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 99;
z-index: 100;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
@ -46,8 +46,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
aggregatedHistory$: Observable<any>;
statsSubscription: Subscription;
aggregatedHistorySubscription: Subscription;
fragmentSubscription: Subscription;
isLoading = true;
formatNumber = formatNumber;
timespan = '';
@ -81,8 +79,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.fragmentSubscription = this.route.fragment.subscribe((fragment) => {
this.route.fragment.subscribe((fragment) => {
if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
@ -115,7 +113,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
share(),
);
this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe();
this.aggregatedHistory$.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
@ -266,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
type: 'bar',
barWidth: '90%',
large: true,
barMinHeight: 3,
barMinHeight: 1,
},
],
dataZoom: (this.widget || data.length === 0 )? undefined : [{
@ -337,8 +335,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
}
ngOnDestroy(): void {
this.aggregatedHistorySubscription?.unsubscribe();
this.fragmentSubscription?.unsubscribe();
this.statsSubscription?.unsubscribe();
if (this.statsSubscription) {
this.statsSubscription.unsubscribe();
}
}
}

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ServicesApiServices } from '@app/services/services-api.service';
import { ServicesApiServices } from '../../../services/services-api.service';
export type AccelerationStats = {
totalRequested: number;

View File

@ -4,8 +4,8 @@
<div class="clearfix"></div>
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
<ng-container *ngIf="pending">
@ -14,15 +14,15 @@
<th class="time text-right" i18n="accelerator.requested">Requested</th>
</ng-container>
<ng-container *ngIf="!pending">
<th class="fee text-right text-truncate" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="shared.block-title">Block</th>
<th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container>
</thead>
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of accelerations; let i= index;">
<td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
@ -33,7 +33,7 @@
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
</td>
<td class="bid text-right">
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sats">sats</span>
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@ -41,7 +41,7 @@
</ng-container>
<ng-container *ngIf="!pending">
<td *ngIf="acceleration.boost != null" class="fee text-right">
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sats">sats</span>
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td *ngIf="acceleration.boost == null" class="fee text-right">
~
@ -62,10 +62,8 @@
</td>
<td class="status text-right">
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed') && acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed') && !acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('completed')" class="badge badge-success" i18n="">Completed <span *ngIf="acceleration.status === 'completed_provisional'">🔄</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger" i18n="accelerator.canceled">Failed <span *ngIf="acceleration.status === 'failed_provisional'">🔄</span></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@ -74,47 +72,22 @@
</tr>
</tbody>
<ng-template #skeleton>
@if (!pending) {
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 200px"></span>
</td>
<td class="fee text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="block text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="pool text-right" *ngIf="!this.widget">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="status text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
</tr>
</tbody>
} @else {
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="fee-rate text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="bid text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="time text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
</tr>
</tbody>
}
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fee text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fee-delta text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="status text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
</tr>
</tbody>
</ng-template>
</table>

View File

@ -85,8 +85,8 @@ tr, td, th {
}
.pool-logo {
width: 18px;
height: 18px;
width: 22px;
height: 22px;
position: relative;
top: -1px;
margin-right: 2px;

View File

@ -1,12 +1,12 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { ServicesApiServices } from '@app/services/services-api.service';
import { SeoService } from '@app/services/seo.service';
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service';
import { SeoService } from '../../../services/seo.service';
import { ActivatedRoute, Router } from '@angular/router';
import { MiningService } from '@app/services/mining.service';
import { MiningService } from '../../../services/mining.service';
@Component({
selector: 'app-accelerations-list',
@ -32,7 +32,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
dir: 'rtl' | 'ltr' = 'ltr';
paramSubscription: Subscription;
pools: { [id: number]: SinglePoolStats } = {};
nonEmptyAccelerations: boolean = true;
constructor(
private servicesApiService: ServicesApiServices,
@ -51,21 +50,12 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.pools[pool.unique_id] = pool;
}
});
if (!this.widget) {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
this.paramSubscription = combineLatest([
this.route.params,
timer(0),
]).pipe(
tap(([params]) => {
this.paramSubscription = this.route.params.pipe(
tap(params => {
this.page = +params['page'] || 1;
this.pageSubject.next(this.page);
})
@ -92,6 +82,12 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
).subscribe(() => {
this.pageChange(this.page);
});
this.miningService.getMiningStats('1m').subscribe(stats => {
for (const pool of stats.pools) {
this.pools[pool.poolUniqueId] = pool;
}
});
}
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
@ -119,7 +115,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
for (const acc of accelerations) {
acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost;
}
this.nonEmptyAccelerations = accelerations.length > 0;
if (this.widget) {
return of(accelerations.slice(0, 6));
} else {
@ -151,4 +146,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.paramSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
}
}
}

View File

@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { WebsocketService } from '@app/services/websocket.service';
import { Acceleration, BlockExtended } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '@components/block-overview-graph/sprite-types';
import { hexToColor } from '@components/block-overview-graph/utils';
import TxView from '@components/block-overview-graph/tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
import { ServicesApiServices } from '@app/services/services-api.service';
import { detectWebGL } from '@app/shared/graphs.utils';
import { AudioService } from '@app/services/audio.service';
import { ThemeService } from '@app/services/theme.service';
import { Color } from '../../block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
import { ServicesApiServices } from '../../../services/services-api.service';
import { detectWebGL } from '../../../shared/graphs.utils';
import { AudioService } from '../../../services/audio.service';
import { ThemeService } from '../../../services/theme.service';
const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));

View File

@ -1,7 +1,7 @@
@if (chartOnly) {
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
} @else {
<table style="width: 100%;">
<table>
<tbody>
<tr>
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
@ -10,17 +10,17 @@
</td>
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) {
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
</div>
</td>
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start">
@if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
}
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
</div>
@ -36,7 +36,7 @@
<tr>
<td colspan="3" class="pt-0">
<div class="d-flex justify-content-end align-items-start">
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</div>
</td>
</tr>

Some files were not shown because too many files have changed in this diff Show More