Compare commits

..

4 Commits

Author SHA1 Message Date
Mononaut
7569c6a394 Avoid initializing rbf cache in worker threads 2023-08-24 22:00:25 +09:00
Mononaut
d67285c683 Avoid initializing redis in worker threads 2023-08-24 22:00:25 +09:00
Mononaut
2b7ac32c22 Parallelize block summary/cpfp indexing with worker threads 2023-08-24 22:00:25 +09:00
Mononaut
df596ab5bf Parallelize block indexing with worker threads 2023-08-24 22:00:25 +09:00
338 changed files with 68466 additions and 108932 deletions

View File

@@ -9,7 +9,7 @@ jobs:
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/')"
strategy: strategy:
matrix: matrix:
node: ["18", "20"] node: ["16", "17", "18", "20"]
flavor: ["dev", "prod"] flavor: ["dev", "prod"]
fail-fast: false fail-fast: false
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
@@ -27,17 +27,8 @@ jobs:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- name: Read rust-toolchain file from repository - name: Install 1.63.x Rust toolchain
id: gettoolchain uses: dtolnay/rust-toolchain@1.63
run: echo "::set-output name=toolchain::$(cat rust-toolchain)"
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
# Latest version available on this commit is 1.71.1
# Commit date is Aug 3, 2023
uses: dtolnay/rust-toolchain@f361669954a8ecfc00a3443f35f9ac8e610ffc06
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
- name: Install - name: Install
if: ${{ matrix.flavor == 'dev'}} if: ${{ matrix.flavor == 'dev'}}
@@ -67,7 +58,7 @@ jobs:
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/')"
strategy: strategy:
matrix: matrix:
node: ["18", "20"] node: ["16", "17", "18", "20"]
flavor: ["dev", "prod"] flavor: ["dev", "prod"]
fail-fast: false fail-fast: false
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"

View File

@@ -38,7 +38,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 18
cache: "npm" cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json

View File

@@ -1,19 +0,0 @@
name: 'Print backend hashes'
on: [workflow_dispatch]
jobs:
print-backend-sha:
runs-on: 'ubuntu-latest'
name: Print backend hashes
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: repo
- name: Run script
working-directory: repo
run: |
chmod +x ./scripts/get_backend_hash.sh
sh ./scripts/get_backend_hash.sh

View File

@@ -68,17 +68,17 @@ jobs:
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project - name: Checkout project
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Init repo for Dockerization - name: Init repo for Dockerization
run: docker/init.sh "$TAG" run: docker/init.sh "$TAG"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
id: qemu id: qemu
- name: Setup Docker buildx action - name: Setup Docker buildx action
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
id: buildx id: buildx
- name: Available platforms - name: Available platforms
@@ -98,7 +98,7 @@ jobs:
docker buildx build \ docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \ --cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \ --cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
--output "type=registry" ./${{ matrix.service }}/ \ --output "type=registry" ./${{ matrix.service }}/ \

2
.nvmrc
View File

@@ -1 +1 @@
v20.8.0 v16.16.0

47
GNUmakefile Executable file
View File

@@ -0,0 +1,47 @@
# If you see pwd_unknown showing up check permissions
PWD ?= pwd_unknown
# DATABASE DEPLOY FOLDER CONFIG - default ./data
ifeq ($(data),)
DATA := data
export DATA
else
DATA := $(data)
export DATA
endif
.PHONY: help
help:
@echo ''
@echo ''
@echo ' Usage: make [COMMAND]'
@echo ''
@echo ' make all # build init mempool and electrs'
@echo ' make init # setup some useful configs'
@echo ' make mempool # build q dockerized mempool.space'
@echo ' make electrs # build a docker electrs image'
@echo ''
.PHONY: init
init:
@echo ''
mkdir -p $(DATA) $(DATA)/mysql $(DATA)/mysql/data
#REF: https://github.com/mempool/mempool/blob/master/docker/README.md
cat docker/docker-compose.yml > docker-compose.yml
cat backend/mempool-config.sample.json > backend/mempool-config.json
.PHONY: mempool
mempool: init
@echo ''
docker-compose up --force-recreate --always-recreate-deps
@echo ''
.PHONY: electrs
electrum:
#REF: https://hub.docker.com/r/beli/electrum
@echo ''
docker build -f docker/electrum/Dockerfile .
@echo ''
.PHONY: all
all: init
make mempool
#######################
-include Makefile

1
Makefile Normal file
View File

@@ -0,0 +1 @@
# For additional configs/scripting

View File

@@ -23,7 +23,6 @@ Mempool can be conveniently installed on the following full-node distros:
- [RoninDojo](https://code.samourai.io/ronindojo/RoninDojo) - [RoninDojo](https://code.samourai.io/ronindojo/RoninDojo)
- [myNode](https://github.com/mynodebtc/mynode) - [myNode](https://github.com/mynodebtc/mynode)
- [Start9](https://github.com/Start9Labs/embassy-os) - [Start9](https://github.com/Start9Labs/embassy-os)
- [nix-bitcoin](https://github.com/fort-nix/nix-bitcoin/blob/a1eacce6768ca4894f365af8f79be5bbd594e1c3/examples/configuration.nix#L129)
**We highly recommend you deploy your own Mempool instance this way.** No matter which option you pick, you'll be able to get your own fully-sovereign instance of Mempool up quickly without needing to fiddle with any settings. **We highly recommend you deploy your own Mempool instance this way.** No matter which option you pick, you'll be able to get your own fully-sovereign instance of Mempool up quickly without needing to fiddle with any settings.

View File

@@ -85,7 +85,7 @@ Install dependencies with `npm` and build the backend:
``` ```
cd backend cd backend
npm install --no-install-links # npm@9.4.2 and later can omit the --no-install-links npm install
npm run build npm run build
``` ```

View File

@@ -40,9 +40,7 @@
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool",
"TIMEOUT": 60000, "TIMEOUT": 60000
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@@ -52,10 +50,7 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000", "REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet", "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 30000, "RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": [] "FALLBACK": []
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
@@ -63,9 +58,7 @@
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool",
"TIMEOUT": 60000, "TIMEOUT": 60000
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
}, },
"DATABASE": { "DATABASE": {
"ENABLED": true, "ENABLED": true,
@@ -75,8 +68,7 @@
"DATABASE": "mempool", "DATABASE": "mempool",
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool",
"TIMEOUT": 180000, "TIMEOUT": 180000
"PID_DIR": ""
}, },
"SYSLOG": { "SYSLOG": {
"ENABLED": true, "ENABLED": true,
@@ -133,11 +125,6 @@
"BISQ_URL": "https://bisq.markets/api", "BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api" "BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
}, },
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
},
"REPLICATION": { "REPLICATION": {
"ENABLED": false, "ENABLED": false,
"AUDIT": false, "AUDIT": false,

File diff suppressed because it is too large Load Diff

View File

@@ -38,12 +38,12 @@
"rust-build": "cd rust-gbt && npm run build-release" "rust-build": "cd rust-gbt && npm run build-release"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.23.2", "@babel/core": "^7.21.3",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "~1.6.1", "axios": "~1.4.0",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.1.1",
"express": "~4.18.2", "express": "~4.18.2",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.6.0", "mysql2": "~3.6.0",
@@ -55,7 +55,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
"@babel/core": "^7.23.2", "@babel/core": "^7.21.3",
"@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

@@ -6,6 +6,8 @@ authors = ["mononaut"]
edition = "2021" edition = "2021"
publish = false publish = false
[workspace]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]

View File

@@ -41,9 +41,7 @@
"PORT": 15, "PORT": 15,
"USERNAME": "__CORE_RPC_USERNAME__", "USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000, "TIMEOUT": 1000
"COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -53,10 +51,7 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__", "REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 888, "RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": [] "FALLBACK": []
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
@@ -64,9 +59,7 @@
"PORT": 17, "PORT": 17,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__", "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__", "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": 2000, "TIMEOUT": 2000
"COOKIE": false,
"COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
}, },
"DATABASE": { "DATABASE": {
"ENABLED": false, "ENABLED": false,
@@ -76,7 +69,6 @@
"DATABASE": "__DATABASE_DATABASE__", "DATABASE": "__DATABASE_DATABASE__",
"USERNAME": "__DATABASE_USERNAME__", "USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__", "PASSWORD": "__DATABASE_PASSWORD__",
"PID_DIR": "__DATABASE_PID_FILE__",
"TIMEOUT": 3000 "TIMEOUT": 3000
}, },
"SYSLOG": { "SYSLOG": {
@@ -141,7 +133,6 @@
}, },
"REDIS": { "REDIS": {
"ENABLED": false, "ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock", "UNIX_SOCKET_PATH": "/tmp/redis.sock"
"BATCH_QUERY_BASE_SIZE": 5000
} }
} }

View File

@@ -55,10 +55,7 @@ describe('Mempool Backend Config', () => {
expect(config.ESPLORA).toStrictEqual({ expect(config.ESPLORA).toStrictEqual({
REST_API_URL: 'http://127.0.0.1:3000', REST_API_URL: 'http://127.0.0.1:3000',
UNIX_SOCKET_PATH: null, UNIX_SOCKET_PATH: null,
BATCH_QUERY_BASE_SIZE: 1000,
RETRY_UNIX_SOCKET_AFTER: 30000, RETRY_UNIX_SOCKET_AFTER: 30000,
REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000,
FALLBACK: [], FALLBACK: [],
}); });
@@ -67,9 +64,7 @@ describe('Mempool Backend Config', () => {
PORT: 8332, PORT: 8332,
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool', PASSWORD: 'mempool',
TIMEOUT: 60000, TIMEOUT: 60000
COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie'
}); });
expect(config.SECOND_CORE_RPC).toStrictEqual({ expect(config.SECOND_CORE_RPC).toStrictEqual({
@@ -77,9 +72,7 @@ describe('Mempool Backend Config', () => {
PORT: 8332, PORT: 8332,
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool', PASSWORD: 'mempool',
TIMEOUT: 60000, TIMEOUT: 60000
COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie'
}); });
expect(config.DATABASE).toStrictEqual({ expect(config.DATABASE).toStrictEqual({
@@ -91,7 +84,6 @@ describe('Mempool Backend Config', () => {
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool', PASSWORD: 'mempool',
TIMEOUT: 180000, TIMEOUT: 180000,
PID_DIR: ''
}); });
expect(config.SYSLOG).toStrictEqual({ expect(config.SYSLOG).toStrictEqual({
@@ -145,8 +137,7 @@ describe('Mempool Backend Config', () => {
expect(config.REDIS).toStrictEqual({ expect(config.REDIS).toStrictEqual({
ENABLED: false, ENABLED: false,
UNIX_SOCKET_PATH: '', UNIX_SOCKET_PATH: ''
BATCH_QUERY_BASE_SIZE: 5000,
}); });
}); });
}); });

View File

@@ -9,7 +9,7 @@ class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false) auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 }; return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
@@ -144,12 +144,7 @@ class Audit {
const numCensored = Object.keys(isCensored).length; const numCensored = Object.keys(isCensored).length;
const numMatches = matches.length - 1; // adjust for coinbase tx const numMatches = matches.length - 1; // adjust for coinbase tx
let score = 0; const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
if (numMatches <= 0 && numCensored <= 0) {
score = 1;
} else if (numMatches > 0) {
score = (numMatches / (numMatches + numCensored));
}
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return { return {

View File

@@ -3,9 +3,8 @@ import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>; $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>; $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>; $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number); $getAllMempoolTransactions(lastTxid: string);
$getTransactionHex(txId: string): Promise<string>; $getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>; $getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>; $getBlockHashTip(): Promise<string>;
@@ -24,8 +23,6 @@ export interface AbstractBitcoinApi {
$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[][]>;
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
startHealthChecks(): void; startHealthChecks(): void;
} }
@@ -35,5 +32,4 @@ export interface BitcoinRpcCredentials {
user: string; user: string;
pass: string; pass: string;
timeout: number; timeout: number;
cookie?: string;
} }

View File

@@ -60,24 +60,11 @@ class BitcoinApi implements AbstractBitcoinApi {
}); });
} }
async $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
const txs: IEsploraApi.Transaction[] = [];
for (const txid of txids) {
try {
const tx = await this.$getRawTransaction(txid, false, true);
txs.push(tx);
} catch (err) {
// skip failures
}
}
return txs;
}
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> { $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.'); throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
} }
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> { $getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.'); throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
} }
@@ -211,19 +198,6 @@ class BitcoinApi implements AbstractBitcoinApi {
return outspends; return outspends;
} }
async $getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
return this.$getBatchedOutspends(txId);
}
async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]> {
const outspends: IEsploraApi.Outspend[] = [];
for (const outpoint of outpoints) {
const outspend = await this.$getOutspend(outpoint.txid, outpoint.vout);
outspends.push(outspend);
}
return outspends;
}
$getEstimatedHashrate(blockHeight: number): Promise<number> { $getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core // 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight); return this.bitcoindClient.getNetworkHashPs(120, blockHeight);

View File

@@ -8,7 +8,6 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
user: config.CORE_RPC.USERNAME, user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD, pass: config.CORE_RPC.PASSWORD,
timeout: config.CORE_RPC.TIMEOUT, timeout: config.CORE_RPC.TIMEOUT,
cookie: config.CORE_RPC.COOKIE ? config.CORE_RPC.COOKIE_PATH : undefined,
}; };
export default new bitcoin.Client(nodeRpcCredentials); export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -8,7 +8,6 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
user: config.SECOND_CORE_RPC.USERNAME, user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD, pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: config.SECOND_CORE_RPC.TIMEOUT, timeout: config.SECOND_CORE_RPC.TIMEOUT,
cookie: config.SECOND_CORE_RPC.COOKIE ? config.SECOND_CORE_RPC.COOKIE_PATH : undefined,
}; };
export default new bitcoin.Client(nodeRpcCredentials); export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -24,6 +24,7 @@ class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes) .get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo) .get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange) .get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees) .get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
@@ -111,7 +112,6 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'txs/outspends', this.$getBatchedOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
@@ -174,20 +174,24 @@ class BitcoinRoutes {
res.json(times); res.json(times);
} }
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> { private async $getBatchedOutspends(req: Request, res: Response) {
const txids_csv = req.query.txids; if (!Array.isArray(req.query.txId)) {
if (!txids_csv || typeof txids_csv !== 'string') { res.status(500).send('Not an array');
res.status(500).send('Invalid txids format');
return; return;
} }
const txids = txids_csv.split(','); if (req.query.txId.length > 50) {
if (txids.length > 50) {
res.status(400).send('Too many txids requested'); res.status(400).send('Too many txids requested');
return; return;
} }
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString());
}
}
try { try {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds);
res.json(batchedOutspends); res.json(batchedOutspends);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
@@ -247,7 +251,7 @@ class BitcoinRoutes {
private async getTransaction(req: Request, res: Response) { private async getTransaction(req: Request, res: Response) {
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction); res.json(transaction);
} catch (e) { } catch (e) {
let statusCode = 500; let statusCode = 500;
@@ -474,7 +478,7 @@ class BitcoinRoutes {
} }
let nextHash = startFromHash; let nextHash = startFromHash;
for (let i = 0; i < 15 && nextHash; i++) { for (let i = 0; i < 10 && nextHash; i++) {
const localBlock = blocks.getBlocks().find((b) => b.id === nextHash); const localBlock = blocks.getBlocks().find((b) => b.id === nextHash);
if (localBlock) { if (localBlock) {
returnBlocks.push(localBlock); returnBlocks.push(localBlock);
@@ -573,9 +577,7 @@ class BitcoinRoutes {
} }
try { try {
// electrum expects scripthashes in little-endian const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
const addressData = await bitcoinApi.$getScriptHash(electrumScripthash);
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)) {
@@ -592,13 +594,11 @@ class BitcoinRoutes {
} }
try { try {
// electrum expects scripthashes in little-endian
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
let lastTxId: string = ''; let lastTxId: string = '';
if (req.query.after_txid && typeof req.query.after_txid === 'string') { if (req.query.after_txid && typeof req.query.after_txid === 'string') {
lastTxId = req.query.after_txid; lastTxId = req.query.after_txid;
} }
const transactions = await bitcoinApi.$getScriptHashTransactions(electrumScripthash, lastTxId); const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
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)) {

View File

@@ -6,7 +6,6 @@ export namespace IEsploraApi {
size: number; size: number;
weight: number; weight: number;
fee: number; fee: number;
sigops?: number;
vin: Vin[]; vin: Vin[];
vout: Vout[]; vout: Vout[];
status: Status; status: Status;

View File

@@ -8,9 +8,8 @@ import logger from '../../logger';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
rtts: number[], rtts: number[],
rtt: number, rtt: number
failures: number, failures: number,
latestHeight?: number,
socket?: boolean, socket?: boolean,
outOfSync?: boolean, outOfSync?: boolean,
unreachable?: boolean, unreachable?: boolean,
@@ -76,9 +75,9 @@ class FailoverRouter {
const results = await Promise.allSettled(this.hosts.map(async (host) => { const results = await Promise.allSettled(this.hosts.map(async (host) => {
if (host.socket) { if (host.socket) {
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }); return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 2000 });
} else { } else {
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT }); return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 2000 });
} }
})); }));
const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0); const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0);
@@ -93,7 +92,6 @@ class FailoverRouter {
host.rtts.unshift(rtt); host.rtts.unshift(rtt);
host.rtts.slice(0, 5); host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length; host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (maxHeight - height > 2)) { if (height == null || isNaN(height) || (maxHeight - height > 2)) {
host.outOfSync = true; host.outOfSync = true;
} else { } else {
@@ -101,23 +99,22 @@ class FailoverRouter {
} }
host.unreachable = false; host.unreachable = false;
} else { } else {
host.outOfSync = true;
host.unreachable = true; host.unreachable = true;
} }
} }
this.sortHosts(); this.sortHosts();
logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`); logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
// switch if the current host is out of sync or significantly slower than the next best alternative // switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) { if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
if (this.activeHost.unreachable) { if (this.activeHost.unreachable) {
logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`); logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`);
} else if (this.activeHost.outOfSync) { } else if (this.activeHost.outOfSync) {
logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`); logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`);
} else { } else {
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`); logger.debug(`${this.activeHost.host} is no longer the best esplora host`);
} }
this.electHost(); this.electHost();
} }
@@ -125,11 +122,6 @@ class FailoverRouter {
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval); this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
} }
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
const heightStatus = host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅');
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${host.unreachable ? '🔥' : '✅'} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
}
// sort hosts by connection quality, and update default fallback // sort hosts by connection quality, and update default fallback
private sortHosts(): void { private sortHosts(): void {
// sort by connection quality // sort by connection quality
@@ -164,7 +156,7 @@ class FailoverRouter {
private addFailure(host: FailoverHost): FailoverHost { private addFailure(host: FailoverHost): FailoverHost {
host.failures++; host.failures++;
if (host.failures > 5 && this.multihost) { if (host.failures > 5 && this.multihost) {
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`); logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
this.electHost(); this.electHost();
return this.activeHost; return this.activeHost;
} else { } else {
@@ -176,15 +168,12 @@ class FailoverRouter {
let axiosConfig; let axiosConfig;
let url; let url;
if (host.socket) { if (host.socket) {
axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType }; axiosConfig = { socketPath: host.host, timeout: 10000, responseType };
url = path; url = path;
} else { } else {
axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType }; axiosConfig = { timeout: 10000, responseType };
url = host.host + path; url = host.host + path;
} }
if (data?.params) {
axiosConfig.params = data.params;
}
return (method === 'post' return (method === 'post'
? this.requestConnection.post<T>(url, data, axiosConfig) ? this.requestConnection.post<T>(url, data, axiosConfig)
: this.requestConnection.get<T>(url, axiosConfig) : this.requestConnection.get<T>(url, axiosConfig)
@@ -192,8 +181,7 @@ class FailoverRouter {
.catch((e) => { .catch((e) => {
let fallbackHost = this.fallbackHost; let fallbackHost = this.fallbackHost;
if (e?.response?.status !== 404) { if (e?.response?.status !== 404) {
logger.warn(`esplora request failed ${e?.response?.status} ${host.host}${path}`); logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`);
logger.warn(e instanceof Error ? e.message : e);
fallbackHost = this.addFailure(host); fallbackHost = this.addFailure(host);
} }
if (retry && e?.code === 'ECONNREFUSED' && this.multihost) { if (retry && e?.code === 'ECONNREFUSED' && this.multihost) {
@@ -205,8 +193,8 @@ class FailoverRouter {
}); });
} }
public async $get<T>(path, responseType = 'json', params: any = null): Promise<T> { public async $get<T>(path, responseType = 'json'): Promise<T> {
return this.$query<T>('get', path, params ? { params } : null, responseType); return this.$query<T>('get', path, null, responseType);
} }
public async $post<T>(path, data: any, responseType = 'json'): Promise<T> { public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
@@ -225,16 +213,12 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId); return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId);
} }
async $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/txs', txids, 'json');
}
async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> { async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json'); return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/mempool/txs', txids, 'json');
} }
async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> { async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null); return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
} }
$getTransactionHex(txId: string): Promise<string> { $getTransactionHex(txId: string): Promise<string> {
@@ -254,7 +238,7 @@ class ElectrsApi implements AbstractBitcoinApi {
} }
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> { $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/block/' + hash + '/txs'); return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/block/' + hash + '/txs');
} }
$getBlockHash(height: number): Promise<string> { $getBlockHash(height: number): Promise<string> {
@@ -306,16 +290,13 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends'); return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends');
} }
async $getBatchedOutspends(txids: string[]): Promise<IEsploraApi.Outspend[][]> { async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
throw new Error('Method not implemented.'); const outspends: IEsploraApi.Outspend[][] = [];
} for (const tx of txId) {
const outspend = await this.$getOutspends(tx);
async $getBatchedOutspendsInternal(txids: string[]): Promise<IEsploraApi.Outspend[][]> { outspends.push(outspend);
return this.failoverRouter.$post<IEsploraApi.Outspend[][]>('/internal/txs/outspends/by-txid', txids, 'json'); }
} return outspends;
async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]> {
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
} }
public startHealthChecks(): void { public startHealthChecks(): void {

View File

@@ -29,6 +29,10 @@ import websocketHandler from './websocket-handler';
import redisCache from './redis-cache'; import redisCache from './redis-cache';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
import { calcBitsDifference } from './difficulty-adjustment'; import { calcBitsDifference } from './difficulty-adjustment';
import os from 'os';
import { Worker } from 'worker_threads';
import path from 'path';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@@ -81,7 +85,6 @@ class Blocks {
private async $getTransactionsExtended( private async $getTransactionsExtended(
blockHash: string, blockHash: string,
blockHeight: number, blockHeight: number,
blockTime: number,
onlyCoinbase: boolean, onlyCoinbase: boolean,
txIds: string[] | null = null, txIds: string[] | null = null,
quiet: boolean = false, quiet: boolean = false,
@@ -102,12 +105,6 @@ class Blocks {
if (!onlyCoinbase) { if (!onlyCoinbase) {
for (const txid of txIds) { for (const txid of txIds) {
if (mempool[txid]) { if (mempool[txid]) {
mempool[txid].status = {
confirmed: true,
block_height: blockHeight,
block_hash: blockHash,
block_time: blockTime,
};
transactionMap[txid] = mempool[txid]; transactionMap[txid] = mempool[txid];
foundInMempool++; foundInMempool++;
totalFound++; totalFound++;
@@ -413,6 +410,8 @@ class Blocks {
return; return;
} }
const workerPool: Worker[] = [];
try { try {
// Get all indexed block hash // Get all indexed block hash
const indexedBlocks = await blocksRepository.$getIndexedBlocks(); const indexedBlocks = await blocksRepository.$getIndexedBlocks();
@@ -427,39 +426,67 @@ class Blocks {
let newlyIndexed = 0; let newlyIndexed = 0;
let totalIndexed = indexedBlockSummariesHashesArray.length; let totalIndexed = indexedBlockSummariesHashesArray.length;
let indexedThisRun = 0; let indexedThisRun = 0;
let timer = Date.now() / 1000; let timer = Date.now();
const startedAt = Date.now() / 1000; const startedAt = Date.now();
for (const block of indexedBlocks) { const blocksToIndex = indexedBlocks.filter(block => !indexedBlockSummariesHashes[block.hash]);
if (indexedBlockSummariesHashes[block.hash] === true) {
continue;
}
// Logging if (!blocksToIndex.length) {
const elapsedSeconds = (Date.now() / 1000) - timer; return;
if (elapsedSeconds > 5) { }
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = indexedThisRun / elapsedSeconds;
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now() / 1000;
indexedThisRun = 0;
}
const numWorkers = Math.max(1, os.cpus().length - 1);
for (let i = 0; i < numWorkers; i++) {
workerPool.push(new Worker(path.resolve(__dirname, '../index-workers/block-summary-worker.js')));
}
if (config.MEMPOOL.BACKEND === 'esplora') { const promises: Promise<void>[] = [];
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs); // This function assigns a task to a worker
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary const assignTask = (worker: Worker): boolean => {
if (blocksToIndex.length === 0) {
return false;
} else { } else {
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary worker.postMessage(blocksToIndex.shift());
return true;
} }
};
// Logging const handleResult = (height: number): void => {
indexedThisRun++; indexedThisRun++;
totalIndexed++; totalIndexed++;
newlyIndexed++; newlyIndexed++;
const elapsed = Date.now() - timer;
if (elapsed > 5000) {
const runningFor = Date.now() - startedAt;
const blockPerSeconds = indexedThisRun / (elapsed / 1000);
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
logger.debug(`Indexing block summary for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${(runningFor / 1000).toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now();
indexedThisRun = 0;
}
};
// Start a task on each worker
for (const worker of workerPool) {
promises.push(new Promise((resolve, reject) => {
worker.removeAllListeners();
worker.on('message', (result) => {
// Handle the result, then assign a new task to the worker
handleResult(result);
if (!assignTask(worker)) {
resolve();
};
});
worker.on('error', reject);
if (!assignTask(worker)) {
resolve();
}
}));
} }
await Promise.all(promises);
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
} else { } else {
@@ -468,6 +495,14 @@ class Blocks {
} catch (e) { } catch (e) {
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e; throw e;
} finally {
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
} }
} }
@@ -564,6 +599,7 @@ class Blocks {
* [INDEXING] Index all blocks metadata for the mining dashboard * [INDEXING] Index all blocks metadata for the mining dashboard
*/ */
public async $generateBlockDatabase(): Promise<boolean> { public async $generateBlockDatabase(): Promise<boolean> {
const workerPool: Worker[] = [];
try { try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo(); const blockchainInfo = await bitcoinClient.getBlockchainInfo();
let currentBlockHeight = blockchainInfo.blocks; let currentBlockHeight = blockchainInfo.blocks;
@@ -582,12 +618,18 @@ class Blocks {
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex); let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
let indexedThisRun = 0; let indexedThisRun = 0;
let newlyIndexed = 0; let newlyIndexed = 0;
const startedAt = Date.now() / 1000; const startedAt = Date.now();
let timer = Date.now() / 1000; let timer = Date.now();
if (currentBlockHeight >= lastBlockToIndex) {
const numWorkers = Math.max(1, os.cpus().length - 1);
for (let i = 0; i < numWorkers; i++) {
workerPool.push(new Worker(path.resolve(__dirname, '../index-workers/block-worker.js')));
}
}
while (currentBlockHeight >= lastBlockToIndex) { while (currentBlockHeight >= lastBlockToIndex) {
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1); const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
const missingBlockHeights: number[] = await blocksRepository.$getMissingBlocksBetweenHeights( const missingBlockHeights: number[] = await blocksRepository.$getMissingBlocksBetweenHeights(
currentBlockHeight, endBlock); currentBlockHeight, endBlock);
if (missingBlockHeights.length <= 0) { if (missingBlockHeights.length <= 0) {
@@ -597,33 +639,65 @@ class Blocks {
logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`, logger.tags.mining); logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`, logger.tags.mining);
for (const blockHeight of missingBlockHeights) { const promises: Promise<void>[] = [];
if (blockHeight < lastBlockToIndex) {
break; // This function assigns a task to a worker
const assignTask = (worker: Worker): boolean => {
if (missingBlockHeights.length === 0) {
return false;
} else {
worker.postMessage({ height: missingBlockHeights.shift() });
return true;
} }
++indexedThisRun; };
++totalIndexed;
const elapsedSeconds = (Date.now() / 1000) - timer; const handleResult = (height: number): void => {
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) { indexedThisRun++;
const runningFor = (Date.now() / 1000) - startedAt; totalIndexed++;
const blockPerSeconds = indexedThisRun / elapsedSeconds; newlyIndexed++;
const elapsed = Date.now() - timer;
if (elapsed > 5000 || height === lastBlockToIndex) {
const runningFor = Date.now() - startedAt;
const blockPerSeconds = indexedThisRun / (elapsed / 1000);
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100; const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining); logger.debug(`Indexing block #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${(runningFor / 1000).toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now() / 1000; timer = Date.now();
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('block-indexing', progress, false); loadingIndicators.setProgress('block-indexing', progress, false);
} }
const blockHash = await bitcoinApi.$getBlockHash(blockHeight); };
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, true, null, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
newlyIndexed++; // Start a task on each worker
await blocksRepository.$saveBlockInDatabase(blockExtended); for (const worker of workerPool) {
promises.push(new Promise((resolve, reject) => {
worker.removeAllListeners();
worker.on('message', (result) => {
// Handle the result, then assign a new task to the worker
handleResult(result);
if (!assignTask(worker)) {
resolve();
};
});
worker.on('error', reject);
if (!assignTask(worker)) {
resolve();
}
}));
} }
await Promise.all(promises);
currentBlockHeight -= chunkSize; currentBlockHeight -= chunkSize;
} }
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
} else { } else {
@@ -634,6 +708,14 @@ class Blocks {
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
loadingIndicators.setProgress('block-indexing', 100); loadingIndicators.setProgress('block-indexing', 100);
throw e; throw e;
} finally {
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
} }
return await BlocksRepository.$validateChain(); return await BlocksRepository.$validateChain();
@@ -708,7 +790,7 @@ class Blocks {
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2); const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
const block = BitcoinApi.convertBlock(verboseBlock); const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid); const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, false, txIds, false, true) as MempoolTransactionExtended[]; const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
// fill in missing transaction fee data from verboseBlock // fill in missing transaction fee data from verboseBlock
for (let i = 0; i < transactions.length; i++) { for (let i = 0; i < transactions.length; i++) {
@@ -761,13 +843,8 @@ class Blocks {
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`); this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
if (!fastForwarded) { if (!fastForwarded) {
let lastestPriceId; const lastestPriceId = await PricesRepository.$getLatestPriceId();
try { this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
} catch (e) {
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
}
if (priceUpdater.historyInserted === true && lastestPriceId !== null) { if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
await blocksRepository.$saveBlockPrices([{ await blocksRepository.$saveBlockPrices([{
height: blockExtended.height, height: blockExtended.height,
@@ -776,7 +853,9 @@ class Blocks {
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`); this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
} else { } else {
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining); logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
indexer.scheduleSingleTask('blocksPrices', 10000); setTimeout(() => {
indexer.runSingleTask('blocksPrices');
}, 10000);
} }
// Save blocks summary for visualization if it's enabled // Save blocks summary for visualization if it's enabled
@@ -900,7 +979,7 @@ class Blocks {
const blockHash = await bitcoinApi.$getBlockHash(height); const blockHash = await bitcoinApi.$getBlockHash(height);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash); const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, true); const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions); const blockExtended = await this.$getBlockExtended(block, transactions);
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
@@ -912,7 +991,7 @@ class Blocks {
public async $indexStaleBlock(hash: string): Promise<BlockExtended> { public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash); const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
const transactions = await this.$getTransactionsExtended(hash, block.height, block.timestamp, true); const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions); const blockExtended = await this.$getBlockExtended(block, transactions);
blockExtended.canonical = await bitcoinApi.$getBlockHash(block.height); blockExtended.canonical = await bitcoinApi.$getBlockHash(block.height);

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 = 66; private static currentVersion = 65;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@@ -553,11 +553,6 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"'); await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(65); await this.updateToSchemaVersion(65);
} }
if (databaseSchemaVersion < 66) {
await this.$executeQuery('ALTER TABLE `statistics` ADD min_fee FLOAT UNSIGNED DEFAULT NULL');
await this.updateToSchemaVersion(66);
}
} }
/** /**

View File

@@ -252,11 +252,7 @@ class DiskCache {
} }
if (rbfData?.rbf) { if (rbfData?.rbf) {
rbfCache.load({ rbfCache.load(rbfData.rbf);
txs: rbfData.rbf.txs.map(([txid, entry]) => ({ value: entry })),
trees: rbfData.rbf.trees,
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
});
} }
} catch (e) { } catch (e) {
logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));

View File

@@ -80,13 +80,7 @@ class ChannelsApi {
public async $searchChannelsById(search: string): Promise<any[]> { public async $searchChannelsById(search: string): Promise<any[]> {
try { try {
// restrict search to valid id/short_id prefix formats const searchStripped = search.replace(/[^0-9x]/g, '') + '%';
let searchStripped = search.match(/[0-9]+[0-9x]*/)?.[0] || '';
if (!searchStripped.length) {
return [];
}
// add wildcard to search by prefix
searchStripped += '%';
const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`; const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]); const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
return rows; return rows;

View File

@@ -42,12 +42,6 @@ class NodesRoutes {
switch (config.MEMPOOL.NETWORK) { switch (config.MEMPOOL.NETWORK) {
case 'testnet': case 'testnet':
nodesList = [ nodesList = [
'0259db43b4e4ac0ff12a805f2d81e521253ba2317f6739bc611d8e2fa156d64256',
'0352b9944b9a52bd2116c91f1ba70c4ef851ac5ba27e1b20f1d92da3ade010dd10',
'03424f5a7601eaa47482cb17100b31a84a04d14fb44b83a57eeceffd8e299878e3',
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', '032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
@@ -70,12 +64,6 @@ class NodesRoutes {
break; break;
case 'signet': case 'signet':
nodesList = [ nodesList = [
'029fe3621fc0c6e08056a14b868f8fb9acca1aa28a129512f6cea0f0d7654d9f92',
'02f60cd7a3a4f1c953dd9554a6ebd51a34f8b10b8124b7fc43a0b381139b55c883',
'03cbbf581774700865eebd1be42d022bc004ba30881274ab304e088a25d70e773d',
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', '03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
@@ -98,12 +86,6 @@ class NodesRoutes {
break; break;
default: default:
nodesList = [ nodesList = [
'02b12b889fe3c943cb05645921040ef13d6d397a2e7a4ad000e28500c505ff26d6',
'0302240ac9d71b39617cbde2764837ec3d6198bd6074b15b75d2ff33108e89d2e1',
'03364a8ace313376e5e4b68c954e287c6388e16df9e9fdbaf0363ecac41105cbf6',
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', '03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',

View File

@@ -3,31 +3,21 @@ import { Common } from './common';
import mempool from './mempool'; import mempool from './mempool';
import projectedBlocks from './mempool-blocks'; import projectedBlocks from './mempool-blocks';
interface RecommendedFees {
fastestFee: number,
halfHourFee: number,
hourFee: number,
economyFee: number,
minimumFee: number,
}
class FeeApi { class FeeApi {
constructor() { } constructor() { }
defaultFee = Common.isLiquid() ? 0.1 : 1; defaultFee = Common.isLiquid() ? 0.1 : 1;
minimumIncrement = Common.isLiquid() ? 0.1 : 1;
public getRecommendedFee(): RecommendedFees { public getRecommendedFee() {
const pBlocks = projectedBlocks.getMempoolBlocks(); const pBlocks = projectedBlocks.getMempoolBlocks();
const mPool = mempool.getMempoolInfo(); const mPool = mempool.getMempoolInfo();
const minimumFee = this.roundUpToNearest(mPool.mempoolminfee * 100000, this.minimumIncrement); const minimumFee = Math.ceil(mPool.mempoolminfee * 100000);
const defaultMinFee = Math.max(minimumFee, this.defaultFee);
if (!pBlocks.length) { if (!pBlocks.length) {
return { return {
'fastestFee': defaultMinFee, 'fastestFee': this.defaultFee,
'halfHourFee': defaultMinFee, 'halfHourFee': this.defaultFee,
'hourFee': defaultMinFee, 'hourFee': this.defaultFee,
'economyFee': minimumFee, 'economyFee': minimumFee,
'minimumFee': minimumFee, 'minimumFee': minimumFee,
}; };
@@ -37,15 +27,11 @@ class FeeApi {
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee; const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee;
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee; const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee;
// explicitly enforce a minimum of ceil(mempoolminfee) on all recommendations.
// simply rounding up recommended rates is insufficient, as the purging rate
// can exceed the median rate of projected blocks in some extreme scenarios
// (see https://bitcoin.stackexchange.com/a/120024)
return { return {
'fastestFee': Math.max(minimumFee, firstMedianFee), 'fastestFee': firstMedianFee,
'halfHourFee': Math.max(minimumFee, secondMedianFee), 'halfHourFee': secondMedianFee,
'hourFee': Math.max(minimumFee, thirdMedianFee), 'hourFee': thirdMedianFee,
'economyFee': Math.max(minimumFee, Math.min(2 * minimumFee, thirdMedianFee)), 'economyFee': Math.min(2 * minimumFee, thirdMedianFee),
'minimumFee': minimumFee, 'minimumFee': minimumFee,
}; };
} }
@@ -59,11 +45,7 @@ class FeeApi {
const multiplier = (pBlock.blockVSize - 500000) / 500000; const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee); return Math.max(Math.round(useFee * multiplier), this.defaultFee);
} }
return this.roundUpToNearest(useFee, this.minimumIncrement); return Math.ceil(useFee);
}
private roundUpToNearest(value: number, nearest: number): number {
return Math.ceil(value / nearest) * nearest;
} }
} }

View File

@@ -44,13 +44,9 @@ export enum FeatureBits {
KeysendOptional = 55, KeysendOptional = 55,
ScriptEnforcedLeaseRequired = 2022, ScriptEnforcedLeaseRequired = 2022,
ScriptEnforcedLeaseOptional = 2023, ScriptEnforcedLeaseOptional = 2023,
SimpleTaprootChannelsRequiredFinal = 80,
SimpleTaprootChannelsOptionalFinal = 81,
SimpleTaprootChannelsRequiredStaging = 180,
SimpleTaprootChannelsOptionalStaging = 181,
MaxBolt11Feature = 5114, MaxBolt11Feature = 5114,
}; };
export const FeaturesMap = new Map<FeatureBits, string>([ export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'], [FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'], [FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
@@ -89,10 +85,6 @@ export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.ZeroConfOptional, 'zero-conf'], [FeatureBits.ZeroConfOptional, 'zero-conf'],
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'], [FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'], [FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
[FeatureBits.SimpleTaprootChannelsRequiredFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsOptionalFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsRequiredStaging, 'taproot-channels-staging'],
[FeatureBits.SimpleTaprootChannelsOptionalStaging, 'taproot-channels-staging'],
]); ]);
/** /**

View File

@@ -31,7 +31,7 @@ class MemoryCache {
} }
private cleanup() { private cleanup() {
this.cache = this.cache.filter((cache) => cache.expires > (new Date())); this.cache = this.cache.filter((cache) => cache.expires < (new Date()));
} }
} }

View File

@@ -451,7 +451,6 @@ class MempoolBlocks {
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] { private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
for (const [txid, rate] of rates) { for (const [txid, rate] of rates) {
if (txid in mempool) { if (txid in mempool) {
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
mempool[txid].effectiveFeePerVsize = rate; mempool[txid].effectiveFeePerVsize = rate;
mempool[txid].cpfpChecked = false; mempool[txid].cpfpChecked = false;
} }
@@ -495,9 +494,6 @@ class MempoolBlocks {
} }
} }
}); });
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true}); Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
} }
} }
@@ -535,21 +531,12 @@ class MempoolBlocks {
const acceleration = accelerations[txid]; const acceleration = accelerations[txid];
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true; mempoolTx.acceleration = true;
for (const ancestor of mempoolTx.ancestors || []) { for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true; mempool[ancestor.txid].acceleration = true;
isAccelerated[ancestor.txid] = true; isAccelerated[ancestor.txid] = true;
} }
} else { } else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
delete mempoolTx.acceleration; delete mempoolTx.acceleration;
} }

View File

@@ -18,7 +18,7 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private spendMap = new Map<string, MempoolTransactionExtended>(); private spendMap = new Map<string, MempoolTransactionExtended>();
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: 0.00001000, minrelaytxfee: 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[],
@@ -94,7 +94,7 @@ class Mempool {
logger.debug(`Migrating ${Object.keys(this.mempoolCache).length} transactions from disk cache to Redis cache`); logger.debug(`Migrating ${Object.keys(this.mempoolCache).length} transactions from disk cache to Redis cache`);
} }
for (const txid of Object.keys(this.mempoolCache)) { for (const txid of Object.keys(this.mempoolCache)) {
if (!this.mempoolCache[txid].adjustedVsize || this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) { if (!this.mempoolCache[txid].sigops || this.mempoolCache[txid].effectiveFeePerVsize == null) {
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]); this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
} }
if (this.mempoolCache[txid].order == null) { if (this.mempoolCache[txid].order == null) {
@@ -126,7 +126,7 @@ class Mempool {
loadingIndicators.setProgress('mempool', count / expectedCount * 100); loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) { while (!done) {
try { try {
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid, config.ESPLORA.BATCH_QUERY_BASE_SIZE); const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
if (result) { if (result) {
for (const tx of result) { for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx); const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
@@ -235,7 +235,7 @@ class Mempool {
if (!loaded) { if (!loaded) {
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]); const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
const sliceLength = config.ESPLORA.BATCH_QUERY_BASE_SIZE; const sliceLength = 10000;
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength); const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false); const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);

View File

@@ -15,13 +15,6 @@ import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface'; import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import database from '../../database'; import database from '../../database';
interface DifficultyBlock {
timestamp: number,
height: number,
bits: number,
difficulty: number,
}
class Mining { class Mining {
private blocksPriceIndexingRunning = false; private blocksPriceIndexingRunning = false;
public lastHashrateIndexingDate: number | null = null; public lastHashrateIndexingDate: number | null = null;
@@ -428,7 +421,6 @@ class Mining {
indexedHeights[height] = true; indexedHeights[height] = true;
} }
// gets {time, height, difficulty, bits} of blocks in ascending order of height
const blocks: any = await BlocksRepository.$getBlocksDifficulty(); const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0)); const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
let currentDifficulty = genesisBlock.difficulty; let currentDifficulty = genesisBlock.difficulty;
@@ -444,45 +436,41 @@ class Mining {
}); });
} }
if (!blocks?.length) { const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
// no blocks in database yet if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
return; currentBits = oldestConsecutiveBlock.bits;
currentDifficulty = oldestConsecutiveBlock.difficulty;
} }
const oldestConsecutiveBlock = this.getOldestConsecutiveBlock(blocks);
currentBits = oldestConsecutiveBlock.bits;
currentDifficulty = oldestConsecutiveBlock.difficulty;
let totalBlockChecked = 0; let totalBlockChecked = 0;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
for (const block of blocks) { for (const block of blocks) {
// skip until the first block after the oldest consecutive block
if (block.height <= oldestConsecutiveBlock.height) {
continue;
}
// difficulty has changed between two consecutive blocks!
if (block.bits !== currentBits) { if (block.bits !== currentBits) {
// skip if already indexed if (indexedHeights[block.height] === true) { // Already indexed
if (indexedHeights[block.height] !== true) { if (block.height >= oldestConsecutiveBlock.height) {
let adjustment = block.difficulty / currentDifficulty; currentDifficulty = block.difficulty;
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise currentBits = block.bits;
}
await DifficultyAdjustmentsRepository.$saveAdjustments({ continue;
time: block.time,
height: block.height,
difficulty: block.difficulty,
adjustment: adjustment,
});
totalIndexed++;
} }
// update the current difficulty
currentDifficulty = block.difficulty; let adjustment = block.difficulty / currentDifficulty;
currentBits = block.bits; adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
}
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.time,
height: block.height,
difficulty: block.difficulty,
adjustment: adjustment,
});
totalIndexed++;
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
}
totalBlockChecked++; totalBlockChecked++;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
@@ -645,17 +633,6 @@ class Mining {
default: return 86400 * scale; default: return 86400 * scale;
} }
} }
// Finds the oldest block in a consecutive chain back from the tip
// assumes `blocks` is sorted in ascending height order
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {
for (let i = blocks.length - 1; i > 0; i--) {
if ((blocks[i].height - blocks[i - 1].height) > 1) {
return blocks[i];
}
}
return blocks[0];
}
} }
export default new Mining(); export default new Mining();

View File

@@ -2,7 +2,6 @@ import config from "../config";
import logger from "../logger"; import logger from "../logger";
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces"; import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
import bitcoinApi from './bitcoin/bitcoin-api-factory'; import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { IEsploraApi } from "./bitcoin/esplora-api.interface";
import { Common } from "./common"; import { Common } from "./common";
import redisCache from "./redis-cache"; import redisCache from "./redis-cache";
@@ -54,10 +53,7 @@ class RbfCache {
private expiring: Map<string, number> = new Map(); private expiring: Map<string, number> = new Map();
private cacheQueue: CacheEvent[] = []; private cacheQueue: CacheEvent[] = [];
private evictionCount = 0; public init(): void {
private staleCount = 0;
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10); setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
} }
@@ -249,7 +245,6 @@ class RbfCache {
// flag a transaction as removed from the mempool // flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void { public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime); this.addExpiration(txid, expiryTime);
@@ -277,23 +272,18 @@ class RbfCache {
this.remove(txid); this.remove(txid);
} }
} }
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire (${this.evictionCount} newly expired)`); logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire`);
this.evictionCount = 0;
} }
// remove a transaction & all previous versions from the cache // remove a transaction & all previous versions from the cache
private remove(txid): void { private remove(txid): void {
// don't remove a transaction if a newer version remains in the mempool // don't remove a transaction if a newer version remains in the mempool
if (!this.replacedBy.has(txid)) { if (!this.replacedBy.has(txid)) {
const root = this.treeMap.get(txid);
const replaces = this.replaces.get(txid); const replaces = this.replaces.get(txid);
this.replaces.delete(txid); this.replaces.delete(txid);
this.treeMap.delete(txid); this.treeMap.delete(txid);
this.removeTx(txid); this.removeTx(txid);
this.removeExpiration(txid); this.removeExpiration(txid);
if (root === txid) {
this.removeTree(txid);
}
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);
@@ -369,27 +359,18 @@ class RbfCache {
} }
public async load({ txs, trees, expiring }): Promise<void> { public async load({ txs, trees, expiring }): Promise<void> {
try { txs.forEach(txEntry => {
txs.forEach(txEntry => { this.txs.set(txEntry.key, txEntry.value);
this.txs.set(txEntry.value.txid, txEntry.value); });
}); for (const deflatedTree of trees) {
this.staleCount = 0; await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
for (const deflatedTree of trees) {
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
}
});
this.staleCount = 0;
await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup();
} catch (e) {
logger.err('failed to restore RBF cache: ' + (e instanceof Error ? e.message : e));
} }
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
}
});
this.cleanup();
} }
exportTree(tree: RbfTree, deflated: any = null) { exportTree(tree: RbfTree, deflated: any = null) {
@@ -417,11 +398,29 @@ class RbfCache {
const treeInfo = deflated[txid]; const treeInfo = deflated[txid];
const replaces: RbfTree[] = []; const replaces: RbfTree[] = [];
// if the root tx is unknown, remove this tree and return early // check if any transactions in this tree have already been confirmed
if (root === txid && !txs.has(txid)) { mined = mined || treeInfo.mined;
this.staleCount++; let exists = mined;
this.removeTree(deflated.key); if (!mined) {
return; try {
const apiTx = await bitcoinApi.$getRawTransaction(txid);
if (apiTx) {
exists = true;
}
if (apiTx?.status?.confirmed) {
mined = true;
treeInfo.txMined = true;
this.evict(txid, true);
}
} catch (e) {
// most transactions do not exist
}
}
// if the root tx is not in the mempool or the blockchain
// evict this tree as soon as possible
if (root === txid && !exists) {
this.evict(txid, true);
} }
// recursively reconstruct child trees // recursively reconstruct child trees
@@ -459,60 +458,6 @@ class RbfCache {
return tree; return tree;
} }
private async checkTrees(): Promise<void> {
const found: { [txid: string]: boolean } = {};
const txids = Array.from(this.txs.values()).map(tx => tx.txid).filter(txid => {
return !this.expiring.has(txid) && !this.getRbfTree(txid)?.mined;
});
const processTxs = (txs: IEsploraApi.Transaction[]): void => {
for (const tx of txs) {
found[tx.txid] = true;
if (tx.status?.confirmed) {
const tree = this.getRbfTree(tx.txid);
if (tree) {
this.setTreeMined(tree, tx.txid);
tree.mined = true;
this.evict(tx.txid, false);
}
}
}
};
if (config.MEMPOOL.BACKEND === 'esplora') {
let processedCount = 0;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
processedCount += slice.length;
try {
const txs = await bitcoinApi.$getRawTransactions(slice);
processTxs(txs);
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`);
} catch (err) {
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
}
}
} else {
const txs: IEsploraApi.Transaction[] = [];
for (const txid of txids) {
try {
const tx = await bitcoinApi.$getRawTransaction(txid, true, false);
txs.push(tx);
} catch (err) {
// some 404s are expected, so continue quietly
}
}
processTxs(txs);
}
for (const txid of txids) {
if (!found[txid]) {
this.evict(txid, false);
}
}
}
public getLatestRbfSummary(): ReplacementInfo[] { public getLatestRbfSummary(): ReplacementInfo[] {
const rbfList = this.getRbfTrees(false); const rbfList = this.getRbfTrees(false);
return rbfList.slice(0, 6).map(rbfTree => { return rbfList.slice(0, 6).map(rbfTree => {

View File

@@ -23,24 +23,21 @@ class RedisCache {
private cacheQueue: MempoolTransactionExtended[] = []; private cacheQueue: MempoolTransactionExtended[] = [];
private txFlushLimit: number = 10000; private txFlushLimit: number = 10000;
constructor() {
if (config.REDIS.ENABLED) {
const redisConfig = {
socket: {
path: config.REDIS.UNIX_SOCKET_PATH
},
database: NetworkDB[config.MEMPOOL.NETWORK],
};
this.client = createClient(redisConfig);
this.client.on('error', (e) => {
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
});
this.$ensureConnected();
}
}
private async $ensureConnected(): Promise<void> { private async $ensureConnected(): Promise<void> {
if (!this.connected && config.REDIS.ENABLED) { if (!this.connected && config.REDIS.ENABLED) {
if (!this.client) {
const redisConfig = {
socket: {
path: config.REDIS.UNIX_SOCKET_PATH
},
database: NetworkDB[config.MEMPOOL.NETWORK],
};
this.client = createClient(redisConfig);
this.client.on('error', (e) => {
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
});
}
return this.client.connect().then(async () => { return this.client.connect().then(async () => {
this.connected = true; this.connected = true;
logger.info(`Redis client connected`); logger.info(`Redis client connected`);
@@ -122,9 +119,8 @@ class RedisCache {
async $removeTransactions(transactions: string[]) { async $removeTransactions(transactions: string[]) {
try { try {
await this.$ensureConnected(); await this.$ensureConnected();
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE; for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) { const slice = transactions.slice(i * 10000, (i + 1) * 10000);
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`)); await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`); logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
} }
@@ -220,7 +216,7 @@ class RedisCache {
await memPool.$setMempool(loadedMempool); await memPool.$setMempool(loadedMempool);
await rbfCache.load({ await rbfCache.load({
txs: rbfTxs, txs: rbfTxs,
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), trees: rbfTrees.map(loadedTree => loadedTree.value),
expiring: rbfExpirations, expiring: rbfExpirations,
}); });
} }

View File

@@ -15,7 +15,6 @@ class StatisticsApi {
mempool_byte_weight, mempool_byte_weight,
fee_data, fee_data,
total_fee, total_fee,
min_fee,
vsize_1, vsize_1,
vsize_2, vsize_2,
vsize_3, vsize_3,
@@ -55,7 +54,7 @@ class StatisticsApi {
vsize_1800, vsize_1800,
vsize_2000 vsize_2000
) )
VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`; 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`;
const [result]: any = await DB.query(query); const [result]: any = await DB.query(query);
return result.insertId; return result.insertId;
@@ -74,7 +73,6 @@ class StatisticsApi {
mempool_byte_weight, mempool_byte_weight,
fee_data, fee_data,
total_fee, total_fee,
min_fee,
vsize_1, vsize_1,
vsize_2, vsize_2,
vsize_3, vsize_3,
@@ -114,7 +112,7 @@ class StatisticsApi {
vsize_1800, vsize_1800,
vsize_2000 vsize_2000
) )
VALUES (${statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, VALUES (${statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params: (string | number)[] = [ const params: (string | number)[] = [
@@ -124,7 +122,6 @@ class StatisticsApi {
statistics.mempool_byte_weight, statistics.mempool_byte_weight,
statistics.fee_data, statistics.fee_data,
statistics.total_fee, statistics.total_fee,
statistics.min_fee,
statistics.vsize_1, statistics.vsize_1,
statistics.vsize_2, statistics.vsize_2,
statistics.vsize_3, statistics.vsize_3,
@@ -174,9 +171,7 @@ class StatisticsApi {
private getQueryForDaysAvg(div: number, interval: string) { private getQueryForDaysAvg(div: number, interval: string) {
return `SELECT return `SELECT
UNIX_TIMESTAMP(added) as added, UNIX_TIMESTAMP(added) as added,
CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions,
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second, CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
CAST(avg(min_fee) as DOUBLE) as min_fee,
CAST(avg(vsize_1) as DOUBLE) as vsize_1, CAST(avg(vsize_1) as DOUBLE) as vsize_1,
CAST(avg(vsize_2) as DOUBLE) as vsize_2, CAST(avg(vsize_2) as DOUBLE) as vsize_2,
CAST(avg(vsize_3) as DOUBLE) as vsize_3, CAST(avg(vsize_3) as DOUBLE) as vsize_3,
@@ -224,9 +219,7 @@ class StatisticsApi {
private getQueryForDays(div: number, interval: string) { private getQueryForDays(div: number, interval: string) {
return `SELECT return `SELECT
UNIX_TIMESTAMP(added) as added, UNIX_TIMESTAMP(added) as added,
CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions,
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second, CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
CAST(avg(min_fee) as DOUBLE) as min_fee,
vsize_1, vsize_1,
vsize_2, vsize_2,
vsize_3, vsize_3,
@@ -408,11 +401,9 @@ class StatisticsApi {
return statistic.map((s) => { return statistic.map((s) => {
return { return {
added: s.added, added: s.added,
count: s.unconfirmed_transactions,
vbytes_per_second: s.vbytes_per_second, vbytes_per_second: s.vbytes_per_second,
mempool_byte_weight: s.mempool_byte_weight, mempool_byte_weight: s.mempool_byte_weight,
total_fee: s.total_fee, total_fee: s.total_fee,
min_fee: s.min_fee,
vsizes: [ vsizes: [
s.vsize_1, s.vsize_1,
s.vsize_2, s.vsize_2,

View File

@@ -89,9 +89,6 @@ class Statistics {
} }
}); });
// get minFee and convert to sats/vb
const minFee = memPool.getMempoolInfo().mempoolminfee * 100000;
try { try {
const insertId = await statisticsApi.$create({ const insertId = await statisticsApi.$create({
added: 'NOW()', added: 'NOW()',
@@ -101,7 +98,6 @@ class Statistics {
mempool_byte_weight: totalWeight, mempool_byte_weight: totalWeight,
total_fee: totalFee, total_fee: totalFee,
fee_data: '', fee_data: '',
min_fee: minFee,
vsize_1: weightVsizeFees['1'] || 0, vsize_1: weightVsizeFees['1'] || 0,
vsize_2: weightVsizeFees['2'] || 0, vsize_2: weightVsizeFees['2'] || 0,
vsize_3: weightVsizeFees['3'] || 0, vsize_3: weightVsizeFees['3'] || 0,

View File

@@ -5,7 +5,6 @@ import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import logger from '../logger'; import logger from '../logger';
import config from '../config'; import config from '../config';
import pLimit from '../utils/p-limit';
class TransactionUtils { class TransactionUtils {
constructor() { } constructor() { }
@@ -75,12 +74,8 @@ class TransactionUtils {
public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> { public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> {
if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') { if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') {
const limiter = pLimit(8); // Run 8 requests at a time const results = await Promise.allSettled(txids.map(txid => this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, true)));
const results = await Promise.allSettled(txids.map( return (results.filter(r => r.status === 'fulfilled') as PromiseFulfilledResult<MempoolTransactionExtended>[]).map(r => r.value);
txid => limiter(() => this.$getMempoolTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore))
));
return results.filter(reply => reply.status === 'fulfilled')
.map(r => (r as PromiseFulfilledResult<MempoolTransactionExtended>).value);
} else { } else {
const transactions = await bitcoinApi.$getMempoolTransactions(txids); const transactions = await bitcoinApi.$getMempoolTransactions(txids);
return transactions.map(transaction => { return transactions.map(transaction => {
@@ -116,7 +111,7 @@ class TransactionUtils {
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended { public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
const vsize = Math.ceil(transaction.weight / 4); const vsize = Math.ceil(transaction.weight / 4);
const fractionalVsize = (transaction.weight / 4); const fractionalVsize = (transaction.weight / 4);
let sigops = Common.isLiquid() ? 0 : (transaction.sigops != null ? transaction.sigops : this.countSigops(transaction)); const sigops = !Common.isLiquid() ? this.countSigops(transaction) : 0;
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298 // https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
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;
@@ -155,7 +150,7 @@ class TransactionUtils {
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0); sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
} else { } else {
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise // in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
const matches = script.matchAll(/(?:OP_(?:PUSHNUM_)?(\d+))? OP_CHECKMULTISIG/g); const matches = script.matchAll(/(?:OP_(\d+))? OP_CHECKMULTISIG/g);
for (const match of matches) { for (const match of matches) {
const n = parseInt(match[1]); const n = parseInt(match[1]);
if (Number.isInteger(n)) { if (Number.isInteger(n)) {
@@ -189,12 +184,6 @@ class TransactionUtils {
sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true); sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
} }
break; break;
case input.prevout.scriptpubkey_type === 'p2sh':
if (input.inner_redeemscript_asm) {
sigops += this.countScriptSigops(input.inner_redeemscript_asm);
}
break;
} }
} }
} }

View File

@@ -94,13 +94,9 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.wss.on('connection', (client: WebSocket, req) => { this.wss.on('connection', (client: WebSocket) => {
this.numConnected++; this.numConnected++;
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown'; client.on('error', logger.info);
client.on('error', (e) => {
logger.info(`websocket client error from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
client.close();
});
client.on('close', () => { client.on('close', () => {
this.numDisconnected++; this.numDisconnected++;
}); });
@@ -286,8 +282,7 @@ class WebsocketHandler {
client.send(serializedResponse); client.send(serializedResponse);
} }
} catch (e) { } catch (e) {
logger.debug(`Error parsing websocket message from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e)); logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
client.close();
} }
}); });
}); });
@@ -491,7 +486,6 @@ class WebsocketHandler {
// pre-compute address transactions // pre-compute address transactions
const addressCache = this.makeAddressCache(newTransactions); const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
this.wss.clients.forEach(async (client) => { this.wss.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) { if (client.readyState !== WebSocket.OPEN) {
@@ -532,15 +526,11 @@ class WebsocketHandler {
} }
if (client['track-address']) { if (client['track-address']) {
const newTransactions = Array.from(addressCache[client['track-address']]?.values() || []); const foundTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
const removedTransactions = Array.from(removedAddressCache[client['track-address']]?.values() || []);
// txs may be missing prevouts in non-esplora backends // txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now // so fetch the full transactions now
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(newTransactions) : newTransactions; const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(foundTransactions) : foundTransactions;
if (removedTransactions.length) {
response['address-removed-transactions'] = JSON.stringify(removedTransactions);
}
if (fullTransactions.length) { if (fullTransactions.length) {
response['address-transactions'] = JSON.stringify(fullTransactions); response['address-transactions'] = JSON.stringify(fullTransactions);
} }
@@ -582,7 +572,7 @@ class WebsocketHandler {
response['utxoSpent'] = JSON.stringify(outspends); response['utxoSpent'] = JSON.stringify(outspends);
} }
const rbfReplacedBy = rbfChanges.map[client['track-tx']] ? rbfCache.getReplacedBy(client['track-tx']) : false; const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']);
if (rbfReplacedBy) { if (rbfReplacedBy) {
response['rbfTransaction'] = JSON.stringify({ response['rbfTransaction'] = JSON.stringify({
txid: rbfReplacedBy, txid: rbfReplacedBy,
@@ -596,25 +586,13 @@ class WebsocketHandler {
const mempoolTx = newMempool[trackTxid]; const mempoolTx = newMempool[trackTxid];
if (mempoolTx && mempoolTx.position) { if (mempoolTx && mempoolTx.position) {
const positionData = { response['txPosition'] = JSON.stringify({
txid: trackTxid, txid: trackTxid,
position: { position: {
...mempoolTx.position, ...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined, accelerated: mempoolTx.acceleration || undefined,
} }
}; });
if (mempoolTx.cpfpDirty) {
positionData['cpfp'] = {
ancestors: mempoolTx.ancestors,
bestDescendant: mempoolTx.bestDescendant || null,
descendants: mempoolTx.descendants || null,
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
sigops: mempoolTx.sigops,
adjustedVsize: mempoolTx.adjustedVsize,
acceleration: mempoolTx.acceleration
};
}
response['txPosition'] = JSON.stringify(positionData);
} }
} }

View File

@@ -43,10 +43,7 @@ interface IConfig {
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null; UNIX_SOCKET_PATH: string | void | null;
BATCH_QUERY_BASE_SIZE: number;
RETRY_UNIX_SOCKET_AFTER: number; RETRY_UNIX_SOCKET_AFTER: number;
REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number;
FALLBACK: string[]; FALLBACK: string[];
}; };
LIGHTNING: { LIGHTNING: {
@@ -79,8 +76,6 @@ interface IConfig {
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number; TIMEOUT: number;
COOKIE: boolean;
COOKIE_PATH: string;
}; };
SECOND_CORE_RPC: { SECOND_CORE_RPC: {
HOST: string; HOST: string;
@@ -88,8 +83,6 @@ interface IConfig {
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number; TIMEOUT: number;
COOKIE: boolean;
COOKIE_PATH: string;
}; };
DATABASE: { DATABASE: {
ENABLED: boolean; ENABLED: boolean;
@@ -100,7 +93,6 @@ interface IConfig {
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number; TIMEOUT: number;
PID_DIR: string;
}; };
SYSLOG: { SYSLOG: {
ENABLED: boolean; ENABLED: boolean;
@@ -152,7 +144,6 @@ interface IConfig {
REDIS: { REDIS: {
ENABLED: boolean; ENABLED: boolean;
UNIX_SOCKET_PATH: string; UNIX_SOCKET_PATH: string;
BATCH_QUERY_BASE_SIZE: number;
}, },
} }
@@ -197,10 +188,7 @@ const defaults: IConfig = {
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null, 'UNIX_SOCKET_PATH': null,
'BATCH_QUERY_BASE_SIZE': 1000,
'RETRY_UNIX_SOCKET_AFTER': 30000, 'RETRY_UNIX_SOCKET_AFTER': 30000,
'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000,
'FALLBACK': [], 'FALLBACK': [],
}, },
'ELECTRUM': { 'ELECTRUM': {
@@ -214,8 +202,6 @@ const defaults: IConfig = {
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool', 'PASSWORD': 'mempool',
'TIMEOUT': 60000, 'TIMEOUT': 60000,
'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie'
}, },
'SECOND_CORE_RPC': { 'SECOND_CORE_RPC': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
@@ -223,8 +209,6 @@ const defaults: IConfig = {
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool', 'PASSWORD': 'mempool',
'TIMEOUT': 60000, 'TIMEOUT': 60000,
'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie'
}, },
'DATABASE': { 'DATABASE': {
'ENABLED': true, 'ENABLED': true,
@@ -235,7 +219,6 @@ const defaults: IConfig = {
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool', 'PASSWORD': 'mempool',
'TIMEOUT': 180000, 'TIMEOUT': 180000,
'PID_DIR': '',
}, },
'SYSLOG': { 'SYSLOG': {
'ENABLED': true, 'ENABLED': true,
@@ -306,7 +289,6 @@ const defaults: IConfig = {
'REDIS': { 'REDIS': {
'ENABLED': false, 'ENABLED': false,
'UNIX_SOCKET_PATH': '', 'UNIX_SOCKET_PATH': '',
'BATCH_QUERY_BASE_SIZE': 5000,
}, },
}; };

View File

@@ -1,11 +1,7 @@
import * as fs from 'fs';
import path from 'path';
import config from './config'; import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise'; import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import { LogLevel } from './logger';
import logger from './logger'; import logger from './logger';
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql'; import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
import { execSync } from 'child_process';
class DB { class DB {
constructor() { constructor() {
@@ -34,7 +30,7 @@ import { execSync } from 'child_process';
} }
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]> OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
{ {
this.checkDBFlag(); this.checkDBFlag();
let hardTimeout; let hardTimeout;
@@ -56,38 +52,19 @@ import { execSync } from 'child_process';
}).then(result => { }).then(result => {
resolve(result); resolve(result);
}).catch(error => { }).catch(error => {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
reject(error); reject(error);
}).finally(() => { }).finally(() => {
clearTimeout(timer); clearTimeout(timer);
}); });
}); });
} else { } else {
try { const pool = await this.getPool();
const pool = await this.getPool(); return pool.query(query, params);
return pool.query(query, params);
} catch (e) {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
throw e;
}
}
}
private async $rollbackAtomic(connection: PoolConnection): Promise<void> {
try {
await connection.rollback();
await connection.release();
} catch (e) {
logger.warn('Failed to rollback incomplete db transaction: ' + (e instanceof Error ? e.message : e));
} }
} }
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]> OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
{ {
const pool = await this.getPool(); const pool = await this.getPool();
const connection = await pool.getConnection(); const connection = await pool.getConnection();
@@ -96,7 +73,7 @@ import { execSync } from 'child_process';
const results: [T, FieldPacket[]][] = []; const results: [T, FieldPacket[]][] = [];
for (const query of queries) { for (const query of queries) {
const result = await this.query(query.query, query.params, errorLogLevel, connection) as [T, FieldPacket[]]; const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
results.push(result); results.push(result);
} }
@@ -104,8 +81,9 @@ import { execSync } from 'child_process';
return results; return results;
} catch (e) { } catch (e) {
logger.warn('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e)); logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
this.$rollbackAtomic(connection); connection.rollback();
connection.release();
throw e; throw e;
} finally { } finally {
connection.release(); connection.release();
@@ -123,50 +101,6 @@ import { execSync } from 'child_process';
} }
} }
public getPidLock(): boolean {
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
this.enforcePidLock(filePath);
fs.writeFileSync(filePath, `${process.pid}`);
return true;
}
private enforcePidLock(filePath: string): void {
if (fs.existsSync(filePath)) {
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
if (pid === process.pid) {
logger.warn('PID file already exists for this process');
return;
}
let cmd;
try {
cmd = execSync(`ps -p ${pid} -o args=`);
} catch (e) {
logger.warn(`Stale PID file at ${filePath}, but no process running on that PID ${pid}`);
return;
}
if (cmd && cmd.toString()?.includes('node')) {
const msg = `Another mempool nodejs process is already running on PID ${pid}`;
logger.err(msg);
throw new Error(msg);
} else {
logger.warn(`Stale PID file at ${filePath}, but the PID ${pid} does not belong to a running mempool instance`);
}
}
}
public releasePidLock(): void {
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
if (fs.existsSync(filePath)) {
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
// only release our own pid file
if (pid === process.pid) {
fs.unlinkSync(filePath);
}
}
}
private async getPool(): Promise<Pool> { private async getPool(): Promise<Pool> {
if (this.pool === null) { if (this.pool === null) {
this.pool = createPool(this.poolConfig); this.pool = createPool(this.poolConfig);

View File

@@ -0,0 +1,38 @@
import { parentPort } from 'worker_threads';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import blocks from '../api/blocks';
import config from '../config';
import transactionUtils from '../api/transaction-utils';
import bitcoinClient from '../api/bitcoin/bitcoin-client';
if (parentPort) {
parentPort.on('message', async ({ hash, height }) => {
if (hash != null && height != null) {
await indexBlockSummary(hash, height);
}
if (parentPort) {
parentPort.postMessage(height);
}
});
}
async function indexBlockSummary(hash: string, height: number): Promise<void> {
let txs;
if (config.MEMPOOL.BACKEND === 'esplora') {
txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
} else {
const block = await bitcoinClient.getBlock(hash, 2);
txs = block.tx.map(tx => {
tx.fee = Math.round(tx.fee * 100_000_000);
tx.vout.forEach((vout) => {
vout.value = Math.round(vout.value * 100000000);
});
tx.vsize = Math.round(tx.weight / 4); // required for backwards compatibility
return tx;
});
}
const cpfpSummary = await blocks.$indexCPFP(hash, height, txs);
await blocks.$getStrippedBlockTransactions(hash, true, true, cpfpSummary, height); // This will index the block summary
}

View File

@@ -0,0 +1,25 @@
import { parentPort } from 'worker_threads';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import blocksRepository from '../repositories/BlocksRepository';
import blocks from '../api/blocks';
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
if (parentPort) {
parentPort.on('message', async (params) => {
if (params.height != null) {
await indexBlock(params.height);
}
if (parentPort) {
parentPort.postMessage(params.height);
}
});
}
async function indexBlock(blockHeight: number): Promise<void> {
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await blocks['$getTransactionsExtended'](blockHash, block.height, true, null, true);
const blockExtended = await blocks['$getBlockExtended'](block, transactions);
await blocksRepository.$saveBlockInDatabase(blockExtended);
}

View File

@@ -43,6 +43,7 @@ import { AxiosError } from 'axios';
import v8 from 'v8'; import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format'; import { formatBytes, getBytesUnit } from './utils/format';
import redisCache from './api/redis-cache'; import redisCache from './api/redis-cache';
import rbfCache from './api/rbf-cache';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -91,24 +92,11 @@ class Server {
async startServer(worker = false): Promise<void> { async startServer(worker = false): Promise<void> {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
// Register cleanup listeners for exit events
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(event => {
process.on(event, () => { this.onExit(event); });
});
process.on('uncaughtException', (error) => {
this.onUnhandledException('uncaughtException', error);
});
process.on('unhandledRejection', (reason, promise) => {
this.onUnhandledException('unhandledRejection', reason);
});
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
bitcoinApi.startHealthChecks(); bitcoinApi.startHealthChecks();
} }
if (config.DATABASE.ENABLED) { if (config.DATABASE.ENABLED) {
DB.getPidLock();
await DB.checkDbConnection(); await DB.checkDbConnection();
try { try {
if (process.env.npm_config_reindex_blocks === 'true') { // Re-index requests if (process.env.npm_config_reindex_blocks === 'true') { // Re-index requests
@@ -120,6 +108,8 @@ class Server {
} }
} }
rbfCache.init();
this.app this.app
.use((req: Request, res: Response, next: NextFunction) => { .use((req: Request, res: Response, next: NextFunction) => {
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
@@ -206,7 +196,7 @@ class Server {
} }
const newMempool = await bitcoinApi.$getRawMempool(); const newMempool = await bitcoinApi.$getRawMempool();
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.indexerRunning ? 10 : 1);
if (numHandledBlocks === 0) { if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, pollRate); await memPool.$updateMempool(newMempool, pollRate);
} }
@@ -319,19 +309,6 @@ class Server {
this.lastHeapLogTime = now; this.lastHeapLogTime = now;
} }
} }
onExit(exitEvent, code = 0): void {
logger.debug(`onExit for signal: ${exitEvent}`);
if (config.DATABASE.ENABLED) {
DB.releasePidLock();
}
process.exit(code);
}
onUnhandledException(type, error): void {
console.error(`${type}:`, error);
this.onExit(type, 1);
}
} }
((): Server => new Server())(); ((): Server => new Server())();

View File

@@ -15,18 +15,11 @@ export interface CoreIndex {
best_block_height: number; best_block_height: number;
} }
type TaskName = 'blocksPrices' | 'coinStatsIndex';
class Indexer { class Indexer {
private runIndexer = true; runIndexer = true;
private indexerRunning = false; indexerRunning = false;
private tasksRunning: { [key in TaskName]?: boolean; } = {}; tasksRunning: string[] = [];
private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {}; coreIndexes: CoreIndex[] = [];
private coreIndexes: CoreIndex[] = [];
public indexerIsRunning(): boolean {
return this.indexerRunning;
}
/** /**
* Check which core index is available for indexing * Check which core index is available for indexing
@@ -76,69 +69,33 @@ class Indexer {
} }
} }
/** public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> {
* schedules a single task to run in `timeout` ms if (!Common.indexingEnabled()) {
* only one task of each type may be scheduled
*
* @param {TaskName} task - the type of task
* @param {number} timeout - delay in ms
* @param {boolean} replace - `true` replaces any already scheduled task (works like a debounce), `false` ignores subsequent requests (works like a throttle)
*/
public scheduleSingleTask(task: TaskName, timeout: number = 10000, replace = false): void {
if (this.tasksScheduled[task]) {
if (!replace) { //throttle
return;
} else { // debounce
clearTimeout(this.tasksScheduled[task]);
}
}
this.tasksScheduled[task] = setTimeout(async () => {
try {
await this.runSingleTask(task);
} catch (e) {
logger.err(`Unexpected error in scheduled task ${task}: ` + (e instanceof Error ? e.message : e));
} finally {
clearTimeout(this.tasksScheduled[task]);
}
}, timeout);
}
/**
* Runs a single task immediately
*
* (use `scheduleSingleTask` instead to queue a task to run after some timeout)
*/
public async runSingleTask(task: TaskName): Promise<void> {
if (!Common.indexingEnabled() || this.tasksRunning[task]) {
return; return;
} }
this.tasksRunning[task] = true;
switch (task) { if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
case 'blocksPrices': { this.tasksRunning.push(task);
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { const lastestPriceId = await PricesRepository.$getLatestPriceId();
let lastestPriceId; if (priceUpdater.historyInserted === false || lastestPriceId === null) {
try { logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
lastestPriceId = await PricesRepository.$getLatestPriceId(); setTimeout(() => {
} catch (e) { this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e)); this.runSingleTask('blocksPrices');
} if (priceUpdater.historyInserted === false || lastestPriceId === null) { }, 10000);
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining); } else {
this.scheduleSingleTask(task, 10000); logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
} else { await mining.$indexBlockPrices();
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
await mining.$indexBlockPrices(); }
}
}
} break;
case 'coinStatsIndex': {
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
} break;
} }
this.tasksRunning[task] = false; if (task === 'coinStatsIndex' && !this.tasksRunning.includes(task)) {
this.tasksRunning.push(task);
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
}
} }
public async $run(): Promise<void> { public async $run(): Promise<void> {

View File

@@ -157,6 +157,4 @@ class Logger {
} }
} }
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
export default new Logger(); export default new Logger();

View File

@@ -104,7 +104,6 @@ export interface MempoolTransactionExtended extends TransactionExtended {
adjustedFeePerVsize: number; adjustedFeePerVsize: number;
inputs?: number[]; inputs?: number[];
lastBoosted?: number; lastBoosted?: number;
cpfpDirty?: boolean;
} }
export interface AuditTransaction { export interface AuditTransaction {
@@ -300,7 +299,6 @@ export interface Statistic {
total_fee: number; total_fee: number;
mempool_byte_weight: number; mempool_byte_weight: number;
fee_data: string; fee_data: string;
min_fee: number;
vsize_1: number; vsize_1: number;
vsize_2: number; vsize_2: number;
@@ -347,7 +345,6 @@ export interface OptimizedStatistic {
vbytes_per_second: number; vbytes_per_second: number;
total_fee: number; total_fee: number;
mempool_byte_weight: number; mempool_byte_weight: number;
min_fee: number;
vsizes: number[]; vsizes: number[];
} }

View File

@@ -541,7 +541,7 @@ class BlocksRepository {
*/ */
public async $getBlocksDifficulty(): Promise<object[]> { public async $getBlocksDifficulty(): Promise<object[]> {
try { try {
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks ORDER BY height ASC`); const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));

View File

@@ -14,7 +14,7 @@ class NodesSocketsRepository {
await DB.query(` await DB.query(`
INSERT INTO nodes_sockets(public_key, socket, type) INSERT INTO nodes_sockets(public_key, socket, type)
VALUE (?, ?, ?) VALUE (?, ?, ?)
`, [socket.publicKey, socket.addr, socket.network], 'silent'); `, [socket.publicKey, socket.addr, socket.network]);
} catch (e: any) { } catch (e: any) {
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e));

View File

@@ -1,6 +1,5 @@
var http = require('http') var http = require('http')
var https = require('https') var https = require('https')
import { readFileSync } from 'fs';
var JsonRPC = function (opts) { var JsonRPC = function (opts) {
// @ts-ignore // @ts-ignore
@@ -56,13 +55,7 @@ JsonRPC.prototype.call = function (method, params) {
} }
// use HTTP auth if user and password set // use HTTP auth if user and password set
if (this.opts.cookie) { if (this.opts.user && this.opts.pass) {
if (!this.cachedCookie) {
this.cachedCookie = readFileSync(this.opts.cookie).toString();
}
// @ts-ignore
requestOptions.auth = this.cachedCookie;
} else if (this.opts.user && this.opts.pass) {
// @ts-ignore // @ts-ignore
requestOptions.auth = this.opts.user + ':' + this.opts.pass requestOptions.auth = this.opts.user + ':' + this.opts.pass
} }
@@ -100,7 +93,7 @@ JsonRPC.prototype.call = function (method, params) {
reject(err) reject(err)
}) })
request.on('response', (response) => { request.on('response', function (response) {
clearTimeout(reqTimeout) clearTimeout(reqTimeout)
// We need to buffer the response chunks in a nonblocking way. // We need to buffer the response chunks in a nonblocking way.
@@ -111,7 +104,7 @@ JsonRPC.prototype.call = function (method, params) {
// When all the responses are finished, we decode the JSON and // When all the responses are finished, we decode the JSON and
// depending on whether it's got a result or an error, we call // depending on whether it's got a result or an error, we call
// emitSuccess or emitError on the promise. // emitSuccess or emitError on the promise.
response.on('end', () => { response.on('end', function () {
var err var err
if (cbCalled) return if (cbCalled) return
@@ -120,14 +113,6 @@ JsonRPC.prototype.call = function (method, params) {
try { try {
var decoded = JSON.parse(buffer) var decoded = JSON.parse(buffer)
} catch (e) { } catch (e) {
// if we authenticated using a cookie and it failed, read the cookie file again
if (
response.statusCode === 401 /* Unauthorized */ &&
this.opts.cookie
) {
this.cachedCookie = undefined;
}
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
err = new Error('Invalid params, response status code: ' + response.statusCode) err = new Error('Invalid params, response status code: ' + response.statusCode)
err.code = -32602 err.code = -32602

View File

@@ -15,6 +15,8 @@ class ForensicsService {
txCache: { [txid: string]: IEsploraApi.Transaction } = {}; txCache: { [txid: string]: IEsploraApi.Transaction } = {};
tempCached: string[] = []; tempCached: string[] = [];
constructor() {}
public async $startService(): Promise<void> { public async $startService(): Promise<void> {
logger.info('Starting lightning network forensics service'); logger.info('Starting lightning network forensics service');
@@ -64,138 +66,93 @@ class ForensicsService {
*/ */
public async $runClosedChannelsForensics(onlyNewChannels: boolean = false): Promise<void> { public async $runClosedChannelsForensics(onlyNewChannels: boolean = false): Promise<void> {
// Only Esplora backend can retrieve spent transaction outputs
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
return; return;
} }
let progress = 0;
try { try {
logger.debug(`Started running closed channel forensics...`); logger.debug(`Started running closed channel forensics...`);
let allChannels; let channels;
if (onlyNewChannels) { if (onlyNewChannels) {
allChannels = await channelsApi.$getClosedChannelsWithoutReason(); channels = await channelsApi.$getClosedChannelsWithoutReason();
} else { } else {
allChannels = await channelsApi.$getUnresolvedClosedChannels(); channels = await channelsApi.$getUnresolvedClosedChannels();
} }
let progress = 0; for (const channel of channels) {
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 10); let reason = 0;
// process batches of 1000 channels let resolvedForceClose = false;
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) { // Only Esplora backend can retrieve spent transaction outputs
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength); const cached: string[] = [];
let allOutspends: IEsploraApi.Outspend[][] = [];
const forceClosedChannels: { channel: any, cachedSpends: string[] }[] = [];
// fetch outspends in bulk
try { try {
const outspendTxids = channels.map(channel => channel.closing_transaction_id); let outspends: IEsploraApi.Outspend[] | undefined;
allOutspends = await bitcoinApi.$getBatchedOutspendsInternal(outspendTxids); try {
logger.info(`Fetched outspends for ${allOutspends.length} txs from esplora for LN forensics`); outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT); await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
} catch (e) { } catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/internal/txs/outspends/by-txid'}. Reason ${e instanceof Error ? e.message : e}`); logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
} continue;
// fetch spending transactions in bulk and load into txCache }
const newSpendingTxids: { [txid: string]: boolean } = {}; const lightningScriptReasons: number[] = [];
for (const outspends of allOutspends) {
for (const outspend of outspends) { for (const outspend of outspends) {
if (outspend.spent && outspend.txid) { if (outspend.spent && outspend.txid) {
newSpendingTxids[outspend.txid] = true; let spendingTx = await this.fetchTransaction(outspend.txid);
if (!spendingTx) {
continue;
}
cached.push(spendingTx.txid);
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
lightningScriptReasons.push(lightningScript);
} }
} }
} const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
const allOutspendTxs = await this.fetchTransactions( if (filteredReasons.length) {
allOutspends.flatMap(outspends => if (filteredReasons.some((r) => r === 2 || r === 4)) {
outspends reason = 3;
.filter(outspend => outspend.spent && outspend.txid) } else {
.map(outspend => outspend.txid) reason = 2;
) resolvedForceClose = true;
); }
logger.info(`Fetched ${allOutspendTxs.length} out-spending txs from esplora for LN forensics`); } else {
/*
// process each outspend We can detect a commitment transaction (force close) by reading Sequence and Locktime
for (const [index, channel] of channels.entries()) { https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
let reason = 0; */
const cached: string[] = []; let closingTx = await this.fetchTransaction(channel.closing_transaction_id, true);
try { if (!closingTx) {
const outspends = allOutspends[index];
if (!outspends || !outspends.length) {
// outspends are missing
continue; continue;
} }
const lightningScriptReasons: number[] = []; cached.push(closingTx.txid);
for (const outspend of outspends) { const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
if (outspend.spent && outspend.txid) { const locktimeHex: string = closingTx.locktime.toString(16);
const spendingTx = this.txCache[outspend.txid]; if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
if (!spendingTx) { reason = 2; // Here we can't be sure if it's a penalty or not
continue; } else {
} reason = 1;
cached.push(spendingTx.txid);
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
lightningScriptReasons.push(lightningScript);
}
} }
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1); }
if (filteredReasons.length) { if (reason) {
if (filteredReasons.some((r) => r === 2 || r === 4)) { logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
// Force closed with penalty await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
reason = 3; if (reason === 2 && resolvedForceClose) {
} else { await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
// Force closed without penalty }
reason = 2; if (reason !== 2 || resolvedForceClose) {
await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
}
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
// clean up cached transactions
cached.forEach(txid => { cached.forEach(txid => {
delete this.txCache[txid]; delete this.txCache[txid];
}); });
} else {
forceClosedChannels.push({ channel, cachedSpends: cached });
}
} catch (e) {
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
}
}
// fetch force-closing transactions in bulk
const closingTxs = await this.fetchTransactions(forceClosedChannels.map(x => x.channel.closing_transaction_id));
logger.info(`Fetched ${closingTxs.length} closing txs from esplora for LN forensics`);
// process channels with no lightning script reasons
for (const { channel, cachedSpends } of forceClosedChannels) {
const closingTx = this.txCache[channel.closing_transaction_id];
if (!closingTx) {
// no channel close transaction found yet
continue;
}
/*
We can detect a commitment transaction (force close) by reading Sequence and Locktime
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
*/
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
const locktimeHex: string = closingTx.locktime.toString(16);
let reason;
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
// Force closed, but we can't be sure if it's a penalty or not
reason = 2;
} else {
// Mutually closed
reason = 1;
// clean up cached transactions
delete this.txCache[closingTx.txid];
for (const txid of cachedSpends) {
delete this.txCache[txid];
} }
} }
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]); } catch (e) {
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
} }
progress += channels.length; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) { if (elapsedSeconds > 10) {
logger.debug(`Updating channel closed channel forensics ${progress}/${allChannels.length}`); logger.debug(`Updating channel closed channel forensics ${progress}/${channels.length}`);
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
} }
} }
@@ -263,11 +220,8 @@ class ForensicsService {
logger.debug(`Started running open channel forensics...`); logger.debug(`Started running open channel forensics...`);
const channels = await channelsApi.$getChannelsWithoutSourceChecked(); const channels = await channelsApi.$getChannelsWithoutSourceChecked();
// preload open channel transactions
await this.fetchTransactions(channels.map(channel => channel.transaction_id), true);
for (const openChannel of channels) { for (const openChannel of channels) {
const openTx = this.txCache[openChannel.transaction_id]; let openTx = await this.fetchTransaction(openChannel.transaction_id, true);
if (!openTx) { if (!openTx) {
continue; continue;
} }
@@ -322,7 +276,7 @@ class ForensicsService {
// Check if a channel open tx input spends the result of a swept channel close output // Check if a channel open tx input spends the result of a swept channel close output
private async $attributeSweptChannelCloses(openChannel: ILightningApi.Channel, input: IEsploraApi.Vin): Promise<void> { private async $attributeSweptChannelCloses(openChannel: ILightningApi.Channel, input: IEsploraApi.Vin): Promise<void> {
const sweepTx = await this.fetchTransaction(input.txid, true); let sweepTx = await this.fetchTransaction(input.txid, true);
if (!sweepTx) { if (!sweepTx) {
logger.err(`couldn't find input transaction for channel forensics ${openChannel.channel_id} ${input.txid}`); logger.err(`couldn't find input transaction for channel forensics ${openChannel.channel_id} ${input.txid}`);
return; return;
@@ -381,7 +335,7 @@ class ForensicsService {
if (matched && !ambiguous) { if (matched && !ambiguous) {
// fetch closing channel transaction and perform forensics on the outputs // fetch closing channel transaction and perform forensics on the outputs
const prevChannelTx = await this.fetchTransaction(input.txid, true); let prevChannelTx = await this.fetchTransaction(input.txid, true);
let outspends: IEsploraApi.Outspend[] | undefined; let outspends: IEsploraApi.Outspend[] | undefined;
try { try {
outspends = await bitcoinApi.$getOutspends(input.txid); outspends = await bitcoinApi.$getOutspends(input.txid);
@@ -401,17 +355,17 @@ class ForensicsService {
}; };
}); });
} }
// preload outspend transactions
await this.fetchTransactions(outspends.filter(o => o.spent && o.txid).map(o => o.txid), true);
for (let i = 0; i < outspends?.length; i++) { for (let i = 0; i < outspends?.length; i++) {
const outspend = outspends[i]; const outspend = outspends[i];
const output = prevChannel.outputs[i]; const output = prevChannel.outputs[i];
if (outspend.spent && outspend.txid) { if (outspend.spent && outspend.txid) {
const spendingTx = this.txCache[outspend.txid]; try {
if (spendingTx) { const spendingTx = await this.fetchTransaction(outspend.txid, true);
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]); if (spendingTx) {
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
}
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
} }
} else { } else {
output.type = 0; output.type = 0;
@@ -476,36 +430,13 @@ class ForensicsService {
} }
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT); await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
} catch (e) { } catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid}. Reason ${e instanceof Error ? e.message : e}`); logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
return null; return null;
} }
} }
return tx; return tx;
} }
// fetches a batch of transactions and adds them to the txCache
// the returned list of txs does *not* preserve ordering or number
async fetchTransactions(txids, temp: boolean = false): Promise<(IEsploraApi.Transaction | null)[]> {
// deduplicate txids
const uniqueTxids = [...new Set<string>(txids)];
// filter out any transactions we already have in the cache
const needToFetch: string[] = uniqueTxids.filter(txid => !this.txCache[txid]);
try {
const txs = await bitcoinApi.$getRawTransactions(needToFetch);
for (const tx of txs) {
this.txCache[tx.txid] = tx;
if (temp) {
this.tempCached.push(tx.txid);
}
}
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/txs'}. Reason ${e instanceof Error ? e.message : e}`);
return [];
}
return txids.map(txid => this.txCache[txid]);
}
clearTempCache(): void { clearTempCache(): void {
for (const txid of this.tempCached) { for (const txid of this.tempCached) {
delete this.txCache[txid]; delete this.txCache[txid];

View File

@@ -288,32 +288,22 @@ class NetworkSyncService {
} }
logger.debug(`${log}`, logger.tags.ln); logger.debug(`${log}`, logger.tags.ln);
const allChannels = await channelsApi.$getChannelsByStatus([0, 1]); const channels = await channelsApi.$getChannelsByStatus([0, 1]);
for (const channel of channels) {
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 2); const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
// process batches of 5000 channels if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) { logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln);
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength); await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
const outspends = await bitcoinApi.$getOutSpendsByOutpoint(channels.map(channel => { [spendingTx.status.block_time, channel.id]);
return { txid: channel.transaction_id, vout: channel.transaction_vout }; if (spendingTx.txid && !channel.closing_transaction_id) {
})); await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
for (const [index, channel] of channels.entries()) {
const spendingTx = outspends[index];
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
// logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln);
await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
[spendingTx.status.block_time, channel.id]);
if (spendingTx.txid && !channel.closing_transaction_id) {
await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
}
} }
} }
progress += channels.length; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
logger.debug(`Checking if channel has been closed ${progress}/${allChannels.length}`, logger.tags.ln); logger.debug(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln);
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
} }
} }

View File

@@ -1,179 +0,0 @@
/*
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
How it works:
`this._head` is an instance of `Node` which keeps track of its current value and nests
another instance of `Node` that keeps the value that comes after it. When a value is
provided to `.enqueue()`, the code needs to iterate through `this._head`, going deeper
and deeper to find the last value. However, iterating through every single item is slow.
This problem is solved by saving a reference to the last value as `this._tail` so that
it can reference it to add a new value.
*/
class Node {
value;
next;
constructor(value) {
this.value = value;
}
}
class Queue {
private _head;
private _tail;
private _size;
constructor() {
this.clear();
}
enqueue(value) {
const node = new Node(value);
if (this._head) {
this._tail.next = node;
this._tail = node;
} else {
this._head = node;
this._tail = node;
}
this._size++;
}
dequeue() {
const current = this._head;
if (!current) {
return;
}
this._head = this._head.next;
this._size--;
return current.value;
}
clear() {
this._head = undefined;
this._tail = undefined;
this._size = 0;
}
get size() {
return this._size;
}
*[Symbol.iterator]() {
let current = this._head;
while (current) {
yield current.value;
current = current.next;
}
}
}
interface LimitFunction {
readonly activeCount: number;
readonly pendingCount: number;
clearQueue: () => void;
<Arguments extends unknown[], ReturnType>(
fn: (...args: Arguments) => PromiseLike<ReturnType> | ReturnType,
...args: Arguments
): Promise<ReturnType>;
}
export default function pLimit(concurrency: number): LimitFunction {
if (
!(
(Number.isInteger(concurrency) ||
concurrency === Number.POSITIVE_INFINITY) &&
concurrency > 0
)
) {
throw new TypeError('Expected `concurrency` to be a number from 1 and up');
}
const queue = new Queue();
let activeCount = 0;
const next = () => {
activeCount--;
if (queue.size > 0) {
queue.dequeue()();
}
};
const run = async (fn, resolve, args) => {
activeCount++;
const result = (async () => fn(...args))();
resolve(result);
try {
await result;
} catch {}
next();
};
const enqueue = (fn, resolve, args) => {
queue.enqueue(run.bind(undefined, fn, resolve, args));
(async () => {
// This function needs to wait until the next microtask before comparing
// `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
// when the run function is dequeued and called. The comparison in the if-statement
// needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
await Promise.resolve();
if (activeCount < concurrency && queue.size > 0) {
queue.dequeue()();
}
})();
};
const generator = (fn, ...args) =>
new Promise((resolve) => {
enqueue(fn, resolve, args);
});
Object.defineProperties(generator, {
activeCount: {
get: () => activeCount,
},
pendingCount: {
get: () => queue.size,
},
clearQueue: {
value: () => {
queue.clear();
},
},
});
return generator as any;
}

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
Signed: fubz

View File

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

View File

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

View File

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

View File

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

View File

@@ -164,9 +164,7 @@ Corresponding `docker-compose.yml` overrides:
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool",
"TIMEOUT": 60000, "TIMEOUT": 60000
"COOKIE": false,
"COOKIE_PATH": ""
}, },
``` ```
@@ -179,8 +177,6 @@ Corresponding `docker-compose.yml` overrides:
CORE_RPC_USERNAME: "" CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: "" CORE_RPC_PASSWORD: ""
CORE_RPC_TIMEOUT: 60000 CORE_RPC_TIMEOUT: 60000
CORE_RPC_COOKIE: false
CORE_RPC_COOKIE_PATH: ""
... ...
``` ```
@@ -235,9 +231,7 @@ Corresponding `docker-compose.yml` overrides:
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool",
"TIMEOUT": 60000, "TIMEOUT": 60000
"COOKIE": false,
"COOKIE_PATH": ""
}, },
``` ```
@@ -250,8 +244,6 @@ Corresponding `docker-compose.yml` overrides:
SECOND_CORE_RPC_USERNAME: "" SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: "" SECOND_CORE_RPC_PASSWORD: ""
SECOND_CORE_RPC_TIMEOUT: "" SECOND_CORE_RPC_TIMEOUT: ""
SECOND_CORE_RPC_COOKIE: false
SECOND_CORE_RPC_COOKIE_PATH: ""
... ...
``` ```

View File

@@ -1,4 +1,4 @@
FROM node:20.8.0-buster-slim AS builder FROM node:16.16.0-buster-slim AS builder
ARG commitHash ARG commitHash
ENV MEMPOOL_COMMIT_HASH=${commitHash} ENV MEMPOOL_COMMIT_HASH=${commitHash}
@@ -17,7 +17,7 @@ ENV PATH="/root/.cargo/bin:$PATH"
RUN npm install --omit=dev --omit=optional RUN npm install --omit=dev --omit=optional
RUN npm run package RUN npm run package
FROM node:20.8.0-buster-slim FROM node:16.16.0-buster-slim
WORKDIR /backend WORKDIR /backend

View File

@@ -41,9 +41,7 @@
"PORT": __CORE_RPC_PORT__, "PORT": __CORE_RPC_PORT__,
"USERNAME": "__CORE_RPC_USERNAME__", "USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__, "TIMEOUT": __CORE_RPC_TIMEOUT__
"COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -53,10 +51,7 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__", "REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": __ESPLORA_BATCH_QUERY_BASE_SIZE__,
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__, "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
"FALLBACK": __ESPLORA_FALLBACK__ "FALLBACK": __ESPLORA_FALLBACK__
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
@@ -64,9 +59,7 @@
"PORT": __SECOND_CORE_RPC_PORT__, "PORT": __SECOND_CORE_RPC_PORT__,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__", "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__", "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__, "TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
"COOKIE": __SECOND_CORE_RPC_COOKIE__,
"COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
}, },
"DATABASE": { "DATABASE": {
"ENABLED": __DATABASE_ENABLED__, "ENABLED": __DATABASE_ENABLED__,
@@ -76,8 +69,7 @@
"DATABASE": "__DATABASE_DATABASE__", "DATABASE": "__DATABASE_DATABASE__",
"USERNAME": "__DATABASE_USERNAME__", "USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__", "PASSWORD": "__DATABASE_PASSWORD__",
"TIMEOUT": __DATABASE_TIMEOUT__, "TIMEOUT": __DATABASE_TIMEOUT__
"PID_DIR": "__DATABASE_PID_DIR__"
}, },
"SYSLOG": { "SYSLOG": {
"ENABLED": __SYSLOG_ENABLED__, "ENABLED": __SYSLOG_ENABLED__,
@@ -147,7 +139,6 @@
}, },
"REDIS": { "REDIS": {
"ENABLED": __REDIS_ENABLED__, "ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
} }
} }

View File

@@ -43,8 +43,6 @@ __CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool} __CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool} __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_PATH__=${CORE_RPC_COOKIE_PATH:=""}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -54,10 +52,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA # ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000} __ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"} __ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000} __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]} __ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
# SECOND_CORE_RPC # SECOND_CORE_RPC
@@ -66,8 +61,6 @@ __SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool} __SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool} __SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000} __SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
__SECOND_CORE_RPC_COOKIE__=${SECOND_CORE_RPC_COOKIE:=false}
__SECOND_CORE_RPC_COOKIE_PATH__=${SECOND_CORE_RPC_COOKIE_PATH:=""}
# DATABASE # DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true} __DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
@@ -78,7 +71,6 @@ __DATABASE_DATABASE__=${DATABASE_DATABASE:=mempool}
__DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool} __DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool} __DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000} __DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
__DATABASE_PID_DIR__=${DATABASE_PID_DIR:=""}
# SYSLOG # SYSLOG
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false} __SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
@@ -147,9 +139,8 @@ __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS # REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_ENABLED__=${REDIS_ENABLED:=true}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@@ -194,8 +185,6 @@ sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json
sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json 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_PATH__!${__CORE_RPC_COOKIE_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
@@ -203,10 +192,7 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_BATCH_QUERY_BASE_SIZE__!${__ESPLORA_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
@@ -214,8 +200,6 @@ sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config
sed -i "s!__SECOND_CORE_RPC_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_COOKIE__!${__SECOND_CORE_RPC_COOKIE__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_COOKIE_PATH__!${__SECOND_CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json
sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json
@@ -225,7 +209,6 @@ sed -i "s!__DATABASE_DATABASE__!${__DATABASE_DATABASE__}!g" mempool-config.json
sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json
sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json
sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
sed -i "s!__DATABASE_PID_DIR__!${__DATABASE_PID_DIR__}!g" mempool-config.json
sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json
sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json
@@ -291,6 +274,5 @@ sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS_
# 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
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
node /backend/package/index.js node /backend/package/index.js

View File

@@ -38,7 +38,7 @@ services:
MYSQL_USER: "mempool" MYSQL_USER: "mempool"
MYSQL_PASSWORD: "mempool" MYSQL_PASSWORD: "mempool"
MYSQL_ROOT_PASSWORD: "admin" MYSQL_ROOT_PASSWORD: "admin"
image: mariadb:10.5.21 image: mariadb:10.5.8
user: "1000:1000" user: "1000:1000"
restart: on-failure restart: on-failure
stop_grace_period: 1m stop_grace_period: 1m

View File

@@ -0,0 +1,32 @@
FROM ubuntu:18.04
MAINTAINER mempool.space developers
EXPOSE 50002
# runs as UID 1000 GID 1000 inside the container
ENV VERSION 4.0.9
RUN set -x \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends gpg gpg-agent dirmngr \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends wget xpra python3-pyqt5 python3-wheel python3-pip python3-setuptools libsecp256k1-0 libsecp256k1-dev python3-numpy python3-dev build-essential \
&& wget -O /tmp/Electrum-${VERSION}.tar.gz https://download.electrum.org/${VERSION}/Electrum-${VERSION}.tar.gz \
&& wget -O /tmp/Electrum-${VERSION}.tar.gz.asc https://download.electrum.org/${VERSION}/Electrum-${VERSION}.tar.gz.asc \
&& gpg --keyserver keys.gnupg.net --recv-keys 6694D8DE7BE8EE5631BED9502BD5824B7F9470E6 \
&& gpg --verify /tmp/Electrum-${VERSION}.tar.gz.asc /tmp/Electrum-${VERSION}.tar.gz \
&& pip3 install /tmp/Electrum-${VERSION}.tar.gz \
&& test -f /usr/local/bin/electrum \
&& rm -vrf /tmp/Electrum-${VERSION}.tar.gz /tmp/Electrum-${VERSION}.tar.gz.asc ${HOME}/.gnupg \
&& apt-get purge --autoremove -y python3-wheel python3-pip python3-setuptools python3-dev build-essential libsecp256k1-dev curl gpg gpg-agent dirmngr \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& useradd -d /home/mempool -m mempool \
&& mkdir /electrum \
&& ln -s /electrum /home/mempool/.electrum \
&& chown mempool:mempool /electrum
USER mempool
ENV HOME /home/mempool
WORKDIR /home/mempool
VOLUME /electrum
CMD ["/usr/bin/xpra", "start", ":100", "--start-child=/usr/local/bin/electrum", "--bind-tcp=0.0.0.0:50002","--daemon=yes", "--notifications=no", "--mdns=no", "--pulseaudio=no", "--html=off", "--speaker=disabled", "--microphone=disabled", "--webcam=no", "--printing=no", "--dbus-launch=", "--exit-with-children"]
ENTRYPOINT ["electrum"]

View File

@@ -1,4 +1,4 @@
FROM node:20.8.0-buster-slim AS builder FROM node:16.16.0-buster-slim AS builder
ARG commitHash ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash} ENV DOCKER_COMMIT_HASH=${commitHash}
@@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
RUN npm run build RUN npm run build
FROM nginx:1.24.0-alpine FROM nginx:1.17.8-alpine
WORKDIR /patch WORKDIR /patch

View File

@@ -39,7 +39,6 @@ __AUDIT__=${AUDIT:=false}
__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0} __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} __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}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
# Export as environment variables to be used by envsubst # Export as environment variables to be used by envsubst
@@ -66,7 +65,6 @@ export __AUDIT__
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
export __ACCELERATOR__
export __HISTORICAL_PRICE__ export __HISTORICAL_PRICE__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname) folder=$(find /var/www/mempool -name "config.js" | xargs dirname)

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging", "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed", "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js", "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'", "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources'",
"sync-assets-dev": "node sync-assets.js 'src/resources/'", "sync-assets-dev": "node sync-assets.js 'src/resources/'",
"generate-config": "node generate-config.js", "generate-config": "node generate-config.js",
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js", "build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
@@ -74,9 +74,9 @@
"@angular/platform-server": "^16.2.2", "@angular/platform-server": "^16.2.2",
"@angular/router": "^16.2.2", "@angular/router": "^16.2.2",
"@fortawesome/angular-fontawesome": "~0.13.0", "@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.5.1", "@fortawesome/fontawesome-common-types": "~6.4.0",
"@fortawesome/fontawesome-svg-core": "~6.5.1", "@fortawesome/fontawesome-svg-core": "~6.4.0",
"@fortawesome/free-solid-svg-icons": "~6.5.1", "@fortawesome/free-solid-svg-icons": "~6.4.0",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^15.1.0", "@ng-bootstrap/ng-bootstrap": "^15.1.0",
"@types/qrcode": "~1.5.0", "@types/qrcode": "~1.5.0",
@@ -85,12 +85,13 @@
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.4.3", "echarts": "~5.4.3",
"echarts-gl": "^2.0.9",
"lightweight-charts": "~3.8.0", "lightweight-charts": "~3.8.0",
"ngx-echarts": "~16.2.0", "ngx-echarts": "~16.0.0",
"ngx-infinite-scroll": "^16.0.0", "ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tinyify": "^3.1.0", "tinyify": "^4.0.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.6.0", "tslib": "~2.6.0",
"zone.js": "~0.13.1" "zone.js": "~0.13.1"
@@ -110,10 +111,10 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.6.0", "cypress": "^12.17.2",
"cypress-fail-on-console-error": "~5.0.0", "cypress-fail-on-console-error": "~4.0.3",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.0",
"mock-socket": "~9.3.1", "mock-socket": "~9.2.1",
"start-server-and-test": "~2.0.0" "start-server-and-test": "~2.0.0"
}, },
"scarfSettings": { "scarfSettings": {

View File

@@ -112,14 +112,6 @@ PROXY_CONFIG.push(...[
"^/testnet": "" "^/testnet": ""
}, },
}, },
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{ {
context: ['/api/v1/**'], context: ['/api/v1/**'],
target: `http://127.0.0.1:8999`, target: `http://127.0.0.1:8999`,

View File

@@ -112,14 +112,6 @@ PROXY_CONFIG.push(...[
"^/testnet": "" "^/testnet": ""
}, },
}, },
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{ {
context: ['/api/v1/**'], context: ['/api/v1/**'],
target: `http://localhost:8999`, target: `http://localhost:8999`,

View File

@@ -95,14 +95,6 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
} }
PROXY_CONFIG.push(...[ PROXY_CONFIG.push(...[
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{ {
context: ['/api/v1/**'], context: ['/api/v1/**'],
target: `http://localhost:8999`, target: `http://localhost:8999`,

View File

@@ -1,11 +1,28 @@
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.preloading-strategy' import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component'; import { StartComponent } from './components/start/start.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component'; import { TransactionComponent } from './components/transaction/transaction.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; import { BlockComponent } from './components/block/block.component';
import { ClockComponent } from './components/clock/clock.component'; import { ClockComponent } from './components/clock/clock.component';
import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component';
import { StatusViewComponent } from './components/status-view/status-view.component'; import { StatusViewComponent } from './components/status-view/status-view.component';
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RbfList } from './components/rbf-list/rbf-list.component';
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
import { AssetsComponent } from './components/assets/assets.component';
import { AssetComponent } from './components/asset/asset.component';
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
import { CalculatorComponent } from './components/calculator/calculator.component';
const browserWindow = window || {}; const browserWindow = window || {};
// @ts-ignore // @ts-ignore
@@ -18,13 +35,95 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), component: MasterPageComponent,
data: { preload: true }, children: [
{
path: 'mining/blocks',
redirectTo: 'blocks',
pathMatch: 'full'
},
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'rbf',
component: RbfList,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
component: StartComponent,
data: { networkSpecific: true },
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
component: StartComponent,
data: { networkSpecific: true },
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
data: { preload: true },
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'lightning',
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
},
],
}, },
{ {
path: 'status', path: 'status',
@@ -33,8 +132,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '**', path: '**',
@@ -53,13 +151,88 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), component: MasterPageComponent,
data: { preload: true }, children: [
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'rbf',
component: RbfList,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'lightning',
data: { networks: ['bitcoin'] },
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
},
],
}, },
{ {
path: 'status', path: 'status',
@@ -68,8 +241,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '**', path: '**',
@@ -80,13 +252,97 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), component: MasterPageComponent,
data: { preload: true }, children: [
{
path: 'mining/blocks',
redirectTo: 'blocks',
pathMatch: 'full'
},
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'rbf',
component: RbfList,
},
{
path: 'tools/calculator',
component: CalculatorComponent
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'lightning',
data: { networks: ['bitcoin'] },
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
},
],
}, },
{ {
path: 'preview', path: 'preview',
@@ -117,18 +373,6 @@ let routes: Routes = [
path: 'clock/:mode/:index', path: 'clock/:mode/:index',
component: ClockComponent, component: ClockComponent,
}, },
{
path: 'view/block/:id',
component: BlockViewComponent,
},
{
path: 'view/mempool-block/:index',
component: MempoolBlockViewComponent,
},
{
path: 'view/blocks',
component: EightBlocksComponent,
},
{ {
path: 'status', path: 'status',
data: { networks: ['bitcoin', 'liquid'] }, data: { networks: ['bitcoin', 'liquid'] },
@@ -136,8 +380,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '**', path: '**',
@@ -148,6 +391,7 @@ let routes: Routes = [
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') { if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
routes = [{ routes = [{
path: '', path: '',
component: BisqMasterPageComponent,
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule) loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
}]; }];
} }
@@ -160,13 +404,105 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), component: LiquidMasterPageComponent,
data: { preload: true }, children: [
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'assets',
data: { networks: ['liquid'] },
component: AssetsNavComponent,
children: [
{
path: 'all',
data: { networks: ['liquid'] },
component: AssetsComponent,
},
{
path: 'asset/:id',
data: { networkSpecific: true },
component: AssetComponent
},
{
path: 'group/:id',
data: { networkSpecific: true },
component: AssetGroupComponent
},
{
path: '**',
redirectTo: 'all'
}
]
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
],
}, },
{ {
path: 'status', path: 'status',
@@ -175,8 +511,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
}, },
{ {
path: '', path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '**', path: '**',
@@ -187,13 +522,110 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), component: LiquidMasterPageComponent,
data: { preload: true }, children: [
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'blocks',
component: BlocksList,
},
{
path: 'terms-of-service',
component: TermsOfServiceComponent
},
{
path: 'privacy-policy',
component: PrivacyPolicyComponent
},
{
path: 'trademark-policy',
component: TrademarkPolicyComponent
},
{
path: 'address/:id',
children: [],
component: AddressComponent,
data: {
ogImage: true,
networkSpecific: true,
}
},
{
path: 'tx',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: TransactionComponent
},
],
},
{
path: 'block',
data: { networkSpecific: true },
component: StartComponent,
children: [
{
path: ':id',
component: BlockComponent,
data: {
ogImage: true
}
},
],
},
{
path: 'assets',
data: { networks: ['liquid'] },
component: AssetsNavComponent,
children: [
{
path: 'featured',
data: { networkSpecific: true },
component: AssetsFeaturedComponent,
},
{
path: 'all',
data: { networks: ['liquid'] },
component: AssetsComponent,
},
{
path: 'asset/:id',
data: { networkSpecific: true },
component: AssetComponent
},
{
path: 'group/:id',
data: { networkSpecific: true },
component: AssetGroupComponent
},
{
path: '**',
redirectTo: 'featured'
}
]
},
{
path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
},
],
}, },
{ {
path: 'preview', path: 'preview',
@@ -215,8 +647,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
}, },
{ {
path: '', path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
data: { preload: true },
}, },
{ {
path: '**', path: '**',

View File

@@ -41,7 +41,6 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
document.body.scrollTo(0, 0); document.body.scrollTo(0, 0);
this.addressString = params.get('id') || ''; this.addressString = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
return this.bisqApiService.getAddress$(this.addressString) return this.bisqApiService.getAddress$(this.addressString)
.pipe( .pipe(

View File

@@ -69,7 +69,6 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
this.location.replaceState( this.location.replaceState(
this.router.createUrlTree(['/bisq/block/', hash]).toString() this.router.createUrlTree(['/bisq/block/', hash]).toString()
); );
this.seoService.updateCanonical(this.location.path());
return this.bisqApiService.getBlock$(this.blockHash) return this.bisqApiService.getBlock$(this.blockHash)
.pipe(catchError(this.caughtHttpError.bind(this))); .pipe(catchError(this.caughtHttpError.bind(this)));
}), }),
@@ -89,7 +88,6 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
this.isLoading = false; this.isLoading = false;
this.blockHeight = block.height; this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`); this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
this.block = block; this.block = block;
}); });
} }

View File

@@ -36,7 +36,6 @@ export class BisqBlocksComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`); this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10); this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage); this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 670) { if (document.body.clientWidth < 670) {

View File

@@ -29,8 +29,7 @@ export class BisqDashboardComponent implements OnInit {
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`); this.seoService.setTitle(`Markets`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project™. See Bisq market prices, trading activity, and more.`);
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$() this.volumes$ = this.bisqApiService.getAllVolumesDay$()

View File

@@ -34,7 +34,6 @@ export class BisqMainDashboardComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.seoService.resetTitle(); this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe( this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(

View File

@@ -48,8 +48,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
map(([markets, routeParams]) => { map(([markets, routeParams]) => {
const pair = routeParams.get('pair'); const pair = routeParams.get('pair');
const pairUpperCase = pair.replace('_', '/').toUpperCase(); const pairUpperCase = pair.replace('_', '/').toUpperCase();
this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`); this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
return { return {
pair: pairUpperCase, pair: pairUpperCase,

View File

@@ -26,7 +26,6 @@ export class BisqStatsComponent implements OnInit {
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`); this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
this.stateService.bsqPrice$ this.stateService.bsqPrice$
.subscribe((bsqPrice) => { .subscribe((bsqPrice) => {
this.price = bsqPrice; this.price = bsqPrice;

View File

@@ -48,7 +48,6 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
document.body.scrollTo(0, 0); document.body.scrollTo(0, 0);
this.txId = params.get('id') || ''; this.txId = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`); this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`);
if (history.state.data) { if (history.state.data) {
return of(history.state.data); return of(history.state.data);
} }

View File

@@ -79,7 +79,6 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`); this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`);
this.radioGroupForm = this.formBuilder.group({ this.radioGroupForm = this.formBuilder.group({
txTypes: [this.txTypesDefaultChecked], txTypes: [this.txTypesDefaultChecked],

View File

@@ -27,11 +27,9 @@ import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/auto
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe'; import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive'; import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component'; import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
BisqMasterPageComponent,
BisqTransactionsComponent, BisqTransactionsComponent,
BisqTransactionComponent, BisqTransactionComponent,
BisqBlockComponent, BisqBlockComponent,

View File

@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component'; import { AboutComponent } from '../components/about/about.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component'; import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component'; import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component'; import { BisqBlockComponent } from './bisq-block/bisq-block.component';
@@ -10,83 +10,78 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component'; import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component'; import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component'; import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: BisqMasterPageComponent, component: BisqMainDashboardComponent,
children: [ },
{ {
path: '', path: 'markets',
component: BisqMainDashboardComponent, data: { networks: ['bisq'] },
}, component: BisqDashboardComponent,
{ },
path: 'markets', {
data: { networks: ['bisq'] }, path: 'transactions',
component: BisqDashboardComponent, data: { networks: ['bisq'] },
}, component: BisqTransactionsComponent
{ },
path: 'transactions', {
data: { networks: ['bisq'] }, path: 'market/:pair',
component: BisqTransactionsComponent data: { networkSpecific: true },
}, component: BisqMarketComponent,
{ },
path: 'market/:pair', {
data: { networkSpecific: true }, path: 'tx/push',
component: BisqMarketComponent, component: PushTransactionComponent,
}, },
{ {
path: 'tx/push', path: 'tx/:id',
component: PushTransactionComponent, data: { networkSpecific: true },
}, component: BisqTransactionComponent
{ },
path: 'tx/:id', {
data: { networkSpecific: true }, path: 'blocks',
component: BisqTransactionComponent children: [],
}, component: BisqBlocksComponent
{ },
path: 'blocks', {
children: [], path: 'block/:id',
component: BisqBlocksComponent data: { networkSpecific: true },
}, component: BisqBlockComponent,
{ },
path: 'block/:id', {
data: { networkSpecific: true }, path: 'address/:id',
component: BisqBlockComponent, data: { networkSpecific: true },
}, component: BisqAddressComponent,
{ },
path: 'address/:id', {
data: { networkSpecific: true }, path: 'stats',
component: BisqAddressComponent, data: { networks: ['bisq'] },
}, component: BisqStatsComponent,
{ },
path: 'stats', {
data: { networks: ['bisq'] }, path: 'about',
component: BisqStatsComponent, component: AboutComponent,
}, },
{ {
path: 'about', path: 'docs',
loadChildren: () => import('../components/about/about.module').then(m => m.AboutModule), loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
}, },
{ {
path: 'docs', path: 'api',
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule) loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
}, },
{ {
path: 'api', path: 'terms-of-service',
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule) component: TermsOfServiceComponent
}, },
{ {
path: 'terms-of-service', path: '**',
loadChildren: () => import('../components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), redirectTo: ''
}, }
{
path: '**',
redirectTo: ''
}
]
}
]; ];
@NgModule({ @NgModule({

View File

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

View File

@@ -225,7 +225,7 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S +
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0; const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
// Power of ten wrapper // Power of ten wrapper
export function selectPowerOfTen(val: number, multiplier = 1): { divider: number, unit: string } { export function selectPowerOfTen(val: number): { divider: number, unit: string } {
const powerOfTen = { const powerOfTen = {
exa: Math.pow(10, 18), exa: Math.pow(10, 18),
peta: Math.pow(10, 15), peta: Math.pow(10, 15),
@@ -236,17 +236,17 @@ export function selectPowerOfTen(val: number, multiplier = 1): { divider: number
}; };
let selectedPowerOfTen: { divider: number, unit: string }; let selectedPowerOfTen: { divider: number, unit: string };
if (val < powerOfTen.kilo * multiplier) { if (val < powerOfTen.kilo) {
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
} else if (val < powerOfTen.mega * multiplier) { } else if (val < powerOfTen.mega) {
selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' }; selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
} else if (val < powerOfTen.giga * multiplier) { } else if (val < powerOfTen.giga) {
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
} else if (val < powerOfTen.tera * multiplier) { } else if (val < powerOfTen.tera) {
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
} else if (val < powerOfTen.peta * multiplier) { } else if (val < powerOfTen.peta) {
selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' }; selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
} else if (val < powerOfTen.exa * multiplier) { } else if (val < powerOfTen.exa) {
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' }; selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
} else { } else {
selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' }; selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };

View File

@@ -1,16 +0,0 @@
<div id="become-sponsor-container">
<div class="become-sponsor community">
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
<a href="https://mempool.space/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
</div>
<div class="become-sponsor enterprise">
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
<a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
</div>
</div>

View File

@@ -1,45 +0,0 @@
#become-sponsor-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
gap: 20px;
margin: 68px auto;
}
.become-sponsor {
background-color: #1d1f31;
border-radius: 16px;
padding: 12px 20px;
width: 400px;
padding: 40px 20px;
}
.become-sponsor a {
margin-top: 10px;
}
#become-sponsor-container .btn {
margin-bottom: 24px;
}
#become-sponsor-container .ng-fa-icon {
color: #2ecc71;
margin-right: 5px;
}
#become-sponsor-container .sponsor-feature {
text-align: left;
width: 250px;
margin: 12px auto;
white-space: nowrap;
}
@media (max-width: 992px) {
#become-sponsor-container {
flex-wrap: wrap;
}
}

View File

@@ -1,22 +0,0 @@
import { Component } from '@angular/core';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({
selector: 'app-about-sponsors',
templateUrl: './about-sponsors.component.html',
styleUrls: ['./about-sponsors.component.scss'],
})
export class AboutSponsorsComponent {
constructor(private enterpriseService: EnterpriseService) {
}
onSponsorClick(e): boolean {
this.enterpriseService.goal(5);
return true;
}
onEnterpriseClick(e): boolean {
this.enterpriseService.goal(6);
return true;
}
}

View File

@@ -4,13 +4,12 @@
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&reg;</span> <span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&reg;</span>
<img class="logo" src="/resources/mempool-logo-bigger.png" /> <img class="logo" src="/resources/mempool-logo-bigger.png" />
<div class="version"> <div class="version">
<span>v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</span> v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
<span *ngIf="stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE">[{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}]</span>
</div> </div>
</div> </div>
<div class="about-text"> <div class="about-text">
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &reg;</ng-template></h5> <h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &trade;</ng-template></h5>
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p> <p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
</div> </div>
@@ -32,8 +31,12 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null"> <track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video> </video>
<ng-container *ngIf="officialMempoolSpace"> <ng-container *ngIf="false && officialMempoolSpace">
<app-about-sponsors></app-about-sponsors> <h3 class="mt-5">Sponsor the project</h3>
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
</div>
</ng-container> </ng-container>
<div class="enterprise-sponsor" id="enterprise-sponsors"> <div class="enterprise-sponsor" id="enterprise-sponsors">
@@ -182,14 +185,14 @@
</div> </div>
<ng-container *ngIf="officialMempoolSpace"> <ng-container *ngIf="officialMempoolSpace">
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor"> <div *ngIf="profiles$ | async as profiles" id="community-sponsors">
<div class="community-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://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> <img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</ng-container> </ng-container>
@@ -201,7 +204,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://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> <img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</div> </div>
@@ -239,7 +242,7 @@
<img class="image" src="/resources/profile/ronindojo.png" /> <img class="image" src="/resources/profile/ronindojo.png" />
<span>RoninDojo</span> <span>RoninDojo</span>
</a> </a>
<a href="https://github.com/runcitadel" target="_blank" title="Citadel"> <a href="https://github.com/runcitadel/core" target="_blank" title="Citadel">
<img class="image" src="/resources/profile/runcitadel.svg" /> <img class="image" src="/resources/profile/runcitadel.svg" />
<span>Citadel</span> <span>Citadel</span>
</a> </a>
@@ -295,9 +298,9 @@
<img class="image" src="/resources/profile/blixt.png" /> <img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span> <span>Blixt</span>
</a> </a>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS"> <a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
<img class="image" src="/resources/profile/zeus.png" /> <img class="image" src="/resources/profile/zeus.png" />
<span>ZEUS</span> <span>Zeus</span>
</a> </a>
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet"> <a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
<img class="image" src="/resources/profile/marina.svg" /> <img class="image" src="/resources/profile/marina.svg" />

View File

@@ -22,7 +22,6 @@
.intro { .intro {
margin: 25px auto 30px; margin: 25px auto 30px;
margin-top: 25px;
width: 250px; width: 250px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -9,7 +9,6 @@ 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 '../../services/enterprise.service';
@Component({ @Component({
selector: 'app-about', selector: 'app-about',
@@ -34,7 +33,6 @@ export class AboutComponent implements OnInit {
private websocketService: WebsocketService, private websocketService: WebsocketService,
private seoService: SeoService, private seoService: SeoService,
public stateService: StateService, public stateService: StateService,
private enterpriseService: EnterpriseService,
private apiService: ApiService, private apiService: ApiService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -45,17 +43,11 @@ export class AboutComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.backendInfo$ = this.stateService.backendInfo$; this.backendInfo$ = this.stateService.backendInfo$;
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`); this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project®\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe( this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
tap((profiles: any) => { tap(() => {
const scrollToSponsors = this.route.snapshot.fragment === 'community-sponsors'; this.goToAnchor()
if (scrollToSponsors && !profiles?.whales?.length && !profiles?.chads?.length) {
return;
} else {
this.goToAnchor(scrollToSponsors)
}
}), }),
share(), share(),
) )
@@ -90,19 +82,11 @@ export class AboutComponent implements OnInit {
this.goToAnchor(); this.goToAnchor();
} }
goToAnchor(scrollToSponsor = false) { goToAnchor() {
if (!scrollToSponsor) {
return;
}
setTimeout(() => { setTimeout(() => {
if (this.route.snapshot.fragment) { if (this.route.snapshot.fragment) {
const el = scrollToSponsor ? this.document.getElementById('community-sponsors-anchor') : this.document.getElementById(this.route.snapshot.fragment); if (this.document.getElementById(this.route.snapshot.fragment)) {
if (el) { this.document.getElementById(this.route.snapshot.fragment).scrollIntoView({behavior: 'smooth'});
if (scrollToSponsor) {
el.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'});
} else {
el.scrollIntoView({behavior: 'smooth'});
}
} }
} }
}, 1); }, 1);
@@ -123,14 +107,4 @@ export class AboutComponent implements OnInit {
unmutePromoVideo(): void { unmutePromoVideo(): void {
this.promoVideo.nativeElement.muted = false; this.promoVideo.nativeElement.muted = false;
} }
onSponsorClick(e): boolean {
this.enterpriseService.goal(5);
return true;
}
onEnterpriseClick(e): boolean {
this.enterpriseService.goal(6);
return true;
}
} }

View File

@@ -1,45 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
import { AboutSponsorsComponent } from './about-sponsors.component';
import { SharedModule } from '../../shared/shared.module';
const routes: Routes = [
{
path: '',
component: AboutComponent,
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [
RouterModule
]
})
export class AboutRoutingModule { }
@NgModule({
imports: [
CommonModule,
AboutRoutingModule,
SharedModule,
],
declarations: [
AboutComponent,
AboutSponsorsComponent,
],
exports: [
AboutSponsorsComponent,
]
})
export class AboutModule { }

View File

@@ -1,21 +0,0 @@
<div class="fee-graph" *ngIf="tx && estimate">
<div class="column">
<ng-container *ngFor="let bar of bars">
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
<div class="fill"></div>
<div class="line">
<p class="fee-rate">
<span class="label">{{ bar.label }}</span>
<span class="rate">
<app-fee-rate [fee]="bar.rate"></app-fee-rate>
</span>
</p>
</div>
<div class="spacer"></div>
<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>
</ng-container>
</div>
</div>

View File

@@ -1,157 +0,0 @@
.fee-graph {
height: 100%;
min-width: 120px;
width: 120px;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;
.column {
width: 100%;
height: 100%;
position: relative;
background: #181b2d;
.bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
min-height: 30px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.fill {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0.75;
pointer-events: none;
}
.fee {
font-size: 0.9em;
opacity: 0;
pointer-events: none;
}
.spacer {
width: 100%;
height: 1px;
flex-grow: 1;
pointer-events: none;
}
.line {
position: absolute;
right: 0;
top: 0;
left: -4.5em;
border-top: dashed white 1.5px;
.fee-rate {
width: 100%;
position: absolute;
left: 0;
right: 0.2em;
font-size: 0.8em;
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
margin: 0;
.label {
margin-right: .2em;
}
.rate .symbol {
color: white;
}
}
}
&.tx {
.fill {
background: #3bcc49;
}
.line {
.fee-rate {
top: 0;
}
}
.fee {
position: absolute;
opacity: 1;
z-index: 11;
}
}
&.target {
.fill {
background: #653b9c;
}
.fee {
position: absolute;
opacity: 1;
z-index: 11;
}
.line .fee-rate {
bottom: 2px;
}
}
&.max {
cursor: pointer;
.line .fee-rate {
.label {
opacity: 0;
}
bottom: 2px;
}
&.active, &:hover {
.fill {
background: #105fb0;
}
.line {
.fee-rate .label {
opacity: 1;
}
}
}
}
&:hover {
.fill {
z-index: 10;
}
.line {
z-index: 11;
}
.fee {
opacity: 1;
z-index: 12;
}
}
}
&:hover > .bar:not(:hover) {
&.target, &.max {
.fee {
opacity: 0;
}
.line .fee-rate .label {
opacity: 0;
}
}
&.max {
.fill {
background: none;
}
}
}
}
}

View File

@@ -1,98 +0,0 @@
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
interface GraphBar {
rate: number;
style: any;
class: 'tx' | 'target' | 'max';
label: string;
active?: boolean;
rateIndex?: number;
fee?: number;
}
@Component({
selector: 'app-accelerate-fee-graph',
templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'],
})
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() tx: Transaction;
@Input() estimate: AccelerationEstimate;
@Input() maxRateOptions: RateOption[] = [];
@Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 };
ngOnInit(): void {
this.initGraph();
}
ngOnChanges(): void {
this.initGraph();
}
initGraph(): void {
if (!this.tx || !this.estimate) {
return;
}
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
const baseHeight = baseRate / maxRate;
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
return {
rate: option.rate,
style: this.getStyle(option.rate, maxRate, baseHeight),
class: 'max',
label: 'maximum',
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
}
});
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
bars.push({
rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
class: 'target',
label: 'next block',
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
}
bars.push({
rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0),
class: 'tx',
label: '',
fee: this.estimate.txSummary.effectiveFee,
});
this.bars = bars;
}
getStyle(rate, maxRate, base) {
const top = (rate / maxRate);
return {
height: `${(top - base) * 100}%`,
bottom: base ? `${base * 100}%` : '0',
}
}
onClick(event, bar): void {
if (bar.rateIndex != null) {
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
}
}

View File

@@ -1,246 +0,0 @@
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="showSuccess">
<div class="col">
<div class="alert alert-success">
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
</div>
</div>
</div>
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="error">
<div class="col">
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error>
</div>
</div>
<div class="accelerate-cols">
<ng-container *ngIf="!isMobile">
<app-accelerate-fee-graph
[tx]="tx"
[estimate]="estimate"
[maxRateOptions]="maxRateOptions"
[maxRateIndex]="selectFeeRateIndex"
(setUserBid)="setUserBid($event)"
></app-accelerate-fee-graph>
</ng-container>
<ng-container *ngIf="estimate">
<div [class]="{estimateDisabled: error}">
<div *ngIf="user && !estimate.hasAccess">
<div class="alert alert-mempool">You are currently on the waitlist</div>
</div>
<h5>Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor{{ estimate.txSummary.ancestorCount > 2 ? 's' : ''}}.
</small>
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<tr class="group-first">
<td class="item">
Virtual size
</td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info" colspan=3>
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
<tr>
<td class="item">
In-band fees
</td>
<td style="text-align: end;">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
<h5>How much more are you willing to pay?</h5>
<div class="row">
<div class="col">
<small class="form-text text-muted mb-2">
Choose the maximum extra transaction fee you're willing to pay to get into the next block.<br>
If the estimated next block rate rises beyond this limit, we will automatically cancel your acceleration request.
</small>
<div class="form-group">
<div class="fee-card">
<div class="d-flex mb-0">
<ng-container *ngFor="let option of maxRateOptions">
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
</button>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<h5>Acceleration summary</h5>
<div class="row mb-3">
<div class="col">
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<!-- ESTIMATED FEE -->
<ng-container>
<tr class="group-first">
<td class="item">
Next block market rate
</td>
<td class="amt" style="font-size: 16px">
{{ estimate.targetFeeRate | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small>Estimated extra fee required</small></i>
</td>
<td class="amt">
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item">
Mempool Accelerator™ fees
</td>
</tr>
<tr class="info">
<td class="info">
<i><small>mempool.space fee</small></i>
</td>
<td class="amt">
+{{ estimate.mempoolBaseFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<i><small>Transaction vsize fee</small></i>
</td>
<td class="amt">
+{{ estimate.vsizeFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container>
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #5E35B1" class="p-1 pl-0">
{{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info" colspan=3>
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td>
</tr>
</ng-container>
<!-- MAX COST -->
<ng-container>
<tr class="group-first">
<td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #105fb0" class="p-1 pl-0">
{{ maxCost | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td>
</tr>
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item">
Available balance
</td>
<td class="amt">
{{ estimate.userBalance | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
<!-- LOGIN CTA -->
<ng-container *ngIf="!isLoggedIn()">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
<div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col">
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>

View File

@@ -1,112 +0,0 @@
.fee-card {
padding: 15px;
background-color: #1d1f31;
.feerate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.rate {
font-size: 0.9em;
.symbol {
color: white;
}
}
}
}
.btn-border {
border: solid 1px black;
background-color: #0c4a87;
}
.feerate.active {
background-color: #105fb0 !important;
opacity: 1;
border: 1px solid #007fff !important;
}
.feerate:focus {
box-shadow: none !important;
}
.estimateDisabled {
opacity: 0.5;
pointer-events: none;
}
.table-toggle {
width: 100%;
margin-top: 0.5em;
}
.tab {
&:first-child {
margin-right: 1px;
}
border: solid 1px black;
border-bottom: none;
background-color: #323655;
border-top-left-radius: 10px !important;
border-top-right-radius: 10px !important;
}
.tab.active {
background-color: #5d659d !important;
opacity: 1;
}
.tab:focus {
box-shadow: none !important;
}
.table-accelerator {
tr {
td {
padding-top: 0;
padding-bottom: 0;
vertical-align: baseline;
}
&.group-first {
td {
padding-top: 0.75rem;
}
}
&.group-last {
td {
padding-bottom: 0.75rem;
}
}
}
td {
&:first-child {
width: 100vw;
}
&.info {
color: #6c757d;
white-space: initial;
}
&.amt {
text-align: right;
padding-right: 0.2em;
}
&.units {
padding-left: 0.2em;
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
.accelerate-cols {
display: flex;
flex-direction: row;
align-items: stretch;
margin-top: 1em;
}
.item {
white-space: initial;
}

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