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