mirror of
https://github.com/mempool/mempool.git
synced 2025-02-23 14:40:38 +01:00
Merge branch 'master' into natsoni/federation-utxos-expiry
This commit is contained in:
commit
e8c5d478fa
63 changed files with 1202 additions and 137 deletions
19
.github/workflows/get_backend_block_height.yml
vendored
Normal file
19
.github/workflows/get_backend_block_height.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
name: 'Check if servers are in sync'
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
print-backend-sha:
|
||||
runs-on: 'ubuntu-latest'
|
||||
name: Get block height
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: repo
|
||||
|
||||
- name: Run script
|
||||
working-directory: repo
|
||||
run: |
|
||||
chmod +x ./scripts/get_block_tip_height.sh
|
||||
sh ./scripts/get_block_tip_height.sh
|
85
Cargo.lock
generated
85
Cargo.lock
generated
|
@ -57,9 +57,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1586fa608b1dab41f667475b4a41faec5ba680aee428bfa5de4ea520fdc6e901"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.20",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "gbt"
|
||||
version = "1.0.0"
|
||||
|
@ -71,15 +77,15 @@ dependencies = [
|
|||
"napi-derive",
|
||||
"priority-queue",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-log 0.2.0",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
|
@ -92,11 +98,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
version = "2.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
|
@ -114,12 +120,12 @@ checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
|
|||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
||||
checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"winapi",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -145,9 +151,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
|||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "2.13.2"
|
||||
version = "2.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e"
|
||||
checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
|
@ -159,29 +165,29 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.0.1"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e"
|
||||
checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "2.13.0"
|
||||
version = "2.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367"
|
||||
checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"convert_case",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "1.0.52"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17"
|
||||
checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"once_cell",
|
||||
|
@ -189,14 +195,14 @@ dependencies = [
|
|||
"quote",
|
||||
"regex",
|
||||
"semver",
|
||||
"syn 1.0.109",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "2.2.3"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3"
|
||||
checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
@ -223,9 +229,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
|
@ -241,11 +247,12 @@ checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
|||
|
||||
[[package]]
|
||||
name = "priority-queue"
|
||||
version = "1.3.2"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61"
|
||||
checksum = "509354d8a769e8d0b567d6821b84495c60213162761a732d68ce87c964bd347f"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"equivalent",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
|
@ -320,17 +327,6 @@ version = "1.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.20"
|
||||
|
@ -384,7 +380,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.20",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -408,6 +404,17 @@ dependencies = [
|
|||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.17"
|
||||
|
@ -423,7 +430,7 @@ dependencies = [
|
|||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-log 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"./backend/rust-gbt",
|
||||
]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": false,
|
||||
"NETWORK": "mainnet",
|
||||
"BACKEND": "electrum",
|
||||
"ENABLED": true,
|
||||
|
|
17
backend/package-lock.json
generated
17
backend/package-lock.json
generated
|
@ -9,6 +9,7 @@
|
|||
"version": "3.0.0-dev",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.6.1",
|
||||
|
@ -1499,9 +1500,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
|
||||
"version": "2.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
|
||||
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==",
|
||||
"bin": {
|
||||
"napi": "scripts/index.js"
|
||||
},
|
||||
|
@ -7669,7 +7670,7 @@
|
|||
"version": "3.0.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/cli": "2.16.1"
|
||||
"@napi-rs/cli": "2.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
|
@ -8774,9 +8775,9 @@
|
|||
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
|
||||
},
|
||||
"@napi-rs/cli": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA=="
|
||||
"version": "2.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
|
||||
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA=="
|
||||
},
|
||||
"@noble/hashes": {
|
||||
"version": "1.3.0",
|
||||
|
@ -12702,7 +12703,7 @@
|
|||
"rust-gbt": {
|
||||
"version": "file:rust-gbt",
|
||||
"requires": {
|
||||
"@napi-rs/cli": "2.16.1"
|
||||
"@napi-rs/cli": "2.18.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
|
|
|
@ -12,14 +12,14 @@ crate-type = ["cdylib"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
priority-queue = "1.3.2"
|
||||
priority-queue = "2.0.2"
|
||||
bytes = "1.4.0"
|
||||
napi = { version = "2.13.2", features = ["napi8", "tokio_rt"] }
|
||||
napi-derive = "2.13.0"
|
||||
napi = { version = "2.16.0", features = ["napi8", "tokio_rt"] }
|
||||
napi-derive = "2.16.0"
|
||||
bytemuck = "1.13.1"
|
||||
tracing = "0.1.36"
|
||||
tracing-log = "0.1.3"
|
||||
tracing-log = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"]}
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.0.1"
|
||||
napi-build = "2.1.2"
|
||||
|
|
|
@ -237,6 +237,49 @@ switch (platform) {
|
|||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'riscv64':
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.linux-riscv64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.linux-riscv64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-linux-riscv64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.linux-riscv64-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.linux-riscv64-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-linux-riscv64-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
}
|
||||
break
|
||||
case 's390x':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'gbt.linux-s390x-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./gbt.linux-s390x-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('gbt-linux-s390x-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
}
|
||||
|
|
8
backend/rust-gbt/package-lock.json
generated
8
backend/rust-gbt/package-lock.json
generated
|
@ -9,16 +9,16 @@
|
|||
"version": "3.0.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/cli": "2.16.1"
|
||||
"@napi-rs/cli": "2.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
|
||||
"version": "2.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
|
||||
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==",
|
||||
"bin": {
|
||||
"napi": "scripts/index.js"
|
||||
},
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@napi-rs/cli": "2.16.1"
|
||||
"@napi-rs/cli": "2.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"ENABLED": true,
|
||||
"OFFICIAL": false,
|
||||
"NETWORK": "__MEMPOOL_NETWORK__",
|
||||
"BACKEND": "__MEMPOOL_BACKEND__",
|
||||
"BLOCKS_SUMMARIES_INDEXING": true,
|
||||
|
@ -79,7 +80,8 @@
|
|||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"PID_DIR": "__DATABASE_PID_FILE__",
|
||||
"TIMEOUT": 3000
|
||||
"TIMEOUT": 3000,
|
||||
"POOL_SIZE": 100
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": false,
|
||||
|
|
|
@ -14,6 +14,7 @@ describe('Mempool Backend Config', () => {
|
|||
|
||||
expect(config.MEMPOOL).toStrictEqual({
|
||||
ENABLED: true,
|
||||
OFFICIAL: false,
|
||||
NETWORK: 'mainnet',
|
||||
BACKEND: 'none',
|
||||
BLOCKS_SUMMARIES_INDEXING: false,
|
||||
|
@ -93,7 +94,8 @@ describe('Mempool Backend Config', () => {
|
|||
USERNAME: 'mempool',
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 180000,
|
||||
PID_DIR: ''
|
||||
PID_DIR: '',
|
||||
POOL_SIZE: 100,
|
||||
});
|
||||
|
||||
expect(config.SYSLOG).toStrictEqual({
|
||||
|
|
|
@ -29,6 +29,7 @@ export interface AbstractBitcoinApi {
|
|||
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
||||
|
||||
startHealthChecks(): void;
|
||||
getHealthStatus(): HealthCheckHost[];
|
||||
}
|
||||
export interface BitcoinRpcCredentials {
|
||||
host: string;
|
||||
|
@ -38,3 +39,15 @@ export interface BitcoinRpcCredentials {
|
|||
timeout: number;
|
||||
cookie?: string;
|
||||
}
|
||||
|
||||
export interface HealthCheckHost {
|
||||
host: string;
|
||||
active: boolean;
|
||||
rtt: number;
|
||||
latestHeight: number;
|
||||
socket: boolean;
|
||||
outOfSync: boolean;
|
||||
unreachable: boolean;
|
||||
checked: boolean;
|
||||
lastChecked: number;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import blocks from '../blocks';
|
||||
|
@ -382,6 +382,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
}
|
||||
|
||||
public startHealthChecks(): void {};
|
||||
|
||||
public getHealthStatus() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default BitcoinApi;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import config from '../../config';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import axios, { AxiosResponse, isAxiosError } from 'axios';
|
||||
import http from 'http';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
import { Common } from '../common';
|
||||
|
@ -10,6 +10,7 @@ interface FailoverHost {
|
|||
host: string,
|
||||
rtts: number[],
|
||||
rtt: number,
|
||||
timedOut?: boolean,
|
||||
failures: number,
|
||||
latestHeight?: number,
|
||||
socket?: boolean,
|
||||
|
@ -17,6 +18,7 @@ interface FailoverHost {
|
|||
unreachable?: boolean,
|
||||
preferred?: boolean,
|
||||
checked: boolean,
|
||||
lastChecked?: number,
|
||||
}
|
||||
|
||||
class FailoverRouter {
|
||||
|
@ -108,14 +110,20 @@ class FailoverRouter {
|
|||
host.rtts = [];
|
||||
host.rtt = Infinity;
|
||||
}
|
||||
host.timedOut = false;
|
||||
} catch (e) {
|
||||
host.outOfSync = true;
|
||||
host.unreachable = true;
|
||||
host.rtts = [];
|
||||
host.rtt = Infinity;
|
||||
if (isAxiosError(e) && (e.code === 'ECONNABORTED' || e.code === 'ETIMEDOUT')) {
|
||||
host.timedOut = true;
|
||||
} else {
|
||||
host.timedOut = false;
|
||||
}
|
||||
}
|
||||
host.checked = true;
|
||||
|
||||
host.lastChecked = Date.now();
|
||||
|
||||
// switch if the current host is out of sync or significantly slower than the next best alternative
|
||||
const rankOrder = this.sortHosts();
|
||||
|
@ -143,7 +151,7 @@ class FailoverRouter {
|
|||
|
||||
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
|
||||
const heightStatus = !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅'));
|
||||
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${!host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅')} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
|
||||
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : (host.timedOut ? ' ⌛️💥 ' : ' - ')} ${!host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅')} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
|
||||
}
|
||||
|
||||
private updateFallback(): FailoverHost[] {
|
||||
|
@ -157,7 +165,7 @@ class FailoverRouter {
|
|||
}
|
||||
|
||||
// sort hosts by connection quality, and update default fallback
|
||||
private sortHosts(): FailoverHost[] {
|
||||
public sortHosts(): FailoverHost[] {
|
||||
// sort by connection quality
|
||||
return this.hosts.slice().sort((a, b) => {
|
||||
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
|
||||
|
@ -342,6 +350,24 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||
public startHealthChecks(): void {
|
||||
this.failoverRouter.startHealthChecks();
|
||||
}
|
||||
|
||||
public getHealthStatus(): HealthCheckHost[] {
|
||||
if (config.MEMPOOL.OFFICIAL) {
|
||||
return this.failoverRouter.sortHosts().map(host => ({
|
||||
host: host.host,
|
||||
active: host === this.failoverRouter.activeHost,
|
||||
rtt: host.rtt,
|
||||
latestHeight: host.latestHeight || 0,
|
||||
socket: !!host.socket,
|
||||
outOfSync: !!host.outOfSync,
|
||||
unreachable: !!host.unreachable,
|
||||
checked: !!host.checked,
|
||||
lastChecked: host.lastChecked || 0,
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ElectrsApi;
|
||||
|
|
|
@ -26,6 +26,7 @@ import mempool from './mempool';
|
|||
import statistics from './statistics/statistics';
|
||||
import accelerationCosts from './acceleration';
|
||||
import accelerationRepository from '../repositories/AccelerationRepository';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
|
||||
interface AddressTransactions {
|
||||
mempool: MempoolTransactionExtended[],
|
||||
|
@ -39,6 +40,7 @@ const wantable = [
|
|||
'mempool-blocks',
|
||||
'live-2h-chart',
|
||||
'stats',
|
||||
'tomahawk',
|
||||
];
|
||||
|
||||
class WebsocketHandler {
|
||||
|
@ -123,7 +125,7 @@ class WebsocketHandler {
|
|||
for (const sub of wantable) {
|
||||
const key = `want-${sub}`;
|
||||
const wants = parsedMessage.data.includes(sub);
|
||||
if (wants && client['wants'] && !client[key]) {
|
||||
if (wants && !client[key]) {
|
||||
wantNow[key] = true;
|
||||
}
|
||||
client[key] = wants;
|
||||
|
@ -147,6 +149,10 @@ class WebsocketHandler {
|
|||
response['da'] = this.socketData['da'];
|
||||
}
|
||||
|
||||
if (wantNow['want-tomahawk']) {
|
||||
response['tomahawk'] = JSON.stringify(bitcoinApi.getHealthStatus());
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-tx']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
|
||||
client['track-tx'] = parsedMessage['track-tx'];
|
||||
|
@ -546,6 +552,10 @@ class WebsocketHandler {
|
|||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||
}
|
||||
|
||||
if (client['want-tomahawk']) {
|
||||
response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
|
||||
}
|
||||
|
||||
if (client['track-mempool-tx']) {
|
||||
const tx = newTransactions.find((t) => t.txid === client['track-mempool-tx']);
|
||||
if (tx) {
|
||||
|
@ -909,6 +919,10 @@ class WebsocketHandler {
|
|||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||
}
|
||||
|
||||
if (client['want-tomahawk']) {
|
||||
response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
|
||||
}
|
||||
|
||||
if (client['track-tx']) {
|
||||
const trackTxid = client['track-tx'];
|
||||
if (trackTxid && confirmedTxids[trackTxid]) {
|
||||
|
|
|
@ -5,6 +5,7 @@ const configFromFile = require(
|
|||
interface IConfig {
|
||||
MEMPOOL: {
|
||||
ENABLED: boolean;
|
||||
OFFICIAL: boolean;
|
||||
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
||||
BACKEND: 'esplora' | 'electrum' | 'none';
|
||||
HTTP_PORT: number;
|
||||
|
@ -103,6 +104,7 @@ interface IConfig {
|
|||
PASSWORD: string;
|
||||
TIMEOUT: number;
|
||||
PID_DIR: string;
|
||||
POOL_SIZE: number;
|
||||
};
|
||||
SYSLOG: {
|
||||
ENABLED: boolean;
|
||||
|
@ -161,6 +163,7 @@ interface IConfig {
|
|||
const defaults: IConfig = {
|
||||
'MEMPOOL': {
|
||||
'ENABLED': true,
|
||||
'OFFICIAL': false,
|
||||
'NETWORK': 'mainnet',
|
||||
'BACKEND': 'none',
|
||||
'HTTP_PORT': 8999,
|
||||
|
@ -240,6 +243,7 @@ const defaults: IConfig = {
|
|||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 180000,
|
||||
'PID_DIR': '',
|
||||
'POOL_SIZE': 100,
|
||||
},
|
||||
'SYSLOG': {
|
||||
'ENABLED': true,
|
||||
|
|
|
@ -21,7 +21,7 @@ import { execSync } from 'child_process';
|
|||
database: config.DATABASE.DATABASE,
|
||||
user: config.DATABASE.USERNAME,
|
||||
password: config.DATABASE.PASSWORD,
|
||||
connectionLimit: 10,
|
||||
connectionLimit: config.DATABASE.POOL_SIZE,
|
||||
supportBigNumbers: true,
|
||||
timezone: '+00:00',
|
||||
};
|
||||
|
|
|
@ -155,11 +155,17 @@ class Server {
|
|||
}
|
||||
|
||||
if (Common.isLiquid()) {
|
||||
try {
|
||||
icons.loadIcons();
|
||||
} catch (e) {
|
||||
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
const refreshIcons = () => {
|
||||
try {
|
||||
icons.loadIcons();
|
||||
} catch (e) {
|
||||
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
};
|
||||
// Run once on startup.
|
||||
refreshIcons();
|
||||
// Matches crontab refresh interval for asset db.
|
||||
setInterval(refreshIcons, 3600_000);
|
||||
}
|
||||
|
||||
priceUpdater.$run();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"NETWORK": "__MEMPOOL_NETWORK__",
|
||||
"BACKEND": "__MEMPOOL_BACKEND__",
|
||||
"ENABLED": __MEMPOOL_ENABLED__,
|
||||
"OFFICIAL": __MEMPOOL_OFFICIAL__,
|
||||
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
||||
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||
|
@ -79,7 +80,8 @@
|
|||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"TIMEOUT": __DATABASE_TIMEOUT__,
|
||||
"PID_DIR": "__DATABASE_PID_DIR__"
|
||||
"PID_DIR": "__DATABASE_PID_DIR__",
|
||||
"POOL_SIZE": __DATABASE_POOL_SIZE__
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": __SYSLOG_ENABLED__,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
|
||||
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
|
||||
__MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
|
||||
__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
|
||||
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
|
||||
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||
|
@ -81,6 +82,7 @@ __DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
|
|||
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
|
||||
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
|
||||
__DATABASE_PID_DIR__=${DATABASE_PID_DIR:=""}
|
||||
__DATABASE_POOL_SIZE__=${DATABASE_POOL_SIZE:=100}
|
||||
|
||||
# SYSLOG
|
||||
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
|
||||
|
@ -158,6 +160,7 @@ mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
|||
sed -i "s!__MEMPOOL_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_BACKEND__!${__MEMPOOL_BACKEND__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||
|
@ -230,6 +233,7 @@ 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_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_PID_DIR__!${__DATABASE_PID_DIR__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_POOL_SIZE__!${__DATABASE_POOL_SIZE__}!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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
#backend
|
||||
cp ./docker/backend/* ./backend/
|
||||
cp -r ./docker/backend/* ./backend/
|
||||
|
||||
#geoip-data
|
||||
mkdir -p ./backend/GeoIP/
|
||||
|
@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
|
|||
cp ./docker/frontend/* ./frontend
|
||||
cp ./nginx.conf ./frontend/
|
||||
cp ./nginx-mempool.conf ./frontend/
|
||||
sed -i "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
|
||||
sed -i "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
|
||||
sed -i "s/user nobody;//g" ./frontend/nginx.conf
|
||||
sed -i "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
|
||||
sed -i "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf
|
||||
sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
|
||||
sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
|
||||
sed -i"" -e "s/user nobody;//g" ./frontend/nginx.conf
|
||||
sed -i"" -e "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
|
||||
sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf
|
||||
|
|
|
@ -6,6 +6,7 @@ import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.com
|
|||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from './components/clock/clock.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
|
@ -26,6 +27,14 @@ let routes: Routes = [
|
|||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
|
@ -61,6 +70,14 @@ let routes: Routes = [
|
|||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
|
@ -88,6 +105,14 @@ let routes: Routes = [
|
|||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
children: [
|
||||
|
@ -168,6 +193,14 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
|
@ -195,6 +228,14 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
children: [
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<div class="frame {{ screenSize }}" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
|
||||
<div class="heading">
|
||||
<app-svg-images name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
|
||||
<h3 i18n="addresses.balance">Balances</h3>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
<tr>
|
||||
<th class="address" i18n="addresses.total">Total</th>
|
||||
<th class="btc"><app-amount [satoshis]="balance" [digitsInfo]="digitsInfo" [noFiat]="true"></app-amount></th>
|
||||
<th class="fiat"><app-fiat [value]="balance"></app-fiat></th>
|
||||
</tr>
|
||||
<tr *ngFor="let address of page">
|
||||
<td class="address">
|
||||
<app-truncate [text]="address" [lastChars]="8" [link]="['/address/' | relativeUrl, address]" [external]="true"></app-truncate>
|
||||
</td>
|
||||
<td class="btc"><app-amount [satoshis]="addresses[address]" [digitsInfo]="digitsInfo" [noFiat]="true"></app-amount></td>
|
||||
<td class="fiat"><app-fiat [value]="addresses[address]"></app-fiat></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div *ngIf="addressStrings.length > itemsPerPage" class="pagination">
|
||||
<ngb-pagination class="pagination-container float-right" [collectionSize]="addressStrings.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="pageIndex" (pageChange)="pageChange(pageIndex)" [boundaryLinks]="false" [ellipses]="false"></ngb-pagination>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,101 @@
|
|||
.frame {
|
||||
position: relative;
|
||||
background: #24273e;
|
||||
padding: 0.5rem;
|
||||
height: calc(100% + 60px);
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
|
||||
& > * {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-align: center;
|
||||
margin: 0 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: 0.5em;
|
||||
|
||||
td, th {
|
||||
padding: 0.15rem 0.5rem;
|
||||
|
||||
&.address {
|
||||
width: auto;
|
||||
}
|
||||
&.btc {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
}
|
||||
&.fiat {
|
||||
width: 142px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
border-collapse: collapse;
|
||||
|
||||
&:first-child {
|
||||
border-bottom: solid 1px white;
|
||||
td, th {
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
}
|
||||
&:nth-child(2) {
|
||||
td, th {
|
||||
padding-top: 0.3rem;
|
||||
}
|
||||
}
|
||||
&:nth-child(even) {
|
||||
background: #181b2d;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 528px) {
|
||||
td, th {
|
||||
&.btc {
|
||||
width: 160px;
|
||||
}
|
||||
&.fiat {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
td, th {
|
||||
&.btc {
|
||||
width: 170px;
|
||||
}
|
||||
&.fiat {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
td, th {
|
||||
&.btc {
|
||||
width: 210px;
|
||||
}
|
||||
&.fiat {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, catchError } from 'rxjs/operators';
|
||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { of, Subscription, forkJoin } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-group',
|
||||
templateUrl: './address-group.component.html',
|
||||
styleUrls: ['./address-group.component.scss']
|
||||
})
|
||||
export class AddressGroupComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
|
||||
balance = 0;
|
||||
confirmed = 0;
|
||||
mempool = 0;
|
||||
addresses: { [address: string]: number | null };
|
||||
addressStrings: string[] = [];
|
||||
addressInfo: { [address: string]: AddressInformation | null };
|
||||
seenTxs: { [txid: string ]: boolean } = {};
|
||||
isLoadingAddress = true;
|
||||
error: any;
|
||||
mainSubscription: Subscription;
|
||||
wsSubscription: Subscription;
|
||||
|
||||
page: string[] = [];
|
||||
pageIndex: number = 1;
|
||||
itemsPerPage: number = 10;
|
||||
|
||||
screenSize: 'lg' | 'md' | 'sm' = 'lg';
|
||||
digitsInfo: string = '1.8-8';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private audioService: AudioService,
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.onResize();
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.mainSubscription = this.route.queryParamMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingAddress = true;
|
||||
this.addresses = {};
|
||||
this.addressInfo = {};
|
||||
this.balance = 0;
|
||||
|
||||
this.addressStrings = params.get('addresses').split(',').map(address => {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) {
|
||||
return address.toLowerCase();
|
||||
} else {
|
||||
return address;
|
||||
}
|
||||
});
|
||||
|
||||
return forkJoin(this.addressStrings.map(address => {
|
||||
const getLiquidInfo = ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([a-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address));
|
||||
return forkJoin([
|
||||
of(address),
|
||||
this.electrsApiService.getAddress$(address),
|
||||
(getLiquidInfo ? this.apiService.validateAddress$(address) : of(null)),
|
||||
]);
|
||||
}));
|
||||
}),
|
||||
catchError(e => {
|
||||
this.error = e;
|
||||
return of([]);
|
||||
})
|
||||
).subscribe((addresses) => {
|
||||
for (const addressData of addresses) {
|
||||
const address = addressData[0];
|
||||
const addressBalance = addressData[1] as Address;
|
||||
if (addressBalance) {
|
||||
this.addresses[address] = addressBalance.chain_stats.funded_txo_sum
|
||||
+ addressBalance.mempool_stats.funded_txo_sum
|
||||
- addressBalance.chain_stats.spent_txo_sum
|
||||
- addressBalance.mempool_stats.spent_txo_sum;
|
||||
this.balance += this.addresses[address];
|
||||
this.confirmed += (addressBalance.chain_stats.funded_txo_sum - addressBalance.chain_stats.spent_txo_sum);
|
||||
}
|
||||
this.addressInfo[address] = addressData[2] ? addressData[2] as AddressInformation : null;
|
||||
}
|
||||
this.websocketService.startTrackAddresses(this.addressStrings);
|
||||
this.isLoadingAddress = false;
|
||||
this.pageChange(this.pageIndex);
|
||||
});
|
||||
|
||||
this.wsSubscription = this.stateService.multiAddressTransactions$.subscribe(update => {
|
||||
for (const address of Object.keys(update)) {
|
||||
for (const tx of update[address].mempool) {
|
||||
this.addTransaction(tx, false, false);
|
||||
}
|
||||
for (const tx of update[address].confirmed) {
|
||||
this.addTransaction(tx, true, false);
|
||||
}
|
||||
for (const tx of update[address].removed) {
|
||||
this.removeTransaction(tx, tx.status.confirmed);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pageChange(index): void {
|
||||
this.page = this.addressStrings.slice((index - 1) * this.itemsPerPage, index * this.itemsPerPage);
|
||||
}
|
||||
|
||||
addTransaction(transaction: Transaction, confirmed = false, playSound: boolean = true): boolean {
|
||||
if (this.seenTxs[transaction.txid]) {
|
||||
this.removeTransaction(transaction, false);
|
||||
}
|
||||
this.seenTxs[transaction.txid] = true;
|
||||
|
||||
let balance = 0;
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (this.addressStrings.includes(vin?.prevout?.scriptpubkey_address)) {
|
||||
this.addresses[vin?.prevout?.scriptpubkey_address] -= vin.prevout.value;
|
||||
balance -= vin.prevout.value;
|
||||
this.balance -= vin.prevout.value;
|
||||
if (confirmed) {
|
||||
this.confirmed -= vin.prevout.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (this.addressStrings.includes(vout?.scriptpubkey_address)) {
|
||||
this.addresses[vout?.scriptpubkey_address] += vout.value;
|
||||
balance += vout.value;
|
||||
this.balance += vout.value;
|
||||
if (confirmed) {
|
||||
this.confirmed += vout.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (playSound) {
|
||||
if (balance > 0) {
|
||||
this.audioService.playSound('cha-ching');
|
||||
} else {
|
||||
this.audioService.playSound('chime');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
removeTransaction(transaction: Transaction, confirmed = false): boolean {
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (this.addressStrings.includes(vin?.prevout?.scriptpubkey_address)) {
|
||||
this.addresses[vin?.prevout?.scriptpubkey_address] += vin.prevout.value;
|
||||
this.balance += vin.prevout.value;
|
||||
if (confirmed) {
|
||||
this.confirmed += vin.prevout.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (this.addressStrings.includes(vout?.scriptpubkey_address)) {
|
||||
this.addresses[vout?.scriptpubkey_address] -= vout.value;
|
||||
this.balance -= vout.value;
|
||||
if (confirmed) {
|
||||
this.confirmed -= vout.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.screenSize = 'lg';
|
||||
this.digitsInfo = '1.8-8';
|
||||
} else if (window.innerWidth >= 528) {
|
||||
this.screenSize = 'md';
|
||||
this.digitsInfo = '1.4-4';
|
||||
} else {
|
||||
this.screenSize = 'sm';
|
||||
this.digitsInfo = '1.2-2';
|
||||
}
|
||||
const newItemsPerPage = Math.floor((window.innerHeight - 150) / 30);
|
||||
if (newItemsPerPage !== this.itemsPerPage) {
|
||||
this.itemsPerPage = newItemsPerPage;
|
||||
this.pageIndex = 1;
|
||||
this.pageChange(this.pageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.mainSubscription.unsubscribe();
|
||||
this.wsSubscription.unsubscribe();
|
||||
this.websocketService.stopTrackingAddresses();
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@
|
|||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
||||
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
{{ block.extras.pool.name}}</a>
|
||||
|
|
|
@ -166,7 +166,7 @@
|
|||
opacity: 1;
|
||||
}
|
||||
.hide {
|
||||
opacity: 0;
|
||||
opacity: 0.4;
|
||||
pointer-events : none;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<footer class="footer">
|
||||
<footer class="footer" [class.inline-footer]="inline">
|
||||
<div class="container-xl">
|
||||
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
|
||||
<div class="col d-none d-sm-block">
|
||||
|
|
|
@ -6,6 +6,12 @@
|
|||
background-color: #1d1f31;
|
||||
box-shadow: 15px 15px 15px 15px #000;
|
||||
z-index: 10;
|
||||
|
||||
&.inline-footer {
|
||||
position: relative;
|
||||
bottom: unset;
|
||||
top: -44px;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
@ -23,6 +23,8 @@ interface MempoolInfoData {
|
|||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
@Input() inline = false;
|
||||
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||
vBytesPerSecondLimit = 1667;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks" [class.minimal]="minimal">
|
||||
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'" *ngIf="(difficultyAdjustments$ | async) as da;">
|
||||
<div class="flashing">
|
||||
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
|
||||
<div class="flashing" *ngIf="(mempoolBlocks$ | async) as mempoolBlocks">
|
||||
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks" let-i="index" [ngForTrackBy]="trackByFn">
|
||||
<div
|
||||
*ngIf="minimal && spotlight > 0 && spotlight === i + 1"
|
||||
class="spotlight-bottom"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
|
||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
|
||||
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { take, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
@ -86,7 +86,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private location: Location
|
||||
private location: Location,
|
||||
) { }
|
||||
|
||||
enabledMiningInfoIfNeeded(url) {
|
||||
|
@ -129,50 +129,44 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||
})
|
||||
);
|
||||
|
||||
this.mempoolBlocks$ = merge(
|
||||
of(true),
|
||||
fromEvent(window, 'resize')
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => combineLatest([
|
||||
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
|
||||
this.stateService.mempoolBlocks$
|
||||
.pipe(
|
||||
map((mempoolBlocks) => {
|
||||
if (!mempoolBlocks.length) {
|
||||
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
|
||||
}
|
||||
return mempoolBlocks;
|
||||
}),
|
||||
)
|
||||
])),
|
||||
map(([lastBlock, mempoolBlocks]) => {
|
||||
mempoolBlocks.forEach((block, i) => {
|
||||
block.index = this.blockIndex + i;
|
||||
block.height = lastBlock.height + i + 1;
|
||||
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
|
||||
});
|
||||
this.mempoolBlocks$ = combineLatest([
|
||||
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
|
||||
this.stateService.mempoolBlocks$
|
||||
.pipe(
|
||||
map((mempoolBlocks) => {
|
||||
if (!mempoolBlocks.length) {
|
||||
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
|
||||
}
|
||||
return mempoolBlocks;
|
||||
}),
|
||||
)
|
||||
]).pipe(
|
||||
map(([lastBlock, mempoolBlocks]) => {
|
||||
mempoolBlocks.forEach((block, i) => {
|
||||
block.index = this.blockIndex + i;
|
||||
block.height = lastBlock.height + i + 1;
|
||||
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
|
||||
});
|
||||
|
||||
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
|
||||
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
||||
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
||||
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
|
||||
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
||||
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
||||
|
||||
this.now = Date.now();
|
||||
this.now = Date.now();
|
||||
|
||||
this.updateMempoolBlockStyles();
|
||||
this.calculateTransactionPosition();
|
||||
|
||||
return this.mempoolBlocks;
|
||||
}),
|
||||
tap(() => {
|
||||
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
|
||||
if (this.mempoolWidth !== width) {
|
||||
this.mempoolWidth = width;
|
||||
this.widthChange.emit(this.mempoolWidth);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
})
|
||||
);
|
||||
this.updateMempoolBlockStyles();
|
||||
this.calculateTransactionPosition();
|
||||
|
||||
return this.mempoolBlocks;
|
||||
}),
|
||||
tap(() => {
|
||||
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
|
||||
if (this.mempoolWidth !== width) {
|
||||
this.mempoolWidth = width;
|
||||
this.widthChange.emit(this.mempoolWidth);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
|
||||
.pipe(
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<div class="tomahawk">
|
||||
<div class="links">
|
||||
<span>Monitoring</span>
|
||||
<a [routerLink]='"/nodes"'>Nodes</a>
|
||||
</div>
|
||||
|
||||
<app-start [showLoadingIndicator]="true"></app-start>
|
||||
<app-footer [inline]="true"></app-footer>
|
||||
|
||||
<ng-container *ngIf="(hosts$ | async) as hosts">
|
||||
<div class="status-panel">
|
||||
<table class="status-table table table-fixed table-borderless table-striped" *ngIf="(tip$ | async) as tip">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="rank"></th>
|
||||
<th class="flag"></th>
|
||||
<th class="host">Host</th>
|
||||
<th class="updated">Last checked</th>
|
||||
<th class="rtt only-small">RTT</th>
|
||||
<th class="rtt only-large">RTT</th>
|
||||
<th class="height">Height</th>
|
||||
</tr>
|
||||
<tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn">
|
||||
<td class="rank">{{ i + 1 }}</td>
|
||||
<td class="flag">{{ host.active ? '⭐️' : host.flag }}</td>
|
||||
<td class="host">{{ host.link }}</td>
|
||||
<td class="updated">{{ getLastUpdateSeconds(host) }}</td>
|
||||
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '✅')) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,75 @@
|
|||
.tomahawk {
|
||||
.links {
|
||||
text-align: right;
|
||||
margin-inline-end: 1em;
|
||||
|
||||
a, span {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
max-width: 720px;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
background: #24273e;
|
||||
}
|
||||
|
||||
.status-table {
|
||||
width: 100%;
|
||||
|
||||
td, th {
|
||||
padding: 0.25em;
|
||||
|
||||
&.rank, &.flag {
|
||||
width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
&.updated {
|
||||
display: none;
|
||||
width: 130px;
|
||||
text-align: right;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
&.rtt, &.height {
|
||||
width: 92px;
|
||||
text-align: right;
|
||||
}
|
||||
&.only-small {
|
||||
display: table-cell;
|
||||
&.rtt {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
&.only-large {
|
||||
display: none;
|
||||
}
|
||||
&.height {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
&.host {
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
&.rank, &.flag {
|
||||
width: 32px;
|
||||
}
|
||||
&.updated {
|
||||
display: table-cell;
|
||||
}
|
||||
&.rtt, &.height {
|
||||
width: 96px;
|
||||
}
|
||||
&.only-small {
|
||||
display: none;
|
||||
}
|
||||
&.only-large {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { Observable, Subject, map } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { HealthCheckHost } from '../../interfaces/websocket.interface';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-server-health',
|
||||
templateUrl: './server-health.component.html',
|
||||
styleUrls: ['./server-health.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ServerHealthComponent implements OnInit {
|
||||
hosts$: Observable<HealthCheckHost[]>;
|
||||
tip$: Subject<number>;
|
||||
interval: number;
|
||||
now: number = Date.now();
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
public sanitizer: DomSanitizer,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hosts$ = this.stateService.serverHealth$.pipe(
|
||||
map((hosts) => {
|
||||
const subpath = window.location.pathname.slice(0, -11);
|
||||
for (const host of hosts) {
|
||||
let statusUrl = '';
|
||||
let linkHost = '';
|
||||
if (host.socket) {
|
||||
statusUrl = 'https://' + window.location.hostname + subpath + '/status';
|
||||
linkHost = window.location.hostname + subpath;
|
||||
} else {
|
||||
const hostUrl = new URL(host.host);
|
||||
statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
|
||||
linkHost = hostUrl.hostname + subpath;
|
||||
}
|
||||
host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
|
||||
host.link = linkHost;
|
||||
host.flag = this.parseFlag(host.host);
|
||||
}
|
||||
return hosts;
|
||||
})
|
||||
);
|
||||
this.tip$ = this.stateService.chainTip$;
|
||||
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
|
||||
|
||||
this.interval = window.setInterval(() => {
|
||||
this.now = Date.now();
|
||||
this.cd.markForCheck();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
trackByFn(index: number, host: HealthCheckHost): string {
|
||||
return host.host;
|
||||
}
|
||||
|
||||
getLastUpdateSeconds(host: HealthCheckHost): string {
|
||||
if (host.lastChecked) {
|
||||
const seconds = Math.ceil((this.now - host.lastChecked) / 1000);
|
||||
return `${seconds} second${seconds > 1 ? 's' : ' '} ago`;
|
||||
} else {
|
||||
return '~';
|
||||
}
|
||||
}
|
||||
|
||||
private parseFlag(host: string): string {
|
||||
if (host.includes('.fra.')) {
|
||||
return '🇩🇪';
|
||||
} else if (host.includes('.tk7.')) {
|
||||
return '🇯🇵';
|
||||
} else if (host.includes('.fmt.')) {
|
||||
return '🇺🇸';
|
||||
} else if (host.includes('.va1.')) {
|
||||
return '🇺🇸';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<div class="tomahawk">
|
||||
<div class="links">
|
||||
<a [routerLink]='"/monitoring"'>Monitoring</a>
|
||||
<span>Nodes</span>
|
||||
</div>
|
||||
|
||||
<app-start [showLoadingIndicator]="true"></app-start>
|
||||
<app-footer [inline]="true"></app-footer>
|
||||
|
||||
<ng-container *ngFor="let host of hosts; trackBy: trackByFn">
|
||||
<h5 [id]="host.host" class="hostLink">
|
||||
<a [href]="'https://' + host.link">{{ host.link }}</a>
|
||||
</h5>
|
||||
<iframe class="mempoolStatus" [src]="host.statusPage"></iframe>
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
.tomahawk {
|
||||
.links {
|
||||
text-align: right;
|
||||
margin-inline-end: 1em;
|
||||
|
||||
a, span {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.mempoolStatus {
|
||||
width: 100%;
|
||||
height: 270px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.hostLink {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { Observable, Subject, Subscription, map, tap } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { HealthCheckHost } from '../../interfaces/websocket.interface';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-server-status',
|
||||
templateUrl: './server-status.component.html',
|
||||
styleUrls: ['./server-status.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ServerStatusComponent implements OnInit, OnDestroy {
|
||||
tip$: Subject<number>;
|
||||
hosts: HealthCheckHost[] = [];
|
||||
hostSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
public sanitizer: DomSanitizer,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hostSubscription = this.stateService.serverHealth$.pipe(
|
||||
map((hosts) => {
|
||||
const subpath = window.location.pathname.slice(0, -6);
|
||||
for (const host of hosts) {
|
||||
let statusUrl = '';
|
||||
let linkHost = '';
|
||||
if (host.socket) {
|
||||
statusUrl = 'https://' + window.location.hostname + subpath + '/status';
|
||||
linkHost = window.location.hostname + subpath;
|
||||
} else {
|
||||
const hostUrl = new URL(host.host);
|
||||
statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
|
||||
linkHost = hostUrl.hostname + subpath;
|
||||
}
|
||||
host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
|
||||
host.link = linkHost;
|
||||
}
|
||||
return hosts;
|
||||
}),
|
||||
tap((hosts) => {
|
||||
if (this.hosts.length !== hosts.length) {
|
||||
this.hosts = hosts.sort((a,b) => {
|
||||
const aParts = (a.host?.split('.') || []).reverse();
|
||||
const bParts = (b.host?.split('.') || []).reverse();
|
||||
let i = 0;
|
||||
while (i < Math.max(aParts.length, bParts.length)) {
|
||||
if (aParts[i] && !bParts[i]) {
|
||||
return 1;
|
||||
} else if (bParts[i] && !aParts[i]) {
|
||||
return -1;
|
||||
} else if (aParts[i] !== bParts[i]) {
|
||||
return aParts[i].localeCompare(bParts[i]);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
).subscribe();
|
||||
this.tip$ = this.stateService.chainTip$;
|
||||
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
|
||||
}
|
||||
|
||||
trackByFn(index: number, host: HealthCheckHost): string {
|
||||
return host.host;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.hosts = [];
|
||||
this.hostSubscription.unsubscribe();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
<div id="enterprise-cta-desktop">
|
||||
<p>Get higher API limits with Mempool Enterprise®</p>
|
||||
<a class="btn btn-small btn-purple" href="/enterprise">More Info <fa-icon [icon]="['fas', 'angle-right']" [styles]="{'font-size': '12px'}"></fa-icon></a>
|
||||
</div>
|
||||
<div *ngFor="let item of tabData">
|
||||
<p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ))">{{ item.title }}</p>
|
||||
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a>
|
||||
|
|
|
@ -11,3 +11,22 @@ a {
|
|||
display: block;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
#enterprise-cta-desktop {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
margin: 20px 20px 20px 0;
|
||||
background-color: #1d1f31;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#enterprise-cta-desktop p {
|
||||
margin: 0 auto 16px auto;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#enterprise-cta-desktop a {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,14 @@
|
|||
|
||||
<div class="doc-content">
|
||||
|
||||
<div id="enterprise-cta-mobile" *ngIf="showMobileEnterpriseUpsell">
|
||||
<p>Get higher API limits with <span class="no-line-break">Mempool Enterprise®</span></p>
|
||||
<div class="button-group">
|
||||
<a class="btn btn-small btn-secondary" (click)="showMobileEnterpriseUpsell = false">No Thanks</a>
|
||||
<a class="btn btn-small btn-purple" href="https://mempool.space/enterprise">More Info <fa-icon [icon]="['fas', 'angle-right']" [styles]="{'font-size': '12px'}"></fa-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title">REST API service</ng-container>.</p>
|
||||
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits.</p>
|
||||
|
||||
|
|
|
@ -315,6 +315,41 @@ h3 {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#enterprise-cta-mobile {
|
||||
padding: 20px;
|
||||
background-color: #1d1f31;
|
||||
border-radius: 0.25rem;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
left: 30px;
|
||||
width: calc(100% - 60px);
|
||||
bottom: 70px;
|
||||
display: none;
|
||||
border: 3px solid #533180;
|
||||
}
|
||||
|
||||
#enterprise-cta-mobile p {
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#enterprise-cta-mobile a {
|
||||
padding: 4px 8px;
|
||||
font-size: 16px;
|
||||
margin: 15px 5px 5px 5px;
|
||||
}
|
||||
|
||||
#enterprise-cta-mobile .btn-secondary:hover {
|
||||
background-color: #2d3348;
|
||||
border-color: #2d3348;
|
||||
}
|
||||
|
||||
#enterprise-cta-mobile .no-line-break {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
h3 {
|
||||
|
@ -373,6 +408,10 @@ h3 {
|
|||
#disclaimer table {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#enterprise-cta-mobile {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
|
|
|
@ -30,6 +30,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
|||
officialMempoolInstance: boolean;
|
||||
auditEnabled: boolean;
|
||||
mobileViewport: boolean = false;
|
||||
showMobileEnterpriseUpsell: boolean = true;
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { ILoadingIndicators } from '../services/state.service';
|
||||
import { Transaction } from './electrs.interface';
|
||||
import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface';
|
||||
|
@ -120,4 +121,19 @@ export interface Recommendedfees {
|
|||
hourFee: number;
|
||||
minimumFee: number;
|
||||
economyFee: number;
|
||||
}
|
||||
|
||||
export interface HealthCheckHost {
|
||||
host: string;
|
||||
active: boolean;
|
||||
rtt: number;
|
||||
latestHeight: number;
|
||||
socket: boolean;
|
||||
outOfSync: boolean;
|
||||
unreachable: boolean;
|
||||
checked: boolean;
|
||||
lastChecked: number;
|
||||
link?: string;
|
||||
statusPage?: SafeResourceUrl;
|
||||
flag?: string;
|
||||
}
|
|
@ -19,6 +19,8 @@ import { RecentPegsListComponent } from '../components/liquid-reserves-audit/rec
|
|||
import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component';
|
||||
import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component';
|
||||
import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component';
|
||||
import { ServerHealthComponent } from '../components/server-health/server-health.component';
|
||||
import { ServerStatusComponent } from '../components/server-health/server-status.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -140,6 +142,19 @@ const routes: Routes = [
|
|||
},
|
||||
];
|
||||
|
||||
if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
|
||||
routes[0].children.push({
|
||||
path: 'nodes',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: ServerHealthComponent
|
||||
});
|
||||
routes[0].children.push({
|
||||
path: 'network',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: ServerStatusComponent
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
|
|
|
@ -6,10 +6,13 @@ import { SharedModule } from './shared/shared.module';
|
|||
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||
import { CalculatorComponent } from './components/calculator/calculator.component';
|
||||
import { BlocksList } from './components/blocks-list/blocks-list.component';
|
||||
import { RbfList } from './components/rbf-list/rbf-list.component';
|
||||
import { ServerHealthComponent } from './components/server-health/server-health.component';
|
||||
import { ServerStatusComponent } from './components/server-health/server-status.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
|
@ -96,6 +99,19 @@ const routes: Routes = [
|
|||
}
|
||||
];
|
||||
|
||||
if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
|
||||
routes[0].children.push({
|
||||
path: 'monitoring',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: ServerHealthComponent
|
||||
});
|
||||
routes[0].children.push({
|
||||
path: 'nodes',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: ServerStatusComponent
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionCompressed, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||
import { StorageService } from './storage.service';
|
||||
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
|
||||
import { ApiService } from './api.service';
|
||||
import { ActiveFilter } from '../shared/filters.utils';
|
||||
|
||||
export interface MarkBlockState {
|
||||
|
@ -119,6 +118,7 @@ export class StateService {
|
|||
mempoolTransactions$ = new Subject<Transaction>();
|
||||
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>();
|
||||
mempoolRemovedTransactions$ = new Subject<Transaction>();
|
||||
multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>();
|
||||
blockTransactions$ = new Subject<Transaction>();
|
||||
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
||||
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
|
||||
|
@ -129,6 +129,7 @@ export class StateService {
|
|||
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
|
||||
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
|
||||
chainTip$ = new ReplaySubject<number>(-1);
|
||||
serverHealth$ = new Subject<HealthCheckHost[]>();
|
||||
|
||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export class WebsocketService {
|
|||
private isTrackingRbf: 'all' | 'fullRbf' | false = false;
|
||||
private isTrackingRbfSummary = false;
|
||||
private isTrackingAddress: string | false = false;
|
||||
private isTrackingAddresses: string[] | false = false;
|
||||
private trackingMempoolBlock: number;
|
||||
private latestGitCommit = '';
|
||||
private onlineCheckTimeout: number;
|
||||
|
@ -126,6 +127,9 @@ export class WebsocketService {
|
|||
if (this.isTrackingAddress) {
|
||||
this.startTrackAddress(this.isTrackingAddress);
|
||||
}
|
||||
if (this.isTrackingAddresses) {
|
||||
this.startTrackAddresses(this.isTrackingAddresses);
|
||||
}
|
||||
this.stateService.connectionState$.next(2);
|
||||
}
|
||||
|
||||
|
@ -175,6 +179,16 @@ export class WebsocketService {
|
|||
this.isTrackingAddress = false;
|
||||
}
|
||||
|
||||
startTrackAddresses(addresses: string[]) {
|
||||
this.websocketSubject.next({ 'track-addresses': addresses });
|
||||
this.isTrackingAddresses = addresses;
|
||||
}
|
||||
|
||||
stopTrackingAddresses() {
|
||||
this.websocketSubject.next({ 'track-addresses': [] });
|
||||
this.isTrackingAddresses = false;
|
||||
}
|
||||
|
||||
startTrackAsset(asset: string) {
|
||||
this.websocketSubject.next({ 'track-asset': asset });
|
||||
}
|
||||
|
@ -374,6 +388,10 @@ export class WebsocketService {
|
|||
});
|
||||
}
|
||||
|
||||
if (response['multi-address-transactions']) {
|
||||
this.stateService.multiAddressTransactions$.next(response['multi-address-transactions']);
|
||||
}
|
||||
|
||||
if (response['block-transactions']) {
|
||||
response['block-transactions'].forEach((addressTransaction: Transaction) => {
|
||||
this.stateService.blockTransactions$.next(addressTransaction);
|
||||
|
@ -415,6 +433,10 @@ export class WebsocketService {
|
|||
this.stateService.previousRetarget$.next(response.previousRetarget);
|
||||
}
|
||||
|
||||
if (response['tomahawk']) {
|
||||
this.stateService.serverHealth$.next(response['tomahawk']);
|
||||
}
|
||||
|
||||
if (response['git-commit']) {
|
||||
this.stateService.backendInfo$.next(response['git-commit']);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
|
||||
<ng-container *ngIf="link">
|
||||
<a [routerLink]="link" class="truncate-link">
|
||||
<a [routerLink]="link" class="truncate-link" [target]="external ? '_blank' : ''">
|
||||
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy } from '@a
|
|||
export class TruncateComponent {
|
||||
@Input() text: string;
|
||||
@Input() link: any = null;
|
||||
@Input() external: boolean = false;
|
||||
@Input() lastChars: number = 4;
|
||||
@Input() maxWidth: number = null;
|
||||
@Input() inline: boolean = false;
|
||||
|
|
|
@ -46,6 +46,7 @@ import { BlockOverviewGraphComponent } from '../components/block-overview-graph/
|
|||
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
|
||||
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
|
||||
import { AddressComponent } from '../components/address/address.component';
|
||||
import { AddressGroupComponent } from '../components/address-group/address-group.component';
|
||||
import { SearchFormComponent } from '../components/search-form/search-form.component';
|
||||
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
|
||||
import { FooterComponent } from '../components/footer/footer.component';
|
||||
|
@ -53,6 +54,8 @@ import { AssetComponent } from '../components/asset/asset.component';
|
|||
import { AssetsComponent } from '../components/assets/assets.component';
|
||||
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
|
||||
import { StatusViewComponent } from '../components/status-view/status-view.component';
|
||||
import { ServerHealthComponent } from '../components/server-health/server-health.component';
|
||||
import { ServerStatusComponent } from '../components/server-health/server-status.component';
|
||||
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
|
||||
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
|
||||
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
|
||||
|
@ -145,12 +148,15 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||
BlockFiltersComponent,
|
||||
TransactionsListComponent,
|
||||
AddressComponent,
|
||||
AddressGroupComponent,
|
||||
SearchFormComponent,
|
||||
AddressLabelsComponent,
|
||||
FooterComponent,
|
||||
AssetComponent,
|
||||
AssetsComponent,
|
||||
StatusViewComponent,
|
||||
ServerHealthComponent,
|
||||
ServerStatusComponent,
|
||||
FeesBoxComponent,
|
||||
DifficultyComponent,
|
||||
DifficultyMiningComponent,
|
||||
|
@ -271,12 +277,15 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||
BlockFiltersComponent,
|
||||
TransactionsListComponent,
|
||||
AddressComponent,
|
||||
AddressGroupComponent,
|
||||
SearchFormComponent,
|
||||
AddressLabelsComponent,
|
||||
FooterComponent,
|
||||
AssetComponent,
|
||||
AssetsComponent,
|
||||
StatusViewComponent,
|
||||
ServerHealthComponent,
|
||||
ServerStatusComponent,
|
||||
FeesBoxComponent,
|
||||
DifficultyComponent,
|
||||
DifficultyMiningComponent,
|
||||
|
|
31
production/check
Executable file
31
production/check
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env zsh
|
||||
#for j in fmt va1 fra tk7;do for i in 1 2 3 4 5 6;do echo -n 20$i.$j: ;curl -i -s https://node20$i.$j.mempool.space/api/v1/services/accelerator/accelerations|head -1;done;done
|
||||
check_mempoolspace_frontend_git_hash() {
|
||||
echo curl -s --connect-to "::node${1}.${2}.mempool.space:443" https://mempool.space/resources/config.js
|
||||
echo -n $(curl -s --connect-to "::node${1}.${2}.mempool.space:443" https://mempool.space/resources/config.js|grep GIT_COMMIT_HASH|cut -d "'" -f2|cut -c1-8)
|
||||
}
|
||||
check_mempoolfoss_frontend_git_hash() {
|
||||
echo -n $(curl -s "https://node${1}.${2}.mempool.space/resources/config.js"|grep GIT_COMMIT_HASH|cut -d "'" -f2|cut -c1-8)
|
||||
}
|
||||
check_mempoolspace_frontend_md5_hash() {
|
||||
echo -n $(curl -s --connect-to "::node${1}.${2}.mempool.space:443" https://mempool.space|md5|cut -c1-8)
|
||||
}
|
||||
check_mempoolfoss_frontend_md5_hash() {
|
||||
echo -n $(curl -s https://node${1}.${2}.mempool.space|md5|cut -c1-8)
|
||||
}
|
||||
for site in fmt va1 fra tk7;do
|
||||
echo "${site}"
|
||||
for node in 201 202 203 204 205 206 207 208 209 210 211 212 213 214;do
|
||||
[ "${site}" = "fmt" ] && [ "${node}" -gt 206 ] && continue
|
||||
[ "${site}" = "tk7" ] && [ "${node}" -gt 206 ] && continue
|
||||
echo -n "node${node}.${site}: "
|
||||
#check_mempoolspace_frontend_git_hash $node $site
|
||||
#echo -n " "
|
||||
check_mempoolspace_frontend_md5_hash $node $site
|
||||
echo -n " "
|
||||
check_mempoolfoss_frontend_git_hash $node $site
|
||||
echo -n " "
|
||||
check_mempoolfoss_frontend_md5_hash $node $site
|
||||
echo
|
||||
done
|
||||
done
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"NETWORK": "bisq",
|
||||
"BACKEND": "esplora",
|
||||
"HTTP_PORT": 8996,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"NETWORK": "liquid",
|
||||
"BACKEND": "esplora",
|
||||
"HTTP_PORT": 8998,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"NETWORK": "liquid",
|
||||
"BACKEND": "esplora",
|
||||
"HTTP_PORT": 8994,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"ENABLED": false,
|
||||
"NETWORK": "mainnet",
|
||||
"BACKEND": "esplora",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"NETWORK": "mainnet",
|
||||
"BACKEND": "esplora",
|
||||
"HTTP_PORT": 8999,
|
||||
|
@ -21,7 +22,8 @@
|
|||
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||
"ALLOW_UNREACHABLE": true,
|
||||
"PRICE_UPDATES_PER_HOUR": 12
|
||||
"PRICE_UPDATES_PER_HOUR": 12,
|
||||
"MAX_TRACKED_ADDRESSES": 10
|
||||
},
|
||||
"SYSLOG" : {
|
||||
"MIN_PRIORITY": "debug"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"ENABLED": false,
|
||||
"NETWORK": "signet",
|
||||
"BACKEND": "esplora",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"NETWORK": "signet",
|
||||
"BACKEND": "esplora",
|
||||
"HTTP_PORT": 8995,
|
||||
|
@ -14,7 +15,8 @@
|
|||
"POLL_RATE_MS": 1000,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||
"ALLOW_UNREACHABLE": true
|
||||
"ALLOW_UNREACHABLE": true,
|
||||
"MAX_TRACKED_ADDRESSES": 10
|
||||
},
|
||||
"SYSLOG" : {
|
||||
"MIN_PRIORITY": "debug"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"ENABLED": false,
|
||||
"NETWORK": "testnet",
|
||||
"BACKEND": "esplora",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"MEMPOOL": {
|
||||
"OFFICIAL": true,
|
||||
"NETWORK": "testnet",
|
||||
"BACKEND": "esplora",
|
||||
"HTTP_PORT": 8997,
|
||||
|
@ -14,7 +15,8 @@
|
|||
"POLL_RATE_MS": 1000,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||
"ALLOW_UNREACHABLE": true
|
||||
"ALLOW_UNREACHABLE": true,
|
||||
"MAX_TRACKED_ADDRESSES": 10
|
||||
},
|
||||
"SYSLOG" : {
|
||||
"MIN_PRIORITY": "debug"
|
||||
|
|
|
@ -8,5 +8,9 @@ proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inac
|
|||
proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m;
|
||||
proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:500m levels=1:2 inactive=365d max_size=5000m;
|
||||
proxy_cache_path /var/cache/nginx/markets keys_zone=markets:20m levels=1:2 inactive=365d max_size=100m;
|
||||
types_hash_max_size 4096;
|
||||
proxy_buffer_size 8k;
|
||||
|
||||
types_hash_max_size 8192;
|
||||
|
||||
proxy_busy_buffers_size 256k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
|
|
25
scripts/get_block_tip_height.sh
Normal file
25
scripts/get_block_tip_height.sh
Normal file
|
@ -0,0 +1,25 @@
|
|||
BASE_HEIGHT=$(curl -sk https://node202.tk7.mempool.space/api/v1/blocks/tip/height)
|
||||
IN_SYNC=true
|
||||
echo "Base height (node202.tk7): $BASE_HEIGHT"
|
||||
|
||||
for LOCATION in fmt va1 fra tk7
|
||||
do
|
||||
for NODE in 201 202 203 204 205 206
|
||||
do
|
||||
NODE_HEIGHT=$(curl -sk https://node$NODE.$LOCATION.mempool.space/api/v1/blocks/tip/height)
|
||||
echo $(echo node$NODE.$LOCATION.mempool.space) - $NODE_HEIGHT
|
||||
if [ "$NODE_HEIGHT" -ne "$BASE_HEIGHT" ]; then
|
||||
COUNT=$((BASE_HEIGHT-NODE_HEIGHT))
|
||||
echo $(echo node$NODE.$LOCATION.mempool.space) is not in sync. delta: $COUNT
|
||||
IN_SYNC=false
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ "$IN_SYNC" = false ]; then
|
||||
echo "One or more servers are out of sync. Check the logs."
|
||||
exit -1
|
||||
else
|
||||
echo "All servers are in sync."
|
||||
fi
|
||||
|
Loading…
Add table
Reference in a new issue