Merge branch 'master' into natsoni/federation-utxos-expiry
This commit is contained in:
commit
e8c5d478fa
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"
|
checksum = "1586fa608b1dab41f667475b4a41faec5ba680aee428bfa5de4ea520fdc6e901"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"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]]
|
[[package]]
|
||||||
name = "gbt"
|
name = "gbt"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@ -71,15 +77,15 @@ dependencies = [
|
|||||||
"napi-derive",
|
"napi-derive",
|
||||||
"priority-queue",
|
"priority-queue",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-log",
|
"tracing-log 0.2.0",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.14.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
@ -92,11 +98,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "2.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -114,12 +120,12 @@ checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.7.4"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"winapi",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -145,9 +151,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi"
|
name = "napi"
|
||||||
version = "2.13.2"
|
version = "2.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e"
|
checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"ctor",
|
"ctor",
|
||||||
@ -159,29 +165,29 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-build"
|
name = "napi-build"
|
||||||
version = "2.0.1"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e"
|
checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-derive"
|
name = "napi-derive"
|
||||||
version = "2.13.0"
|
version = "2.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367"
|
checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"napi-derive-backend",
|
"napi-derive-backend",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-derive-backend"
|
name = "napi-derive-backend"
|
||||||
version = "1.0.52"
|
version = "1.0.62"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17"
|
checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -189,14 +195,14 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
"semver",
|
"semver",
|
||||||
"syn 1.0.109",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-sys"
|
name = "napi-sys"
|
||||||
version = "2.2.3"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3"
|
checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
@ -223,9 +229,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
@ -241,11 +247,12 @@ checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "priority-queue"
|
name = "priority-queue"
|
||||||
version = "1.3.2"
|
version = "2.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61"
|
checksum = "509354d8a769e8d0b567d6821b84495c60213162761a732d68ce87c964bd347f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
|
"equivalent",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -320,17 +327,6 @@ version = "1.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.20"
|
version = "2.0.20"
|
||||||
@ -384,7 +380,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.20",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -408,6 +404,17 @@ dependencies = [
|
|||||||
"tracing-core",
|
"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]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@ -423,7 +430,7 @@ dependencies = [
|
|||||||
"thread_local",
|
"thread_local",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"./backend/rust-gbt",
|
"./backend/rust-gbt",
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": false,
|
||||||
"NETWORK": "mainnet",
|
"NETWORK": "mainnet",
|
||||||
"BACKEND": "electrum",
|
"BACKEND": "electrum",
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
|
17
backend/package-lock.json
generated
17
backend/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "3.0.0-dev",
|
"version": "3.0.0-dev",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.24.0",
|
||||||
"@mempool/electrum-client": "1.1.9",
|
"@mempool/electrum-client": "1.1.9",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
"axios": "~1.6.1",
|
"axios": "~1.6.1",
|
||||||
@ -1499,9 +1500,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/cli": {
|
"node_modules/@napi-rs/cli": {
|
||||||
"version": "2.16.1",
|
"version": "2.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
|
||||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
|
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"napi": "scripts/index.js"
|
"napi": "scripts/index.js"
|
||||||
},
|
},
|
||||||
@ -7669,7 +7670,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/cli": "2.16.1"
|
"@napi-rs/cli": "2.18.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
@ -8774,9 +8775,9 @@
|
|||||||
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
|
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
|
||||||
},
|
},
|
||||||
"@napi-rs/cli": {
|
"@napi-rs/cli": {
|
||||||
"version": "2.16.1",
|
"version": "2.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
|
||||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA=="
|
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA=="
|
||||||
},
|
},
|
||||||
"@noble/hashes": {
|
"@noble/hashes": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
@ -12702,7 +12703,7 @@
|
|||||||
"rust-gbt": {
|
"rust-gbt": {
|
||||||
"version": "file:rust-gbt",
|
"version": "file:rust-gbt",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@napi-rs/cli": "2.16.1"
|
"@napi-rs/cli": "2.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"safe-buffer": {
|
"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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
priority-queue = "1.3.2"
|
priority-queue = "2.0.2"
|
||||||
bytes = "1.4.0"
|
bytes = "1.4.0"
|
||||||
napi = { version = "2.13.2", features = ["napi8", "tokio_rt"] }
|
napi = { version = "2.16.0", features = ["napi8", "tokio_rt"] }
|
||||||
napi-derive = "2.13.0"
|
napi-derive = "2.16.0"
|
||||||
bytemuck = "1.13.1"
|
bytemuck = "1.13.1"
|
||||||
tracing = "0.1.36"
|
tracing = "0.1.36"
|
||||||
tracing-log = "0.1.3"
|
tracing-log = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"]}
|
tracing-subscriber = { version = "0.3.15", features = ["env-filter"]}
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
napi-build = "2.0.1"
|
napi-build = "2.1.2"
|
||||||
|
@ -237,6 +237,49 @@ switch (platform) {
|
|||||||
loadError = e
|
loadError = e
|
||||||
}
|
}
|
||||||
break
|
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:
|
default:
|
||||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
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",
|
"version": "3.0.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/cli": "2.16.1"
|
"@napi-rs/cli": "2.18.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/cli": {
|
"node_modules/@napi-rs/cli": {
|
||||||
"version": "2.16.1",
|
"version": "2.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
|
||||||
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
|
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"napi": "scripts/index.js"
|
"napi": "scripts/index.js"
|
||||||
},
|
},
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/cli": "2.16.1"
|
"@napi-rs/cli": "2.18.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
|
"OFFICIAL": false,
|
||||||
"NETWORK": "__MEMPOOL_NETWORK__",
|
"NETWORK": "__MEMPOOL_NETWORK__",
|
||||||
"BACKEND": "__MEMPOOL_BACKEND__",
|
"BACKEND": "__MEMPOOL_BACKEND__",
|
||||||
"BLOCKS_SUMMARIES_INDEXING": true,
|
"BLOCKS_SUMMARIES_INDEXING": true,
|
||||||
@ -79,7 +80,8 @@
|
|||||||
"USERNAME": "__DATABASE_USERNAME__",
|
"USERNAME": "__DATABASE_USERNAME__",
|
||||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||||
"PID_DIR": "__DATABASE_PID_FILE__",
|
"PID_DIR": "__DATABASE_PID_FILE__",
|
||||||
"TIMEOUT": 3000
|
"TIMEOUT": 3000,
|
||||||
|
"POOL_SIZE": 100
|
||||||
},
|
},
|
||||||
"SYSLOG": {
|
"SYSLOG": {
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
|
@ -14,6 +14,7 @@ describe('Mempool Backend Config', () => {
|
|||||||
|
|
||||||
expect(config.MEMPOOL).toStrictEqual({
|
expect(config.MEMPOOL).toStrictEqual({
|
||||||
ENABLED: true,
|
ENABLED: true,
|
||||||
|
OFFICIAL: false,
|
||||||
NETWORK: 'mainnet',
|
NETWORK: 'mainnet',
|
||||||
BACKEND: 'none',
|
BACKEND: 'none',
|
||||||
BLOCKS_SUMMARIES_INDEXING: false,
|
BLOCKS_SUMMARIES_INDEXING: false,
|
||||||
@ -93,7 +94,8 @@ describe('Mempool Backend Config', () => {
|
|||||||
USERNAME: 'mempool',
|
USERNAME: 'mempool',
|
||||||
PASSWORD: 'mempool',
|
PASSWORD: 'mempool',
|
||||||
TIMEOUT: 180000,
|
TIMEOUT: 180000,
|
||||||
PID_DIR: ''
|
PID_DIR: '',
|
||||||
|
POOL_SIZE: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.SYSLOG).toStrictEqual({
|
expect(config.SYSLOG).toStrictEqual({
|
||||||
|
@ -29,6 +29,7 @@ export interface AbstractBitcoinApi {
|
|||||||
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
||||||
|
|
||||||
startHealthChecks(): void;
|
startHealthChecks(): void;
|
||||||
|
getHealthStatus(): HealthCheckHost[];
|
||||||
}
|
}
|
||||||
export interface BitcoinRpcCredentials {
|
export interface BitcoinRpcCredentials {
|
||||||
host: string;
|
host: string;
|
||||||
@ -38,3 +39,15 @@ export interface BitcoinRpcCredentials {
|
|||||||
timeout: number;
|
timeout: number;
|
||||||
cookie?: string;
|
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 * 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 { IBitcoinApi } from './bitcoin-api.interface';
|
||||||
import { IEsploraApi } from './esplora-api.interface';
|
import { IEsploraApi } from './esplora-api.interface';
|
||||||
import blocks from '../blocks';
|
import blocks from '../blocks';
|
||||||
@ -382,6 +382,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public startHealthChecks(): void {};
|
public startHealthChecks(): void {};
|
||||||
|
|
||||||
|
public getHealthStatus() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BitcoinApi;
|
export default BitcoinApi;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse, isAxiosError } from 'axios';
|
||||||
import http from 'http';
|
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 { IEsploraApi } from './esplora-api.interface';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import { Common } from '../common';
|
import { Common } from '../common';
|
||||||
@ -10,6 +10,7 @@ interface FailoverHost {
|
|||||||
host: string,
|
host: string,
|
||||||
rtts: number[],
|
rtts: number[],
|
||||||
rtt: number,
|
rtt: number,
|
||||||
|
timedOut?: boolean,
|
||||||
failures: number,
|
failures: number,
|
||||||
latestHeight?: number,
|
latestHeight?: number,
|
||||||
socket?: boolean,
|
socket?: boolean,
|
||||||
@ -17,6 +18,7 @@ interface FailoverHost {
|
|||||||
unreachable?: boolean,
|
unreachable?: boolean,
|
||||||
preferred?: boolean,
|
preferred?: boolean,
|
||||||
checked: boolean,
|
checked: boolean,
|
||||||
|
lastChecked?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
class FailoverRouter {
|
class FailoverRouter {
|
||||||
@ -108,14 +110,20 @@ class FailoverRouter {
|
|||||||
host.rtts = [];
|
host.rtts = [];
|
||||||
host.rtt = Infinity;
|
host.rtt = Infinity;
|
||||||
}
|
}
|
||||||
|
host.timedOut = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
host.outOfSync = true;
|
host.outOfSync = true;
|
||||||
host.unreachable = true;
|
host.unreachable = true;
|
||||||
host.rtts = [];
|
host.rtts = [];
|
||||||
host.rtt = Infinity;
|
host.rtt = Infinity;
|
||||||
|
if (isAxiosError(e) && (e.code === 'ECONNABORTED' || e.code === 'ETIMEDOUT')) {
|
||||||
|
host.timedOut = true;
|
||||||
|
} else {
|
||||||
|
host.timedOut = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
host.checked = true;
|
host.checked = true;
|
||||||
|
host.lastChecked = Date.now();
|
||||||
|
|
||||||
// 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
|
||||||
const rankOrder = this.sortHosts();
|
const rankOrder = this.sortHosts();
|
||||||
@ -143,7 +151,7 @@ class FailoverRouter {
|
|||||||
|
|
||||||
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
|
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
|
||||||
const heightStatus = !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅'));
|
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[] {
|
private updateFallback(): FailoverHost[] {
|
||||||
@ -157,7 +165,7 @@ class FailoverRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sort hosts by connection quality, and update default fallback
|
// sort hosts by connection quality, and update default fallback
|
||||||
private sortHosts(): FailoverHost[] {
|
public sortHosts(): FailoverHost[] {
|
||||||
// sort by connection quality
|
// sort by connection quality
|
||||||
return this.hosts.slice().sort((a, b) => {
|
return this.hosts.slice().sort((a, b) => {
|
||||||
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
|
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
|
||||||
@ -342,6 +350,24 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
public startHealthChecks(): void {
|
public startHealthChecks(): void {
|
||||||
this.failoverRouter.startHealthChecks();
|
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;
|
export default ElectrsApi;
|
||||||
|
@ -26,6 +26,7 @@ import mempool from './mempool';
|
|||||||
import statistics from './statistics/statistics';
|
import statistics from './statistics/statistics';
|
||||||
import accelerationCosts from './acceleration';
|
import accelerationCosts from './acceleration';
|
||||||
import accelerationRepository from '../repositories/AccelerationRepository';
|
import accelerationRepository from '../repositories/AccelerationRepository';
|
||||||
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
|
|
||||||
interface AddressTransactions {
|
interface AddressTransactions {
|
||||||
mempool: MempoolTransactionExtended[],
|
mempool: MempoolTransactionExtended[],
|
||||||
@ -39,6 +40,7 @@ const wantable = [
|
|||||||
'mempool-blocks',
|
'mempool-blocks',
|
||||||
'live-2h-chart',
|
'live-2h-chart',
|
||||||
'stats',
|
'stats',
|
||||||
|
'tomahawk',
|
||||||
];
|
];
|
||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
@ -123,7 +125,7 @@ class WebsocketHandler {
|
|||||||
for (const sub of wantable) {
|
for (const sub of wantable) {
|
||||||
const key = `want-${sub}`;
|
const key = `want-${sub}`;
|
||||||
const wants = parsedMessage.data.includes(sub);
|
const wants = parsedMessage.data.includes(sub);
|
||||||
if (wants && client['wants'] && !client[key]) {
|
if (wants && !client[key]) {
|
||||||
wantNow[key] = true;
|
wantNow[key] = true;
|
||||||
}
|
}
|
||||||
client[key] = wants;
|
client[key] = wants;
|
||||||
@ -147,6 +149,10 @@ class WebsocketHandler {
|
|||||||
response['da'] = this.socketData['da'];
|
response['da'] = this.socketData['da'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wantNow['want-tomahawk']) {
|
||||||
|
response['tomahawk'] = JSON.stringify(bitcoinApi.getHealthStatus());
|
||||||
|
}
|
||||||
|
|
||||||
if (parsedMessage && parsedMessage['track-tx']) {
|
if (parsedMessage && parsedMessage['track-tx']) {
|
||||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
|
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
|
||||||
client['track-tx'] = parsedMessage['track-tx'];
|
client['track-tx'] = parsedMessage['track-tx'];
|
||||||
@ -546,6 +552,10 @@ class WebsocketHandler {
|
|||||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client['want-tomahawk']) {
|
||||||
|
response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
|
||||||
|
}
|
||||||
|
|
||||||
if (client['track-mempool-tx']) {
|
if (client['track-mempool-tx']) {
|
||||||
const tx = newTransactions.find((t) => t.txid === client['track-mempool-tx']);
|
const tx = newTransactions.find((t) => t.txid === client['track-mempool-tx']);
|
||||||
if (tx) {
|
if (tx) {
|
||||||
@ -909,6 +919,10 @@ class WebsocketHandler {
|
|||||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client['want-tomahawk']) {
|
||||||
|
response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
|
||||||
|
}
|
||||||
|
|
||||||
if (client['track-tx']) {
|
if (client['track-tx']) {
|
||||||
const trackTxid = client['track-tx'];
|
const trackTxid = client['track-tx'];
|
||||||
if (trackTxid && confirmedTxids[trackTxid]) {
|
if (trackTxid && confirmedTxids[trackTxid]) {
|
||||||
|
@ -5,6 +5,7 @@ const configFromFile = require(
|
|||||||
interface IConfig {
|
interface IConfig {
|
||||||
MEMPOOL: {
|
MEMPOOL: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
|
OFFICIAL: boolean;
|
||||||
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
||||||
BACKEND: 'esplora' | 'electrum' | 'none';
|
BACKEND: 'esplora' | 'electrum' | 'none';
|
||||||
HTTP_PORT: number;
|
HTTP_PORT: number;
|
||||||
@ -103,6 +104,7 @@ interface IConfig {
|
|||||||
PASSWORD: string;
|
PASSWORD: string;
|
||||||
TIMEOUT: number;
|
TIMEOUT: number;
|
||||||
PID_DIR: string;
|
PID_DIR: string;
|
||||||
|
POOL_SIZE: number;
|
||||||
};
|
};
|
||||||
SYSLOG: {
|
SYSLOG: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
@ -161,6 +163,7 @@ interface IConfig {
|
|||||||
const defaults: IConfig = {
|
const defaults: IConfig = {
|
||||||
'MEMPOOL': {
|
'MEMPOOL': {
|
||||||
'ENABLED': true,
|
'ENABLED': true,
|
||||||
|
'OFFICIAL': false,
|
||||||
'NETWORK': 'mainnet',
|
'NETWORK': 'mainnet',
|
||||||
'BACKEND': 'none',
|
'BACKEND': 'none',
|
||||||
'HTTP_PORT': 8999,
|
'HTTP_PORT': 8999,
|
||||||
@ -240,6 +243,7 @@ const defaults: IConfig = {
|
|||||||
'PASSWORD': 'mempool',
|
'PASSWORD': 'mempool',
|
||||||
'TIMEOUT': 180000,
|
'TIMEOUT': 180000,
|
||||||
'PID_DIR': '',
|
'PID_DIR': '',
|
||||||
|
'POOL_SIZE': 100,
|
||||||
},
|
},
|
||||||
'SYSLOG': {
|
'SYSLOG': {
|
||||||
'ENABLED': true,
|
'ENABLED': true,
|
||||||
|
@ -21,7 +21,7 @@ import { execSync } from 'child_process';
|
|||||||
database: config.DATABASE.DATABASE,
|
database: config.DATABASE.DATABASE,
|
||||||
user: config.DATABASE.USERNAME,
|
user: config.DATABASE.USERNAME,
|
||||||
password: config.DATABASE.PASSWORD,
|
password: config.DATABASE.PASSWORD,
|
||||||
connectionLimit: 10,
|
connectionLimit: config.DATABASE.POOL_SIZE,
|
||||||
supportBigNumbers: true,
|
supportBigNumbers: true,
|
||||||
timezone: '+00:00',
|
timezone: '+00:00',
|
||||||
};
|
};
|
||||||
|
@ -155,11 +155,17 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Common.isLiquid()) {
|
if (Common.isLiquid()) {
|
||||||
try {
|
const refreshIcons = () => {
|
||||||
icons.loadIcons();
|
try {
|
||||||
} catch (e) {
|
icons.loadIcons();
|
||||||
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
|
} 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();
|
priceUpdater.$run();
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"NETWORK": "__MEMPOOL_NETWORK__",
|
"NETWORK": "__MEMPOOL_NETWORK__",
|
||||||
"BACKEND": "__MEMPOOL_BACKEND__",
|
"BACKEND": "__MEMPOOL_BACKEND__",
|
||||||
"ENABLED": __MEMPOOL_ENABLED__,
|
"ENABLED": __MEMPOOL_ENABLED__,
|
||||||
|
"OFFICIAL": __MEMPOOL_OFFICIAL__,
|
||||||
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
||||||
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
||||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||||
@ -79,7 +80,8 @@
|
|||||||
"USERNAME": "__DATABASE_USERNAME__",
|
"USERNAME": "__DATABASE_USERNAME__",
|
||||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||||
"TIMEOUT": __DATABASE_TIMEOUT__,
|
"TIMEOUT": __DATABASE_TIMEOUT__,
|
||||||
"PID_DIR": "__DATABASE_PID_DIR__"
|
"PID_DIR": "__DATABASE_PID_DIR__",
|
||||||
|
"POOL_SIZE": __DATABASE_POOL_SIZE__
|
||||||
},
|
},
|
||||||
"SYSLOG": {
|
"SYSLOG": {
|
||||||
"ENABLED": __SYSLOG_ENABLED__,
|
"ENABLED": __SYSLOG_ENABLED__,
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
|
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
|
||||||
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
|
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
|
||||||
__MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
|
__MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
|
||||||
|
__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
|
||||||
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
|
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
|
||||||
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
||||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
__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_PASSWORD__=${DATABASE_PASSWORD:=mempool}
|
||||||
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
|
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
|
||||||
__DATABASE_PID_DIR__=${DATABASE_PID_DIR:=""}
|
__DATABASE_PID_DIR__=${DATABASE_PID_DIR:=""}
|
||||||
|
__DATABASE_POOL_SIZE__=${DATABASE_POOL_SIZE:=100}
|
||||||
|
|
||||||
# SYSLOG
|
# SYSLOG
|
||||||
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
|
__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_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_BACKEND__!${__MEMPOOL_BACKEND__}!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_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_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_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
|
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_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!__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_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
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#backend
|
#backend
|
||||||
cp ./docker/backend/* ./backend/
|
cp -r ./docker/backend/* ./backend/
|
||||||
|
|
||||||
#geoip-data
|
#geoip-data
|
||||||
mkdir -p ./backend/GeoIP/
|
mkdir -p ./backend/GeoIP/
|
||||||
@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
|
|||||||
cp ./docker/frontend/* ./frontend
|
cp ./docker/frontend/* ./frontend
|
||||||
cp ./nginx.conf ./frontend/
|
cp ./nginx.conf ./frontend/
|
||||||
cp ./nginx-mempool.conf ./frontend/
|
cp ./nginx-mempool.conf ./frontend/
|
||||||
sed -i "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
|
sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
|
||||||
sed -i "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
|
sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
|
||||||
sed -i "s/user nobody;//g" ./frontend/nginx.conf
|
sed -i"" -e "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"" -e "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}: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 { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||||
import { ClockComponent } from './components/clock/clock.component';
|
import { ClockComponent } from './components/clock/clock.component';
|
||||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||||
|
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||||
|
|
||||||
const browserWindow = window || {};
|
const browserWindow = window || {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -26,6 +27,14 @@ let routes: Routes = [
|
|||||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'wallet',
|
||||||
|
children: [],
|
||||||
|
component: AddressGroupComponent,
|
||||||
|
data: {
|
||||||
|
networkSpecific: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
data: { networks: ['bitcoin', 'liquid'] },
|
data: { networks: ['bitcoin', 'liquid'] },
|
||||||
@ -61,6 +70,14 @@ let routes: Routes = [
|
|||||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'wallet',
|
||||||
|
children: [],
|
||||||
|
component: AddressGroupComponent,
|
||||||
|
data: {
|
||||||
|
networkSpecific: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
data: { networks: ['bitcoin', 'liquid'] },
|
data: { networks: ['bitcoin', 'liquid'] },
|
||||||
@ -88,6 +105,14 @@ let routes: Routes = [
|
|||||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'wallet',
|
||||||
|
children: [],
|
||||||
|
component: AddressGroupComponent,
|
||||||
|
data: {
|
||||||
|
networkSpecific: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'preview',
|
path: 'preview',
|
||||||
children: [
|
children: [
|
||||||
@ -168,6 +193,14 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'wallet',
|
||||||
|
children: [],
|
||||||
|
component: AddressGroupComponent,
|
||||||
|
data: {
|
||||||
|
networkSpecific: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
data: { networks: ['bitcoin', 'liquid'] },
|
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),
|
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||||
data: { preload: true },
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'wallet',
|
||||||
|
children: [],
|
||||||
|
component: AddressGroupComponent,
|
||||||
|
data: {
|
||||||
|
networkSpecific: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'preview',
|
path: 'preview',
|
||||||
children: [
|
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>
|
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</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"
|
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
||||||
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||||
{{ block.extras.pool.name}}</a>
|
{{ block.extras.pool.name}}</a>
|
||||||
|
@ -166,7 +166,7 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.hide {
|
.hide {
|
||||||
opacity: 0;
|
opacity: 0.4;
|
||||||
pointer-events : none;
|
pointer-events : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<footer class="footer">
|
<footer class="footer" [class.inline-footer]="inline">
|
||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
|
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
|
||||||
<div class="col d-none d-sm-block">
|
<div class="col d-none d-sm-block">
|
||||||
|
@ -6,6 +6,12 @@
|
|||||||
background-color: #1d1f31;
|
background-color: #1d1f31;
|
||||||
box-shadow: 15px 15px 15px 15px #000;
|
box-shadow: 15px 15px 15px 15px #000;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
|
&.inline-footer {
|
||||||
|
position: relative;
|
||||||
|
bottom: unset;
|
||||||
|
top: -44px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-text {
|
.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 { StateService } from '../../services/state.service';
|
||||||
import { Observable, combineLatest } from 'rxjs';
|
import { Observable, combineLatest } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
@ -23,6 +23,8 @@ interface MempoolInfoData {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class FooterComponent implements OnInit {
|
export class FooterComponent implements OnInit {
|
||||||
|
@Input() inline = false;
|
||||||
|
|
||||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||||
vBytesPerSecondLimit = 1667;
|
vBytesPerSecondLimit = 1667;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks" [class.minimal]="minimal">
|
<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="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'" *ngIf="(difficultyAdjustments$ | async) as da;">
|
||||||
<div class="flashing">
|
<div class="flashing" *ngIf="(mempoolBlocks$ | async) as mempoolBlocks">
|
||||||
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
|
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks" let-i="index" [ngForTrackBy]="trackByFn">
|
||||||
<div
|
<div
|
||||||
*ngIf="minimal && spotlight > 0 && spotlight === i + 1"
|
*ngIf="minimal && spotlight > 0 && spotlight === i + 1"
|
||||||
class="spotlight-bottom"
|
class="spotlight-bottom"
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
|
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 { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Router } from '@angular/router';
|
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 { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||||
import { specialBlocks } from '../../app.constants';
|
import { specialBlocks } from '../../app.constants';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
@ -86,7 +86,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
private location: Location
|
private location: Location,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
enabledMiningInfoIfNeeded(url) {
|
enabledMiningInfoIfNeeded(url) {
|
||||||
@ -129,50 +129,44 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.mempoolBlocks$ = merge(
|
this.mempoolBlocks$ = combineLatest([
|
||||||
of(true),
|
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
|
||||||
fromEvent(window, 'resize')
|
this.stateService.mempoolBlocks$
|
||||||
)
|
.pipe(
|
||||||
.pipe(
|
map((mempoolBlocks) => {
|
||||||
switchMap(() => combineLatest([
|
if (!mempoolBlocks.length) {
|
||||||
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
|
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
|
||||||
this.stateService.mempoolBlocks$
|
}
|
||||||
.pipe(
|
return mempoolBlocks;
|
||||||
map((mempoolBlocks) => {
|
}),
|
||||||
if (!mempoolBlocks.length) {
|
)
|
||||||
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
|
]).pipe(
|
||||||
}
|
map(([lastBlock, mempoolBlocks]) => {
|
||||||
return 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;
|
||||||
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);
|
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
|
||||||
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
||||||
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
||||||
|
|
||||||
this.now = Date.now();
|
this.now = Date.now();
|
||||||
|
|
||||||
this.updateMempoolBlockStyles();
|
this.updateMempoolBlockStyles();
|
||||||
this.calculateTransactionPosition();
|
this.calculateTransactionPosition();
|
||||||
|
|
||||||
return this.mempoolBlocks;
|
return this.mempoolBlocks;
|
||||||
}),
|
}),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
|
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
|
||||||
if (this.mempoolWidth !== width) {
|
if (this.mempoolWidth !== width) {
|
||||||
this.mempoolWidth = width;
|
this.mempoolWidth = width;
|
||||||
this.widthChange.emit(this.mempoolWidth);
|
this.widthChange.emit(this.mempoolWidth);
|
||||||
this.cd.markForCheck();
|
}
|
||||||
}
|
})
|
||||||
})
|
);
|
||||||
);
|
|
||||||
|
|
||||||
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
|
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
|
||||||
.pipe(
|
.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">
|
<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>
|
<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>
|
<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;
|
display: block;
|
||||||
margin: 5px 0;
|
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 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">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>
|
<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;
|
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) {
|
@media (max-width: 992px) {
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@ -373,6 +408,10 @@ h3 {
|
|||||||
#disclaimer table {
|
#disclaimer table {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#enterprise-cta-mobile {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
|
@ -30,6 +30,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
|||||||
officialMempoolInstance: boolean;
|
officialMempoolInstance: boolean;
|
||||||
auditEnabled: boolean;
|
auditEnabled: boolean;
|
||||||
mobileViewport: boolean = false;
|
mobileViewport: boolean = false;
|
||||||
|
showMobileEnterpriseUpsell: boolean = true;
|
||||||
timeLtrSubscription: Subscription;
|
timeLtrSubscription: Subscription;
|
||||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SafeResourceUrl } from '@angular/platform-browser';
|
||||||
import { ILoadingIndicators } from '../services/state.service';
|
import { ILoadingIndicators } from '../services/state.service';
|
||||||
import { Transaction } from './electrs.interface';
|
import { Transaction } from './electrs.interface';
|
||||||
import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface';
|
import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface';
|
||||||
@ -120,4 +121,19 @@ export interface Recommendedfees {
|
|||||||
hourFee: number;
|
hourFee: number;
|
||||||
minimumFee: number;
|
minimumFee: number;
|
||||||
economyFee: 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 { 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 { 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 { 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 = [
|
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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild(routes)
|
RouterModule.forChild(routes)
|
||||||
|
@ -6,10 +6,13 @@ import { SharedModule } from './shared/shared.module';
|
|||||||
|
|
||||||
import { StartComponent } from './components/start/start.component';
|
import { StartComponent } from './components/start/start.component';
|
||||||
import { AddressComponent } from './components/address/address.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 { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||||
import { CalculatorComponent } from './components/calculator/calculator.component';
|
import { CalculatorComponent } from './components/calculator/calculator.component';
|
||||||
import { BlocksList } from './components/blocks-list/blocks-list.component';
|
import { BlocksList } from './components/blocks-list/blocks-list.component';
|
||||||
import { RbfList } from './components/rbf-list/rbf-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 || {};
|
const browserWindow = window || {};
|
||||||
// @ts-ignore
|
// @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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild(routes)
|
RouterModule.forChild(routes)
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
||||||
import { Transaction } from '../interfaces/electrs.interface';
|
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 { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||||
import { Router, NavigationStart } from '@angular/router';
|
import { Router, NavigationStart } from '@angular/router';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
|
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
|
||||||
import { ApiService } from './api.service';
|
|
||||||
import { ActiveFilter } from '../shared/filters.utils';
|
import { ActiveFilter } from '../shared/filters.utils';
|
||||||
|
|
||||||
export interface MarkBlockState {
|
export interface MarkBlockState {
|
||||||
@ -119,6 +118,7 @@ export class StateService {
|
|||||||
mempoolTransactions$ = new Subject<Transaction>();
|
mempoolTransactions$ = new Subject<Transaction>();
|
||||||
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>();
|
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>();
|
||||||
mempoolRemovedTransactions$ = new Subject<Transaction>();
|
mempoolRemovedTransactions$ = new Subject<Transaction>();
|
||||||
|
multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>();
|
||||||
blockTransactions$ = new Subject<Transaction>();
|
blockTransactions$ = new Subject<Transaction>();
|
||||||
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
||||||
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
|
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
|
||||||
@ -129,6 +129,7 @@ export class StateService {
|
|||||||
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
|
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
|
||||||
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
|
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
|
||||||
chainTip$ = new ReplaySubject<number>(-1);
|
chainTip$ = new ReplaySubject<number>(-1);
|
||||||
|
serverHealth$ = new Subject<HealthCheckHost[]>();
|
||||||
|
|
||||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ export class WebsocketService {
|
|||||||
private isTrackingRbf: 'all' | 'fullRbf' | false = false;
|
private isTrackingRbf: 'all' | 'fullRbf' | false = false;
|
||||||
private isTrackingRbfSummary = false;
|
private isTrackingRbfSummary = false;
|
||||||
private isTrackingAddress: string | false = false;
|
private isTrackingAddress: string | false = false;
|
||||||
|
private isTrackingAddresses: string[] | false = false;
|
||||||
private trackingMempoolBlock: number;
|
private trackingMempoolBlock: number;
|
||||||
private latestGitCommit = '';
|
private latestGitCommit = '';
|
||||||
private onlineCheckTimeout: number;
|
private onlineCheckTimeout: number;
|
||||||
@ -126,6 +127,9 @@ export class WebsocketService {
|
|||||||
if (this.isTrackingAddress) {
|
if (this.isTrackingAddress) {
|
||||||
this.startTrackAddress(this.isTrackingAddress);
|
this.startTrackAddress(this.isTrackingAddress);
|
||||||
}
|
}
|
||||||
|
if (this.isTrackingAddresses) {
|
||||||
|
this.startTrackAddresses(this.isTrackingAddresses);
|
||||||
|
}
|
||||||
this.stateService.connectionState$.next(2);
|
this.stateService.connectionState$.next(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,6 +179,16 @@ export class WebsocketService {
|
|||||||
this.isTrackingAddress = false;
|
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) {
|
startTrackAsset(asset: string) {
|
||||||
this.websocketSubject.next({ 'track-asset': asset });
|
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']) {
|
if (response['block-transactions']) {
|
||||||
response['block-transactions'].forEach((addressTransaction: Transaction) => {
|
response['block-transactions'].forEach((addressTransaction: Transaction) => {
|
||||||
this.stateService.blockTransactions$.next(addressTransaction);
|
this.stateService.blockTransactions$.next(addressTransaction);
|
||||||
@ -415,6 +433,10 @@ export class WebsocketService {
|
|||||||
this.stateService.previousRetarget$.next(response.previousRetarget);
|
this.stateService.previousRetarget$.next(response.previousRetarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response['tomahawk']) {
|
||||||
|
this.stateService.serverHealth$.next(response['tomahawk']);
|
||||||
|
}
|
||||||
|
|
||||||
if (response['git-commit']) {
|
if (response['git-commit']) {
|
||||||
this.stateService.backendInfo$.next(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">
|
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
|
||||||
<ng-container *ngIf="link">
|
<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>
|
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -9,6 +9,7 @@ import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy } from '@a
|
|||||||
export class TruncateComponent {
|
export class TruncateComponent {
|
||||||
@Input() text: string;
|
@Input() text: string;
|
||||||
@Input() link: any = null;
|
@Input() link: any = null;
|
||||||
|
@Input() external: boolean = false;
|
||||||
@Input() lastChars: number = 4;
|
@Input() lastChars: number = 4;
|
||||||
@Input() maxWidth: number = null;
|
@Input() maxWidth: number = null;
|
||||||
@Input() inline: boolean = false;
|
@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 { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
|
||||||
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
|
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
|
||||||
import { AddressComponent } from '../components/address/address.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 { SearchFormComponent } from '../components/search-form/search-form.component';
|
||||||
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
|
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
|
||||||
import { FooterComponent } from '../components/footer/footer.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 { AssetsComponent } from '../components/assets/assets.component';
|
||||||
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
|
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
|
||||||
import { StatusViewComponent } from '../components/status-view/status-view.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 { FeesBoxComponent } from '../components/fees-box/fees-box.component';
|
||||||
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
|
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
|
||||||
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
|
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
|
||||||
@ -145,12 +148,15 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
BlockFiltersComponent,
|
BlockFiltersComponent,
|
||||||
TransactionsListComponent,
|
TransactionsListComponent,
|
||||||
AddressComponent,
|
AddressComponent,
|
||||||
|
AddressGroupComponent,
|
||||||
SearchFormComponent,
|
SearchFormComponent,
|
||||||
AddressLabelsComponent,
|
AddressLabelsComponent,
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
AssetComponent,
|
AssetComponent,
|
||||||
AssetsComponent,
|
AssetsComponent,
|
||||||
StatusViewComponent,
|
StatusViewComponent,
|
||||||
|
ServerHealthComponent,
|
||||||
|
ServerStatusComponent,
|
||||||
FeesBoxComponent,
|
FeesBoxComponent,
|
||||||
DifficultyComponent,
|
DifficultyComponent,
|
||||||
DifficultyMiningComponent,
|
DifficultyMiningComponent,
|
||||||
@ -271,12 +277,15 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
BlockFiltersComponent,
|
BlockFiltersComponent,
|
||||||
TransactionsListComponent,
|
TransactionsListComponent,
|
||||||
AddressComponent,
|
AddressComponent,
|
||||||
|
AddressGroupComponent,
|
||||||
SearchFormComponent,
|
SearchFormComponent,
|
||||||
AddressLabelsComponent,
|
AddressLabelsComponent,
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
AssetComponent,
|
AssetComponent,
|
||||||
AssetsComponent,
|
AssetsComponent,
|
||||||
StatusViewComponent,
|
StatusViewComponent,
|
||||||
|
ServerHealthComponent,
|
||||||
|
ServerStatusComponent,
|
||||||
FeesBoxComponent,
|
FeesBoxComponent,
|
||||||
DifficultyComponent,
|
DifficultyComponent,
|
||||||
DifficultyMiningComponent,
|
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": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"NETWORK": "bisq",
|
"NETWORK": "bisq",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
"HTTP_PORT": 8996,
|
"HTTP_PORT": 8996,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"NETWORK": "liquid",
|
"NETWORK": "liquid",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
"HTTP_PORT": 8998,
|
"HTTP_PORT": 8998,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"NETWORK": "liquid",
|
"NETWORK": "liquid",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
"HTTP_PORT": 8994,
|
"HTTP_PORT": 8994,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"NETWORK": "mainnet",
|
"NETWORK": "mainnet",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"NETWORK": "mainnet",
|
"NETWORK": "mainnet",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
"HTTP_PORT": 8999,
|
"HTTP_PORT": 8999,
|
||||||
@ -21,7 +22,8 @@
|
|||||||
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
"ALLOW_UNREACHABLE": true,
|
"ALLOW_UNREACHABLE": true,
|
||||||
"PRICE_UPDATES_PER_HOUR": 12
|
"PRICE_UPDATES_PER_HOUR": 12,
|
||||||
|
"MAX_TRACKED_ADDRESSES": 10
|
||||||
},
|
},
|
||||||
"SYSLOG" : {
|
"SYSLOG" : {
|
||||||
"MIN_PRIORITY": "debug"
|
"MIN_PRIORITY": "debug"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"NETWORK": "signet",
|
"NETWORK": "signet",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"NETWORK": "signet",
|
"NETWORK": "signet",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
"HTTP_PORT": 8995,
|
"HTTP_PORT": 8995,
|
||||||
@ -14,7 +15,8 @@
|
|||||||
"POLL_RATE_MS": 1000,
|
"POLL_RATE_MS": 1000,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
"ALLOW_UNREACHABLE": true
|
"ALLOW_UNREACHABLE": true,
|
||||||
|
"MAX_TRACKED_ADDRESSES": 10
|
||||||
},
|
},
|
||||||
"SYSLOG" : {
|
"SYSLOG" : {
|
||||||
"MIN_PRIORITY": "debug"
|
"MIN_PRIORITY": "debug"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"NETWORK": "testnet",
|
"NETWORK": "testnet",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
|
"OFFICIAL": true,
|
||||||
"NETWORK": "testnet",
|
"NETWORK": "testnet",
|
||||||
"BACKEND": "esplora",
|
"BACKEND": "esplora",
|
||||||
"HTTP_PORT": 8997,
|
"HTTP_PORT": 8997,
|
||||||
@ -14,7 +15,8 @@
|
|||||||
"POLL_RATE_MS": 1000,
|
"POLL_RATE_MS": 1000,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
"DISK_CACHE_BLOCK_INTERVAL": 1,
|
||||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
"ALLOW_UNREACHABLE": true
|
"ALLOW_UNREACHABLE": true,
|
||||||
|
"MAX_TRACKED_ADDRESSES": 10
|
||||||
},
|
},
|
||||||
"SYSLOG" : {
|
"SYSLOG" : {
|
||||||
"MIN_PRIORITY": "debug"
|
"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/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/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;
|
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…
x
Reference in New Issue
Block a user