Compare commits

..

2 Commits

Author SHA1 Message Date
Mononaut
6bb9ffd21a Extend esplora api to support returning origin, improve stats pause logic 2023-09-14 22:44:24 +00:00
Mononaut
9a8e5b7896 Avoid logging statistics while affected by esplora failover 2023-09-14 18:33:11 +00:00
284 changed files with 68262 additions and 106883 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"
@@ -67,7 +67,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

@@ -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

@@ -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

@@ -1,11 +1,10 @@
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>; $getRawMempool(): Promise<{ txids: IEsploraApi.Transaction['txid'][], local: boolean}>;
$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,10 +23,9 @@ 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;
isFailedOver(): boolean;
} }
export interface BitcoinRpcCredentials { export interface BitcoinRpcCredentials {
host: string; host: string;
@@ -35,5 +33,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.');
} }
@@ -150,8 +137,12 @@ class BitcoinApi implements AbstractBitcoinApi {
throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.'); throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.');
} }
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> { async $getRawMempool(): Promise<{ txids: IEsploraApi.Transaction['txid'][], local: boolean}> {
return this.bitcoindClient.getRawMemPool(); const txids = await this.bitcoindClient.getRawMemPool();
return {
txids,
local: true,
};
} }
$getAddressPrefix(prefix: string): string[] { $getAddressPrefix(prefix: string): string[] {
@@ -211,19 +202,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);
@@ -382,6 +360,9 @@ class BitcoinApi implements AbstractBitcoinApi {
} }
public startHealthChecks(): void {}; public startHealthChecks(): void {};
public isFailedOver(): boolean {
return false;
}
} }
export default BitcoinApi; export default BitcoinApi;

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)) {
@@ -638,8 +638,8 @@ class BitcoinRoutes {
private async getMempoolTxIds(req: Request, res: Response) { private async getMempoolTxIds(req: Request, res: Response) {
try { try {
const rawMempool = await bitcoinApi.$getRawMempool(); const { txids } = await bitcoinApi.$getRawMempool();
res.send(rawMempool); res.send(txids);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }

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

@@ -4,13 +4,13 @@ import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger'; import logger from '../../logger';
import mempool from '../mempool';
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,
@@ -18,6 +18,8 @@ interface FailoverHost {
} }
class FailoverRouter { class FailoverRouter {
isFailedOver: boolean = false;
preferredHost: FailoverHost;
activeHost: FailoverHost; activeHost: FailoverHost;
fallbackHost: FailoverHost; fallbackHost: FailoverHost;
hosts: FailoverHost[]; hosts: FailoverHost[];
@@ -47,6 +49,7 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH, socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true, preferred: true,
}; };
this.preferredHost = this.activeHost;
this.fallbackHost = this.activeHost; this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost); this.hosts.unshift(this.activeHost);
this.multihost = this.hosts.length > 1; this.multihost = this.hosts.length > 1;
@@ -76,9 +79,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: 5000 });
} 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: 5000 });
} }
})); }));
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 +96,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 +103,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 +126,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
@@ -159,12 +155,13 @@ class FailoverRouter {
this.sortHosts(); this.sortHosts();
this.activeHost = this.hosts[0]; this.activeHost = this.hosts[0];
logger.warn(`Switching esplora host to ${this.activeHost.host}`); logger.warn(`Switching esplora host to ${this.activeHost.host}`);
this.isFailedOver = this.activeHost !== this.preferredHost;
} }
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 {
@@ -172,69 +169,78 @@ class FailoverRouter {
} }
} }
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> { private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true, withSource = false): Promise<T | { data: T, host: FailoverHost }> {
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)
).then((response) => { host.failures = Math.max(0, host.failures - 1); return response.data; }) ).then((response) => {
.catch((e) => { host.failures = Math.max(0, host.failures - 1);
if (withSource) {
return {
data: response.data,
host,
};
} else {
return response.data;
}
}).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) {
// Retry immediately // Retry immediately
return this.$query(method, path, data, responseType, fallbackHost, false); return this.$query(method, path, data, responseType, fallbackHost, false, withSource);
} else { } else {
throw e; throw e;
} }
}); });
} }
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, this.activeHost, true) as Promise<T>;
} }
public async $post<T>(path, data: any, responseType = 'json'): Promise<T> { public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
return this.$query<T>('post', path, data, responseType); return this.$query<T>('post', path, data, responseType) as Promise<T>;
}
public async $getWithSource<T>(path, responseType = 'json'): Promise<{ data: T, host: FailoverHost }> {
return this.$query<T>('get', path, null, responseType, this.activeHost, true, true) as Promise<{ data: T, host: FailoverHost }>;
} }
} }
class ElectrsApi implements AbstractBitcoinApi { class ElectrsApi implements AbstractBitcoinApi {
private failoverRouter = new FailoverRouter(); private failoverRouter = new FailoverRouter();
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> { async $getRawMempool(): Promise<{ txids: IEsploraApi.Transaction['txid'][], local: boolean}> {
return this.failoverRouter.$get<IEsploraApi.Transaction['txid'][]>('/mempool/txids'); const result = await this.failoverRouter.$getWithSource<IEsploraApi.Transaction['txid'][]>('/mempool/txids', 'json');
return {
txids: result.data,
local: result.host === this.failoverRouter.preferredHost,
};
} }
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> { $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
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 +260,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,21 +312,22 @@ 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 {
this.failoverRouter.startHealthChecks(); this.failoverRouter.startHealthChecks();
} }
public isFailedOver(): boolean {
return this.failoverRouter.isFailedOver;
}
} }
export default ElectrsApi; export default ElectrsApi;

View File

@@ -81,7 +81,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 +101,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++;
@@ -615,7 +608,7 @@ class Blocks {
} }
const blockHash = await bitcoinApi.$getBlockHash(blockHeight); const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
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, null, true); const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
const blockExtended = await this.$getBlockExtended(block, transactions); const blockExtended = await this.$getBlockExtended(block, transactions);
newlyIndexed++; newlyIndexed++;
@@ -708,7 +701,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 +754,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 +764,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 +890,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 +902,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

@@ -9,7 +9,7 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
import { Acceleration } from './services/acceleration'; import accelerationApi, { Acceleration } from './services/acceleration';
import redisCache from './redis-cache'; import redisCache from './redis-cache';
class Mempool { class Mempool {
@@ -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[],
@@ -26,6 +26,9 @@ class Mempool {
private accelerations: { [txId: string]: Acceleration } = {}; private accelerations: { [txId: string]: Acceleration } = {};
private failoverTimes: number[] = [];
private statisticsPaused: boolean = false;
private txPerSecondArray: number[] = []; private txPerSecondArray: number[] = [];
private txPerSecond: number = 0; private txPerSecond: number = 0;
@@ -94,7 +97,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 +129,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);
@@ -164,6 +167,15 @@ class Mempool {
return this.mempoolInfo; return this.mempoolInfo;
} }
public getStatisticsIsPaused(): boolean {
return this.statisticsPaused;
}
public logFailover(): void {
this.failoverTimes.push(Date.now());
this.statisticsPaused = true;
}
public getTxPerSecond(): number { public getTxPerSecond(): number {
return this.txPerSecond; return this.txPerSecond;
} }
@@ -185,7 +197,7 @@ class Mempool {
return txTimes; return txTimes;
} }
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, pollRate: number): Promise<void> { public async $updateMempool(transactions: string[], pollRate: number): Promise<void> {
logger.debug(`Updating mempool...`); logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes // warn if this run stalls the main loop for more than 2 minutes
@@ -235,13 +247,17 @@ 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);
logger.debug(`fetched ${txs.length} transactions`); logger.debug(`fetched ${txs.length} transactions`);
this.updateTimerProgress(timer, 'fetched new transactions'); this.updateTimerProgress(timer, 'fetched new transactions');
if (bitcoinApi.isFailedOver()) {
this.failoverTimes.push(Date.now());
}
for (const transaction of txs) { for (const transaction of txs) {
this.mempoolCache[transaction.txid] = transaction; this.mempoolCache[transaction.txid] = transaction;
if (this.inSync) { if (this.inSync) {
@@ -259,6 +275,10 @@ class Mempool {
} }
} }
if (bitcoinApi.isFailedOver()) {
this.failoverTimes.push(Date.now());
}
if (txs.length < slice.length) { if (txs.length < slice.length) {
const missing = slice.length - txs.length; const missing = slice.length - txs.length;
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
@@ -330,7 +350,7 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : []; const accelerationDelta = await this.$updateAccelerations();
if (accelerationDelta.length) { if (accelerationDelta.length) {
hasChange = true; hasChange = true;
} }
@@ -370,12 +390,14 @@ class Mempool {
return this.accelerations; return this.accelerations;
} }
public $updateAccelerations(newAccelerations: Acceleration[]): string[] { public async $updateAccelerations(): Promise<string[]> {
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
return []; return [];
} }
try { try {
const newAccelerations = await accelerationApi.$fetchAccelerations();
const changed: string[] = []; const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {}; const newAccelerationMap: { [txid: string]: Acceleration } = {};
@@ -489,6 +511,10 @@ class Mempool {
private updateTxPerSecond() { private updateTxPerSecond() {
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD); const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
this.failoverTimes = this.failoverTimes.filter((unixTime) => unixTime > nowMinusTimeSpan);
this.statisticsPaused = this.failoverTimes.length > 0;
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan); this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
this.txPerSecond = this.txPerSecondArray.length / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD || 0; this.txPerSecond = this.txPerSecondArray.length / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD || 0;

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,9 +53,6 @@ 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;
private staleCount = 0;
constructor() { 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

@@ -122,9 +122,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 +219,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

@@ -1,7 +1,6 @@
import { query } from '../../utils/axios-query';
import config from '../../config'; import config from '../../config';
import logger from '../../logger';
import { BlockExtended, PoolTag } from '../../mempool.interfaces'; import { BlockExtended, PoolTag } from '../../mempool.interfaces';
import axios from 'axios';
export interface Acceleration { export interface Acceleration {
txid: string, txid: string,
@@ -10,15 +9,10 @@ export interface Acceleration {
} }
class AccelerationApi { class AccelerationApi {
public async $fetchAccelerations(): Promise<Acceleration[] | null> { public async $fetchAccelerations(): Promise<Acceleration[]> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) { if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
try { const response = await query(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`);
const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 }); return (response as Acceleration[]) || [];
return response.data as Acceleration[];
} catch (e) {
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
return null;
}
} else { } else {
return []; return [];
} }

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

@@ -29,9 +29,10 @@ class Statistics {
} }
private async runStatistics(): Promise<void> { private async runStatistics(): Promise<void> {
if (!memPool.isInSync()) { if (!memPool.isInSync() || memPool.getStatisticsIsPaused()) {
return; return;
} }
const currentMempool = memPool.getMempool(); const currentMempool = memPool.getMempool();
const txPerSecond = memPool.getTxPerSecond(); const txPerSecond = memPool.getTxPerSecond();
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
@@ -89,9 +90,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 +99,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

@@ -116,7 +116,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 +155,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 +189,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

@@ -71,9 +71,8 @@ class WebsocketHandler {
private updateSocketData(): void { private updateSocketData(): void {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT); const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
this.updateSocketDataFields({ const socketData = {
'mempoolInfo': memPool.getMempoolInfo(), 'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks, 'blocks': _blocks,
'conversions': priceUpdater.getLatestPrices(), 'conversions': priceUpdater.getLatestPrices(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(), 'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
@@ -82,7 +81,11 @@ class WebsocketHandler {
'loadingIndicators': loadingIndicators.getLoadingIndicators(), 'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined, 'da': da?.previousTime ? da : undefined,
'fees': feeApi.getRecommendedFee(), 'fees': feeApi.getRecommendedFee(),
}); };
if (!memPool.getStatisticsIsPaused()) {
socketData['vBytesPerSecond'] = memPool.getVBytesPerSecond();
}
this.updateSocketDataFields(socketData);
} }
public getSerializedInitData(): string { public getSerializedInitData(): string {
@@ -94,13 +97,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 +285,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();
} }
}); });
}); });
@@ -419,7 +417,7 @@ class WebsocketHandler {
const mBlocks = mempoolBlocks.getMempoolBlocks(); const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo(); const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getStatisticsIsPaused() ? null : memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
@@ -445,13 +443,15 @@ class WebsocketHandler {
// update init data // update init data
const socketDataFields = { const socketDataFields = {
'mempoolInfo': mempoolInfo, 'mempoolInfo': mempoolInfo,
'vBytesPerSecond': vBytesPerSecond,
'mempool-blocks': mBlocks, 'mempool-blocks': mBlocks,
'transactions': latestTransactions, 'transactions': latestTransactions,
'loadingIndicators': loadingIndicators.getLoadingIndicators(), 'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined, 'da': da?.previousTime ? da : undefined,
'fees': recommendedFees, 'fees': recommendedFees,
}; };
if (vBytesPerSecond != null) {
socketDataFields['vBytesPerSecond'] = vBytesPerSecond;
}
if (rbfSummary) { if (rbfSummary) {
socketDataFields['rbfSummary'] = rbfSummary; socketDataFields['rbfSummary'] = rbfSummary;
} }
@@ -491,7 +491,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) {
@@ -502,7 +501,9 @@ class WebsocketHandler {
if (client['want-stats']) { if (client['want-stats']) {
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo); response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', vBytesPerSecond); if (vBytesPerSecond != null) {
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', vBytesPerSecond);
}
response['transactions'] = getCachedResponse('transactions', latestTransactions); response['transactions'] = getCachedResponse('transactions', latestTransactions);
if (da?.previousTime) { if (da?.previousTime) {
response['da'] = getCachedResponse('da', da); response['da'] = getCachedResponse('da', da);
@@ -532,15 +533,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 +579,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,
@@ -794,7 +791,9 @@ class WebsocketHandler {
if (client['want-stats']) { if (client['want-stats']) {
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo); response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond()); if (!memPool.getStatisticsIsPaused()) {
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond());
}
response['fees'] = getCachedResponse('fees', fees); response['fees'] = getCachedResponse('fees', fees);
if (da?.previousTime) { if (da?.previousTime) {

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

@@ -43,7 +43,6 @@ 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 accelerationApi from './api/services/acceleration';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -92,24 +91,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
@@ -205,12 +191,15 @@ class Server {
logger.debug(msg); logger.debug(msg);
} }
} }
const newMempool = await bitcoinApi.$getRawMempool();
const newAccelerations = await accelerationApi.$fetchAccelerations(); const { txids: newMempool, local: fromLocalNode } = 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, newAccelerations, pollRate); if (!fromLocalNode) {
memPool.logFailover();
}
await memPool.$updateMempool(newMempool, pollRate);
} }
indexer.$run(); indexer.$run();
priceUpdater.$run(); priceUpdater.$run();
@@ -321,19 +310,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

@@ -300,7 +300,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 +346,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,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

@@ -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

@@ -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)));
}), }),

View File

@@ -30,7 +30,7 @@ export class BisqDashboardComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`); this.seoService.setTitle($localize`:@@meta.title.bisq.markets: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.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open 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

@@ -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

@@ -10,7 +10,7 @@
</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 +32,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 +186,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 +205,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>
@@ -295,9 +299,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

@@ -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,12 @@ 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.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 +83,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 +108,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

@@ -2,6 +2,7 @@
height: 100%; height: 100%;
min-width: 120px; min-width: 120px;
width: 120px; width: 120px;
max-height: 90vh;
margin-left: 4em; margin-left: 4em;
margin-right: 1.5em; margin-right: 1.5em;
padding-bottom: 63px; padding-bottom: 63px;
@@ -17,7 +18,6 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
min-height: 30px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;

View File

@@ -58,15 +58,13 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
fee: option.fee, fee: option.fee,
} }
}); });
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) { bars.push({
bars.push({ rate: this.estimate.targetFeeRate,
rate: this.estimate.targetFeeRate, style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight), class: 'target',
class: 'target', label: 'next block',
label: 'next block', fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee });
});
}
bars.push({ bars.push({
rate: baseRate, rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0), style: this.getStyle(baseRate, maxRate, 0),

View File

@@ -1,16 +1,14 @@
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="showSuccess"> <div class="row" *ngIf="showSuccess">
<div class="col"> <div class="col" id="successAlert">
<div class="alert alert-success"> <div class="alert alert-success">
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration. Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>.
</div> </div>
</div> </div>
</div> </div>
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="error"> <div class="row" *ngIf="error">
<div class="col"> <div class="col" id="mempoolError">
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error> <app-mempool-error [error]="error"></app-mempool-error>
</div> </div>
</div> </div>
@@ -27,11 +25,6 @@
<ng-container *ngIf="estimate"> <ng-container *ngIf="estimate">
<div [class]="{estimateDisabled: error}"> <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> <h5>Your transaction</h5>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@@ -44,10 +37,10 @@
<td class="item"> <td class="item">
Virtual size Virtual size
</td> </td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td> <td class="units" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr> </tr>
<tr class="info"> <tr class="info">
<td class="info" colspan=3> <td class="info">
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i> <i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td> </td>
</tr> </tr>
@@ -55,12 +48,12 @@
<td class="item"> <td class="item">
In-band fees In-band fees
</td> </td>
<td style="text-align: end;"> <td class="units">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span> {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span>
</td> </td>
</tr> </tr>
<tr class="info group-last"> <tr class="info group-last">
<td class="info" colspan=3> <td class="info">
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i> <i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td> </td>
</tr> </tr>
@@ -81,8 +74,8 @@
<div class="d-flex mb-0"> <div class="d-flex mb-0">
<ng-container *ngFor="let option of maxRateOptions"> <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)"> <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="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span>
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span> <span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
</button> </button>
</ng-container> </ng-container>
</div> </div>
@@ -94,15 +87,23 @@
<h5>Acceleration summary</h5> <h5>Acceleration summary</h5>
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">
<div class="table-toggle btn-group btn-group-toggle">
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'">
<span>Estimated cost</span>
</div>
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'">
<span>Maximum cost</span>
</div>
</div>
<table class="table table-borderless table-border table-dark table-accelerator"> <table class="table table-borderless table-border table-dark table-accelerator">
<tbody> <tbody>
<!-- ESTIMATED FEE --> <!-- ESTIMATED FEE -->
<ng-container> <ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first"> <tr class="group-first">
<td class="item"> <td class="item">
Next block market rate Next block market rate
</td> </td>
<td class="amt" style="font-size: 16px"> <td class="amt" style="font-size: 20px">
{{ estimate.targetFeeRate | number : '1.0-0' }} {{ estimate.targetFeeRate | number : '1.0-0' }}
</td> </td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td> <td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
@@ -115,8 +116,34 @@
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span> <span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- USER MAX BID -->
<ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first">
<td class="item">
Your maximum
</td>
<td class="amt" style="width: 45%; font-size: 20px">
~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | 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>The maximum extra transaction fee you could pay</small></i>
</td>
<td class="amt">
<span>
{{ userBid | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="userBid"></app-fiat></span>
</td> </td>
</tr> </tr>
</ng-container> </ng-container>
@@ -135,11 +162,11 @@
+{{ estimate.mempoolBaseFee | number }} +{{ estimate.mempoolBaseFee | number }}
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span> <span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td> </td>
</tr> </tr>
<tr class="info group-last"> <tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info"> <td class="info">
<i><small>Transaction vsize fee</small></i> <i><small>Transaction vsize fee</small></i>
</td> </td>
@@ -147,14 +174,14 @@
+{{ estimate.vsizeFee | number }} +{{ estimate.vsizeFee | number }}
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span> <span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td> </td>
</tr> </tr>
<!-- NEXT BLOCK ESTIMATE --> <!-- NEXT BLOCK ESTIMATE -->
<ng-container> <ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;"> <tr class="group-first">
<td class="item"> <td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b> <b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td> </td>
@@ -164,19 +191,19 @@
</span> </span>
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span> <span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td> </td>
</tr> </tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey"> <tr class="info group-last">
<td class="info" colspan=3> <td class="info">
<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> <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> </td>
</tr> </tr>
</ng-container> </ng-container>
<!-- MAX COST --> <!-- MAX COST -->
<ng-container> <ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first"> <tr class="group-first">
<td class="item"> <td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b> <b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
@@ -187,21 +214,21 @@
</span> </span>
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"> <span class="fiat">
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> <app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span> </span>
</td> </td>
</tr> </tr>
<tr class="info group-last"> <tr class="info group-last">
<td class="info" colspan=3> <td class="info">
<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> <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> </td>
</tr> </tr>
</ng-container> </ng-container>
<!-- USER BALANCE --> <!-- USER BALANCE -->
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost"> <ng-container *ngIf="estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey"> <tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"> <td class="item">
Available balance Available balance
@@ -210,24 +237,13 @@
{{ estimate.userBalance | number }} {{ estimate.userBalance | number }}
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"> <span class="fiat">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> <app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span> </span>
</td> </td>
</tr> </tr>
</ng-container> </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> </tbody>
</table> </table>
</div> </div>
@@ -235,7 +251,7 @@
<div class="row mb-3" *ngIf="isLoggedIn()"> <div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col"> <div class="col">
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess"> <div class="d-flex justify-content-end">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button> <button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,9 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.fee {
font-size: 1.2em;
}
.rate { .rate {
font-size: 0.9em; font-size: 0.9em;
.symbol { .symbol {
@@ -25,10 +28,7 @@
.feerate.active { .feerate.active {
background-color: #105fb0 !important; background-color: #105fb0 !important;
opacity: 1; opacity: 1;
border: 1px solid #007fff !important; border: 1px solid white !important;
}
.feerate:focus {
box-shadow: none !important;
} }
.estimateDisabled { .estimateDisabled {
@@ -41,26 +41,10 @@
margin-top: 0.5em; 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 { .table-accelerator {
tr { tr {
text-wrap: wrap;
td { td {
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
@@ -84,7 +68,6 @@
} }
&.info { &.info {
color: #6c757d; color: #6c757d;
white-space: initial;
} }
&.amt { &.amt {
text-align: right; text-align: right;
@@ -93,9 +76,6 @@
&.units { &.units {
padding-left: 0.2em; padding-left: 0.2em;
white-space: nowrap; white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
} }
} }
} }
@@ -105,8 +85,4 @@
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
margin-top: 1em; margin-top: 1em;
}
.item {
white-space: initial;
} }

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs'; import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
@@ -55,15 +55,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
maxCost = 0; maxCost = 0;
userBid = 0; userBid = 0;
selectFeeRateIndex = 1; selectFeeRateIndex = 1;
showTable: 'estimated' | 'maximum' = 'maximum';
isMobile: boolean = window.innerWidth <= 767.98; isMobile: boolean = window.innerWidth <= 767.98;
user: any = undefined;
maxRateOptions: RateOption[] = []; maxRateOptions: RateOption[] = [];
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private storageService: StorageService, private storageService: StorageService
private cd: ChangeDetectorRef
) { } ) { }
ngOnDestroy(): void { ngOnDestroy(): void {
@@ -74,13 +73,11 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) { if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'start'); this.scrollToPreview('acceleratePreviewAnchor', 'center');
} }
} }
ngOnInit() { ngOnInit() {
this.user = this.storageService.getAuth()?.user ?? null;
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe( this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
tap((response) => { tap((response) => {
if (response.status === 204) { if (response.status === 204) {
@@ -96,7 +93,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
} }
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) { if (this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) { if (this.isLoggedIn()) {
this.error = `not_enough_balance`; this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.scrollToPreviewWithTimeout('mempoolError', 'center');
@@ -129,7 +126,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) { if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'start'); this.scrollToPreview('acceleratePreviewAnchor', 'center');
} }
} }
}), }),
@@ -165,14 +162,13 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
scrollToPreview(id: string, position: ScrollLogicalPosition) { scrollToPreview(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id); const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) { if (acceleratePreviewAnchor) {
this.cd.markForCheck();
acceleratePreviewAnchor.scrollIntoView({ acceleratePreviewAnchor.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
inline: position, inline: position,
block: position, block: position,
}); });
} }
} }
/** /**
* Send acceleration request * Send acceleration request
@@ -191,11 +187,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
}, },
error: (response) => { error: (response) => {
if (response.status === 403 && response.error === 'not_available') { this.error = response.error;
this.error = 'waitlisted';
} else {
this.error = response.error;
}
this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.scrollToPreviewWithTimeout('mempoolError', 'center');
} }
}); });

View File

@@ -69,7 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.addressString = this.addressString.toLowerCase(); this.addressString = this.addressString.toLowerCase();
} }
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'} ${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`); this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/) return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString) ? this.electrsApiService.getPubKeyAddress$(this.addressString)

View File

@@ -174,11 +174,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.addTransaction(tx); this.addTransaction(tx);
}); });
this.stateService.mempoolRemovedTransactions$
.subscribe(tx => {
this.removeTransaction(tx);
});
this.stateService.blockTransactions$ this.stateService.blockTransactions$
.subscribe((transaction) => { .subscribe((transaction) => {
const tx = this.transactions.find((t) => t.txid === transaction.txid); const tx = this.transactions.find((t) => t.txid === transaction.txid);
@@ -227,30 +222,6 @@ export class AddressComponent implements OnInit, OnDestroy {
return true; return true;
} }
removeTransaction(transaction: Transaction): boolean {
const index = this.transactions.findIndex(((tx) => tx.txid === transaction.txid));
if (index === -1) {
return false;
}
this.transactions.splice(index, 1);
this.transactions = this.transactions.slice();
this.txCount--;
transaction.vin.forEach((vin) => {
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
this.sent -= vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout?.scriptpubkey_address === this.address.address) {
this.received -= vout.value;
}
});
return true;
}
loadMore() { loadMore() {
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) { if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
return; return;

View File

@@ -1,4 +1,4 @@
<header class="sticky-header"> <header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark"> <nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;"> <a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

View File

@@ -1,11 +1,3 @@
.sticky-header {
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
z-index: 100;
}
li.nav-item.active { li.nav-item.active {
background-color: #653b9c; background-color: #653b9c;
} }

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from '../../graphs/echarts'; import { EChartsOption } from 'echarts';
import { Observable, Subscription, combineLatest } from 'rxjs'; import { Observable, Subscription, combineLatest } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts'; import { EChartsOption, graphic } from 'echarts';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
@@ -123,11 +123,11 @@ export class BlockFeesGraphComponent implements OnInit {
this.chartOptions = { this.chartOptions = {
title: title, title: title,
color: [ color: [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [ new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FDD835' }, { offset: 0, color: '#FDD835' },
{ offset: 1, color: '#FB8C00' }, { offset: 1, color: '#FB8C00' },
]), ]),
new echarts.graphic.LinearGradient(0, 0, 0, 1, [ new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#C0CA33' }, { offset: 0, color: '#C0CA33' },
{ offset: 1, color: '#1B5E20' }, { offset: 1, color: '#1B5E20' },
]), ]),

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from '../../graphs/echarts'; import { EChartsOption } from 'echarts';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';

View File

@@ -20,8 +20,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() blockLimit: number; @Input() blockLimit: number;
@Input() orientation = 'left'; @Input() orientation = 'left';
@Input() flip = true; @Input() flip = true;
@Input() animationDuration: number = 1000;
@Input() animationOffset: number | null = null;
@Input() disableSpinner = false; @Input() disableSpinner = false;
@Input() mirrorTxid: string | void; @Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false; @Input() unavailable: boolean = false;
@@ -143,9 +141,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void { replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
if (this.scene) { if (this.scene) {
this.scene.replace(transactions || [], direction, sort, startTime); this.scene.replace(transactions || [], direction, sort);
this.start(); this.start();
this.updateSearchHighlight(); this.updateSearchHighlight();
} }
@@ -228,7 +226,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else { } else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset }); highlighting: this.auditHighlighting });
this.start(); this.start();
} }
} }

View File

@@ -9,9 +9,6 @@ export default class BlockScene {
txs: { [key: string]: TxView }; txs: { [key: string]: TxView };
orientation: string; orientation: string;
flip: boolean; flip: boolean;
animationDuration: number = 1000;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean; highlightingEnabled: boolean;
width: number; width: number;
height: number; height: number;
@@ -26,11 +23,11 @@ export default class BlockScene {
animateUntil = 0; animateUntil = 0;
dirty: boolean; dirty: boolean;
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
) { ) {
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }); this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
} }
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
@@ -39,7 +36,6 @@ export default class BlockScene {
this.gridSize = this.width / this.gridWidth; this.gridSize = this.width / this.gridWidth;
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5)); this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
this.unitWidth = this.gridSize - (this.unitPadding * 2); this.unitWidth = this.gridSize - (this.unitPadding * 2);
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.dirty = true; this.dirty = true;
if (this.initialised && this.scene) { if (this.initialised && this.scene) {
@@ -94,8 +90,8 @@ export default class BlockScene {
} }
// Animate new block entering scene // Animate new block entering scene
enter(txs: TransactionStripped[], direction, startTime?: number) { enter(txs: TransactionStripped[], direction) {
this.replace(txs, direction, false, startTime); this.replace(txs, direction);
} }
// Animate block leaving scene // Animate block leaving scene
@@ -112,7 +108,8 @@ export default class BlockScene {
} }
// Reset layout and replace with new set of transactions // Reset layout and replace with new set of transactions
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void { replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
const startTime = performance.now();
const nextIds = {}; const nextIds = {};
const remove = []; const remove = [];
txs.forEach(tx => { txs.forEach(tx => {
@@ -136,7 +133,7 @@ export default class BlockScene {
removed.forEach(tx => { removed.forEach(tx => {
tx.destroy(); tx.destroy();
}); });
}, (startTime - performance.now()) + this.animationDuration + 1000); }, 1000);
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
@@ -150,7 +147,7 @@ export default class BlockScene {
}); });
} }
this.updateAll(startTime, 50, direction); this.updateAll(startTime, 200, direction);
} }
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
@@ -217,13 +214,10 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
} }
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
): void { ): void {
this.animationDuration = animationDuration || 1000;
this.configAnimationOffset = animationOffset;
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.orientation = orientation; this.orientation = orientation;
this.flip = flip; this.flip = flip;
this.vertexArray = vertexArray; this.vertexArray = vertexArray;
@@ -267,8 +261,8 @@ export default class BlockScene {
this.applyTxUpdate(tx, { this.applyTxUpdate(tx, {
display: { display: {
position: { position: {
x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)), x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)), y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
s: tx.screenPosition.s s: tx.screenPosition.s
}, },
color: txColor, color: txColor,
@@ -281,7 +275,7 @@ export default class BlockScene {
position: tx.screenPosition, position: tx.screenPosition,
color: txColor color: txColor
}, },
duration: animate ? this.animationDuration : 1, duration: animate ? 1000 : 1,
start: startTime, start: startTime,
delay: animate ? delay : 0, delay: animate ? delay : 0,
}); });
@@ -290,8 +284,8 @@ export default class BlockScene {
display: { display: {
position: tx.screenPosition position: tx.screenPosition
}, },
duration: animate ? this.animationDuration : 0, duration: animate ? 1000 : 0,
minDuration: animate ? (this.animationDuration / 2) : 0, minDuration: animate ? 500 : 0,
start: startTime, start: startTime,
delay: animate ? delay : 0, delay: animate ? delay : 0,
adjust: animate adjust: animate
@@ -328,11 +322,11 @@ export default class BlockScene {
this.applyTxUpdate(tx, { this.applyTxUpdate(tx, {
display: { display: {
position: { position: {
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)), x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)), y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
} }
}, },
duration: this.animationDuration, duration: 1000,
start: startTime, start: startTime,
delay: 50 delay: 50
}); });

View File

@@ -55,7 +55,7 @@
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td> <td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td> <td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td> <td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
<td *ngSwitchCase="'accelerated'"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td> <td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
</ng-container> </ng-container>
</tr> </tr>
</tbody> </tbody>

View File

@@ -19,17 +19,4 @@
.td-width { .td-width {
padding-right: 10px; padding-right: 10px;
}
.badge.badge-accelerated {
background-color: #653b9c;
box-shadow: #ad7de57f 0px 0px 12px -2px;
color: white;
animation: acceleratePulse 1s infinite;
}
@keyframes acceleratePulse {
0% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;}
100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
} }

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts'; import { EChartsOption, graphic } from 'echarts';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
@@ -123,11 +123,11 @@ export class BlockRewardsGraphComponent implements OnInit {
title: title, title: title,
animation: false, animation: false,
color: [ color: [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [ new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FDD835' }, { offset: 0, color: '#FDD835' },
{ offset: 1, color: '#FB8C00' }, { offset: 1, color: '#FB8C00' },
]), ]),
new echarts.graphic.LinearGradient(0, 0, 0, 1, [ new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#C0CA33' }, { offset: 0, color: '#C0CA33' },
{ offset: 1, color: '#1B5E20' }, { offset: 1, color: '#1B5E20' },
]), ]),

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption} from '../../graphs/echarts'; import { EChartsOption} from 'echarts';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';

View File

@@ -1,14 +0,0 @@
<div class="block-wrapper">
<div class="block-container">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[disableSpinner]="true"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
</div>
</div>

View File

@@ -1,22 +0,0 @@
.block-wrapper {
width: 100vw;
height: 100vh;
background: #181b2d;
}
.block-container {
flex-grow: 0;
flex-shrink: 0;
width: 100vw;
max-width: 100vh;
height: 100vh;
padding: 0;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
* {
flex-grow: 1;
}
}

View File

@@ -1,180 +0,0 @@
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, catchError, shareReplay, filter } from 'rxjs/operators';
import { of, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
@Component({
selector: 'app-block-view',
templateUrl: './block-view.component.html',
styleUrls: ['./block-view.component.scss']
})
export class BlockViewComponent implements OnInit, OnDestroy {
network = '';
block: BlockExtended;
blockHeight: number;
blockHash: string;
rawId: string;
isLoadingBlock = true;
strippedTransactions: TransactionStripped[];
isLoadingOverview = true;
autofit: boolean = false;
resolution: number = 80;
overviewSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
constructor(
private route: ActivatedRoute,
private router: Router,
private electrsApiService: ElectrsApiService,
public stateService: StateService,
private seoService: SeoService,
private apiService: ApiService
) { }
ngOnInit(): void {
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.autofit = params.autofit === 'true';
if (this.autofit) {
this.onResize();
}
});
const block$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.rawId = params.get('id') || '';
const blockHash: string = params.get('id') || '';
this.block = undefined;
let isBlockHeight = false;
if (/^[0-9]+$/.test(blockHash)) {
isBlockHeight = true;
} else {
this.blockHash = blockHash;
}
this.isLoadingBlock = true;
this.isLoadingOverview = true;
if (isBlockHeight) {
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
.pipe(
switchMap((hash) => {
if (hash) {
this.blockHash = hash;
return this.apiService.getBlock$(hash);
} else {
return null;
}
}),
catchError(() => {
return of(null);
}),
);
}
return this.apiService.getBlock$(blockHash);
}),
filter((block: BlockExtended | void) => block != null),
tap((block: BlockExtended) => {
this.block = block;
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
} else {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
}
this.isLoadingBlock = false;
this.isLoadingOverview = true;
}),
shareReplay(1)
);
this.overviewSubscription = block$.pipe(
switchMap((block) => this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError(() => {
return of([]);
}),
switchMap((transactions) => {
return of(transactions);
})
)
),
)
.subscribe((transactions: TransactionStripped[]) => {
this.strippedTransactions = transactions;
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();
this.blockGraph.setup(this.strippedTransactions);
}
},
() => {
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();
}
});
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (this.autofit) {
this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight));
}
}
ngOnDestroy(): void {
if (this.overviewSubscription) {
this.overviewSubscription.unsubscribe();
}
if (this.networkChangedSubscription) {
this.networkChangedSubscription.unsubscribe();
}
if (this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe();
}
}
}

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