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: strategy:
fail-fast: false fail-fast: false
matrix: 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 }} name: E2E tests for ${{ matrix.module }}
steps: steps:
@ -300,10 +310,8 @@ jobs:
- name: Unzip assets before building (src/resources) - name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }}) - name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5 uses: cypress-io/github-action@v5
with: with:
tag: ${{ github.event_name }} tag: ${{ github.event_name }}
@ -314,9 +322,7 @@ jobs:
wait-on-timeout: 120 wait-on-timeout: 120
record: true record: true
parallel: true parallel: true
spec: | spec: ${{ matrix.spec }}
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
group: Tests on Chrome (${{ matrix.module }}) group: Tests on Chrome (${{ matrix.module }})
browser: "chrome" browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@ -326,56 +332,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} 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: validate_docker_json:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
@ -403,4 +359,4 @@ jobs:
- name: Validate JSON syntax - name: Validate JSON syntax
run: | run: |
cat mempool-config.json | jq 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® 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 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 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®, The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo, Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
the mempool block visualization Logo, the mempool Blocks Logo, the mempool the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are of Mempool Space K.K in Japan, the United States, and/or other countries.
registered trademarks or trademarks of Mempool Space K.K in Japan,
the United States, and/or other countries.
See our full Trademark Policy and Guidelines for more details, published on See our full Trademark Policy and Guidelines for more details, published on
<https://mempool.space/trademark-policy>. <https://mempool.space/trademark-policy>.

View File

@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build #### 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._ _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, automock: false,
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"], collectCoverageFrom: ["./src/**/**.ts"],
coverageProvider: "v8", coverageProvider: "babel",
coverageThreshold: { coverageThreshold: {
global: { global: {
lines: 1 lines: 1

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -28,7 +28,6 @@
"INDEXING_BLOCKS_AMOUNT": 14, "INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": true, "AUDIT": true,
"RUST_GBT": false, "RUST_GBT": false,
"LIMIT_GBT": false, "LIMIT_GBT": false,
@ -47,8 +46,7 @@
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000, "TIMEOUT": 1000,
"COOKIE": false, "COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__", "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@ -151,9 +149,5 @@
"ENABLED": true, "ENABLED": true,
"PAID": false, "PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__" "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 { Common } from '../../api/common';
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; import { MempoolTransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json'); const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json'); const replacedTransactions = require('./test-data/transactions-replaced.json');
@ -10,14 +10,14 @@ describe('Common', () => {
describe('RBF', () => { describe('RBF', () => {
const newTransactions = rbfTransactions.concat(randomTransactions); const newTransactions = rbfTransactions.concat(randomTransactions);
test('should detect RBF transactions with fast method', () => { 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(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
}); });
test('should detect RBF transactions with scalable method', () => { 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(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');

View File

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

View File

@ -70,7 +70,7 @@ class AboutRoutes {
res.status(500).end(); 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/', '')}`; const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try { try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); 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 logger from '../logger';
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache'; 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 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 { class Audit {
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }) auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { : { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { 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 matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const unseen: string[] = []; // present in the mined block, not in our mempool const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the 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 fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN 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 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 accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
const isAccelerated = {};
let displacedWeight = 0; let displacedWeight = 0;
let matchedWeight = 0; let matchedWeight = 0;
let projectedWeight = 0; let projectedWeight = 0;
@ -36,7 +32,6 @@ class Audit {
inBlock[tx.txid] = tx; inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) { if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid); accelerated.push(tx.txid);
isAccelerated[tx.txid] = true;
} }
} }
// coinbase is always expected // coinbase is always expected
@ -118,16 +113,11 @@ class Audit {
} else { } else {
if (rbfCache.has(tx.txid)) { if (rbfCache.has(tx.txid)) {
rbf.push(tx.txid); rbf.push(tx.txid);
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) { } else if (!isDisplaced[tx.txid]) {
unseen.push(tx.txid);
}
} else {
if (mempool[tx.txid]) { if (mempool[tx.txid]) {
if (isDisplaced[tx.txid]) { prioritized.push(tx.txid);
added.push(tx.txid);
}
} else { } else {
unseen.push(tx.txid); added.push(tx.txid);
} }
} }
overflowWeight += tx.weight; overflowWeight += tx.weight;
@ -135,8 +125,6 @@ class Audit {
totalWeight += tx.weight; totalWeight += tx.weight;
} }
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
// transactions missing from near the end of our template are probably not being censored // transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
let maxOverflowRate = 0; let maxOverflowRate = 0;
@ -177,7 +165,6 @@ class Audit {
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return { return {
unseen,
censored: Object.keys(isCensored), censored: Object.keys(isCensored),
added, added,
prioritized, 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'; import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {
@ -23,7 +23,6 @@ export interface AbstractBitcoinApi {
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>; $sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>; $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;

View File

@ -218,21 +218,3 @@ export interface TestMempoolAcceptResult {
}, },
['reject-reason']?: string, ['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 * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; 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 { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks'; import blocks from '../blocks';
import mempool from '../mempool'; 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> { async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return { return {
@ -331,7 +327,6 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr', 'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard', 'nonstandard': 'nonstandard',
'multisig': 'multisig', 'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return' 'nulldata': 'op_return'
}; };

View File

@ -1,11 +1,6 @@
import { Application, NextFunction, Request, Response } from 'express'; import { Application, NextFunction, Request, Response } from 'express';
import logger from '../../logger'; import logger from '../../logger';
import bitcoinClient from './bitcoin-client'; 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 * 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 { class BitcoinBackendRoutes {
private static tag = 'BitcoinBackendRoutes'; private static tag = 'BitcoinBackendRoutes';
public initRoutes(app: Application): void { public initRoutes(app: Application) {
app app
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) .get('/api/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) .post('/api/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) .get('/api/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('/api/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) .post('/api/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('/api/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('/api/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('/api/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-block-count', this.disableCache, this.$getBlockCount)
; ;
} }
/** /**
* Disable caching for bitcoin core routes * Disable caching for bitcoin core routes
* *
* @param req * @param req
* @param res * @param res
* @param next * @param next
*/ */
private disableCache(req: Request, res: Response, next: NextFunction): void { private disableCache(req: Request, res: Response, next: NextFunction): void {
res.setHeader('Pragma', 'no-cache'); res.setHeader('Pragma', 'no-cache');
@ -44,16 +39,16 @@ class BitcoinBackendRoutes {
/** /**
* Exeption handler to return proper details to the accelerator server * Exeption handler to return proper details to the accelerator server
* *
* @param e * @param e
* @param fnName * @param fnName
* @param res * @param res
*/ */
private static handleException(e: any, fnName: string, res: Response): void { private static handleException(e: any, fnName: string, res: Response): void {
if (typeof(e.code) === 'number') { if (typeof(e.code) === 'number') {
res.status(400).send(JSON.stringify(e, ['code'])); res.status(400).send(JSON.stringify(e, ['code', 'message']));
} else { } else {
const err = `unknown exception in ${fnName}`; const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`;
logger.err(err, BitcoinBackendRoutes.tag); logger.err(err, BitcoinBackendRoutes.tag);
res.status(500).send(err); res.status(500).send(err);
} }
@ -62,13 +57,13 @@ class BitcoinBackendRoutes {
private async $getMempoolEntry(req: Request, res: Response): Promise<void> { private async $getMempoolEntry(req: Request, res: Response): Promise<void> {
const txid = req.query.txid; const txid = req.query.txid;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid); const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
if (!mempoolEntry) { if (!mempoolEntry) {
res.status(404).send(); res.status(404).send(`no mempool entry found for txid ${txid}`);
return; return;
} }
res.status(200).send(mempoolEntry); res.status(200).send(mempoolEntry);
@ -80,13 +75,13 @@ class BitcoinBackendRoutes {
private async $decodeRawTransaction(req: Request, res: Response): Promise<void> { private async $decodeRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx; const rawTx = req.body.rawTx;
try { try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return; return;
} }
const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx); const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!decodedTx) { if (!decodedTx) {
res.status(400).send(`unable to decode rawTx`); res.status(400).send(`unable to decode rawTx ${rawTx}`);
return; return;
} }
res.status(200).send(decodedTx); res.status(200).send(decodedTx);
@ -99,23 +94,23 @@ class BitcoinBackendRoutes {
const txid = req.query.txid; const txid = req.query.txid;
const verbose = req.query.verbose; const verbose = req.query.verbose;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbose) !== 'string') { 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; return;
} }
const verboseNumber = parseInt(verbose, 10); const verboseNumber = parseInt(verbose, 10);
if (typeof(verboseNumber) !== 'number') { 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; return;
} }
const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber); const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber);
if (!decodedTx) { if (!decodedTx) {
res.status(400).send(`unable to get raw transaction`); res.status(400).send(`unable to get raw transaction for txid ${txid}`);
return; return;
} }
res.status(200).send(decodedTx); res.status(200).send(decodedTx);
@ -127,13 +122,13 @@ class BitcoinBackendRoutes {
private async $sendRawTransaction(req: Request, res: Response): Promise<void> { private async $sendRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx; const rawTx = req.body.rawTx;
try { try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return; return;
} }
const txHex = await bitcoinClient.sendRawTransaction(rawTx); const txHex = await bitcoinClient.sendRawTransaction(rawTx);
if (!txHex) { if (!txHex) {
res.status(400).send(`unable to send rawTx`); res.status(400).send(`unable to send rawTx ${rawTx}`);
return; return;
} }
res.status(200).send(txHex); res.status(200).send(txHex);
@ -145,13 +140,13 @@ class BitcoinBackendRoutes {
private async $testMempoolAccept(req: Request, res: Response): Promise<void> { private async $testMempoolAccept(req: Request, res: Response): Promise<void> {
const rawTxs = req.body.rawTxs; const rawTxs = req.body.rawTxs;
try { try {
if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) { if (typeof(rawTxs) !== 'object') {
res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`); res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`);
return; return;
} }
const txHex = await bitcoinClient.testMempoolAccept(rawTxs); const txHex = await bitcoinClient.testMempoolAccept(rawTxs);
if (typeof(txHex) !== 'object' || txHex.length === 0) { 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; return;
} }
res.status(200).send(txHex); res.status(200).send(txHex);
@ -164,18 +159,18 @@ class BitcoinBackendRoutes {
const txid = req.query.txid; const txid = req.query.txid;
const verbose = req.query.verbose; const verbose = req.query.verbose;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) { 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; return;
} }
const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false); const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false);
if (!ancestors) { if (!ancestors) {
res.status(400).send(`unable to get mempool ancestors`); res.status(400).send(`unable to get mempool ancestors for txid ${txid}`);
return; return;
} }
res.status(200).send(ancestors); res.status(200).send(ancestors);
@ -188,23 +183,23 @@ class BitcoinBackendRoutes {
const blockHash = req.query.hash; const blockHash = req.query.hash;
const verbosity = req.query.verbosity; const verbosity = req.query.verbosity;
try { try {
if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) { if (typeof(blockHash) !== 'string' || blockHash.length !== 64) {
res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`); res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbosity) !== 'string') { 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; return;
} }
const verbosityNumber = parseInt(verbosity, 10); const verbosityNumber = parseInt(verbosity, 10);
if (typeof(verbosityNumber) !== 'number') { 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; return;
} }
const block = await bitcoinClient.getBlock(blockHash, verbosityNumber); const block = await bitcoinClient.getBlock(blockHash, verbosityNumber);
if (!block) { if (!block) {
res.status(400).send(`unable to get block`); res.status(400).send(`unable to get block for block hash ${blockHash}`);
return; return;
} }
res.status(200).send(block); res.status(200).send(block);
@ -217,18 +212,18 @@ class BitcoinBackendRoutes {
const blockHeight = req.query.height; const blockHeight = req.query.height;
try { try {
if (typeof(blockHeight) !== 'string') { 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; return;
} }
const blockHeightNumber = parseInt(blockHeight, 10); const blockHeightNumber = parseInt(blockHeight, 10);
if (typeof(blockHeightNumber) !== 'number') { 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; return;
} }
const block = await bitcoinClient.getBlockHash(blockHeightNumber); const block = await bitcoinClient.getBlockHash(blockHeightNumber);
if (!block) { 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; return;
} }
res.status(200).send(block); 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 transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache'; import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp'; 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 { class BitcoinRoutes {
public initRoutes(app: Application) { 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 + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .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/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/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .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', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', 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') { if (config.MEMPOOL.BACKEND !== 'esplora') {
@ -100,7 +86,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json'); res.set('Content-Type', 'application/json');
res.send(result); res.send(result);
} catch (e) { } 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(); const result = mempoolBlocks.getMempoolBlocks();
res.json(result); res.json(result);
} catch (e) { } 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) { private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
handleError(req, res, 500, 'Not an array'); res.status(500).send('Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
for (const _txId in req.query.txId) { for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') { if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString(); txIds.push(req.query.txId[_txId].toString());
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
} }
} }
@ -145,16 +128,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> { private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids; const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') { if (!txids_csv || typeof txids_csv !== 'string') {
handleError(req, res, 500, 'Invalid txids format'); res.status(500).send('Invalid txids format');
return; return;
} }
const txids = txids_csv.split(','); const txids = txids_csv.split(',');
if (txids.length > 50) { if (txids.length > 50) {
handleError(req, res, 400, 'Too many txids requested'); res.status(400).send('Too many txids requested');
return;
}
if (txids.some((txid) => !TXID_REGEX.test(txid))) {
handleError(req, res, 400, 'Invalid txids format');
return; return;
} }
@ -162,13 +141,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends); res.json(batchedOutspends);
} catch (e) { } 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) { private async $getCpfpInfo(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`); res.status(501).send(`Invalid transaction ID.`);
return; return;
} }
@ -186,7 +165,6 @@ class BitcoinRoutes {
acceleration: tx.acceleration, acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined, acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined, acceleratedAt: tx.acceleratedAt || undefined,
feeDelta: tx.feeDelta || undefined,
}); });
return; return;
} }
@ -201,7 +179,7 @@ class BitcoinRoutes {
try { try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get CPFP info'); res.status(500).send('failed to get CPFP info');
return; return;
} }
} }
@ -222,10 +200,6 @@ class BitcoinRoutes {
} }
private async getTransaction(req: Request, res: Response) { private async getTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction); res.json(transaction);
@ -233,18 +207,12 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; 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) { private async getRawTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
@ -253,10 +221,8 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; 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 // Not modified
// 422 Unprocessable Entity // 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 // 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) { } catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
handleError(req, res, 404, notFoundError); res.status(404).send(e.message);
} else { } 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) { private async getTransactionStatus(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status); res.json(transaction.status);
@ -340,54 +302,22 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; 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) { private async getStrippedBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block summary'); res.status(500).send(e instanceof Error ? e.message : e);
}
}
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');
} }
} }
private async getBlock(req: Request, res: Response) { private async getBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const block = await blocks.$getBlock(req.params.hash); const block = await blocks.$getBlock(req.params.hash);
@ -399,69 +329,51 @@ class BitcoinRoutes {
} else if (blockAge > 30 * day) { } else if (blockAge > 30 * day) {
cacheDuration = 10 * day; cacheDuration = 10 * day;
} else { } else {
cacheDuration = 600; cacheDuration = 600
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block); res.json(block);
} catch (e) { } 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) { private async getBlockHeader(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash); const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(blockHeader); res.send(blockHeader);
} catch (e) { } 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) { private async getBlockAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash); const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) { if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
handleError(req, res, 404, `Audit not available`); return res.status(404).send(`audit not available`);
return;
} }
} catch (e) { } 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) { 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 { try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) { if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
handleError(req, res, 404, `Transaction audit not available`); return res.status(404).send(`transaction audit not available`);
return;
} }
} catch (e) { } 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); return await this.getLegacyBlocks(req, res);
} }
} catch (e) { } 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) { private async getBlocksByBulk(req: Request, res: Response) {
try { try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented 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 res.status(404).send(`This API is only available for Bitcoin networks`);
return;
} }
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) { 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 res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
return;
} }
if (!Common.indexingEnabled()) { if (!Common.indexingEnabled()) {
handleError(req, res, 404, `Indexing is required for this API`); return res.status(404).send(`Indexing is required for this API`);
return;
} }
const from = parseInt(req.params.from, 10); const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) { if (!req.params.from || from < 0) {
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`); return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
return;
} }
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) { if (to < 0) {
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`); return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
return;
} }
if (from > to) { if (from > to) {
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`); return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
return;
} }
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) { 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 res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
return;
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to)); res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks); res.json(returnBlocks);
} catch (e) { } 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) { private async getBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@ -581,7 +482,7 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); 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)); const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash); res.send(blockHash);
} catch (e) { } 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) { private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return; return;
} }
@ -609,20 +506,15 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { 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 res.status(413).send(e instanceof Error ? e.message : e);
return;
} }
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> { private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return; return;
} }
@ -635,27 +527,23 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { 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; 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> { private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { 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; return;
} }
} }
private async getScriptHash(req: Request, res: Response) { private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return; return;
} }
@ -666,20 +554,15 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { 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 res.status(413).send(e instanceof Error ? e.message : e);
return;
} }
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> { private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return; return;
} }
@ -694,26 +577,26 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { 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; 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> { private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { 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; return;
} }
} }
private async getAddressPrefix(req: Request, res: Response) { private async getAddressPrefix(req: Request, res: Response) {
try { try {
const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix); const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(addressPrefix); res.send(blockHash);
} catch (e) { } 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(); const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool); res.send(rawMempool);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); res.status(500).send(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);
} }
} }
@ -794,13 +631,12 @@ class BitcoinRoutes {
try { try {
const result = blocks.getCurrentBlockHeight(); const result = blocks.getCurrentBlockHeight();
if (!result) { if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`); return res.status(503).send(`Service Temporarily Unavailable`);
return;
} }
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result.toString()); res.send(result.toString());
} catch (e) { } 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.setHeader('content-type', 'text/plain');
res.send(result); res.send(result);
} catch (e) { } 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) { private async getRawBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const result = await bitcoinApi.$getRawBlock(req.params.hash); const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream'); res.setHeader('content-type', 'application/octet-stream');
res.send(result); res.send(result);
} catch (e) { } 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) { private async getTxIdsForBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result); res.json(result);
} catch (e) { } 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) { private async validateAddress(req: Request, res: Response) {
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try { try {
const result = await bitcoinClient.validateAddress(req.params.address); const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result); res.json(result);
} catch (e) { } 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) { private async getRbfHistory(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null; const replacements = rbfCache.getRbfTree(req.params.txId) || null;
const replaces = rbfCache.getReplaces(req.params.txId) || null; const replaces = rbfCache.getReplaces(req.params.txId) || null;
@ -867,7 +687,7 @@ class BitcoinRoutes {
replaces replaces
}); });
} catch (e) { } 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); const result = rbfCache.getRbfTrees(false);
res.json(result); res.json(result);
} catch (e) { } 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); const result = rbfCache.getRbfTrees(true);
res.json(result); res.json(result);
} catch (e) { } 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) { private async getCachedTx(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const result = rbfCache.getTx(req.params.txId); const result = rbfCache.getTx(req.params.txId);
if (result) { if (result) {
@ -902,20 +718,16 @@ class BitcoinRoutes {
res.status(204).send(); res.status(204).send();
} }
} catch (e) { } 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) { private async getTransactionOutspends(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const result = await bitcoinApi.$getOutspends(req.params.txId); const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result); res.json(result);
} catch (e) { } 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) { if (da) {
res.json(da); res.json(da);
} else { } else {
handleError(req, res, 503, `Service Temporarily Unavailable`); res.status(503).send(`Service Temporarily Unavailable`);
} }
} catch (e) { } 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); const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: 'Failed to send raw transaction'); : (e.message || 'Error'));
} }
} }
@ -951,8 +763,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex); const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: 'Failed to send raw transaction'); : (e.message || 'Error'));
} }
} }
@ -963,21 +775,9 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result); res.send(result);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code }) res.setHeader('content-type', 'text/plain');
: 'Failed to test transactions'); res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
} : (e.message || 'Error'));
}
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');
} }
} }

View File

@ -1,12 +1,12 @@
import config from '../../config'; import config from '../../config';
import axios, { isAxiosError } from 'axios'; import axios, { AxiosResponse, isAxiosError } from 'axios';
import http from 'http'; import http from 'http';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger'; import logger from '../../logger';
import { Common } from '../common'; import { Common } from '../common';
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { TestMempoolAcceptResult } from './bitcoin-api.interface';
import os from 'os';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
rtts: number[], rtts: number[],
@ -20,13 +20,6 @@ interface FailoverHost {
preferred?: boolean, preferred?: boolean,
checked: boolean, checked: boolean,
lastChecked?: number, lastChecked?: number,
publicDomain: string,
hashes: {
frontend?: string,
backend?: string,
electrs?: string,
lastUpdated: number,
}
} }
class FailoverRouter { class FailoverRouter {
@ -36,21 +29,14 @@ class FailoverRouter {
maxHeight: number = 0; maxHeight: number = 0;
hosts: FailoverHost[]; hosts: FailoverHost[];
multihost: boolean; multihost: boolean;
gitHashInterval: number = 600000; // 10 minutes pollInterval: number = 60000;
pollInterval: number = 60000; // 1 minute
pollTimer: NodeJS.Timeout | null = null; pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create(); pollConnection = axios.create();
localHostname: string = 'localhost';
requestConnection = axios.create({ requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true }) httpAgent: new http.Agent({ keepAlive: true })
}); });
constructor() { constructor() {
try {
this.localHostname = os.hostname();
} catch (e) {
logger.warn('Failed to set local hostname, using "localhost"');
}
// setup list of hosts // setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return { return {
@ -59,10 +45,6 @@ class FailoverRouter {
rtts: [], rtts: [],
rtt: Infinity, rtt: Infinity,
failures: 0, failures: 0,
publicDomain: 'https://' + this.extractPublicDomain(domain),
hashes: {
lastUpdated: 0,
},
}; };
}); });
this.activeHost = { this.activeHost = {
@ -73,10 +55,6 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH, socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true, preferred: true,
checked: false, checked: false,
publicDomain: `http://${this.localHostname}`,
hashes: {
lastUpdated: 0,
},
}; };
this.fallbackHost = this.activeHost; this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost); this.hosts.unshift(this.activeHost);
@ -128,24 +106,6 @@ class FailoverRouter {
host.outOfSync = false; host.outOfSync = false;
} }
host.unreachable = 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 { } else {
host.outOfSync = true; host.outOfSync = true;
host.unreachable = 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> { private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
let axiosConfig; let axiosConfig;
let url; let url;
@ -386,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
} }
$getAddress(address: string): Promise<IEsploraApi.Address> { $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[]> { $getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
@ -413,10 +332,6 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.'); 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> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
} }
@ -462,7 +377,6 @@ class ElectrsApi implements AbstractBitcoinApi {
unreachable: !!host.unreachable, unreachable: !!host.unreachable,
checked: !!host.checked, checked: !!host.checked,
lastChecked: host.lastChecked || 0, lastChecked: host.lastChecked || 0,
hashes: host.hashes,
})); }));
} else { } else {
return []; return [];

View File

@ -33,8 +33,6 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool'; import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository'; import CpfpRepository from '../repositories/CpfpRepository';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import database from '../database';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; 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 { return {
id: hash, id: hash,
transactions: Common.classifyTransactions(transactions, height), transactions: Common.classifyTransactions(transactions),
}; };
} }
@ -343,12 +341,7 @@ class Blocks {
id: pool.uniqueId, id: pool.uniqueId,
name: pool.name, name: pool.name,
slug: pool.slug, slug: pool.slug,
minerNames: null,
}; };
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
} }
extras.matchRate = null; extras.matchRate = null;
@ -412,16 +405,8 @@ class Blocks {
} }
try { 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 // 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 indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
@ -454,7 +439,7 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') { 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); const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
if (cpfpSummary) { if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
@ -630,7 +615,7 @@ class Blocks {
// add CPFP // add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify // 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); await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height); const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@ -667,7 +652,7 @@ class Blocks {
} }
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify // classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) { for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx; classifiedTxMap[tx.txid] = tx;
@ -919,14 +904,9 @@ class Blocks {
} }
} }
let accelerations = Object.values(mempool.getAccelerations()); const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
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 blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); 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}`); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
@ -947,12 +927,12 @@ class Blocks {
const newBlock = await this.$indexBlock(lastBlock.height - i); const newBlock = await this.$indexBlock(lastBlock.height - i);
this.blocks.push(newBlock); this.blocks.push(newBlock);
this.updateTimerProgress(timer, `reindexed block`); this.updateTimerProgress(timer, `reindexed block`);
let newCpfpSummary; let cpfpSummary;
if (config.MEMPOOL.CPFP_INDEXING) { 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`); 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`); this.updateTimerProgress(timer, `reindexed block summary`);
} }
await mining.$indexDifficultyAdjustments(); await mining.$indexDifficultyAdjustments();
@ -1001,7 +981,7 @@ class Blocks {
// start async callbacks // start async callbacks
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`); 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 (block.height % 2016 === 0) {
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
@ -1183,7 +1163,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => { transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0; let flags: number = 0;
try { try {
flags = Common.getTransactionFlags(tx, height); flags = Common.getTransactionFlags(tx);
} catch (e) { } catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : 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 { } else {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); 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; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC
@ -1224,11 +1204,6 @@ class Blocks {
return summary.transactions; 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 * Get 15 blocks
* *
@ -1343,7 +1318,7 @@ class Blocks {
let summaryVersion = 0; let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); 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; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC
@ -1422,11 +1397,11 @@ class Blocks {
return this.currentBlockHeight; 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; let transactions = txs;
if (!transactions) { if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') { 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) { if (!transactions) {
const block = await bitcoinClient.getBlock(hash, 2); const block = await bitcoinClient.getBlock(hash, 2);
@ -1438,7 +1413,7 @@ class Blocks {
} }
if (transactions?.length != null) { if (transactions?.length != null) {
const summary = calculateFastBlockCpfp(height, transactions); const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
await this.$saveCpfp(hash, height, summary); 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 // 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(); export default new Blocks();

View File

@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express'; 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 config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net'; import { isIP } from 'net';
@ -10,6 +10,7 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings // Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@ -79,8 +80,8 @@ export class Common {
return arr; return arr;
} }
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} { static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {}; const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale // For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) { 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)); addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
}); });
if (foundMatches?.length) { if (foundMatches?.length) {
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx }; matches[addedTx.txid] = [...new Set(foundMatches)];
} }
}); });
} else { } else {
@ -123,7 +124,7 @@ export class Common {
foundMatches.add(deletedTx); foundMatches.add(deletedTx);
} }
if (foundMatches.size) { 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(); const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) { for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i]; const vin = tx.vin[i];
const key = `${vin.txid}:${vin.vout}`; const match = spendMap.get(`${vin.txid}:${vin.vout}`);
const match = spendMap.get(key);
if (match && match.txid !== tx.txid) { if (match && match.txid !== tx.txid) {
replaced.add(match); replaced.add(match);
// remove this tx from the spendMap // remove this tx from the spendMap
// prevents the same tx being replaced more than once // prevents the same tx being replaced more than once
for (const replacedVin of match.vin) { for (const replacedVin of match.vin) {
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`; const key = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(replacedKey); spendMap.delete(key);
} }
} }
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key); spendMap.delete(key);
} }
if (replaced.size) { if (replaced.size) {
@ -199,13 +200,10 @@ export class Common {
* *
* returns true early if any standardness rule is violated, otherwise false * 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) * (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 // version
if (this.isNonStandardVersion(tx, height)) { if (tx.version > TX_MAX_STANDARD_VERSION) {
return true; return true;
} }
@ -252,8 +250,6 @@ export class Common {
} }
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true; return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
} }
// TODO: bad-witness-nonstandard // TODO: bad-witness-nonstandard
} }
@ -339,49 +335,6 @@ export class Common {
return false; 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 { static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight; let weight = tx.weight;
let hasWitness = false; let hasWitness = false;
@ -462,7 +415,7 @@ export class Common {
return flags; return flags;
} }
static getTransactionFlags(tx: TransactionExtended, height?: number): number { static getTransactionFlags(tx: TransactionExtended): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n; let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF) // Update variable flags (CPFP, RBF)
@ -595,7 +548,7 @@ export class Common {
if (hasFakePubkey) { if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey; flags |= TransactionFlags.fake_pubkey;
} }
// fast but bad heuristic to detect possible coinjoins // 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) // (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; 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; flags |= TransactionFlags.batch_payout;
} }
if (this.isNonStandard(tx, height)) { if (this.isNonStandard(tx)) {
flags |= TransactionFlags.nonstandard; flags |= TransactionFlags.nonstandard;
} }
return Number(flags); return Number(flags);
} }
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified { static classifyTransaction(tx: TransactionExtended): TransactionClassified {
let flags = 0; let flags = 0;
try { try {
flags = Common.getTransactionFlags(tx, height); flags = Common.getTransactionFlags(tx);
} catch (e) { } catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : 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[] { static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
return txs.map(tx => Common.classifyTransaction(tx, height)); return txs.map(Common.classifyTransaction);
} }
static stripTransaction(tx: TransactionExtended): TransactionStripped { 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 CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
const MAX_CLUSTER_ITERATIONS = 100; 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 clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
@ -93,7 +93,6 @@ export function calculateFastBlockCpfp(height: number, transactions: MempoolTran
return { return {
transactions, transactions,
clusters, clusters,
version: 1,
}; };
} }
@ -160,7 +159,6 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
return { return {
transactions: transactions.map(tx => txMap[tx.txid]), transactions: transactions.map(tx => txMap[tx.txid]),
clusters: clusterArray, clusters: clusterArray,
version: 2,
}; };
} }

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 95; private static currentVersion = 80;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; 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 `TRY` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` 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 hashrates'); await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery('TRUNCATE difficulty_adjustments'); await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
}
await this.updateToSchemaVersion(75); await this.updateToSchemaVersion(75);
} }
@ -693,443 +691,6 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
await this.updateToSchemaVersion(80); 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`); 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(); export default new DatabaseMigration();

View File

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

View File

@ -1,9 +1,6 @@
import config from '../../config'; import config from '../../config';
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
class ChannelsRoutes { class ChannelsRoutes {
constructor() { } constructor() { }
@ -25,7 +22,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search); const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels); res.json(channels);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel); res.json(channel);
} catch (e) { } 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 : ''; const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) { if (index < -1) {
handleError(req, res, 400, 'Invalid index'); res.status(400).send('Invalid index');
return; return;
} }
if (['open', 'active', 'closed'].includes(status) === false) { if (['open', 'active', 'closed'].includes(status) === false) {
handleError(req, res, 400, 'Invalid status'); res.status(400).send('Invalid status');
return; return;
} }
@ -72,23 +69,20 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString()); res.header('X-Total-Count', channelsCount.toString());
res.json(channels); res.json(channels);
} catch (e) { } 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> { private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try { try {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
handleError(req, res, 400, 'Not an array'); res.status(400).send('Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
for (const _txId in req.query.txId) { for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') { if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString(); txIds.push(req.query.txId[_txId].toString());
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
} }
} }
const channels = await channelsApi.$getChannelsByTransactionId(txIds); const channels = await channelsApi.$getChannelsByTransactionId(txIds);
@ -113,7 +107,7 @@ class ChannelsRoutes {
res.json(result); res.json(result);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } 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 nodesApi from './nodes.api';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import statisticsApi from './statistics.api'; import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes { class GeneralLightningRoutes {
constructor() { } constructor() { }
@ -29,7 +27,7 @@ class GeneralLightningRoutes {
channels: channels, channels: channels,
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } 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(); const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics); res.json(statistics);
} catch (e) { } 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 nodesApi from './nodes.api';
import DB from '../../database'; import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces'; import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes { class NodesRoutes {
constructor() { } constructor() { }
@ -32,7 +31,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes); res.json(nodes);
} catch (e) { } 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) {} } catch (e) {}
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes); res.json(nodes);
} catch (e) { } 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 { try {
const node = await nodesApi.$getNode(req.params.public_key); const node = await nodesApi.$getNode(req.params.public_key);
if (!node) { if (!node) {
handleError(req, res, 404, 'Node not found'); res.status(404).send('Node not found');
return; return;
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
@ -204,7 +203,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } 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 { try {
const node = await nodesApi.$getFeeHistogram(req.params.public_key); const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) { if (!node) {
handleError(req, res, 404, 'Node not found'); res.status(404).send('Node not found');
return; return;
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
@ -232,7 +231,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } 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, topByChannels: topChannelsNodes,
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes); res.json(worldNodes);
} catch (e) { } 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) { 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; return;
} }
@ -336,7 +335,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } 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) { 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; return;
} }
@ -363,7 +362,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } 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 config from '../../config';
import elementsParser from './elements-parser'; import elementsParser from './elements-parser';
import icons from './icons'; import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes { class LiquidRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@ -43,7 +42,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length); res.setHeader('content-length', result.length);
res.send(result); res.send(result);
} else { } 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) { if (result) {
res.json(result); res.json(result);
} else { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs); res.json(pegs);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves); res.json(reserves);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply); res.json(currentSupply);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves); res.json(currentReserves);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus); res.json(auditStatus);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos); res.json(expiredUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs); res.json(recentPegs);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume); res.json(pegsVolume);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount); res.json(pegsCount);
} catch (e) { } 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; const lastBlockIndex = blocks.length - 1;
let hasBlockStack = blocks.length >= 8; let hasBlockStack = blocks.length >= 8;
let stackWeight; let stackWeight;
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null; let feeStatsCalculator: OnlineFeeStatsCalculator | void;
if (hasBlockStack) { if (hasBlockStack) {
if (blockWeights && blockWeights[7] !== null) { if (blockWeights && blockWeights[7] !== null) {
stackWeight = blockWeights[7]; stackWeight = blockWeights[7];
@ -380,36 +380,28 @@ class MempoolBlocks {
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); 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 cluster of clusters) {
for (const memberTxid of cluster) { for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid]; const mempoolTx = mempool[memberTxid];
if (mempoolTx) { if (mempoolTx) {
// ugly micro-optimization to avoid allocating new arrays const ancestors: Ancestor[] = [];
ancestors.length = 0; const descendants: Ancestor[] = [];
descendants.length = 0;
let matched = false; let matched = false;
cluster.forEach(txid => { cluster.forEach(txid => {
ancestor = mempool[txid];
if (txid === memberTxid) { if (txid === memberTxid) {
matched = true; matched = true;
} else { } else {
if (!ancestor) { if (!mempool[txid]) {
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]); console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
return;
} }
const relative = { const relative = {
txid: txid, txid: txid,
fee: ancestor.fee, fee: mempool[txid].fee,
weight: (ancestor.adjustedVsize * 4), weight: (mempool[txid].adjustedVsize * 4),
}; };
if (matched) { if (matched) {
descendants.push(relative); descendants.push(relative);
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) { mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
mempoolTx.lastBoosted = ancestor.firstSeen;
}
} else { } else {
ancestors.push(relative); ancestors.push(relative);
} }
@ -418,20 +410,7 @@ class MempoolBlocks {
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true; mempoolTx.cpfpDirty = true;
} }
// ugly micro-optimization to avoid allocating new arrays or objects Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
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;
} }
} }
} }
@ -441,10 +420,7 @@ class MempoolBlocks {
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results // update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended; let mempoolTx: MempoolTransactionExtended;
let acceleration: Acceleration; const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block = blocks[blockIndex];
let totalSize = 0; let totalSize = 0;
let totalVsize = 0; let totalVsize = 0;
let totalWeight = 0; let totalWeight = 0;
@ -460,9 +436,8 @@ class MempoolBlocks {
} }
} }
for (let i = 0; i < block.length; i++) { for (const txid of block) {
const txid = block[i]; if (txid) {
if (txid in mempool) {
mempoolTx = mempool[txid]; mempoolTx = mempool[txid];
// save position in projected blocks // save position in projected blocks
mempoolTx.position = { mempoolTx.position = {
@ -470,40 +445,28 @@ class MempoolBlocks {
vsize: totalVsize + (mempoolTx.vsize / 2), vsize: totalVsize + (mempoolTx.vsize / 2),
}; };
if (txid in accelerations) { const acceleration = accelerations[txid];
acceleration = accelerations[txid]; if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { if (!mempoolTx.acceleration) {
if (!mempoolTx.acceleration) { mempoolTx.cpfpDirty = true;
mempoolTx.cpfpDirty = true; }
} mempoolTx.acceleration = true;
mempoolTx.acceleration = true; mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.acceleratedAt = acceleration?.added; for (const ancestor of mempoolTx.ancestors || []) {
mempoolTx.feeDelta = acceleration?.feeDelta; if (!mempool[ancestor.txid].acceleration) {
for (const ancestor of mempoolTx.ancestors || []) { mempool[ancestor.txid].cpfpDirty = true;
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;
} }
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
} }
} else { } else {
if (mempoolTx.acceleration) { if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true; mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
} }
delete mempoolTx.acceleration;
} }
// online calculation of stack-of-blocks fee stats // online calculation of stack-of-blocks fee stats
@ -521,7 +484,7 @@ class MempoolBlocks {
} }
} }
} }
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks( return this.dataToMempoolBlocks(
block, block,
transactions, transactions,
totalSize, totalSize,
@ -529,7 +492,7 @@ class MempoolBlocks {
totalFees, totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined, (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
); );
}; });
if (saveResults) { if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); 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 }; [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) // 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; let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) { for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4); vsize += (ancestor.weight / 4);

View File

@ -10,7 +10,6 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
import { Acceleration } from './services/acceleration'; import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import redisCache from './redis-cache'; import redisCache from './redis-cache';
import blocks from './blocks'; import blocks from './blocks';
@ -20,13 +19,12 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {}; private mempoolCandidates: { [txid: string ]: boolean } = {};
private spendMap = new Map<string, MempoolTransactionExtended>(); 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, 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 }; maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], 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[], 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 accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; 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; }, 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; this.mempoolChangedCallback = fn;
} }
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, 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 { candidates?: GbtCandidates) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn; this.$asyncMempoolChangedCallback = fn;
} }
@ -208,7 +206,7 @@ class Mempool {
return txTimes; 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...`); logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes // 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)); const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); 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) { if (accelerationDelta.length) {
hasChange = true; hasChange = true;
} }
@ -364,15 +362,12 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length; const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
this.recentlyDeleted.unshift(deletedTransactions); if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
} }
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) { if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
this.updateTimerProgress(timer, 'running async mempool callback'); 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'); this.updateTimerProgress(timer, 'completed async mempool callback');
} }
@ -400,11 +395,58 @@ class Mempool {
return this.accelerations; return this.accelerations;
} }
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] { public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
try { 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; this.accelerations = newAccelerationMap;
return accelerationDelta;
return changed;
} catch (e: any) { } catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e)); logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return []; 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) { for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions // Store replaced transactions

View File

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

View File

@ -10,7 +10,6 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository'; import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration'; import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes { class MiningRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@ -54,12 +53,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string; const currency = req.query.currency as string;
let response; let response;
if (timestamp && currency) { if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@ -72,7 +71,7 @@ class MiningRoutes {
} }
res.status(200).send(response); res.status(200).send(response);
} catch (e) { } 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); res.json(stats);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { 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 { } 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); res.json(poolBlocks);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { 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 { } 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); res.json(pools);
} }
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats); res.json(stats);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates); res.json(hashrates);
} catch (e) { } 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); res.json(hashrates);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { 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 { } 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) { private async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0; let currentHashrate = 0, currentDifficulty = 0;
try { try {
currentHashrate = await bitcoinClient.getNetworkHashPs(1008); currentHashrate = await bitcoinClient.getNetworkHashPs();
currentDifficulty = await bitcoinClient.getDifficulty(); currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) { } catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
@ -204,7 +203,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty, currentDifficulty: currentDifficulty,
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards); res.json(blockRewards);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates); res.json(blockFeeRates);
} catch (e) { } 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 weights: blockWeights
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response); res.json(response);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) { } 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); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
handleError(req, res, 204, `This block has not been audited.`); res.status(204).send(`This block has not been audited.`);
return; return;
} }
@ -336,7 +335,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit); res.json(audit);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result); res.json(result);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null'); res.json(audit || 'null');
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {})); res.status(200).send(accelerationApi.accelerations || []);
} catch (e) { } 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> { 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('Pragma', 'no-cache');
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
res.setHeader('expires', -1); res.setHeader('expires', -1);
try { try {
accelerationApi.accelerationRequested(req.params.txid); accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send(); res.status(200).send('ok');
} catch (e) { } 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; poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h'); 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 { try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
} catch (e) { } catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0; poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining); 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': '[]', 'addresses': '[]',
'slug': 'unknown' '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 { public setMiningPools(pools): void {
for (const pool of pools) { for (const pool of pools) {

View File

@ -1,15 +1,10 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import pricesUpdater from '../../tasks/price-updater'; import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes { class PricesRoutes {
public initRoutes(app: Application): void { public initRoutes(app: Application): void {
app app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
.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))
;
} }
private $getCurrentPrices(req: Request, res: Response): void { private $getCurrentPrices(req: Request, res: Response): void {
@ -19,23 +14,6 @@ class PricesRoutes {
res.json(pricesUpdater.getLatestPrices()); 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(); export default new PricesRoutes();

View File

@ -44,22 +44,6 @@ interface CacheEvent {
value?: any, 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 { class RbfCache {
private replacedBy: Map<string, string> = new Map(); private replacedBy: Map<string, string> = new Map();
private replaces: 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); setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
} }
/**
* Low level cache operations
*/
private addTx(txid: string, tx: MempoolTransactionExtended): void { private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx); this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid }); this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
@ -112,18 +92,8 @@ class RbfCache {
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid }); this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
} }
/**
* Basic data structure operations
* must uphold tree invariants
*/
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if ( !newTxExtended if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|| !replaced?.length
|| this.txs.has(newTxExtended.txid)
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
) {
return; return;
} }
@ -144,10 +114,6 @@ class RbfCache {
if (!replacedTx.rbf) { if (!replacedTx.rbf) {
txFullRbf = true; txFullRbf = true;
} }
if (this.replacedBy.has(replacedTx.txid)) {
// should never happen
continue;
}
this.replacedBy.set(replacedTx.txid, newTx.txid); this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) { if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid); const treeId = this.treeMap.get(replacedTx.txid);
@ -174,47 +140,18 @@ class RbfCache {
} }
} }
newTx.fullRbf = txFullRbf; newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid;
const newTree = { const newTree = {
tx: newTx, tx: newTx,
time: newTime, time: newTime,
fullRbf: treeFullRbf, fullRbf: treeFullRbf,
replaces: replacedTrees replaces: replacedTrees
}; };
this.addTree(newTree.tx.txid, newTree); this.addTree(treeId, newTree);
this.updateTreeMap(newTree.tx.txid, newTree); this.updateTreeMap(treeId, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid)); 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 { public has(txId: string): boolean {
return this.txs.has(txId); return this.txs.has(txId);
} }
@ -295,6 +232,32 @@ class RbfCache {
return changes; 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? // is the transaction involved in a full rbf replacement?
public isFullRbf(txid: string): boolean { public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid); const treeId = this.treeMap.get(txid);
@ -308,10 +271,6 @@ class RbfCache {
return tree?.fullRbf; return tree?.fullRbf;
} }
/**
* Cache maintenance & utility functions
*/
private cleanup(): void { private cleanup(): void {
const now = Date.now(); const now = Date.now();
for (const txid of this.expiring.keys()) { for (const txid of this.expiring.keys()) {
@ -340,6 +299,10 @@ class RbfCache {
for (const tx of (replaces || [])) { for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache // recursively remove prior versions from the cache
this.replacedBy.delete(tx); 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); 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 { try {
txs.forEach(txEntry => { txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value); this.txs.set(txEntry.value.txid, txEntry.value);
}); });
this.staleCount = 0; this.staleCount = 0;
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) { for (const deflatedTree of trees) {
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); 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);
}
}
} }
expiring.forEach(expiringEntry => { expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) { if (this.txs.has(expiringEntry.key)) {
@ -429,31 +385,6 @@ class RbfCache {
} }
}); });
this.staleCount = 0; 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(); await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup(); this.cleanup();
@ -495,12 +426,6 @@ class RbfCache {
return; 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 // recursively reconstruct child trees
for (const childId of treeInfo.replaces) { for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
@ -532,6 +457,10 @@ class RbfCache {
fullRbf: treeInfo.fullRbf, fullRbf: treeInfo.fullRbf,
replaces, replaces,
}; };
this.treeMap.set(txid, root);
if (root === txid) {
this.addTree(root, tree);
}
return tree; return tree;
} }
@ -582,7 +511,6 @@ class RbfCache {
processTxs(txs); processTxs(txs);
} }
// evict missing transactions
for (const txid of txids) { for (const txid of txids) {
if (!found[txid]) { if (!found[txid]) {
this.evict(txid, false); this.evict(txid, false);

View File

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

View File

@ -1,10 +1,7 @@
import { WebSocket } from 'ws';
import config from '../../config'; import config from '../../config';
import logger from '../../logger'; import logger from '../../logger';
import { BlockExtended } from '../../mempool.interfaces'; import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios'; import axios from 'axios';
import mempool from '../mempool';
import websocketHandler from '../websocket-handler';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
@ -40,23 +37,13 @@ export interface AccelerationHistory {
}; };
class AccelerationApi { 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 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: Acceleration[] | null = null;
private _accelerations: Record<string, Acceleration> = {};
private lastPoll = 0; private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false; private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {}; private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public constructor() {} public get accelerations(): Acceleration[] | null {
public getAccelerations(): Record<string, Acceleration> {
return this._accelerations; return this._accelerations;
} }
@ -65,9 +52,7 @@ class AccelerationApi {
} }
public accelerationRequested(txid: string): void { 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 { public accelerationConfirmed(): void {
@ -84,18 +69,11 @@ class AccelerationApi {
} }
} }
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> { public async $updateAccelerations(): Promise<Acceleration[] | null> {
if (this.useWebsocket && this.websocketConnected) { if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
return this._accelerations;
}
if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations(); const accelerations = await this.$fetchAccelerations();
if (accelerations) { if (accelerations) {
const latestAccelerations = {}; this._accelerations = accelerations;
for (const acc of accelerations) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations; return this._accelerations;
} }
} else { } else {
@ -104,7 +82,7 @@ class AccelerationApi {
return null; return null;
} }
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> { private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
const shouldUpdate = this.forcePoll const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0 || this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000))); || (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
@ -139,11 +117,7 @@ class AccelerationApi {
} }
} }
const latestAccelerations = {}; this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations; return this._accelerations;
} }
@ -175,148 +149,6 @@ class AccelerationApi {
} }
return anyAccelerated; 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(); export default new AccelerationApi();

View File

@ -1,7 +1,6 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import WalletApi from './wallets'; import WalletApi from './wallets';
import { handleError } from '../../utils/api';
class ServicesRoutes { class ServicesRoutes {
public initRoutes(app: Application): void { public initRoutes(app: Application): void {
@ -19,7 +18,7 @@ class ServicesRoutes {
const wallet = await WalletApi.getWallet(walletId); const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet); res.status(200).send(wallet);
} catch (e) { } 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 { interface WalletAddress {
address: string; address: string;
active: boolean; active: boolean;
stats: { transactions?: IEsploraApi.AddressTxSummary[];
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
};
transactions: IEsploraApi.AddressTxSummary[];
lastSync: number;
} }
interface Wallet { interface WalletConfig {
url: string;
name: string; name: string;
apiKey: string;
}
interface Wallet extends WalletConfig {
addresses: Record<string, WalletAddress>; addresses: Record<string, WalletAddress>;
lastPoll: number; lastPoll: number;
} }
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes const POLL_FREQUENCY = 60 * 60 * 1000; // 1 hour
class WalletApi { class WalletApi {
private wallets: Record<string, Wallet> = {}; private wallets: Record<string, Wallet> = {};
private syncing = false; private syncing = false;
constructor() { constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => { this.wallets = (config.WALLETS.WALLETS as WalletConfig[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 }; acc[wallet.name] = { ...wallet, addresses: {}, lastPoll: 0 };
return acc; return acc;
}, {} as Record<string, Wallet>) : {}; }, {} as Record<string, Wallet>);
} }
public getWallet(wallet: string): Record<string, WalletAddress> { public getWallet(wallet: string): Record<string, WalletAddress> {
return this.wallets?.[wallet]?.addresses || {}; return this.wallets?.[wallet]?.addresses || {};
} }
// resync wallet addresses from the services backend // resync wallet addresses from the provided API
async $syncWallets(): Promise<void> { async $syncWallets(): Promise<void> {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true; this.syncing = true;
for (const walletKey of Object.keys(this.wallets)) { for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey]; const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) { if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try { try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`); const response = await axios.get(`${wallet.url}/${wallet.name}`, { headers: { 'Authorization': `${wallet.apiKey}` } });
const addresses: Record<string, WalletAddress> = response.data; const data: { walletBalances: WalletAddress[] } = response.data;
const addressList: WalletAddress[] = Object.values(addresses); const addresses = data.walletBalances;
const newAddresses: Record<string, boolean> = {};
// sync all current addresses // sync all current addresses
for (const address of addressList) { for (const address of addresses) {
await this.$syncWalletAddress(wallet, address); await this.$syncWalletAddress(wallet, address);
newAddresses[address.address] = true;
} }
// remove old addresses // remove old addresses
for (const address of Object.keys(wallet.addresses)) { for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) { if (!newAddresses[address]) {
delete wallet.addresses[address]; delete wallet.addresses[address];
} }
} }
@ -77,19 +73,16 @@ class WalletApi {
// resync address transactions from esplora // resync address transactions from esplora
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> { 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 // fetch full transaction data if the address is new or still active
const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000); const refreshTransactions = !wallet.addresses[address.address] || address.active;
if (refreshTransactions) { if (refreshTransactions) {
try { try {
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
const addressInfo = await bitcoinApi.$getAddress(address.address);
const walletAddress: WalletAddress = { const walletAddress: WalletAddress = {
address: address.address, address: address.address,
active: address.active, active: address.active,
transactions: summary, transactions: await bitcoinApi.$getAddressTransactionSummary(address.address),
stats: addressInfo.chain_stats,
lastSync: Date.now(),
}; };
logger.debug(`Synced ${walletAddress.transactions?.length || 0} transactions for wallet ${wallet.name} address ${address.address}`);
wallet.addresses[address.address] = walletAddress; wallet.addresses[address.address] = walletAddress;
} catch (e) { } catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : 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 // 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[]> { processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> {
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {}; const walletTransactions: Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> = {};
for (const walletKey of Object.keys(this.wallets)) { for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey]; const wallet = this.wallets[walletKey];
walletTransactions[walletKey] = []; walletTransactions[walletKey] = {};
for (const tx of blockTxs) { for (const tx of blockTxs) {
const funded: Record<string, number> = {}; const funded: Record<string, number> = {};
const spent: 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) { for (const vin of tx.vin) {
const address = vin.prevout?.scriptpubkey_address; const address = vin.prevout?.scriptpubkey_address;
if (address && wallet.addresses[address]) { if (address && wallet.addresses[address]) {
anyMatch = true;
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
spentCount[address] = (spentCount[address] ?? 0) + 1;
} }
} }
for (const vout of tx.vout) { for (const vout of tx.vout) {
const address = vout.scriptpubkey_address; const address = vout.scriptpubkey_address;
if (address && wallet.addresses[address]) { if (address && wallet.addresses[address]) {
anyMatch = true;
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
} }
} }
for (const address of Object.keys({ ...funded, ...spent })) { for (const address of Object.keys({ ...funded, ...spent })) {
// update address stats if (!walletTransactions[walletKey][address]) {
wallet.addresses[address].stats.tx_count++; walletTransactions[walletKey][address] = [];
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0; }
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0; walletTransactions[walletKey][address].push({
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 = {
txid: tx.txid, txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0), value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height, height: block.height,
time: block.timestamp, 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 { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import statisticsApi from './statistics-api'; import statisticsApi from './statistics-api';
import { handleError } from '../../utils/api';
class StatisticsRoutes { class StatisticsRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
@ -65,7 +65,7 @@ class StatisticsRoutes {
} }
res.json(result); res.json(result);
} catch (e) { } 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 adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
const feePerVbytes = (transaction.fee || 0) / fractionalVsize; const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid), order: this.txidToOrdering(transaction.txid),
vsize, vsize,
@ -129,7 +128,7 @@ class TransactionUtils {
sigops, sigops,
feePerVsize: feePerVbytes, feePerVsize: feePerVbytes,
adjustedFeePerVsize: adjustedFeePerVsize, adjustedFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: effectiveFeePerVsize, effectiveFeePerVsize: adjustedFeePerVsize,
}); });
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) { if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((Date.now() / 1000)); transactionExtended.firstSeen = Math.round((Date.now() / 1000));
@ -339,87 +338,6 @@ class TransactionUtils {
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript]; 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(); export default new TransactionUtils();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,11 +31,11 @@ class AuditReplication {
const missingAudits = await this.$getMissingAuditBlocks(); const missingAudits = await this.$getMissingAuditBlocks();
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication'); logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
let totalSynced = 0; let totalSynced = 0;
let totalMissed = 0; let totalMissed = 0;
let loggerTimer = Date.now(); 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) { for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
const slice = missingAudits.slice(i, i + BATCH_SIZE); const slice = missingAudits.slice(i, i + BATCH_SIZE);
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash))); const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
@ -109,11 +109,9 @@ class AuditReplication {
version: 1, version: 1,
}); });
await blocksAuditsRepository.$saveAudit({ await blocksAuditsRepository.$saveAudit({
version: auditSummary.version || 0,
hash: blockHash, hash: blockHash,
height: auditSummary.height, height: auditSummary.height,
time: auditSummary.timestamp || auditSummary.time, time: auditSummary.timestamp || auditSummary.time,
unseenTxs: auditSummary.unseenTxs || [],
missingTxs: auditSummary.missingTxs || [], missingTxs: auditSummary.missingTxs || [],
addedTxs: auditSummary.addedTxs || [], addedTxs: auditSummary.addedTxs || [],
prioritizedTxs: auditSummary.prioritizedTxs || [], 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> { public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {}; const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of transactions) { for (const tx of transactions) {

View File

@ -1,24 +1,13 @@
import blocks from '../api/blocks';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
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[],
}
class BlocksAuditRepositories { class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> { public async $saveAudit(audit: BlockAudit): Promise<void> {
try { 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) 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.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs), 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]); 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) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart 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> { public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
try { try {
const [rows]: any[] = await DB.query( const [rows]: any[] = await DB.query(
`SELECT `SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
blocks_audits.version, template,
blocks_audits.height, missing_txs as missingTxs,
blocks_audits.hash as id, added_txs as addedTxs,
UNIX_TIMESTAMP(blocks_audits.time) as timestamp, prioritized_txs as prioritizedTxs,
template, fresh_txs as freshTxs,
unseen_txs as unseenTxs, sigop_txs as sigopTxs,
missing_txs as missingTxs, fullrbf_txs as fullrbfTxs,
added_txs as addedTxs, accelerated_txs as acceleratedTxs,
prioritized_txs as prioritizedTxs, match_rate as matchRate,
fresh_txs as freshTxs, expected_fees as expectedFees,
sigop_txs as sigopTxs, expected_weight as expectedWeight
fullrbf_txs as fullrbfTxs,
accelerated_txs as acceleratedTxs,
match_rate as matchRate,
expected_fees as expectedFees,
expected_weight as expectedWeight
FROM blocks_audits FROM blocks_audits
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.hash = ? WHERE blocks_audits.hash = ?
`, [hash]); `, [hash]);
if (rows.length) { if (rows.length) {
rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
rows[0].missingTxs = JSON.parse(rows[0].missingTxs); rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs); rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
@ -118,7 +101,7 @@ class BlocksAuditRepositories {
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> { public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
try { try {
const blockAudit = await this.$getBlockAudit(hash); const blockAudit = await this.$getBlockAudit(hash);
if (blockAudit) { if (blockAudit) {
const isAdded = blockAudit.addedTxs.includes(txid); const isAdded = blockAudit.addedTxs.includes(txid);
const isPrioritized = blockAudit.prioritizedTxs.includes(txid); const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
@ -132,17 +115,16 @@ class BlocksAuditRepositories {
firstSeen = tx.time; firstSeen = tx.time;
} }
}); });
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return { return {
seen: wasSeen, seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected, expected: isExpected,
added: isAdded && (blockAudit.version === 0 || !wasSeen), added: isAdded,
prioritized: isPrioritized, prioritized: isPrioritized,
conflict: isConflict, conflict: isConflict,
accelerated: isAccelerated, accelerated: isAccelerated,
firstSeen, firstSeen,
}; }
} }
return null; return null;
} catch (e: any) { } catch (e: any) {
@ -204,96 +186,6 @@ class BlocksAuditRepositories {
throw e; 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(); export default new BlocksAuditRepositories();

View File

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

View File

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

View File

@ -6,30 +6,16 @@ import backendInfo from '../api/backend-info';
import logger from '../logger'; import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent'; import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https'; import * as https from 'https';
import { Common } from '../api/common';
/** /**
* Maintain the most recent version of pools-v2.json * Maintain the most recent version of pools-v2.json
*/ */
class PoolsUpdater { class PoolsUpdater {
tag = 'PoolsUpdater';
lastRun: number = 0; lastRun: number = 0;
currentSha: string | null = null; currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_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> { public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false config.MEMPOOL.ENABLED === false
@ -37,8 +23,11 @@ class PoolsUpdater {
return; return;
} }
const oneWeek = 604800;
const oneDay = 86400;
const now = new Date().getTime() / 1000; 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; return;
} }
@ -54,7 +43,7 @@ class PoolsUpdater {
this.currentSha = await this.getShaFromDb(); 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) { if (this.currentSha !== null && this.currentSha === githubSha) {
return; return;
} }
@ -64,16 +53,16 @@ class PoolsUpdater {
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool !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.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`, 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`);
return; return;
} }
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === null) { 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 { } 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); const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) { if (poolsJson === undefined) {
@ -82,24 +71,24 @@ class PoolsUpdater {
poolsParser.setMiningPools(poolsJson); poolsParser.setMiningPools(poolsJson);
if (config.DATABASE.ENABLED === false) { // Don't run db operations 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; return;
} }
try { try {
await DB.query('START TRANSACTION;'); await DB.query('START TRANSACTION;');
await this.updateDBSha(githubSha);
await poolsParser.migratePoolsJson(); await poolsParser.migratePoolsJson();
await this.updateDBSha(githubSha);
await DB.query('COMMIT;'); await DB.query('COMMIT;');
} catch (e) { } 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;'); 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) { } catch (e) {
this.lastRun = now - 600; // Try again in 10 minutes this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag); 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('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) { } 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 * Fetch our latest pools-v2.json sha from the db
*/ */
public async getShaFromDb(): Promise<string | null> { private async getShaFromDb(): Promise<string | null> {
try { try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null); return (rows.length > 0 ? rows[0].string : null);
} catch (e) { } 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; 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; return null;
} }
@ -197,7 +186,7 @@ class PoolsUpdater {
} }
return data.data; return data.data;
} catch (e) { } 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++; retry++;
} }
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL); 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) { if (!opN) {
return; return;
} }
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { if (!opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); 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) { if (!opM) {
return; return;
} }
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { if (!opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
@ -200,28 +200,4 @@ export function getVarIntLength(n: number): number {
} else { } else {
return 9; 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, "AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "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_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0, "MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6, "DISK_CACHE_BLOCK_INTERVAL": 6,
@ -141,7 +140,6 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_AUTOMATIC_POOLS_UPDATE: "" MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
MEMPOOL_POOLS_JSON_URL: "" MEMPOOL_POOLS_JSON_URL: ""
MEMPOOL_POOLS_JSON_TREE_URL: "" MEMPOOL_POOLS_JSON_TREE_URL: ""
MEMPOOL_POOLS_UPDATE_DELAY: ""
MEMPOOL_CPFP_INDEXING: "" MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""

View File

@ -36,7 +36,6 @@
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__, "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
}, },
@ -47,8 +46,7 @@
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__, "TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__, "COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__", "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@ -148,10 +146,6 @@
"API": "__MEMPOOL_SERVICES_API__", "API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__ "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
}, },
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": { "REDIS": {
"ENABLED": __REDIS_ENABLED__, "ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__", "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_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_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_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_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_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} __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_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false} __CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""} __CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __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:=[]} __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES # 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} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS # REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""} __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_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_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_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_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!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 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_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__!${__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_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_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!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_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!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 # REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json 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 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 stop_grace_period: 1m
command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'" command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports: ports:
- 8080:8080 - 80:8080
api: api:
environment: environment:
MEMPOOL_BACKEND: "electrum" MEMPOOL_BACKEND: "none"
ELECTRUM_HOST: "172.27.0.1"
ELECTRUM_PORT: "50001"
ELECTRUM_TLS_ENABLED: "false"
CORE_RPC_HOST: "172.27.0.1" CORE_RPC_HOST: "172.27.0.1"
CORE_RPC_PORT: "8332" CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "mempool" 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} __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
__ACCELERATOR__=${ACCELERATOR:=false} __ACCELERATOR__=${ACCELERATOR:=false}
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true} __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} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
# Export as environment variables to be used by envsubst # Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__ export __MAINNET_ENABLED__
@ -77,7 +76,6 @@ export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__ export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__ export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__ export __ADDITIONAL_CURRENCIES__
export __STRATUM_ENABLED__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname) folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder} echo ${folder}

View File

@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend cp ./docker/frontend/* ./frontend
cp ./nginx.conf ./frontend/ cp ./nginx.conf ./frontend/
cp ./nginx-mempool.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}: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}/0.0.0.0/g" ./frontend/nginx.conf
sed -i"" -e "s/user nobody;//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!/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 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 ### 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: 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 ### 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: Build the frontend:

View File

@ -54,10 +54,6 @@
"translation": "src/locale/messages.fr.xlf", "translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/" "baseHref": "/fr/"
}, },
"hr": {
"translation": "src/locale/messages.hr.xlf",
"baseHref": "/hr/"
},
"ja": { "ja": {
"translation": "src/locale/messages.ja.xlf", "translation": "src/locale/messages.ja.xlf",
"baseHref": "/ja/" "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.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
//TODO(knorrium): add a check for the proxied server cy.changeNetwork('testnet4');
// cy.changeNetwork('testnet4');
cy.changeNetwork('signet'); cy.changeNetwork('signet');
cy.changeNetwork('mainnet'); 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)', () => { it('shows RBF transactions properly (desktop)', () => {

View File

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

View File

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

View File

@ -3,10 +3,8 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf'); let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => { PROXY_CONFIG.forEach(entry => {
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space'; entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
console.log(`e2e tests running against ${hostname}`); entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
entry.target = entry.target.replace("mempool.space", hostname);
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
}); });
module.exports = PROXY_CONFIG; module.exports = PROXY_CONFIG;

View File

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

View File

@ -151,7 +151,7 @@ export const languages: Language[] = [
{ code: 'fr', name: 'Français' }, // French { code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician // { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean { code: 'ko', name: '한국어' }, // Korean
{ code: 'hr', name: 'Hrvatski' }, // Croatian // { code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian // { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi { code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese { code: 'ne', name: 'नेपाली' }, // Nepalese
@ -439,39 +439,4 @@ export const fiatCurrencies = {
code: 'ZAR', code: 'ZAR',
indexed: true, 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 { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server'; 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 { AppModule } from './app.module';
import { AppComponent } from '@components/app/app.component'; import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor'; import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { ZoneService } from '@app/services/zone.service'; import { ZoneService } from './services/zone.service';
@NgModule({ @NgModule({
@ -20,4 +20,4 @@ import { ZoneService } from '@app/services/zone.service';
], ],
bootstrap: [AppComponent], 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 { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 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 { AppRoutingModule } from './app-routing.module';
import { AppComponent } from '@components/app/app.component'; import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from '@app/services/electrs-api.service'; import { ElectrsApiService } from './services/electrs-api.service';
import { OrdApiService } from '@app/services/ord-api.service'; import { StateService } from './services/state.service';
import { StateService } from '@app/services/state.service'; import { CacheService } from './services/cache.service';
import { CacheService } from '@app/services/cache.service'; import { PriceService } from './services/price.service';
import { PriceService } from '@app/services/price.service'; import { EnterpriseService } from './services/enterprise.service';
import { EnterpriseService } from '@app/services/enterprise.service'; import { WebsocketService } from './services/websocket.service';
import { WebsocketService } from '@app/services/websocket.service'; import { AudioService } from './services/audio.service';
import { AudioService } from '@app/services/audio.service'; import { PreloadService } from './services/preload.service';
import { PreloadService } from '@app/services/preload.service'; import { SeoService } from './services/seo.service';
import { SeoService } from '@app/services/seo.service'; import { OpenGraphService } from './services/opengraph.service';
import { OpenGraphService } from '@app/services/opengraph.service'; import { ZoneService } from './services/zone-shim.service';
import { ZoneService } from '@app/services/zone-shim.service'; import { SharedModule } from './shared/shared.module';
import { SharedModule } from '@app/shared/shared.module'; import { StorageService } from './services/storage.service';
import { StorageService } from '@app/services/storage.service'; import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor'; import { LanguageService } from './services/language.service';
import { LanguageService } from '@app/services/language.service'; import { ThemeService } from './services/theme.service';
import { ThemeService } from '@app/services/theme.service'; import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { TimeService } from '@app/services/time.service'; import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe'; import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe'; import { AppPreloadingStrategy } from './app.preloading-strategy';
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe'; import { ServicesApiServices } from './services/services-api.service';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
import { ServicesApiServices } from '@app/services/services-api.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
const providers = [ const providers = [
ElectrsApiService, ElectrsApiService,
OrdApiService,
StateService, StateService,
CacheService, CacheService,
PriceService, PriceService,
@ -45,7 +42,6 @@ const providers = [
EnterpriseService, EnterpriseService,
LanguageService, LanguageService,
ThemeService, ThemeService,
TimeService,
ShortenStringPipe, ShortenStringPipe,
FiatShortenerPipe, FiatShortenerPipe,
FiatCurrencyPipe, FiatCurrencyPipe,

View File

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

View File

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

View File

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

View File

@ -53,7 +53,7 @@
<span>Spiral</span> <span>Spiral</span>
</a> </a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry"> <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> <defs>
<style> <style>
.d { .d {
@ -125,14 +125,17 @@
<span>Blockstream</span> <span>Blockstream</span>
</a> </a>
<a href="https://unchained.com/" target="_blank" title="Unchained"> <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"> <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>
<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> <span>Unchained</span>
</a> </a>
<a href="https://bitkey.world/" target="_blank" title="Bitkey"> <a href="https://gemini.com/" target="_blank" title="Gemini">
<img class="image" src="/resources/profile/bitkey.svg" /> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<span>Bitkey</span> <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>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin"> <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"> <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> <span>Bull Bitcoin</span>
</a> </a>
<a href="https://exodus.com/" target="_blank" title="Exodus"> <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"/> <circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)"> <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)"/> <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> </svg>
<span>Exodus</span> <span>Exodus</span>
</a> </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>
</div> </div>
<ng-container> <ng-container>
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor"> <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> <h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper"> <div class="wrapper">
<ng-container> <ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales"> <ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <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> </a>
</ng-template> </ng-template>
</ng-container> </ng-container>
@ -229,7 +214,7 @@
<div class="wrapper"> <div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads"> <ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <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> </a>
</ng-template> </ng-template>
</div> </div>
@ -450,7 +435,7 @@
Trademark Notice<br> Trademark Notice<br>
</div> </div>
<p> <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>
<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;. 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 { .image.not-rounded {
border-radius: 0; border-radius: 0;
width: 60px;
height: 60px;
} }
.intro { .intro {
@ -92,13 +94,6 @@
} }
} }
.whale-sponsor {
img {
width: 70px;
height: 70px;
}
}
.alliances { .alliances {
margin-bottom: 100px; margin-bottom: 100px;
a { a {
@ -163,8 +158,9 @@
margin: 40px 29px 10px; margin: 40px 29px 10px;
&.image.coldcard { &.image.coldcard {
border-radius: 0; border-radius: 0;
height: auto; width: auto;
margin: 20px 29px 20px; max-height: 50px;
margin: 40px 29px 14px 29px;
} }
} }
} }
@ -258,12 +254,3 @@
width: 64px; width: 64px;
height: 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 { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '@app/services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service'; import { OpenGraphService } from '../../services/opengraph.service';
import { StateService } from '@app/services/state.service'; import { StateService } from '../../services/state.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from '@app/services/api.service'; import { ApiService } from '../../services/api.service';
import { IBackendInfo } from '@interfaces/websocket.interface'; import { IBackendInfo } from '../../interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators'; 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 { DOCUMENT } from '@angular/common';
import { EnterpriseService } from '@app/services/enterprise.service'; import { EnterpriseService } from '../../services/enterprise.service';
@Component({ @Component({
selector: 'app-about', selector: 'app-about',

View File

@ -1,9 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from '@components/about/about.component'; import { AboutComponent } from './about.component';
import { AboutSponsorsComponent } from '@components/about/about-sponsors.component'; import { AboutSponsorsComponent } from './about-sponsors.component';
import { SharedModule } from '@app/shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
const routes: Routes = [ 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) {
@if (accelerateError.includes('Payment declined')) { <div class="row mb-1 text-center">
<div class="row mb-1 text-center"> <div class="col-sm">
<div class="col-sm"> <h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
<h1 style="font-size: larger;">{{ accelerateError }}</h1>
</div>
</div> </div>
} @else { </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>
</div>
}
<div class="row text-center mt-1"> <div class="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center"> <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> <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
</div> </div>
</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"> <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> </div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) { @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
<div class="row"> <div class="row">
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> <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> <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> <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> <app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
} @else if (btcpayInvoiceFailed) { } @else if (btcpayInvoiceFailed) {
<div class="btcpay-invoice"> <p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p>
<fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon> <div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;">
<span i18n="accelerator.failed-to-load-invoice">Failed to load invoice</span> <fa-icon style="font-size: 24px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
@if (!loadingBtcpayInvoice) {
<button class="btn btn-sm btn-secondary mt-0 mt-md-1" (click)="requestBTCPayInvoice()">Retry ↻</button>
}
</div> </div>
} @else { } @else {
<p i18n="accelerator.loading-invoice">Loading invoice...</p> <p i18n="accelerator.loading-invoice">Loading invoice...</p>
@ -400,40 +389,21 @@
</div> </div>
} }
</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"> <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> </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"> <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> <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
@if (canPayWithCashapp) { @if (canPayWithCashapp) {
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')"> <img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
} }
@if (canPayWithApplePay) { @if (canPayWithApplePay) {
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> } @if (canPayWithCashapp) { <hr class="w-25 mt-2 mb-2"> }
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('applepay')"> <img style="cursor: pointer;" src="/resources/apple-pay.svg" height=55 (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>
} }
</div> </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> <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>
</div> </div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') { } @else if (step === 'cashapp' || step === 'applepay') {
<!-- Show checkout page --> <!-- Show checkout page -->
<div class="row mb-md-1 text-center" id="confirm-title"> <div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title"> <div class="col-sm" id="confirm-payment-title">
@ -473,7 +443,7 @@
</div> </div>
</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="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="form-group w-100"> <div class="form-group w-100">
@ -493,29 +463,17 @@
<div class="col-sm"> <div class="col-sm">
<div class="form-group w-100"> <div class="form-group w-100">
@if (step === 'applepay') { @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') { } @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> <div id="cash-app-pay" class="d-inline-block" [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>
} }
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) { @if (loadingCashapp || loadingApplePay) {
<div display="d-flex flex-row justify-content-center"> <div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span> <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 class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div> </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>
</div> </div>
@ -557,7 +515,7 @@
<div class="col-sm"> <div class="col-sm">
<div class="d-flex flex-row flex-column justify-content-center align-items-center"> <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> <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> <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> <div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>

View File

@ -8,13 +8,6 @@
color: var(--green) color: var(--green)
} }
.accelerate-checkout-inner {
&.input-disabled {
pointer-events: none;
opacity: 0.75;
}
}
.paymentMethod { .paymentMethod {
padding: 10px; padding: 10px;
background-color: var(--secondary); background-color: var(--secondary);
@ -153,11 +146,6 @@
.payment-area { .payment-area {
background: var(--bg); background: var(--bg);
margin-top: 0.5rem;
padding: 0.5rem;
@media (max-width: 575px) {
padding-bottom: 1.25rem;
}
} }
.col.pie { .col.pie {
@ -184,6 +172,10 @@
background-color: var(--tertiary); background-color: var(--tertiary);
} }
.btn-small-height {
line-height: 1;
}
.summary-row { .summary-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -224,17 +216,4 @@
} }
.apple-pay-button-white-with-line { .apple-pay-button-white-with-line {
-apple-pay-button-style: white-outline; -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> </p>
</div> </div>
<div class="spacer"></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 class="spacer"></div> <div class="spacer"></div>
</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 { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '@interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component'; import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
interface GraphBar { interface GraphBar {
rate: number; 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="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
<div class="timeline-wrapper"> <div class="timeline-wrapper">
@if (!tx.status.confirmed || canceled) { @if (!tx.status.confirmed) {
<div class="timeline"> <div class="timeline">
<div class="intervals"> <div class="intervals">
<div class="node-spacer"></div> <div class="node-spacer"></div>
@ -8,8 +8,8 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (eta && !canceled) { @if (eta) {
~<app-time [time]="eta?.wait / 1000"></app-time> ~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
} }
</div> </div>
</div> </div>
@ -19,20 +19,16 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval-spacer"></div> <div class="interval-spacer"></div>
<div class="node"> <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>
<div class="interval-spacer"> <div class="interval-spacer">
</div> </div>
<div class="node" [id]="'confirmed'"> <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-border waiting">
<div class="shape"></div> <div class="shape animate"></div>
</div> </div>
@if (canceled) { <div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
<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> </div>
</div> </div>
</div> </div>
@ -42,16 +38,18 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
<app-time [time]="firstSeenToAccelerated"></app-time> <app-time [time]="acceleratedAt - transactionTime"></app-time>
</div> </div>
</div> </div>
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
<app-time [time]="acceleratedToMined"></app-time> <div class="interval-time">
} @else if (eta && canceled) { <app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
~<app-time [time]="eta?.wait / 1000"></app-time> </div>
} @else if (standardETA && !tx.status.confirmed) {
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
} }
</div> </div>
</div> </div>
@ -60,7 +58,7 @@
<div class="nodes"> <div class="nodes">
<div class="node" [id]="'first-seen'"> <div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div> <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 class="shape"></div>
</div> </div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></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="interval-spacer">
<div class="seen-to-acc"></div> <div class="seen-to-acc"></div>
</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> <div class="seen-to-acc left"></div>
@if (tx.status.confirmed && !canceled) { @if (tx.status.confirmed) {
<div class="acc-to-confirmed right"></div> <div class="acc-to-confirmed right"></div>
} @else { } @else {
<div class="seen-to-acc right"></div> <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> <div class="shape"></div>
@if (!tx.status.confirmed || canceled) { @if (!tx.status.confirmed) {
<div class="connector down" [class.loading]="!canceled"></div> <div class="connector down loading"></div>
} }
</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="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) { @if (!tx.status.confirmed) {
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }} <span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
} }
@if (useAbsoluteTime) { @if (useAbsoluteTime) {
<span>{{ acceleratedAt * 1000 | date }}</span> <span>{{ acceleratedAt * 1000 | date }}</span>
} @else { } @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> </div>
<div class="interval-spacer"> <div class="interval-spacer">
@if (tx.status.confirmed && !canceled) { @if (tx.status.confirmed) {
<div class="acc-to-confirmed"></div> <div class="acc-to-confirmed"></div>
} @else { } @else {
<div class="seen-to-acc"></div> <div class="seen-to-acc"></div>
} }
</div> </div>
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'"> <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> <div class="acc-to-confirmed left"></div>
} @else { } @else {
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>
} }
<div class="shape-border" <div class="shape-border" [class.waiting]="!tx.status.confirmed">
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
(pointerout)="onBlur($event);">
<div class="shape"></div> <div class="shape"></div>
</div> </div>
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
@ -135,10 +130,4 @@
</div> </div>
</div> </div>
</div> </div>
<app-acceleration-timeline-tooltip
[accelerationInfo]="hoverInfo"
[cursorPosition]="tooltipPosition"
></app-acceleration-timeline-tooltip>
</div> </div>

View File

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

View File

@ -1,8 +1,6 @@
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core'; import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { ETA } from '@app/services/eta.service'; import { ETA } from '../../services/eta.service';
import { Transaction } from '@interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { MiningService } from '@app/services/mining.service';
@Component({ @Component({
selector: 'app-acceleration-timeline', selector: 'app-acceleration-timeline',
@ -11,82 +9,47 @@ import { MiningService } from '@app/services/mining.service';
}) })
export class AccelerationTimelineComponent implements OnInit, OnChanges { export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number; @Input() transactionTime: number;
@Input() acceleratedAt: number;
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() eta: ETA; @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; now: number;
accelerateRatio: number; accelerateRatio: number;
useAbsoluteTime: boolean = false; useAbsoluteTime: boolean = false;
firstSeenToAccelerated: number; interval: number;
acceleratedToMined: number;
tooltipPosition = null; constructor() {}
hoverInfo: any = null;
poolsData: { [id: number]: SinglePoolStats } = {};
constructor(
private miningService: MiningService,
) {}
ngOnInit(): void { 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 => { this.interval = window.setInterval(() => {
for (const pool of pools) { this.now = Math.floor(new Date().getTime() / 1000);
this.poolsData[pool.unique_id] = pool; this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
} }, 60000);
});
} }
ngOnChanges(changes): void { 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 { ngOnDestroy(): void {
this.now = Math.floor(new Date().getTime() / 1000); clearInterval(this.interval);
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 };
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
@if (chartOnly) { @if (chartOnly) {
<ng-container *ngTemplateOutlet="pieChart"></ng-container> <ng-container *ngTemplateOutlet="pieChart"></ng-container>
} @else { } @else {
<table style="width: 100%;"> <table>
<tbody> <tbody>
<tr> <tr>
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td> <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>
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''"> <td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container"> <div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) { @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate> <app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else { } @else {
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate> <app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
} }
</div> </div>
</td> </td>
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft"> <td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
@if (hasCpfp) { @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> <ng-container *ngTemplateOutlet="pieChart"></ng-container>
</div> </div>
@ -36,7 +36,7 @@
<tr> <tr>
<td colspan="3" class="pt-0"> <td colspan="3" class="pt-0">
<div class="d-flex justify-content-end align-items-start"> <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> </div>
</td> </td>
</tr> </tr>

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