Merge branch 'master' into nymkappa/scan-closed-channel-no-mempool
This commit is contained in:
		
						commit
						d39261680e
					
				
							
								
								
									
										9
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@ -9,7 +9,7 @@ jobs:
 | 
			
		||||
    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        node: ["16.16.0", "18.14.1"]
 | 
			
		||||
        node: ["16", "17", "18"]
 | 
			
		||||
        flavor: ["dev", "prod"]
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
    runs-on: "ubuntu-latest"
 | 
			
		||||
@ -27,6 +27,11 @@ jobs:
 | 
			
		||||
          node-version: ${{ matrix.node }}
 | 
			
		||||
          registry-url: "https://registry.npmjs.org"
 | 
			
		||||
 | 
			
		||||
      - name: Install 1.70.x Rust toolchain
 | 
			
		||||
        uses: actions-rs/toolchain@v1
 | 
			
		||||
        with:
 | 
			
		||||
            toolchain: 1.70
 | 
			
		||||
 | 
			
		||||
      - name: Install
 | 
			
		||||
        if: ${{ matrix.flavor == 'dev'}}
 | 
			
		||||
        run: npm ci
 | 
			
		||||
@ -55,7 +60,7 @@ jobs:
 | 
			
		||||
    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        node: ["16.16.0", "18.14.1"]
 | 
			
		||||
        node: ["16", "17", "18"]
 | 
			
		||||
        flavor: ["dev", "prod"]
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
    runs-on: "ubuntu-latest"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -5,3 +5,4 @@ backend/mempool-config.json
 | 
			
		||||
*.swp
 | 
			
		||||
frontend/src/resources/config.template.js
 | 
			
		||||
frontend/src/resources/config.js
 | 
			
		||||
target
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "editor.tabSize": 2,
 | 
			
		||||
  "typescript.preferences.importModuleSpecifier": "relative",
 | 
			
		||||
  "typescript.tsdk": "./backend/node_modules/typescript/lib"
 | 
			
		||||
  "typescript.tsdk": "./backend/node_modules/typescript/lib",
 | 
			
		||||
  "rust-analyzer.procMacro.ignored": { "napi-derive": ["napi"] }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										533
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										533
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,533 @@
 | 
			
		||||
# This file is automatically @generated by Cargo.
 | 
			
		||||
# It is not intended for manual editing.
 | 
			
		||||
version = 3
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "aho-corasick"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "memchr",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "autocfg"
 | 
			
		||||
version = "1.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "bitflags"
 | 
			
		||||
version = "2.3.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "bytemuck"
 | 
			
		||||
version = "1.13.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "bytes"
 | 
			
		||||
version = "1.4.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cfg-if"
 | 
			
		||||
version = "1.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "convert_case"
 | 
			
		||||
version = "0.6.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "unicode-segmentation",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ctor"
 | 
			
		||||
version = "0.2.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1586fa608b1dab41f667475b4a41faec5ba680aee428bfa5de4ea520fdc6e901"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.20",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gbt"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bytemuck",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "napi",
 | 
			
		||||
 "napi-build",
 | 
			
		||||
 "napi-derive",
 | 
			
		||||
 "priority-queue",
 | 
			
		||||
 "tracing",
 | 
			
		||||
 "tracing-log",
 | 
			
		||||
 "tracing-subscriber",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hashbrown"
 | 
			
		||||
version = "0.12.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hermit-abi"
 | 
			
		||||
version = "0.2.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "indexmap"
 | 
			
		||||
version = "1.9.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
 "hashbrown",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lazy_static"
 | 
			
		||||
version = "1.4.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "libc"
 | 
			
		||||
version = "0.2.146"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "libloading"
 | 
			
		||||
version = "0.7.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "log"
 | 
			
		||||
version = "0.4.19"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "matchers"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "regex-automata",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "memchr"
 | 
			
		||||
version = "2.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "napi"
 | 
			
		||||
version = "2.13.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bitflags",
 | 
			
		||||
 "ctor",
 | 
			
		||||
 "napi-derive",
 | 
			
		||||
 "napi-sys",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "tokio",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "napi-build"
 | 
			
		||||
version = "2.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "napi-derive"
 | 
			
		||||
version = "2.13.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "convert_case",
 | 
			
		||||
 "napi-derive-backend",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 1.0.109",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "napi-derive-backend"
 | 
			
		||||
version = "1.0.52"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "convert_case",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "regex",
 | 
			
		||||
 "semver",
 | 
			
		||||
 "syn 1.0.109",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "napi-sys"
 | 
			
		||||
version = "2.2.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libloading",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "nu-ansi-term"
 | 
			
		||||
version = "0.46.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "overload",
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num_cpus"
 | 
			
		||||
version = "1.15.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "hermit-abi",
 | 
			
		||||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "once_cell"
 | 
			
		||||
version = "1.18.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "overload"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pin-project-lite"
 | 
			
		||||
version = "0.2.9"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "priority-queue"
 | 
			
		||||
version = "1.3.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
 "indexmap",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "proc-macro2"
 | 
			
		||||
version = "1.0.60"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "unicode-ident",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "quote"
 | 
			
		||||
version = "1.0.28"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex"
 | 
			
		||||
version = "1.8.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "aho-corasick",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "regex-syntax 0.7.2",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex-automata"
 | 
			
		||||
version = "0.1.10"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "regex-syntax 0.6.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex-syntax"
 | 
			
		||||
version = "0.6.29"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex-syntax"
 | 
			
		||||
version = "0.7.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "semver"
 | 
			
		||||
version = "1.0.17"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sharded-slab"
 | 
			
		||||
version = "0.1.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "smallvec"
 | 
			
		||||
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"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "fcb8d4cebc40aa517dfb69618fa647a346562e67228e2236ae0042ee6ac14775"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "unicode-ident",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "thread_local"
 | 
			
		||||
version = "1.1.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tokio"
 | 
			
		||||
version = "1.28.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
 "num_cpus",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "windows-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing"
 | 
			
		||||
version = "0.1.37"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "tracing-attributes",
 | 
			
		||||
 "tracing-core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing-attributes"
 | 
			
		||||
version = "0.1.26"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.20",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing-core"
 | 
			
		||||
version = "0.1.31"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "valuable",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing-log"
 | 
			
		||||
version = "0.1.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "log",
 | 
			
		||||
 "tracing-core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing-subscriber"
 | 
			
		||||
version = "0.3.17"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "matchers",
 | 
			
		||||
 "nu-ansi-term",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "regex",
 | 
			
		||||
 "sharded-slab",
 | 
			
		||||
 "smallvec",
 | 
			
		||||
 "thread_local",
 | 
			
		||||
 "tracing",
 | 
			
		||||
 "tracing-core",
 | 
			
		||||
 "tracing-log",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicode-ident"
 | 
			
		||||
version = "1.0.9"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicode-segmentation"
 | 
			
		||||
version = "1.10.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "valuable"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winapi"
 | 
			
		||||
version = "0.3.9"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "winapi-i686-pc-windows-gnu",
 | 
			
		||||
 "winapi-x86_64-pc-windows-gnu",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winapi-i686-pc-windows-gnu"
 | 
			
		||||
version = "0.4.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winapi-x86_64-pc-windows-gnu"
 | 
			
		||||
version = "0.4.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows-sys"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-targets",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows-targets"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows_aarch64_gnullvm",
 | 
			
		||||
 "windows_aarch64_msvc",
 | 
			
		||||
 "windows_i686_gnu",
 | 
			
		||||
 "windows_i686_msvc",
 | 
			
		||||
 "windows_x86_64_gnu",
 | 
			
		||||
 "windows_x86_64_gnullvm",
 | 
			
		||||
 "windows_x86_64_msvc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_aarch64_gnullvm"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_aarch64_msvc"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_i686_gnu"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_i686_msvc"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_gnu"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_gnullvm"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "windows_x86_64_msvc"
 | 
			
		||||
version = "0.48.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
 | 
			
		||||
							
								
								
									
										8
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
[workspace]
 | 
			
		||||
members = [
 | 
			
		||||
	"./backend/rust-gbt",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[profile.release]
 | 
			
		||||
lto = true
 | 
			
		||||
codegen-units = 1
 | 
			
		||||
@ -79,6 +79,8 @@ Query OK, 0 rows affected (0.00 sec)
 | 
			
		||||
 | 
			
		||||
_Make sure to use Node.js 16.10 and npm 7._
 | 
			
		||||
 | 
			
		||||
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
 | 
			
		||||
 | 
			
		||||
Install dependencies with `npm` and build the backend:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@
 | 
			
		||||
    "AUDIT": false,
 | 
			
		||||
    "ADVANCED_GBT_AUDIT": false,
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": false,
 | 
			
		||||
    "RUST_GBT": false,
 | 
			
		||||
    "CPFP_INDEXING": false,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 6
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										333
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										333
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -19,6 +19,7 @@
 | 
			
		||||
        "maxmind": "~4.3.8",
 | 
			
		||||
        "mysql2": "~3.2.0",
 | 
			
		||||
        "node-worker-threads-pool": "~1.5.1",
 | 
			
		||||
        "rust-gbt": "file:./rust-gbt",
 | 
			
		||||
        "socks-proxy-agent": "~7.0.0",
 | 
			
		||||
        "typescript": "~4.7.4",
 | 
			
		||||
        "ws": "~8.13.0"
 | 
			
		||||
@ -1485,6 +1486,33 @@
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "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==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "napi": "scripts/index.js"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 10"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "github",
 | 
			
		||||
        "url": "https://github.com/sponsors/Brooooooklyn"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@noble/hashes": {
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "individual",
 | 
			
		||||
          "url": "https://paulmillr.com/funding/"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@nodelib/fs.scandir": {
 | 
			
		||||
      "version": "2.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 | 
			
		||||
@ -2403,12 +2431,9 @@
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/base-x": {
 | 
			
		||||
      "version": "3.0.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz",
 | 
			
		||||
      "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "safe-buffer": "^5.0.1"
 | 
			
		||||
      }
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/bech32": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
@ -2424,18 +2449,16 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/bitcoinjs-lib": {
 | 
			
		||||
      "version": "6.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-eupi1FBTJmPuAZdChnzTXLv2HBqFW2AICpzXZQLniP0V9FWWeeUQSMKES6sP8isy/xO0ijDexbgkdEyFVrsuJw==",
 | 
			
		||||
      "version": "6.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@noble/hashes": "^1.2.0",
 | 
			
		||||
        "bech32": "^2.0.0",
 | 
			
		||||
        "bip174": "^2.1.0",
 | 
			
		||||
        "bs58check": "^2.1.2",
 | 
			
		||||
        "create-hash": "^1.1.0",
 | 
			
		||||
        "ripemd160": "^2.0.2",
 | 
			
		||||
        "bs58check": "^3.0.1",
 | 
			
		||||
        "typeforce": "^1.11.3",
 | 
			
		||||
        "varuint-bitcoin": "^1.1.2",
 | 
			
		||||
        "wif": "^2.0.1"
 | 
			
		||||
        "varuint-bitcoin": "^1.1.2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8.0.0"
 | 
			
		||||
@ -2540,21 +2563,20 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/bs58": {
 | 
			
		||||
      "version": "4.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "base-x": "^3.0.2"
 | 
			
		||||
        "base-x": "^4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/bs58check": {
 | 
			
		||||
      "version": "2.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==",
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "bs58": "^4.0.0",
 | 
			
		||||
        "create-hash": "^1.1.0",
 | 
			
		||||
        "safe-buffer": "^5.1.2"
 | 
			
		||||
        "@noble/hashes": "^1.2.0",
 | 
			
		||||
        "bs58": "^5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/bser": {
 | 
			
		||||
@ -2668,15 +2690,6 @@
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cipher-base": {
 | 
			
		||||
      "version": "1.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
 | 
			
		||||
      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "inherits": "^2.0.1",
 | 
			
		||||
        "safe-buffer": "^5.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cjs-module-lexer": {
 | 
			
		||||
      "version": "1.2.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
 | 
			
		||||
@ -2783,18 +2796,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/create-hash": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "cipher-base": "^1.0.1",
 | 
			
		||||
        "inherits": "^2.0.1",
 | 
			
		||||
        "md5.js": "^1.3.4",
 | 
			
		||||
        "ripemd160": "^2.0.1",
 | 
			
		||||
        "sha.js": "^2.4.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/create-require": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
 | 
			
		||||
@ -3825,19 +3826,6 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/ljharb"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/hash-base": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "inherits": "^2.0.4",
 | 
			
		||||
        "readable-stream": "^3.6.0",
 | 
			
		||||
        "safe-buffer": "^5.2.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/html-escaper": {
 | 
			
		||||
      "version": "2.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
 | 
			
		||||
@ -5916,16 +5904,6 @@
 | 
			
		||||
        "npm": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/md5.js": {
 | 
			
		||||
      "version": "1.3.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
 | 
			
		||||
      "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "hash-base": "^3.0.0",
 | 
			
		||||
        "inherits": "^2.0.1",
 | 
			
		||||
        "safe-buffer": "^5.1.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/media-typer": {
 | 
			
		||||
      "version": "0.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
 | 
			
		||||
@ -6591,19 +6569,6 @@
 | 
			
		||||
      "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/readable-stream": {
 | 
			
		||||
      "version": "3.6.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
 | 
			
		||||
      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "inherits": "^2.0.3",
 | 
			
		||||
        "string_decoder": "^1.1.1",
 | 
			
		||||
        "util-deprecate": "^1.0.1"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/require-directory": {
 | 
			
		||||
      "version": "2.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
 | 
			
		||||
@ -6694,15 +6659,6 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/isaacs"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ripemd160": {
 | 
			
		||||
      "version": "2.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "hash-base": "^3.0.0",
 | 
			
		||||
        "inherits": "^2.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/run-parallel": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
 | 
			
		||||
@ -6726,6 +6682,10 @@
 | 
			
		||||
        "queue-microtask": "^1.2.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/rust-gbt": {
 | 
			
		||||
      "resolved": "rust-gbt",
 | 
			
		||||
      "link": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/safe-buffer": {
 | 
			
		||||
      "version": "5.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
 | 
			
		||||
@ -6824,18 +6784,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/sha.js": {
 | 
			
		||||
      "version": "2.4.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
 | 
			
		||||
      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "inherits": "^2.0.1",
 | 
			
		||||
        "safe-buffer": "^5.0.1"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "sha.js": "bin.js"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/shebang-command": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
 | 
			
		||||
@ -6988,14 +6936,6 @@
 | 
			
		||||
        "node": ">= 0.8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/string_decoder": {
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "safe-buffer": "~5.2.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/string-length": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
 | 
			
		||||
@ -7397,11 +7337,6 @@
 | 
			
		||||
        "punycode": "^2.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/util-deprecate": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/utils-merge": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
 | 
			
		||||
@ -7470,14 +7405,6 @@
 | 
			
		||||
        "node": ">= 8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/wif": {
 | 
			
		||||
      "version": "2.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "bs58check": "<3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/word-wrap": {
 | 
			
		||||
      "version": "1.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
 | 
			
		||||
@ -7638,6 +7565,17 @@
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "rust-gbt": {
 | 
			
		||||
      "name": "gbt",
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@napi-rs/cli": "^2.16.1"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 12"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
@ -8725,6 +8663,17 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.9.tgz",
 | 
			
		||||
      "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==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "@noble/hashes": {
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg=="
 | 
			
		||||
    },
 | 
			
		||||
    "@nodelib/fs.scandir": {
 | 
			
		||||
      "version": "2.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 | 
			
		||||
@ -9445,12 +9394,9 @@
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "base-x": {
 | 
			
		||||
      "version": "3.0.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz",
 | 
			
		||||
      "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "safe-buffer": "^5.0.1"
 | 
			
		||||
      }
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
 | 
			
		||||
    },
 | 
			
		||||
    "bech32": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
@ -9463,18 +9409,16 @@
 | 
			
		||||
      "integrity": "sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA=="
 | 
			
		||||
    },
 | 
			
		||||
    "bitcoinjs-lib": {
 | 
			
		||||
      "version": "6.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-eupi1FBTJmPuAZdChnzTXLv2HBqFW2AICpzXZQLniP0V9FWWeeUQSMKES6sP8isy/xO0ijDexbgkdEyFVrsuJw==",
 | 
			
		||||
      "version": "6.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@noble/hashes": "^1.2.0",
 | 
			
		||||
        "bech32": "^2.0.0",
 | 
			
		||||
        "bip174": "^2.1.0",
 | 
			
		||||
        "bs58check": "^2.1.2",
 | 
			
		||||
        "create-hash": "^1.1.0",
 | 
			
		||||
        "ripemd160": "^2.0.2",
 | 
			
		||||
        "bs58check": "^3.0.1",
 | 
			
		||||
        "typeforce": "^1.11.3",
 | 
			
		||||
        "varuint-bitcoin": "^1.1.2",
 | 
			
		||||
        "wif": "^2.0.1"
 | 
			
		||||
        "varuint-bitcoin": "^1.1.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "body-parser": {
 | 
			
		||||
@ -9552,21 +9496,20 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "bs58": {
 | 
			
		||||
      "version": "4.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "base-x": "^3.0.2"
 | 
			
		||||
        "base-x": "^4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "bs58check": {
 | 
			
		||||
      "version": "2.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==",
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "bs58": "^4.0.0",
 | 
			
		||||
        "create-hash": "^1.1.0",
 | 
			
		||||
        "safe-buffer": "^5.1.2"
 | 
			
		||||
        "@noble/hashes": "^1.2.0",
 | 
			
		||||
        "bs58": "^5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "bser": {
 | 
			
		||||
@ -9639,15 +9582,6 @@
 | 
			
		||||
      "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "cipher-base": {
 | 
			
		||||
      "version": "1.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
 | 
			
		||||
      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "inherits": "^2.0.1",
 | 
			
		||||
        "safe-buffer": "^5.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "cjs-module-lexer": {
 | 
			
		||||
      "version": "1.2.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
 | 
			
		||||
@ -9735,18 +9669,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "create-hash": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "cipher-base": "^1.0.1",
 | 
			
		||||
        "inherits": "^2.0.1",
 | 
			
		||||
        "md5.js": "^1.3.4",
 | 
			
		||||
        "ripemd160": "^2.0.1",
 | 
			
		||||
        "sha.js": "^2.4.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "create-require": {
 | 
			
		||||
      "version": "1.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
 | 
			
		||||
@ -10513,16 +10435,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
 | 
			
		||||
    },
 | 
			
		||||
    "hash-base": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "inherits": "^2.0.4",
 | 
			
		||||
        "readable-stream": "^3.6.0",
 | 
			
		||||
        "safe-buffer": "^5.2.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "html-escaper": {
 | 
			
		||||
      "version": "2.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
 | 
			
		||||
@ -12069,16 +11981,6 @@
 | 
			
		||||
        "tiny-lru": "10.3.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "md5.js": {
 | 
			
		||||
      "version": "1.3.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
 | 
			
		||||
      "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "hash-base": "^3.0.0",
 | 
			
		||||
        "inherits": "^2.0.1",
 | 
			
		||||
        "safe-buffer": "^5.1.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "media-typer": {
 | 
			
		||||
      "version": "0.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
 | 
			
		||||
@ -12547,16 +12449,6 @@
 | 
			
		||||
      "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "readable-stream": {
 | 
			
		||||
      "version": "3.6.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
 | 
			
		||||
      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "inherits": "^2.0.3",
 | 
			
		||||
        "string_decoder": "^1.1.1",
 | 
			
		||||
        "util-deprecate": "^1.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "require-directory": {
 | 
			
		||||
      "version": "2.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
 | 
			
		||||
@ -12618,15 +12510,6 @@
 | 
			
		||||
        "glob": "^7.1.3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "ripemd160": {
 | 
			
		||||
      "version": "2.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "hash-base": "^3.0.0",
 | 
			
		||||
        "inherits": "^2.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "run-parallel": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
 | 
			
		||||
@ -12636,6 +12519,12 @@
 | 
			
		||||
        "queue-microtask": "^1.2.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "rust-gbt": {
 | 
			
		||||
      "version": "file:rust-gbt",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@napi-rs/cli": "^2.16.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "safe-buffer": {
 | 
			
		||||
      "version": "5.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
 | 
			
		||||
@ -12715,15 +12604,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
 | 
			
		||||
    },
 | 
			
		||||
    "sha.js": {
 | 
			
		||||
      "version": "2.4.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
 | 
			
		||||
      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "inherits": "^2.0.1",
 | 
			
		||||
        "safe-buffer": "^5.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "shebang-command": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
 | 
			
		||||
@ -12840,14 +12720,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "string_decoder": {
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "safe-buffer": "~5.2.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "string-length": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
 | 
			
		||||
@ -13101,11 +12973,6 @@
 | 
			
		||||
        "punycode": "^2.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "util-deprecate": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
 | 
			
		||||
    },
 | 
			
		||||
    "utils-merge": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
 | 
			
		||||
@ -13159,14 +13026,6 @@
 | 
			
		||||
        "isexe": "^2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "wif": {
 | 
			
		||||
      "version": "2.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "bs58check": "<3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "word-wrap": {
 | 
			
		||||
      "version": "1.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
 | 
			
		||||
 | 
			
		||||
@ -22,16 +22,19 @@
 | 
			
		||||
  "main": "index.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json",
 | 
			
		||||
    "build": "npm run tsc && npm run create-resources",
 | 
			
		||||
    "build": "npm run build-rust && npm run tsc && npm run create-resources",
 | 
			
		||||
    "create-resources": "cp ./src/tasks/price-feeds/mtgox-weekly.json ./dist/tasks && node dist/api/fetch-version.js",
 | 
			
		||||
    "package": "npm run build && rm -rf package && mv dist package && mv node_modules package && npm run package-rm-build-deps",
 | 
			
		||||
    "package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)",
 | 
			
		||||
    "package": "npm run build && rm -rf package && mv dist package && mv node_modules package && mv rust-gbt package && npm run package-rm-build-deps",
 | 
			
		||||
    "package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint @napi-rs ../rust-gbt/target ../rust-gbt/node_modules ../rust-gbt/src)",
 | 
			
		||||
    "start": "node --max-old-space-size=2048 dist/index.js",
 | 
			
		||||
    "start-production": "node --max-old-space-size=16384 dist/index.js",
 | 
			
		||||
    "reindex-updated-pools": "npm run start-production --update-pools",
 | 
			
		||||
    "reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks",
 | 
			
		||||
    "test": "./node_modules/.bin/jest --coverage",
 | 
			
		||||
    "lint": "./node_modules/.bin/eslint . --ext .ts",
 | 
			
		||||
    "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
 | 
			
		||||
    "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
 | 
			
		||||
    "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"",
 | 
			
		||||
    "build-rust": "cd rust-gbt && npm install"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@babel/core": "^7.21.3",
 | 
			
		||||
@ -44,6 +47,7 @@
 | 
			
		||||
    "maxmind": "~4.3.8",
 | 
			
		||||
    "mysql2": "~3.2.0",
 | 
			
		||||
    "node-worker-threads-pool": "~1.5.1",
 | 
			
		||||
    "rust-gbt": "file:./rust-gbt",
 | 
			
		||||
    "socks-proxy-agent": "~7.0.0",
 | 
			
		||||
    "typescript": "~4.7.4",
 | 
			
		||||
    "ws": "~8.13.0"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								backend/rust-gbt/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								backend/rust-gbt/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
*.node
 | 
			
		||||
**/node_modules
 | 
			
		||||
**/.DS_Store
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
							
								
								
									
										25
									
								
								backend/rust-gbt/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								backend/rust-gbt/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "gbt"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
description = "An inefficient re-implementation of the getBlockTemplate algorithm in Rust"
 | 
			
		||||
authors = ["mononaut"]
 | 
			
		||||
edition = "2021"
 | 
			
		||||
publish = false
 | 
			
		||||
 | 
			
		||||
[lib]
 | 
			
		||||
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"
 | 
			
		||||
bytes = "1.4.0"
 | 
			
		||||
napi = { version = "2.13.2", features = ["napi8", "tokio_rt"] }
 | 
			
		||||
napi-derive = "2.13.0"
 | 
			
		||||
bytemuck = "1.13.1"
 | 
			
		||||
tracing = "0.1.36"
 | 
			
		||||
tracing-log = "0.1.3"
 | 
			
		||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter"]}
 | 
			
		||||
 | 
			
		||||
[build-dependencies]
 | 
			
		||||
napi-build = "2.0.1"
 | 
			
		||||
							
								
								
									
										123
									
								
								backend/rust-gbt/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								backend/rust-gbt/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
			
		||||
# gbt
 | 
			
		||||
 | 
			
		||||
**gbt:** rust implementation of the getBlockTemplate algorithm
 | 
			
		||||
 | 
			
		||||
This project was bootstrapped by [napi](https://www.npmjs.com/package/@napi-rs/cli).
 | 
			
		||||
 | 
			
		||||
## Installing gbt
 | 
			
		||||
 | 
			
		||||
Installing gbt requires a [supported version of Node and Rust](https://github.com/napi-rs/napi-rs#platform-support).
 | 
			
		||||
 | 
			
		||||
The build process also requires [Rust](https://www.rust-lang.org/tools/install) to be installed.
 | 
			
		||||
 | 
			
		||||
You can install the project with npm. In the project directory, run:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
$ npm install
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This fully installs the project, including installing any dependencies and running the build.
 | 
			
		||||
 | 
			
		||||
## Building gbt
 | 
			
		||||
 | 
			
		||||
If you have already installed the project and only want to run the build, run:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
$ npm run build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This command uses the [napi build](https://www.npmjs.com/package/@napi-rs/cli) utility to run the Rust build and copy the built library into `./gbt.[TARGET_TRIPLE].node`.
 | 
			
		||||
 | 
			
		||||
## Exploring gbt
 | 
			
		||||
 | 
			
		||||
After building gbt, you can explore its exports at the Node REPL:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
$ npm install
 | 
			
		||||
$ node
 | 
			
		||||
> require('.').hello()
 | 
			
		||||
"hello node"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Available Scripts
 | 
			
		||||
 | 
			
		||||
In the project directory, you can run:
 | 
			
		||||
 | 
			
		||||
### `npm install`
 | 
			
		||||
 | 
			
		||||
Installs the project, including running `npm run build-release`.
 | 
			
		||||
 | 
			
		||||
### `npm build`
 | 
			
		||||
 | 
			
		||||
Builds the Node addon (`gbt.[TARGET_TRIPLE].node`) from source.
 | 
			
		||||
 | 
			
		||||
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm build` and `npm build-*` commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
npm run build -- --feature=beetle
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### `npm build-debug`
 | 
			
		||||
 | 
			
		||||
Alias for `npm build`.
 | 
			
		||||
 | 
			
		||||
#### `npm build-release`
 | 
			
		||||
 | 
			
		||||
Same as [`npm build`](#npm-build) but, builds the module with the [`release`](https://doc.rust-lang.org/cargo/reference/profiles.html#release) profile. Release builds will compile slower, but run faster.
 | 
			
		||||
 | 
			
		||||
### `npm test`
 | 
			
		||||
 | 
			
		||||
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
 | 
			
		||||
 | 
			
		||||
## Project Layout
 | 
			
		||||
 | 
			
		||||
The directory structure of this project is:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
gbt/
 | 
			
		||||
├── Cargo.toml
 | 
			
		||||
├── README.md
 | 
			
		||||
├── gbt.[TARGET_TRIPLE].node
 | 
			
		||||
├── package.json
 | 
			
		||||
├── src/
 | 
			
		||||
|   └── lib.rs
 | 
			
		||||
└── target/
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Cargo.toml
 | 
			
		||||
 | 
			
		||||
The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command.
 | 
			
		||||
 | 
			
		||||
### README.md
 | 
			
		||||
 | 
			
		||||
This file.
 | 
			
		||||
 | 
			
		||||
### gbt.\[TARGET_TRIPLE\].node
 | 
			
		||||
 | 
			
		||||
The Node addon—i.e., a binary Node module—generated by building the project. This is the main module for this package, as dictated by the `"main"` key in `package.json`.
 | 
			
		||||
 | 
			
		||||
Under the hood, a [Node addon](https://nodejs.org/api/addons.html) is a [dynamically-linked shared object](https://en.wikipedia.org/wiki/Library_(computing)#Shared_libraries). The `"build"` script produces this file by copying it from within the `target/` directory, which is where the Rust build produces the shared object.
 | 
			
		||||
 | 
			
		||||
### package.json
 | 
			
		||||
 | 
			
		||||
The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command.
 | 
			
		||||
 | 
			
		||||
### src/
 | 
			
		||||
 | 
			
		||||
The directory tree containing the Rust source code for the project.
 | 
			
		||||
 | 
			
		||||
### src/lib.rs
 | 
			
		||||
 | 
			
		||||
The Rust library's main module.
 | 
			
		||||
 | 
			
		||||
### target/
 | 
			
		||||
 | 
			
		||||
Binary artifacts generated by the Rust build.
 | 
			
		||||
 | 
			
		||||
## Learn More
 | 
			
		||||
 | 
			
		||||
To learn more about Neon, see the [Napi-RS documentation](https://napi.rs/docs/introduction/getting-started).
 | 
			
		||||
 | 
			
		||||
To learn more about Rust, see the [Rust documentation](https://www.rust-lang.org).
 | 
			
		||||
 | 
			
		||||
To learn more about Node, see the [Node documentation](https://nodejs.org).
 | 
			
		||||
							
								
								
									
										3
									
								
								backend/rust-gbt/build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								backend/rust-gbt/build.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
fn main() {
 | 
			
		||||
    napi_build::setup();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								backend/rust-gbt/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								backend/rust-gbt/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
 | 
			
		||||
/* auto-generated by NAPI-RS */
 | 
			
		||||
 | 
			
		||||
export interface ThreadTransaction {
 | 
			
		||||
  uid: number
 | 
			
		||||
  order: number
 | 
			
		||||
  fee: number
 | 
			
		||||
  weight: number
 | 
			
		||||
  sigops: number
 | 
			
		||||
  effectiveFeePerVsize: number
 | 
			
		||||
  inputs: Array<number>
 | 
			
		||||
}
 | 
			
		||||
export class GbtGenerator {
 | 
			
		||||
  constructor()
 | 
			
		||||
  /**
 | 
			
		||||
   * # Errors
 | 
			
		||||
   *
 | 
			
		||||
   * Rejects if the thread panics or if the Mutex is poisoned.
 | 
			
		||||
   */
 | 
			
		||||
  make(mempool: Array<ThreadTransaction>, maxUid: number): Promise<GbtResult>
 | 
			
		||||
  /**
 | 
			
		||||
   * # Errors
 | 
			
		||||
   *
 | 
			
		||||
   * Rejects if the thread panics or if the Mutex is poisoned.
 | 
			
		||||
   */
 | 
			
		||||
  update(newTxs: Array<ThreadTransaction>, removeTxs: Array<number>, maxUid: number): Promise<GbtResult>
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * The result from calling the gbt function.
 | 
			
		||||
 *
 | 
			
		||||
 * This tuple contains the following:
 | 
			
		||||
 *        blocks: A 2D Vector of transaction IDs (u32), the inner Vecs each represent a block.
 | 
			
		||||
 * block_weights: A Vector of total weights per block.
 | 
			
		||||
 *      clusters: A 2D Vector of transaction IDs representing clusters of dependent mempool transactions
 | 
			
		||||
 *         rates: A Vector of tuples containing transaction IDs (u32) and effective fee per vsize (f64)
 | 
			
		||||
 */
 | 
			
		||||
export class GbtResult {
 | 
			
		||||
  blocks: Array<Array<number>>
 | 
			
		||||
  blockWeights: Array<number>
 | 
			
		||||
  clusters: Array<Array<number>>
 | 
			
		||||
  rates: Array<Array<number>>
 | 
			
		||||
  constructor(blocks: Array<Array<number>>, blockWeights: Array<number>, clusters: Array<Array<number>>, rates: Array<Array<number>>)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										258
									
								
								backend/rust-gbt/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								backend/rust-gbt/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,258 @@
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
/* prettier-ignore */
 | 
			
		||||
 | 
			
		||||
/* auto-generated by NAPI-RS */
 | 
			
		||||
 | 
			
		||||
const { existsSync, readFileSync } = require('fs')
 | 
			
		||||
const { join } = require('path')
 | 
			
		||||
 | 
			
		||||
const { platform, arch } = process
 | 
			
		||||
 | 
			
		||||
let nativeBinding = null
 | 
			
		||||
let localFileExisted = false
 | 
			
		||||
let loadError = null
 | 
			
		||||
 | 
			
		||||
function isMusl() {
 | 
			
		||||
  // For Node 10
 | 
			
		||||
  if (!process.report || typeof process.report.getReport !== 'function') {
 | 
			
		||||
    try {
 | 
			
		||||
      const lddPath = require('child_process').execSync('which ldd').toString().trim()
 | 
			
		||||
      return readFileSync(lddPath, 'utf8').includes('musl')
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    const { glibcVersionRuntime } = process.report.getReport().header
 | 
			
		||||
    return !glibcVersionRuntime
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
switch (platform) {
 | 
			
		||||
  case 'android':
 | 
			
		||||
    switch (arch) {
 | 
			
		||||
      case 'arm64':
 | 
			
		||||
        localFileExisted = existsSync(join(__dirname, 'gbt.android-arm64.node'))
 | 
			
		||||
        try {
 | 
			
		||||
          if (localFileExisted) {
 | 
			
		||||
            nativeBinding = require('./gbt.android-arm64.node')
 | 
			
		||||
          } else {
 | 
			
		||||
            nativeBinding = require('gbt-android-arm64')
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          loadError = e
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case 'arm':
 | 
			
		||||
        localFileExisted = existsSync(join(__dirname, 'gbt.android-arm-eabi.node'))
 | 
			
		||||
        try {
 | 
			
		||||
          if (localFileExisted) {
 | 
			
		||||
            nativeBinding = require('./gbt.android-arm-eabi.node')
 | 
			
		||||
          } else {
 | 
			
		||||
            nativeBinding = require('gbt-android-arm-eabi')
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          loadError = e
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      default:
 | 
			
		||||
        throw new Error(`Unsupported architecture on Android ${arch}`)
 | 
			
		||||
    }
 | 
			
		||||
    break
 | 
			
		||||
  case 'win32':
 | 
			
		||||
    switch (arch) {
 | 
			
		||||
      case 'x64':
 | 
			
		||||
        localFileExisted = existsSync(
 | 
			
		||||
          join(__dirname, 'gbt.win32-x64-msvc.node')
 | 
			
		||||
        )
 | 
			
		||||
        try {
 | 
			
		||||
          if (localFileExisted) {
 | 
			
		||||
            nativeBinding = require('./gbt.win32-x64-msvc.node')
 | 
			
		||||
          } else {
 | 
			
		||||
            nativeBinding = require('gbt-win32-x64-msvc')
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          loadError = e
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case 'ia32':
 | 
			
		||||
        localFileExisted = existsSync(
 | 
			
		||||
          join(__dirname, 'gbt.win32-ia32-msvc.node')
 | 
			
		||||
        )
 | 
			
		||||
        try {
 | 
			
		||||
          if (localFileExisted) {
 | 
			
		||||
            nativeBinding = require('./gbt.win32-ia32-msvc.node')
 | 
			
		||||
          } else {
 | 
			
		||||
            nativeBinding = require('gbt-win32-ia32-msvc')
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          loadError = e
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case 'arm64':
 | 
			
		||||
        localFileExisted = existsSync(
 | 
			
		||||
          join(__dirname, 'gbt.win32-arm64-msvc.node')
 | 
			
		||||
        )
 | 
			
		||||
        try {
 | 
			
		||||
          if (localFileExisted) {
 | 
			
		||||
            nativeBinding = require('./gbt.win32-arm64-msvc.node')
 | 
			
		||||
          } else {
 | 
			
		||||
            nativeBinding = require('gbt-win32-arm64-msvc')
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          loadError = e
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      default:
 | 
			
		||||
        throw new Error(`Unsupported architecture on Windows: ${arch}`)
 | 
			
		||||
    }
 | 
			
		||||
    break
 | 
			
		||||
  case 'darwin':
 | 
			
		||||
    localFileExisted = existsSync(join(__dirname, 'gbt.darwin-universal.node'))
 | 
			
		||||
    try {
 | 
			
		||||
      if (localFileExisted) {
 | 
			
		||||
        nativeBinding = require('./gbt.darwin-universal.node')
 | 
			
		||||
      } else {
 | 
			
		||||
        nativeBinding = require('gbt-darwin-universal')
 | 
			
		||||
      }
 | 
			
		||||
      break
 | 
			
		||||
    } catch {}
 | 
			
		||||
    switch (arch) {
 | 
			
		||||
      case 'x64':
 | 
			
		||||
        localFileExisted = existsSync(join(__dirname, 'gbt.darwin-x64.node'))
 | 
			
		||||
        try {
 | 
			
		||||
          if (localFileExisted) {
 | 
			
		||||
            nativeBinding = require('./gbt.darwin-x64.node')
 | 
			
		||||
          } else {
 | 
			
		||||
            nativeBinding = require('gbt-darwin-x64')
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          loadError = e
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case 'arm64':
 | 
			
		||||
        localFileExisted = existsSync(
 | 
			
		||||
          join(__dirname, 'gbt.darwin-arm64.node')
 | 
			
		||||
        )
 | 
			
		||||
        try {
 | 
			
		||||
          if (localFileExisted) {
 | 
			
		||||
            nativeBinding = require('./gbt.darwin-arm64.node')
 | 
			
		||||
          } else {
 | 
			
		||||
            nativeBinding = require('gbt-darwin-arm64')
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          loadError = e
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      default:
 | 
			
		||||
        throw new Error(`Unsupported architecture on macOS: ${arch}`)
 | 
			
		||||
    }
 | 
			
		||||
    break
 | 
			
		||||
  case 'freebsd':
 | 
			
		||||
    if (arch !== 'x64') {
 | 
			
		||||
      throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
 | 
			
		||||
    }
 | 
			
		||||
    localFileExisted = existsSync(join(__dirname, 'gbt.freebsd-x64.node'))
 | 
			
		||||
    try {
 | 
			
		||||
      if (localFileExisted) {
 | 
			
		||||
        nativeBinding = require('./gbt.freebsd-x64.node')
 | 
			
		||||
      } else {
 | 
			
		||||
        nativeBinding = require('gbt-freebsd-x64')
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      loadError = e
 | 
			
		||||
    }
 | 
			
		||||
    break
 | 
			
		||||
  case 'linux':
 | 
			
		||||
    switch (arch) {
 | 
			
		||||
      case 'x64':
 | 
			
		||||
        if (isMusl()) {
 | 
			
		||||
          localFileExisted = existsSync(
 | 
			
		||||
            join(__dirname, 'gbt.linux-x64-musl.node')
 | 
			
		||||
          )
 | 
			
		||||
          try {
 | 
			
		||||
            if (localFileExisted) {
 | 
			
		||||
              nativeBinding = require('./gbt.linux-x64-musl.node')
 | 
			
		||||
            } else {
 | 
			
		||||
              nativeBinding = require('gbt-linux-x64-musl')
 | 
			
		||||
            }
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            loadError = e
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          localFileExisted = existsSync(
 | 
			
		||||
            join(__dirname, 'gbt.linux-x64-gnu.node')
 | 
			
		||||
          )
 | 
			
		||||
          try {
 | 
			
		||||
            if (localFileExisted) {
 | 
			
		||||
              nativeBinding = require('./gbt.linux-x64-gnu.node')
 | 
			
		||||
            } else {
 | 
			
		||||
              nativeBinding = require('gbt-linux-x64-gnu')
 | 
			
		||||
            }
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            loadError = e
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case 'arm64':
 | 
			
		||||
        if (isMusl()) {
 | 
			
		||||
          localFileExisted = existsSync(
 | 
			
		||||
            join(__dirname, 'gbt.linux-arm64-musl.node')
 | 
			
		||||
          )
 | 
			
		||||
          try {
 | 
			
		||||
            if (localFileExisted) {
 | 
			
		||||
              nativeBinding = require('./gbt.linux-arm64-musl.node')
 | 
			
		||||
            } else {
 | 
			
		||||
              nativeBinding = require('gbt-linux-arm64-musl')
 | 
			
		||||
            }
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            loadError = e
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          localFileExisted = existsSync(
 | 
			
		||||
            join(__dirname, 'gbt.linux-arm64-gnu.node')
 | 
			
		||||
          )
 | 
			
		||||
          try {
 | 
			
		||||
            if (localFileExisted) {
 | 
			
		||||
              nativeBinding = require('./gbt.linux-arm64-gnu.node')
 | 
			
		||||
            } else {
 | 
			
		||||
              nativeBinding = require('gbt-linux-arm64-gnu')
 | 
			
		||||
            }
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            loadError = e
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case 'arm':
 | 
			
		||||
        localFileExisted = existsSync(
 | 
			
		||||
          join(__dirname, 'gbt.linux-arm-gnueabihf.node')
 | 
			
		||||
        )
 | 
			
		||||
        try {
 | 
			
		||||
          if (localFileExisted) {
 | 
			
		||||
            nativeBinding = require('./gbt.linux-arm-gnueabihf.node')
 | 
			
		||||
          } else {
 | 
			
		||||
            nativeBinding = require('gbt-linux-arm-gnueabihf')
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          loadError = e
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      default:
 | 
			
		||||
        throw new Error(`Unsupported architecture on Linux: ${arch}`)
 | 
			
		||||
    }
 | 
			
		||||
    break
 | 
			
		||||
  default:
 | 
			
		||||
    throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (!nativeBinding) {
 | 
			
		||||
  if (loadError) {
 | 
			
		||||
    throw loadError
 | 
			
		||||
  }
 | 
			
		||||
  throw new Error(`Failed to load native binding`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { GbtGenerator, GbtResult } = nativeBinding
 | 
			
		||||
 | 
			
		||||
module.exports.GbtGenerator = GbtGenerator
 | 
			
		||||
module.exports.GbtResult = GbtResult
 | 
			
		||||
							
								
								
									
										34
									
								
								backend/rust-gbt/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								backend/rust-gbt/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "gbt",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "gbt",
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@napi-rs/cli": "^2.16.1"
 | 
			
		||||
      },
 | 
			
		||||
      "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==",
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "napi": "scripts/index.js"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 10"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "github",
 | 
			
		||||
        "url": "https://github.com/sponsors/Brooooooklyn"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								backend/rust-gbt/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								backend/rust-gbt/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "gbt",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "description": "An inefficient re-implementation of the getBlockTemplate algorithm in Rust",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "types": "index.d.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "artifacts": "napi artifacts",
 | 
			
		||||
    "build": "napi build --platform",
 | 
			
		||||
    "build-debug": "npm run build",
 | 
			
		||||
    "build-release": "npm run build -- --release --strip",
 | 
			
		||||
    "install": "npm run build-release",
 | 
			
		||||
    "prepublishOnly": "napi prepublish -t npm",
 | 
			
		||||
    "test": "cargo test"
 | 
			
		||||
  },
 | 
			
		||||
  "author": "mononaut",
 | 
			
		||||
  "napi": {
 | 
			
		||||
    "name": "gbt",
 | 
			
		||||
    "triples": {
 | 
			
		||||
      "defaults": false,
 | 
			
		||||
      "additional": [
 | 
			
		||||
        "x86_64-unknown-linux-gnu",
 | 
			
		||||
        "x86_64-unknown-freebsd"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@napi-rs/cli": "^2.16.1"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">= 12"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										220
									
								
								backend/rust-gbt/src/audit_transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								backend/rust-gbt/src/audit_transaction.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,220 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    u32_hasher_types::{u32hashset_new, U32HasherState},
 | 
			
		||||
    ThreadTransaction,
 | 
			
		||||
};
 | 
			
		||||
use std::{
 | 
			
		||||
    cmp::Ordering,
 | 
			
		||||
    collections::HashSet,
 | 
			
		||||
    hash::{Hash, Hasher},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[allow(clippy::struct_excessive_bools)]
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
pub struct AuditTransaction {
 | 
			
		||||
    pub uid: u32,
 | 
			
		||||
    order: u32,
 | 
			
		||||
    pub fee: u64,
 | 
			
		||||
    pub weight: u32,
 | 
			
		||||
    // exact sigop-adjusted weight
 | 
			
		||||
    pub sigop_adjusted_weight: u32,
 | 
			
		||||
    // sigop-adjusted vsize rounded up the the next integer
 | 
			
		||||
    pub sigop_adjusted_vsize: u32,
 | 
			
		||||
    pub sigops: u32,
 | 
			
		||||
    adjusted_fee_per_vsize: f64,
 | 
			
		||||
    pub effective_fee_per_vsize: f64,
 | 
			
		||||
    pub dependency_rate: f64,
 | 
			
		||||
    pub inputs: Vec<u32>,
 | 
			
		||||
    pub relatives_set_flag: bool,
 | 
			
		||||
    pub ancestors: HashSet<u32, U32HasherState>,
 | 
			
		||||
    pub children: HashSet<u32, U32HasherState>,
 | 
			
		||||
    ancestor_fee: u64,
 | 
			
		||||
    ancestor_sigop_adjusted_weight: u32,
 | 
			
		||||
    ancestor_sigop_adjusted_vsize: u32,
 | 
			
		||||
    ancestor_sigops: u32,
 | 
			
		||||
    // Safety: Must be private to prevent NaN breaking Ord impl.
 | 
			
		||||
    score: f64,
 | 
			
		||||
    pub used: bool,
 | 
			
		||||
    /// whether this transaction has been moved to the "modified" priority queue
 | 
			
		||||
    pub modified: bool,
 | 
			
		||||
    pub dirty: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Hash for AuditTransaction {
 | 
			
		||||
    fn hash<H: Hasher>(&self, state: &mut H) {
 | 
			
		||||
        self.uid.hash(state);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialEq for AuditTransaction {
 | 
			
		||||
    fn eq(&self, other: &Self) -> bool {
 | 
			
		||||
        self.uid == other.uid
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Eq for AuditTransaction {}
 | 
			
		||||
 | 
			
		||||
#[inline]
 | 
			
		||||
pub fn partial_cmp_uid_score(a: (u32, u32, f64), b: (u32, u32, f64)) -> Option<Ordering> {
 | 
			
		||||
    // If either score is NaN, this is false,
 | 
			
		||||
    // and partial_cmp will return None
 | 
			
		||||
    if a.2 != b.2 {
 | 
			
		||||
        // compare by score (sorts by ascending score)
 | 
			
		||||
        a.2.partial_cmp(&b.2)
 | 
			
		||||
    } else if a.1 != b.1 {
 | 
			
		||||
        // tie-break by comparing partial txids (sorts by descending txid)
 | 
			
		||||
        Some(b.1.cmp(&a.1))
 | 
			
		||||
    } else {
 | 
			
		||||
        // tie-break partial txid collisions by comparing uids (sorts by descending uid)
 | 
			
		||||
        Some(b.0.cmp(&a.0))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialOrd for AuditTransaction {
 | 
			
		||||
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
 | 
			
		||||
        partial_cmp_uid_score(
 | 
			
		||||
            (self.uid, self.order, self.score),
 | 
			
		||||
            (other.uid, other.order, other.score),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Ord for AuditTransaction {
 | 
			
		||||
    fn cmp(&self, other: &Self) -> Ordering {
 | 
			
		||||
        // Safety: The only possible values for score are f64
 | 
			
		||||
        // that are not NaN. This is because outside code can not
 | 
			
		||||
        // freely assign score. Also, calc_new_score guarantees no NaN.
 | 
			
		||||
        self.partial_cmp(other).expect("score will never be NaN")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[inline]
 | 
			
		||||
fn calc_fee_rate(fee: f64, vsize: f64) -> f64 {
 | 
			
		||||
    fee / (if vsize == 0.0 { 1.0 } else { vsize })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AuditTransaction {
 | 
			
		||||
    pub fn from_thread_transaction(tx: &ThreadTransaction) -> Self {
 | 
			
		||||
        // rounded up to the nearest integer
 | 
			
		||||
        let is_adjusted = tx.weight < (tx.sigops * 20);
 | 
			
		||||
        let sigop_adjusted_vsize = ((tx.weight + 3) / 4).max(tx.sigops * 5);
 | 
			
		||||
        let sigop_adjusted_weight = tx.weight.max(tx.sigops * 20);
 | 
			
		||||
        let effective_fee_per_vsize = if is_adjusted {
 | 
			
		||||
            calc_fee_rate(tx.fee, f64::from(sigop_adjusted_weight) / 4.0)
 | 
			
		||||
        } else {
 | 
			
		||||
            tx.effective_fee_per_vsize
 | 
			
		||||
        };
 | 
			
		||||
        Self {
 | 
			
		||||
            uid: tx.uid,
 | 
			
		||||
            order: tx.order,
 | 
			
		||||
            fee: tx.fee as u64,
 | 
			
		||||
            weight: tx.weight,
 | 
			
		||||
            sigop_adjusted_weight,
 | 
			
		||||
            sigop_adjusted_vsize,
 | 
			
		||||
            sigops: tx.sigops,
 | 
			
		||||
            adjusted_fee_per_vsize: calc_fee_rate(tx.fee, f64::from(sigop_adjusted_vsize)),
 | 
			
		||||
            effective_fee_per_vsize,
 | 
			
		||||
            dependency_rate: f64::INFINITY,
 | 
			
		||||
            inputs: tx.inputs.clone(),
 | 
			
		||||
            relatives_set_flag: false,
 | 
			
		||||
            ancestors: u32hashset_new(),
 | 
			
		||||
            children: u32hashset_new(),
 | 
			
		||||
            ancestor_fee: tx.fee as u64,
 | 
			
		||||
            ancestor_sigop_adjusted_weight: sigop_adjusted_weight,
 | 
			
		||||
            ancestor_sigop_adjusted_vsize: sigop_adjusted_vsize,
 | 
			
		||||
            ancestor_sigops: tx.sigops,
 | 
			
		||||
            score: 0.0,
 | 
			
		||||
            used: false,
 | 
			
		||||
            modified: false,
 | 
			
		||||
            dirty: effective_fee_per_vsize != tx.effective_fee_per_vsize,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    pub const fn score(&self) -> f64 {
 | 
			
		||||
        self.score
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    pub const fn order(&self) -> u32 {
 | 
			
		||||
        self.order
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    pub const fn ancestor_sigop_adjusted_vsize(&self) -> u32 {
 | 
			
		||||
        self.ancestor_sigop_adjusted_vsize
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    pub const fn ancestor_sigops(&self) -> u32 {
 | 
			
		||||
        self.ancestor_sigops
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    pub fn cluster_rate(&self) -> f64 {
 | 
			
		||||
        // Safety: self.ancestor_weight can never be 0.
 | 
			
		||||
        // Even if it could, as it approaches 0, the value inside the min() call
 | 
			
		||||
        // grows, so if we think of 0 as "grew infinitely" then dependency_rate would be
 | 
			
		||||
        // the smaller of the two. If either side is NaN, the other side is returned.
 | 
			
		||||
        self.dependency_rate.min(calc_fee_rate(
 | 
			
		||||
            self.ancestor_fee as f64,
 | 
			
		||||
            f64::from(self.ancestor_sigop_adjusted_weight) / 4.0,
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_dirty_if_different(&mut self, cluster_rate: f64) {
 | 
			
		||||
        if self.effective_fee_per_vsize != cluster_rate {
 | 
			
		||||
            self.effective_fee_per_vsize = cluster_rate;
 | 
			
		||||
            self.dirty = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Safety: This function must NEVER set score to NaN.
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn calc_new_score(&mut self) {
 | 
			
		||||
        self.score = self.adjusted_fee_per_vsize.min(calc_fee_rate(
 | 
			
		||||
            self.ancestor_fee as f64,
 | 
			
		||||
            f64::from(self.ancestor_sigop_adjusted_vsize),
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    pub fn set_ancestors(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ancestors: HashSet<u32, U32HasherState>,
 | 
			
		||||
        total_fee: u64,
 | 
			
		||||
        total_sigop_adjusted_weight: u32,
 | 
			
		||||
        total_sigop_adjusted_vsize: u32,
 | 
			
		||||
        total_sigops: u32,
 | 
			
		||||
    ) {
 | 
			
		||||
        self.ancestors = ancestors;
 | 
			
		||||
        self.ancestor_fee = self.fee + total_fee;
 | 
			
		||||
        self.ancestor_sigop_adjusted_weight =
 | 
			
		||||
            self.sigop_adjusted_weight + total_sigop_adjusted_weight;
 | 
			
		||||
        self.ancestor_sigop_adjusted_vsize = self.sigop_adjusted_vsize + total_sigop_adjusted_vsize;
 | 
			
		||||
        self.ancestor_sigops = self.sigops + total_sigops;
 | 
			
		||||
        self.calc_new_score();
 | 
			
		||||
        self.relatives_set_flag = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    pub fn remove_root(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        root_txid: u32,
 | 
			
		||||
        root_fee: u64,
 | 
			
		||||
        root_sigop_adjusted_weight: u32,
 | 
			
		||||
        root_sigop_adjusted_vsize: u32,
 | 
			
		||||
        root_sigops: u32,
 | 
			
		||||
        cluster_rate: f64,
 | 
			
		||||
    ) -> f64 {
 | 
			
		||||
        let old_score = self.score();
 | 
			
		||||
        self.dependency_rate = self.dependency_rate.min(cluster_rate);
 | 
			
		||||
        if self.ancestors.remove(&root_txid) {
 | 
			
		||||
            self.ancestor_fee -= root_fee;
 | 
			
		||||
            self.ancestor_sigop_adjusted_weight -= root_sigop_adjusted_weight;
 | 
			
		||||
            self.ancestor_sigop_adjusted_vsize -= root_sigop_adjusted_vsize;
 | 
			
		||||
            self.ancestor_sigops -= root_sigops;
 | 
			
		||||
            self.calc_new_score();
 | 
			
		||||
        }
 | 
			
		||||
        old_score
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										421
									
								
								backend/rust-gbt/src/gbt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								backend/rust-gbt/src/gbt.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,421 @@
 | 
			
		||||
use priority_queue::PriorityQueue;
 | 
			
		||||
use std::{cmp::Ordering, collections::HashSet, mem::ManuallyDrop};
 | 
			
		||||
use tracing::{info, trace};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    audit_transaction::{partial_cmp_uid_score, AuditTransaction},
 | 
			
		||||
    u32_hasher_types::{u32hashset_new, u32priority_queue_with_capacity, U32HasherState},
 | 
			
		||||
    GbtResult, ThreadTransactionsMap,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MAX_BLOCK_WEIGHT_UNITS: u32 = 4_000_000 - 4_000;
 | 
			
		||||
const BLOCK_SIGOPS: u32 = 80_000;
 | 
			
		||||
const BLOCK_RESERVED_WEIGHT: u32 = 4_000;
 | 
			
		||||
const BLOCK_RESERVED_SIGOPS: u32 = 400;
 | 
			
		||||
const MAX_BLOCKS: usize = 8;
 | 
			
		||||
 | 
			
		||||
type AuditPool = Vec<Option<ManuallyDrop<AuditTransaction>>>;
 | 
			
		||||
type ModifiedQueue = PriorityQueue<u32, TxPriority, U32HasherState>;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
struct TxPriority {
 | 
			
		||||
    uid: u32,
 | 
			
		||||
    order: u32,
 | 
			
		||||
    score: f64,
 | 
			
		||||
}
 | 
			
		||||
impl PartialEq for TxPriority {
 | 
			
		||||
    fn eq(&self, other: &Self) -> bool {
 | 
			
		||||
        self.uid == other.uid
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl Eq for TxPriority {}
 | 
			
		||||
impl PartialOrd for TxPriority {
 | 
			
		||||
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
 | 
			
		||||
        partial_cmp_uid_score(
 | 
			
		||||
            (self.uid, self.order, self.score),
 | 
			
		||||
            (other.uid, other.order, other.score),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl Ord for TxPriority {
 | 
			
		||||
    fn cmp(&self, other: &Self) -> Ordering {
 | 
			
		||||
        self.partial_cmp(other).expect("score will never be NaN")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core.
 | 
			
		||||
///
 | 
			
		||||
/// See `BlockAssembler` in Bitcoin Core's
 | 
			
		||||
/// [miner.cpp](https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp).
 | 
			
		||||
/// Ported from mempool backend's
 | 
			
		||||
/// [tx-selection-worker.ts](https://github.com/mempool/mempool/blob/master/backend/src/api/tx-selection-worker.ts).
 | 
			
		||||
//
 | 
			
		||||
// TODO: Make gbt smaller to fix these lints.
 | 
			
		||||
#[allow(clippy::too_many_lines)]
 | 
			
		||||
#[allow(clippy::cognitive_complexity)]
 | 
			
		||||
pub fn gbt(mempool: &mut ThreadTransactionsMap, max_uid: usize) -> GbtResult {
 | 
			
		||||
    let mempool_len = mempool.len();
 | 
			
		||||
    let mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1);
 | 
			
		||||
    audit_pool.resize(max_uid + 1, None);
 | 
			
		||||
    let mut mempool_stack: Vec<u32> = Vec::with_capacity(mempool_len);
 | 
			
		||||
    let mut clusters: Vec<Vec<u32>> = Vec::new();
 | 
			
		||||
    let mut block_weights: Vec<u32> = Vec::new();
 | 
			
		||||
 | 
			
		||||
    info!("Initializing working structs");
 | 
			
		||||
    for (uid, tx) in &mut *mempool {
 | 
			
		||||
        let audit_tx = AuditTransaction::from_thread_transaction(tx);
 | 
			
		||||
        // Safety: audit_pool and mempool_stack must always contain the same transactions
 | 
			
		||||
        audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx));
 | 
			
		||||
        mempool_stack.push(*uid);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    info!("Building relatives graph & calculate ancestor scores");
 | 
			
		||||
    for txid in &mempool_stack {
 | 
			
		||||
        set_relatives(*txid, &mut audit_pool);
 | 
			
		||||
    }
 | 
			
		||||
    trace!("Post relative graph Audit Pool: {:#?}", audit_pool);
 | 
			
		||||
 | 
			
		||||
    info!("Sorting by descending ancestor score");
 | 
			
		||||
    let mut mempool_stack: Vec<(u32, u32, f64)> = mempool_stack
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .map(|txid| {
 | 
			
		||||
            let atx = audit_pool
 | 
			
		||||
                .get(txid as usize)
 | 
			
		||||
                .and_then(Option::as_ref)
 | 
			
		||||
                .expect("All txids are from audit_pool");
 | 
			
		||||
            (txid, atx.order(), atx.score())
 | 
			
		||||
        })
 | 
			
		||||
        .collect();
 | 
			
		||||
    mempool_stack.sort_unstable_by(|a, b| partial_cmp_uid_score(*a, *b).expect("Not NaN"));
 | 
			
		||||
    let mut mempool_stack: Vec<u32> = mempool_stack.into_iter().map(|(txid, _, _)| txid).collect();
 | 
			
		||||
 | 
			
		||||
    info!("Building blocks by greedily choosing the highest feerate package");
 | 
			
		||||
    info!("(i.e. the package rooted in the transaction with the best ancestor score)");
 | 
			
		||||
    let mut blocks: Vec<Vec<u32>> = Vec::new();
 | 
			
		||||
    let mut block_weight: u32 = BLOCK_RESERVED_WEIGHT;
 | 
			
		||||
    let mut block_sigops: u32 = BLOCK_RESERVED_SIGOPS;
 | 
			
		||||
    // No need to be bigger than 4096 transactions for the per-block transaction Vec.
 | 
			
		||||
    let initial_txes_per_block: usize = 4096.min(mempool_len);
 | 
			
		||||
    let mut transactions: Vec<u32> = Vec::with_capacity(initial_txes_per_block);
 | 
			
		||||
    let mut modified: ModifiedQueue = u32priority_queue_with_capacity(mempool_len);
 | 
			
		||||
    let mut overflow: Vec<u32> = Vec::new();
 | 
			
		||||
    let mut failures = 0;
 | 
			
		||||
    while !mempool_stack.is_empty() || !modified.is_empty() {
 | 
			
		||||
        // This trace log storm is big, so to make scrolling through
 | 
			
		||||
        // Each iteration easier, leaving a bunch of empty rows
 | 
			
		||||
        // And a header of ======
 | 
			
		||||
        trace!("\n\n\n\n\n\n\n\n\n\n==================================");
 | 
			
		||||
        trace!("mempool_array: {:#?}", mempool_stack);
 | 
			
		||||
        trace!("clusters: {:#?}", clusters);
 | 
			
		||||
        trace!("modified: {:#?}", modified);
 | 
			
		||||
        trace!("audit_pool: {:#?}", audit_pool);
 | 
			
		||||
        trace!("blocks: {:#?}", blocks);
 | 
			
		||||
        trace!("block_weight: {:#?}", block_weight);
 | 
			
		||||
        trace!("block_sigops: {:#?}", block_sigops);
 | 
			
		||||
        trace!("transactions: {:#?}", transactions);
 | 
			
		||||
        trace!("overflow: {:#?}", overflow);
 | 
			
		||||
        trace!("failures: {:#?}", failures);
 | 
			
		||||
        trace!("\n==================================");
 | 
			
		||||
 | 
			
		||||
        let next_from_stack = next_valid_from_stack(&mut mempool_stack, &audit_pool);
 | 
			
		||||
        let next_from_queue = next_valid_from_queue(&mut modified, &audit_pool);
 | 
			
		||||
        if next_from_stack.is_none() && next_from_queue.is_none() {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        let (next_tx, from_stack) = match (next_from_stack, next_from_queue) {
 | 
			
		||||
            (Some(stack_tx), Some(queue_tx)) => match queue_tx.cmp(stack_tx) {
 | 
			
		||||
                std::cmp::Ordering::Less => (stack_tx, true),
 | 
			
		||||
                _ => (queue_tx, false),
 | 
			
		||||
            },
 | 
			
		||||
            (Some(stack_tx), None) => (stack_tx, true),
 | 
			
		||||
            (None, Some(queue_tx)) => (queue_tx, false),
 | 
			
		||||
            (None, None) => unreachable!(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if from_stack {
 | 
			
		||||
            mempool_stack.pop();
 | 
			
		||||
        } else {
 | 
			
		||||
            modified.pop();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if blocks.len() < (MAX_BLOCKS - 1)
 | 
			
		||||
            && ((block_weight + (4 * next_tx.ancestor_sigop_adjusted_vsize())
 | 
			
		||||
                >= MAX_BLOCK_WEIGHT_UNITS)
 | 
			
		||||
                || (block_sigops + next_tx.ancestor_sigops() > BLOCK_SIGOPS))
 | 
			
		||||
        {
 | 
			
		||||
            // hold this package in an overflow list while we check for smaller options
 | 
			
		||||
            overflow.push(next_tx.uid);
 | 
			
		||||
            failures += 1;
 | 
			
		||||
        } else {
 | 
			
		||||
            let mut package: Vec<(u32, u32, usize)> = Vec::new();
 | 
			
		||||
            let mut cluster: Vec<u32> = Vec::new();
 | 
			
		||||
            let is_cluster: bool = !next_tx.ancestors.is_empty();
 | 
			
		||||
            for ancestor_id in &next_tx.ancestors {
 | 
			
		||||
                if let Some(Some(ancestor)) = audit_pool.get(*ancestor_id as usize) {
 | 
			
		||||
                    package.push((*ancestor_id, ancestor.order(), ancestor.ancestors.len()));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            package.sort_unstable_by(|a, b| -> Ordering {
 | 
			
		||||
                if a.2 != b.2 {
 | 
			
		||||
                    // order by ascending ancestor count
 | 
			
		||||
                    a.2.cmp(&b.2)
 | 
			
		||||
                } else if a.1 != b.1 {
 | 
			
		||||
                    // tie-break by ascending partial txid
 | 
			
		||||
                    a.1.cmp(&b.1)
 | 
			
		||||
                } else {
 | 
			
		||||
                    // tie-break partial txid collisions by ascending uid
 | 
			
		||||
                    a.0.cmp(&b.0)
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            package.push((next_tx.uid, next_tx.order(), next_tx.ancestors.len()));
 | 
			
		||||
 | 
			
		||||
            let cluster_rate = next_tx.cluster_rate();
 | 
			
		||||
 | 
			
		||||
            for (txid, _, _) in &package {
 | 
			
		||||
                cluster.push(*txid);
 | 
			
		||||
                if let Some(Some(tx)) = audit_pool.get_mut(*txid as usize) {
 | 
			
		||||
                    tx.used = true;
 | 
			
		||||
                    tx.set_dirty_if_different(cluster_rate);
 | 
			
		||||
                    transactions.push(tx.uid);
 | 
			
		||||
                    block_weight += tx.weight;
 | 
			
		||||
                    block_sigops += tx.sigops;
 | 
			
		||||
                }
 | 
			
		||||
                update_descendants(*txid, &mut audit_pool, &mut modified, cluster_rate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if is_cluster {
 | 
			
		||||
                clusters.push(cluster);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            failures = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // this block is full
 | 
			
		||||
        let exceeded_package_tries =
 | 
			
		||||
            failures > 1000 && block_weight > (MAX_BLOCK_WEIGHT_UNITS - BLOCK_RESERVED_WEIGHT);
 | 
			
		||||
        let queue_is_empty = mempool_stack.is_empty() && modified.is_empty();
 | 
			
		||||
        if (exceeded_package_tries || queue_is_empty) && blocks.len() < (MAX_BLOCKS - 1) {
 | 
			
		||||
            // finalize this block
 | 
			
		||||
            if !transactions.is_empty() {
 | 
			
		||||
                blocks.push(transactions);
 | 
			
		||||
                block_weights.push(block_weight);
 | 
			
		||||
            }
 | 
			
		||||
            // reset for the next block
 | 
			
		||||
            transactions = Vec::with_capacity(initial_txes_per_block);
 | 
			
		||||
            block_weight = BLOCK_RESERVED_WEIGHT;
 | 
			
		||||
            block_sigops = BLOCK_RESERVED_SIGOPS;
 | 
			
		||||
            failures = 0;
 | 
			
		||||
            // 'overflow' packages didn't fit in this block, but are valid candidates for the next
 | 
			
		||||
            overflow.reverse();
 | 
			
		||||
            for overflowed in &overflow {
 | 
			
		||||
                if let Some(Some(overflowed_tx)) = audit_pool.get(*overflowed as usize) {
 | 
			
		||||
                    if overflowed_tx.modified {
 | 
			
		||||
                        modified.push(
 | 
			
		||||
                            *overflowed,
 | 
			
		||||
                            TxPriority {
 | 
			
		||||
                                uid: *overflowed,
 | 
			
		||||
                                order: overflowed_tx.order(),
 | 
			
		||||
                                score: overflowed_tx.score(),
 | 
			
		||||
                            },
 | 
			
		||||
                        );
 | 
			
		||||
                    } else {
 | 
			
		||||
                        mempool_stack.push(*overflowed);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            overflow = Vec::new();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    info!("add the final unbounded block if it contains any transactions");
 | 
			
		||||
    if !transactions.is_empty() {
 | 
			
		||||
        blocks.push(transactions);
 | 
			
		||||
        block_weights.push(block_weight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    info!("make a list of dirty transactions and their new rates");
 | 
			
		||||
    let mut rates: Vec<Vec<f64>> = Vec::new();
 | 
			
		||||
    for (uid, thread_tx) in mempool {
 | 
			
		||||
        // Takes ownership of the audit_tx and replaces with None
 | 
			
		||||
        if let Some(Some(audit_tx)) = audit_pool.get_mut(*uid as usize).map(Option::take) {
 | 
			
		||||
            trace!("txid: {}, is_dirty: {}", uid, audit_tx.dirty);
 | 
			
		||||
            if audit_tx.dirty {
 | 
			
		||||
                rates.push(vec![f64::from(*uid), audit_tx.effective_fee_per_vsize]);
 | 
			
		||||
                thread_tx.effective_fee_per_vsize = audit_tx.effective_fee_per_vsize;
 | 
			
		||||
            }
 | 
			
		||||
            // Drops the AuditTransaction manually
 | 
			
		||||
            // There are no audit_txs that are not in the mempool HashMap
 | 
			
		||||
            // So there is guaranteed to be no memory leaks.
 | 
			
		||||
            ManuallyDrop::into_inner(audit_tx);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    trace!("\n\n\n\n\n====================");
 | 
			
		||||
    trace!("blocks: {:#?}", blocks);
 | 
			
		||||
    trace!("clusters: {:#?}", clusters);
 | 
			
		||||
    trace!("rates: {:#?}\n====================\n\n\n\n\n", rates);
 | 
			
		||||
 | 
			
		||||
    GbtResult {
 | 
			
		||||
        blocks,
 | 
			
		||||
        block_weights,
 | 
			
		||||
        clusters,
 | 
			
		||||
        rates,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn next_valid_from_stack<'a>(
 | 
			
		||||
    mempool_stack: &mut Vec<u32>,
 | 
			
		||||
    audit_pool: &'a AuditPool,
 | 
			
		||||
) -> Option<&'a AuditTransaction> {
 | 
			
		||||
    while let Some(next_txid) = mempool_stack.last() {
 | 
			
		||||
        match audit_pool.get(*next_txid as usize) {
 | 
			
		||||
            Some(Some(tx)) if !tx.used && !tx.modified => {
 | 
			
		||||
                return Some(tx);
 | 
			
		||||
            }
 | 
			
		||||
            _ => {
 | 
			
		||||
                mempool_stack.pop();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn next_valid_from_queue<'a>(
 | 
			
		||||
    queue: &mut ModifiedQueue,
 | 
			
		||||
    audit_pool: &'a AuditPool,
 | 
			
		||||
) -> Option<&'a AuditTransaction> {
 | 
			
		||||
    while let Some((next_txid, _)) = queue.peek() {
 | 
			
		||||
        match audit_pool.get(*next_txid as usize) {
 | 
			
		||||
            Some(Some(tx)) if !tx.used => {
 | 
			
		||||
                return Some(tx);
 | 
			
		||||
            }
 | 
			
		||||
            _ => {
 | 
			
		||||
                queue.pop();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn set_relatives(txid: u32, audit_pool: &mut AuditPool) {
 | 
			
		||||
    let mut parents: HashSet<u32, U32HasherState> = u32hashset_new();
 | 
			
		||||
    if let Some(Some(tx)) = audit_pool.get(txid as usize) {
 | 
			
		||||
        if tx.relatives_set_flag {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        for input in &tx.inputs {
 | 
			
		||||
            parents.insert(*input);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut ancestors: HashSet<u32, U32HasherState> = u32hashset_new();
 | 
			
		||||
    for parent_id in &parents {
 | 
			
		||||
        set_relatives(*parent_id, audit_pool);
 | 
			
		||||
 | 
			
		||||
        if let Some(Some(parent)) = audit_pool.get_mut(*parent_id as usize) {
 | 
			
		||||
            // Safety: ancestors must always contain only txes in audit_pool
 | 
			
		||||
            ancestors.insert(*parent_id);
 | 
			
		||||
            parent.children.insert(txid);
 | 
			
		||||
            for ancestor in &parent.ancestors {
 | 
			
		||||
                ancestors.insert(*ancestor);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut total_fee: u64 = 0;
 | 
			
		||||
    let mut total_sigop_adjusted_weight: u32 = 0;
 | 
			
		||||
    let mut total_sigop_adjusted_vsize: u32 = 0;
 | 
			
		||||
    let mut total_sigops: u32 = 0;
 | 
			
		||||
 | 
			
		||||
    for ancestor_id in &ancestors {
 | 
			
		||||
        let Some(ancestor) = audit_pool
 | 
			
		||||
            .get(*ancestor_id as usize)
 | 
			
		||||
            .expect("audit_pool contains all ancestors") else { todo!() };
 | 
			
		||||
        total_fee += ancestor.fee;
 | 
			
		||||
        total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight;
 | 
			
		||||
        total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize;
 | 
			
		||||
        total_sigops += ancestor.sigops;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Some(Some(tx)) = audit_pool.get_mut(txid as usize) {
 | 
			
		||||
        tx.set_ancestors(
 | 
			
		||||
            ancestors,
 | 
			
		||||
            total_fee,
 | 
			
		||||
            total_sigop_adjusted_weight,
 | 
			
		||||
            total_sigop_adjusted_vsize,
 | 
			
		||||
            total_sigops,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
 | 
			
		||||
fn update_descendants(
 | 
			
		||||
    root_txid: u32,
 | 
			
		||||
    audit_pool: &mut AuditPool,
 | 
			
		||||
    modified: &mut ModifiedQueue,
 | 
			
		||||
    cluster_rate: f64,
 | 
			
		||||
) {
 | 
			
		||||
    let mut visited: HashSet<u32, U32HasherState> = u32hashset_new();
 | 
			
		||||
    let mut descendant_stack: Vec<u32> = Vec::new();
 | 
			
		||||
    let root_fee: u64;
 | 
			
		||||
    let root_sigop_adjusted_weight: u32;
 | 
			
		||||
    let root_sigop_adjusted_vsize: u32;
 | 
			
		||||
    let root_sigops: u32;
 | 
			
		||||
    if let Some(Some(root_tx)) = audit_pool.get(root_txid as usize) {
 | 
			
		||||
        for descendant_id in &root_tx.children {
 | 
			
		||||
            if !visited.contains(descendant_id) {
 | 
			
		||||
                descendant_stack.push(*descendant_id);
 | 
			
		||||
                visited.insert(*descendant_id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        root_fee = root_tx.fee;
 | 
			
		||||
        root_sigop_adjusted_weight = root_tx.sigop_adjusted_weight;
 | 
			
		||||
        root_sigop_adjusted_vsize = root_tx.sigop_adjusted_vsize;
 | 
			
		||||
        root_sigops = root_tx.sigops;
 | 
			
		||||
    } else {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    while let Some(next_txid) = descendant_stack.pop() {
 | 
			
		||||
        if let Some(Some(descendant)) = audit_pool.get_mut(next_txid as usize) {
 | 
			
		||||
            // remove root tx as ancestor
 | 
			
		||||
            let old_score = descendant.remove_root(
 | 
			
		||||
                root_txid,
 | 
			
		||||
                root_fee,
 | 
			
		||||
                root_sigop_adjusted_weight,
 | 
			
		||||
                root_sigop_adjusted_vsize,
 | 
			
		||||
                root_sigops,
 | 
			
		||||
                cluster_rate,
 | 
			
		||||
            );
 | 
			
		||||
            // add to priority queue or update priority if score has changed
 | 
			
		||||
            if descendant.score() < old_score {
 | 
			
		||||
                descendant.modified = true;
 | 
			
		||||
                modified.push_decrease(
 | 
			
		||||
                    descendant.uid,
 | 
			
		||||
                    TxPriority {
 | 
			
		||||
                        uid: descendant.uid,
 | 
			
		||||
                        order: descendant.order(),
 | 
			
		||||
                        score: descendant.score(),
 | 
			
		||||
                    },
 | 
			
		||||
                );
 | 
			
		||||
            } else if descendant.score() > old_score {
 | 
			
		||||
                descendant.modified = true;
 | 
			
		||||
                modified.push_increase(
 | 
			
		||||
                    descendant.uid,
 | 
			
		||||
                    TxPriority {
 | 
			
		||||
                        uid: descendant.uid,
 | 
			
		||||
                        order: descendant.order(),
 | 
			
		||||
                        score: descendant.score(),
 | 
			
		||||
                    },
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // add this node's children to the stack
 | 
			
		||||
            for child_id in &descendant.children {
 | 
			
		||||
                if !visited.contains(child_id) {
 | 
			
		||||
                    descendant_stack.push(*child_id);
 | 
			
		||||
                    visited.insert(*child_id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										177
									
								
								backend/rust-gbt/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								backend/rust-gbt/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,177 @@
 | 
			
		||||
#![warn(clippy::all)]
 | 
			
		||||
#![warn(clippy::pedantic)]
 | 
			
		||||
#![warn(clippy::nursery)]
 | 
			
		||||
#![allow(clippy::cast_precision_loss)]
 | 
			
		||||
#![allow(clippy::cast_possible_truncation)]
 | 
			
		||||
#![allow(clippy::cast_sign_loss)]
 | 
			
		||||
#![allow(clippy::float_cmp)]
 | 
			
		||||
 | 
			
		||||
use napi::bindgen_prelude::Result;
 | 
			
		||||
use napi_derive::napi;
 | 
			
		||||
use thread_transaction::ThreadTransaction;
 | 
			
		||||
use tracing::{debug, info, trace};
 | 
			
		||||
use tracing_log::LogTracer;
 | 
			
		||||
use tracing_subscriber::{EnvFilter, FmtSubscriber};
 | 
			
		||||
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
 | 
			
		||||
mod audit_transaction;
 | 
			
		||||
mod gbt;
 | 
			
		||||
mod thread_transaction;
 | 
			
		||||
mod u32_hasher_types;
 | 
			
		||||
 | 
			
		||||
use u32_hasher_types::{u32hashmap_with_capacity, U32HasherState};
 | 
			
		||||
 | 
			
		||||
/// This is the initial capacity of the `GbtGenerator` struct's inner `HashMap`.
 | 
			
		||||
///
 | 
			
		||||
/// Note: This doesn't *have* to be a power of 2. (uwu)
 | 
			
		||||
const STARTING_CAPACITY: usize = 1_048_576;
 | 
			
		||||
 | 
			
		||||
type ThreadTransactionsMap = HashMap<u32, ThreadTransaction, U32HasherState>;
 | 
			
		||||
 | 
			
		||||
#[napi]
 | 
			
		||||
pub struct GbtGenerator {
 | 
			
		||||
    thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[napi::module_init]
 | 
			
		||||
fn init() {
 | 
			
		||||
    // Set all `tracing` logs to print to STDOUT
 | 
			
		||||
    // Note: Passing RUST_LOG env variable to the node process
 | 
			
		||||
    //       will change the log level for the rust module.
 | 
			
		||||
    tracing::subscriber::set_global_default(
 | 
			
		||||
        FmtSubscriber::builder()
 | 
			
		||||
            .with_env_filter(EnvFilter::from_default_env())
 | 
			
		||||
            .with_ansi(
 | 
			
		||||
                // Default to no-color logs.
 | 
			
		||||
                // Setting RUST_LOG_COLOR to 1 or true|TRUE|True etc.
 | 
			
		||||
                // will enable color
 | 
			
		||||
                std::env::var("RUST_LOG_COLOR")
 | 
			
		||||
                    .map(|s| ["1", "true"].contains(&&*s.to_lowercase()))
 | 
			
		||||
                    .unwrap_or(false),
 | 
			
		||||
            )
 | 
			
		||||
            .finish(),
 | 
			
		||||
    )
 | 
			
		||||
    .expect("Logging subscriber failed");
 | 
			
		||||
    // Convert all `log` logs into `tracing` events
 | 
			
		||||
    LogTracer::init().expect("Legacy log subscriber failed");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[napi]
 | 
			
		||||
impl GbtGenerator {
 | 
			
		||||
    #[napi(constructor)]
 | 
			
		||||
    #[allow(clippy::new_without_default)]
 | 
			
		||||
    #[must_use]
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        debug!("Created new GbtGenerator");
 | 
			
		||||
        Self {
 | 
			
		||||
            thread_transactions: Arc::new(Mutex::new(u32hashmap_with_capacity(STARTING_CAPACITY))),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// # Errors
 | 
			
		||||
    ///
 | 
			
		||||
    /// Rejects if the thread panics or if the Mutex is poisoned.
 | 
			
		||||
    #[napi]
 | 
			
		||||
    pub async fn make(&self, mempool: Vec<ThreadTransaction>, max_uid: u32) -> Result<GbtResult> {
 | 
			
		||||
        trace!("make: Current State {:#?}", self.thread_transactions);
 | 
			
		||||
        run_task(
 | 
			
		||||
            Arc::clone(&self.thread_transactions),
 | 
			
		||||
            max_uid as usize,
 | 
			
		||||
            move |map| {
 | 
			
		||||
                for tx in mempool {
 | 
			
		||||
                    map.insert(tx.uid, tx);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// # Errors
 | 
			
		||||
    ///
 | 
			
		||||
    /// Rejects if the thread panics or if the Mutex is poisoned.
 | 
			
		||||
    #[napi]
 | 
			
		||||
    pub async fn update(
 | 
			
		||||
        &self,
 | 
			
		||||
        new_txs: Vec<ThreadTransaction>,
 | 
			
		||||
        remove_txs: Vec<u32>,
 | 
			
		||||
        max_uid: u32,
 | 
			
		||||
    ) -> Result<GbtResult> {
 | 
			
		||||
        trace!("update: Current State {:#?}", self.thread_transactions);
 | 
			
		||||
        run_task(
 | 
			
		||||
            Arc::clone(&self.thread_transactions),
 | 
			
		||||
            max_uid as usize,
 | 
			
		||||
            move |map| {
 | 
			
		||||
                for tx in new_txs {
 | 
			
		||||
                    map.insert(tx.uid, tx);
 | 
			
		||||
                }
 | 
			
		||||
                for txid in &remove_txs {
 | 
			
		||||
                    map.remove(txid);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// The result from calling the gbt function.
 | 
			
		||||
///
 | 
			
		||||
/// This tuple contains the following:
 | 
			
		||||
///        blocks: A 2D Vector of transaction IDs (u32), the inner Vecs each represent a block.
 | 
			
		||||
/// block_weights: A Vector of total weights per block.
 | 
			
		||||
///      clusters: A 2D Vector of transaction IDs representing clusters of dependent mempool transactions
 | 
			
		||||
///         rates: A Vector of tuples containing transaction IDs (u32) and effective fee per vsize (f64)
 | 
			
		||||
#[napi(constructor)]
 | 
			
		||||
pub struct GbtResult {
 | 
			
		||||
    pub blocks: Vec<Vec<u32>>,
 | 
			
		||||
    pub block_weights: Vec<u32>,
 | 
			
		||||
    pub clusters: Vec<Vec<u32>>,
 | 
			
		||||
    pub rates: Vec<Vec<f64>>, // Tuples not supported. u32 fits inside f64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// All on another thread, this runs an arbitrary task in between
 | 
			
		||||
/// taking the lock and running gbt.
 | 
			
		||||
///
 | 
			
		||||
/// Rather than filling / updating the `HashMap` on the main thread,
 | 
			
		||||
/// this allows for `HashMap` modifying tasks to be run before running and returning gbt results.
 | 
			
		||||
///
 | 
			
		||||
/// `thread_transactions` is a cloned `Arc` of the `Mutex` for the `HashMap` state.
 | 
			
		||||
/// `callback` is a `'static + Send` `FnOnce` closure/function that takes a mutable reference
 | 
			
		||||
/// to the `HashMap` as the only argument. (A move closure is recommended to meet the bounds)
 | 
			
		||||
async fn run_task<F>(
 | 
			
		||||
    thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
 | 
			
		||||
    max_uid: usize,
 | 
			
		||||
    callback: F,
 | 
			
		||||
) -> Result<GbtResult>
 | 
			
		||||
where
 | 
			
		||||
    F: FnOnce(&mut ThreadTransactionsMap) + Send + 'static,
 | 
			
		||||
{
 | 
			
		||||
    debug!("Spawning thread...");
 | 
			
		||||
    let handle = napi::tokio::task::spawn_blocking(move || {
 | 
			
		||||
        debug!(
 | 
			
		||||
            "Getting lock for thread_transactions from thread {:?}...",
 | 
			
		||||
            std::thread::current().id()
 | 
			
		||||
        );
 | 
			
		||||
        let mut map = thread_transactions
 | 
			
		||||
            .lock()
 | 
			
		||||
            .map_err(|_| napi::Error::from_reason("THREAD_TRANSACTIONS Mutex poisoned"))?;
 | 
			
		||||
        callback(&mut map);
 | 
			
		||||
 | 
			
		||||
        info!("Starting gbt algorithm for {} elements...", map.len());
 | 
			
		||||
        let result = gbt::gbt(&mut map, max_uid);
 | 
			
		||||
        info!("Finished gbt algorithm for {} elements...", map.len());
 | 
			
		||||
 | 
			
		||||
        debug!(
 | 
			
		||||
            "Releasing lock for thread_transactions from thread {:?}...",
 | 
			
		||||
            std::thread::current().id()
 | 
			
		||||
        );
 | 
			
		||||
        drop(map);
 | 
			
		||||
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    handle
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|_| napi::Error::from_reason("thread panicked"))?
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								backend/rust-gbt/src/thread_transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/rust-gbt/src/thread_transaction.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
use napi_derive::napi;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
#[napi(object)]
 | 
			
		||||
pub struct ThreadTransaction {
 | 
			
		||||
    pub uid: u32,
 | 
			
		||||
    pub order: u32,
 | 
			
		||||
    pub fee: f64,
 | 
			
		||||
    pub weight: u32,
 | 
			
		||||
    pub sigops: u32,
 | 
			
		||||
    pub effective_fee_per_vsize: f64,
 | 
			
		||||
    pub inputs: Vec<u32>,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										132
									
								
								backend/rust-gbt/src/u32_hasher_types.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								backend/rust-gbt/src/u32_hasher_types.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,132 @@
 | 
			
		||||
use priority_queue::PriorityQueue;
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::{HashMap, HashSet},
 | 
			
		||||
    fmt::Debug,
 | 
			
		||||
    hash::{BuildHasher, Hasher},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// This is the only way to create a `HashMap` with the `U32HasherState` and capacity
 | 
			
		||||
pub fn u32hashmap_with_capacity<V>(capacity: usize) -> HashMap<u32, V, U32HasherState> {
 | 
			
		||||
    HashMap::with_capacity_and_hasher(capacity, U32HasherState(()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// This is the only way to create a `PriorityQueue` with the `U32HasherState` and capacity
 | 
			
		||||
pub fn u32priority_queue_with_capacity<V: Ord>(
 | 
			
		||||
    capacity: usize,
 | 
			
		||||
) -> PriorityQueue<u32, V, U32HasherState> {
 | 
			
		||||
    PriorityQueue::with_capacity_and_hasher(capacity, U32HasherState(()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// This is the only way to create a `HashSet` with the `U32HasherState`
 | 
			
		||||
pub fn u32hashset_new() -> HashSet<u32, U32HasherState> {
 | 
			
		||||
    HashSet::with_hasher(U32HasherState(()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A private unit type is contained so no one can make an instance of it.
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct U32HasherState(());
 | 
			
		||||
 | 
			
		||||
impl Debug for U32HasherState {
 | 
			
		||||
    fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl BuildHasher for U32HasherState {
 | 
			
		||||
    type Hasher = U32Hasher;
 | 
			
		||||
 | 
			
		||||
    fn build_hasher(&self) -> Self::Hasher {
 | 
			
		||||
        U32Hasher(0)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// This also can't be created outside this module due to private field.
 | 
			
		||||
pub struct U32Hasher(u32);
 | 
			
		||||
 | 
			
		||||
impl Hasher for U32Hasher {
 | 
			
		||||
    fn finish(&self) -> u64 {
 | 
			
		||||
        // Safety: Two u32s next to each other will make a u64
 | 
			
		||||
        bytemuck::cast([self.0, 0])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn write(&mut self, bytes: &[u8]) {
 | 
			
		||||
        // Assert in debug builds (testing too) that only 4 byte keys (u32, i32, f32, etc.) run
 | 
			
		||||
        debug_assert!(bytes.len() == 4);
 | 
			
		||||
        // Safety: We know that the size of the key is 4 bytes
 | 
			
		||||
        // We also know that the only way to get an instance of HashMap using this "hasher"
 | 
			
		||||
        // is through the public functions in this module which set the key type to u32.
 | 
			
		||||
        self.0 = *bytemuck::from_bytes(bytes);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::U32HasherState;
 | 
			
		||||
    use priority_queue::PriorityQueue;
 | 
			
		||||
    use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_hashmap() {
 | 
			
		||||
        let mut hm: HashMap<u32, String, U32HasherState> = HashMap::with_hasher(U32HasherState(()));
 | 
			
		||||
 | 
			
		||||
        // Testing basic operations with the custom hasher
 | 
			
		||||
        hm.insert(0, String::from("0"));
 | 
			
		||||
        hm.insert(42, String::from("42"));
 | 
			
		||||
        hm.insert(256, String::from("256"));
 | 
			
		||||
        hm.insert(u32::MAX, String::from("MAX"));
 | 
			
		||||
        hm.insert(u32::MAX >> 2, String::from("MAX >> 2"));
 | 
			
		||||
 | 
			
		||||
        assert_eq!(hm.get(&0), Some(&String::from("0")));
 | 
			
		||||
        assert_eq!(hm.get(&42), Some(&String::from("42")));
 | 
			
		||||
        assert_eq!(hm.get(&256), Some(&String::from("256")));
 | 
			
		||||
        assert_eq!(hm.get(&u32::MAX), Some(&String::from("MAX")));
 | 
			
		||||
        assert_eq!(hm.get(&(u32::MAX >> 2)), Some(&String::from("MAX >> 2")));
 | 
			
		||||
        assert_eq!(hm.get(&(u32::MAX >> 4)), None);
 | 
			
		||||
        assert_eq!(hm.get(&3), None);
 | 
			
		||||
        assert_eq!(hm.get(&43), None);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_priority_queue() {
 | 
			
		||||
        let mut pq: PriorityQueue<u32, i32, U32HasherState> =
 | 
			
		||||
            PriorityQueue::with_hasher(U32HasherState(()));
 | 
			
		||||
 | 
			
		||||
        // Testing basic operations with the custom hasher
 | 
			
		||||
        assert_eq!(pq.push(1, 5), None);
 | 
			
		||||
        assert_eq!(pq.push(2, -10), None);
 | 
			
		||||
        assert_eq!(pq.push(3, 7), None);
 | 
			
		||||
        assert_eq!(pq.push(4, 20), None);
 | 
			
		||||
        assert_eq!(pq.push(u32::MAX, -42), None);
 | 
			
		||||
 | 
			
		||||
        assert_eq!(pq.push_increase(1, 4), Some(4));
 | 
			
		||||
        assert_eq!(pq.push_increase(2, -8), Some(-10));
 | 
			
		||||
        assert_eq!(pq.push_increase(3, 5), Some(5));
 | 
			
		||||
        assert_eq!(pq.push_increase(4, 21), Some(20));
 | 
			
		||||
        assert_eq!(pq.push_increase(u32::MAX, -99), Some(-99));
 | 
			
		||||
        assert_eq!(pq.push_increase(42, 1337), None);
 | 
			
		||||
 | 
			
		||||
        assert_eq!(pq.push_decrease(1, 4), Some(5));
 | 
			
		||||
        assert_eq!(pq.push_decrease(2, -10), Some(-8));
 | 
			
		||||
        assert_eq!(pq.push_decrease(3, 5), Some(7));
 | 
			
		||||
        assert_eq!(pq.push_decrease(4, 20), Some(21));
 | 
			
		||||
        assert_eq!(pq.push_decrease(u32::MAX, 100), Some(100));
 | 
			
		||||
        assert_eq!(pq.push_decrease(69, 420), None);
 | 
			
		||||
 | 
			
		||||
        assert_eq!(pq.peek(), Some((&42, &1337)));
 | 
			
		||||
        assert_eq!(pq.pop(), Some((42, 1337)));
 | 
			
		||||
        assert_eq!(pq.peek(), Some((&69, &420)));
 | 
			
		||||
        assert_eq!(pq.pop(), Some((69, 420)));
 | 
			
		||||
        assert_eq!(pq.peek(), Some((&4, &20)));
 | 
			
		||||
        assert_eq!(pq.pop(), Some((4, 20)));
 | 
			
		||||
        assert_eq!(pq.peek(), Some((&3, &5)));
 | 
			
		||||
        assert_eq!(pq.pop(), Some((3, 5)));
 | 
			
		||||
        assert_eq!(pq.peek(), Some((&1, &4)));
 | 
			
		||||
        assert_eq!(pq.pop(), Some((1, 4)));
 | 
			
		||||
        assert_eq!(pq.peek(), Some((&2, &-10)));
 | 
			
		||||
        assert_eq!(pq.pop(), Some((2, -10)));
 | 
			
		||||
        assert_eq!(pq.peek(), Some((&u32::MAX, &-42)));
 | 
			
		||||
        assert_eq!(pq.pop(), Some((u32::MAX, -42)));
 | 
			
		||||
        assert_eq!(pq.peek(), None);
 | 
			
		||||
        assert_eq!(pq.pop(), None);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
    "INITIAL_BLOCKS_AMOUNT": 7,
 | 
			
		||||
    "MEMPOOL_BLOCKS_AMOUNT": 8,
 | 
			
		||||
    "USE_SECOND_NODE_FOR_MINFEE": 10,
 | 
			
		||||
    "EXTERNAL_ASSETS": 11,
 | 
			
		||||
    "EXTERNAL_ASSETS": [],
 | 
			
		||||
    "EXTERNAL_MAX_RETRY": 12,
 | 
			
		||||
    "EXTERNAL_RETRY_INTERVAL": 13,
 | 
			
		||||
    "USER_AGENT": "__MEMPOOL_USER_AGENT__",
 | 
			
		||||
@ -24,19 +24,20 @@
 | 
			
		||||
    "INDEXING_BLOCKS_AMOUNT": 14,
 | 
			
		||||
    "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
 | 
			
		||||
    "POOLS_JSON_URL": "__POOLS_JSON_URL__",
 | 
			
		||||
    "AUDIT": "__MEMPOOL_AUDIT__",
 | 
			
		||||
    "ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
 | 
			
		||||
    "CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__",
 | 
			
		||||
    "MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__",
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": "__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__"
 | 
			
		||||
    "AUDIT": true,
 | 
			
		||||
    "ADVANCED_GBT_AUDIT": true,
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": true,
 | 
			
		||||
    "RUST_GBT": false,
 | 
			
		||||
    "CPFP_INDEXING": true,
 | 
			
		||||
    "MAX_BLOCKS_BULK_QUERY": 999,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 999
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "__CORE_RPC_HOST__",
 | 
			
		||||
    "PORT": 15,
 | 
			
		||||
    "USERNAME": "__CORE_RPC_USERNAME__",
 | 
			
		||||
    "PASSWORD": "__CORE_RPC_PASSWORD__",
 | 
			
		||||
    "TIMEOUT": "__CORE_RPC_TIMEOUT__"
 | 
			
		||||
    "TIMEOUT": 1000
 | 
			
		||||
  },
 | 
			
		||||
  "ELECTRUM": {
 | 
			
		||||
    "HOST": "__ELECTRUM_HOST__",
 | 
			
		||||
@ -46,14 +47,14 @@
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "__ESPLORA_REST_API_URL__",
 | 
			
		||||
    "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
 | 
			
		||||
    "RETRY_UNIX_SOCKET_AFTER": "__ESPLORA_RETRY_UNIX_SOCKET_AFTER__"
 | 
			
		||||
    "RETRY_UNIX_SOCKET_AFTER": 888
 | 
			
		||||
  },
 | 
			
		||||
  "SECOND_CORE_RPC": {
 | 
			
		||||
    "HOST": "__SECOND_CORE_RPC_HOST__",
 | 
			
		||||
    "PORT": 17,
 | 
			
		||||
    "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
 | 
			
		||||
    "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
 | 
			
		||||
    "TIMEOUT": "__SECOND_CORE_RPC_TIMEOUT__"
 | 
			
		||||
    "TIMEOUT": 2000
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
@ -63,7 +64,7 @@
 | 
			
		||||
    "DATABASE": "__DATABASE_DATABASE__",
 | 
			
		||||
    "USERNAME": "__DATABASE_USERNAME__",
 | 
			
		||||
    "PASSWORD": "__DATABASE_PASSWORD__",
 | 
			
		||||
    "TIMEOUT": "__DATABASE_TIMEOUT__"
 | 
			
		||||
    "TIMEOUT": 3000
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
@ -101,14 +102,14 @@
 | 
			
		||||
    "BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
    "ENABLED": "__LIGHTNING_ENABLED__",
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "BACKEND": "__LIGHTNING_BACKEND__",
 | 
			
		||||
    "TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
 | 
			
		||||
    "STATS_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": 30,
 | 
			
		||||
    "FORENSICS_INTERVAL": 43200,
 | 
			
		||||
    "FORENSICS_RATE_LIMIT": "__FORENSICS_RATE_LIMIT__"
 | 
			
		||||
    "FORENSICS_RATE_LIMIT": 1234
 | 
			
		||||
  },
 | 
			
		||||
  "LND": {
 | 
			
		||||
    "TLS_CERT_PATH": "",
 | 
			
		||||
@ -119,4 +120,4 @@
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "__CLIGHTNING_SOCKET__"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -14,11 +14,11 @@ describe('Mempool Difficulty Adjustment', () => {
 | 
			
		||||
          750134,                         // Current block height
 | 
			
		||||
          0.6280047707459726,             // Previous retarget % (Passed through)
 | 
			
		||||
          'mainnet',                      // Network (if testnet, next value is non-zero)
 | 
			
		||||
          0,                              // If not testnet, not used
 | 
			
		||||
          0,                              // Latest block timestamp in seconds (only used if difficulty already locked in)
 | 
			
		||||
        ],
 | 
			
		||||
        { // Expected Result
 | 
			
		||||
          progressPercent: 9.027777777777777,
 | 
			
		||||
          difficultyChange: 12.562233927411782,
 | 
			
		||||
          difficultyChange: 13.180707740199772,
 | 
			
		||||
          estimatedRetargetDate: 1661895424692,
 | 
			
		||||
          remainingBlocks: 1834,
 | 
			
		||||
          remainingTime: 977591692,
 | 
			
		||||
@ -41,7 +41,7 @@ describe('Mempool Difficulty Adjustment', () => {
 | 
			
		||||
        ],
 | 
			
		||||
        { // Expected Result is same other than timeOffset
 | 
			
		||||
          progressPercent: 9.027777777777777,
 | 
			
		||||
          difficultyChange: 12.562233927411782,
 | 
			
		||||
          difficultyChange: 13.180707740199772,
 | 
			
		||||
          estimatedRetargetDate: 1661895424692,
 | 
			
		||||
          remainingBlocks: 1834,
 | 
			
		||||
          remainingTime: 977591692,
 | 
			
		||||
@ -54,6 +54,29 @@ describe('Mempool Difficulty Adjustment', () => {
 | 
			
		||||
          expectedBlocks: 161.68833333333333,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      [ // Vector 3 (mainnet lock-in (epoch ending 788255))
 | 
			
		||||
        [ // Inputs
 | 
			
		||||
          dt('2023-04-20T09:57:33.000Z'), // Last DA time (in seconds)
 | 
			
		||||
          dt('2023-05-04T14:54:09.000Z'), // Current time (now) (in seconds)
 | 
			
		||||
          788255,                         // Current block height
 | 
			
		||||
          1.7220298879531821,             // Previous retarget % (Passed through)
 | 
			
		||||
          'mainnet',                      // Network (if testnet, next value is non-zero)
 | 
			
		||||
          dt('2023-05-04T14:54:26.000Z'), // Latest block timestamp in seconds
 | 
			
		||||
        ],
 | 
			
		||||
        { // Expected Result
 | 
			
		||||
          progressPercent: 99.95039682539682,
 | 
			
		||||
          difficultyChange: -1.4512637555574193,
 | 
			
		||||
          estimatedRetargetDate: 1683212658129,
 | 
			
		||||
          remainingBlocks: 1,
 | 
			
		||||
          remainingTime: 609129,
 | 
			
		||||
          previousRetarget: 1.7220298879531821,
 | 
			
		||||
          previousTime: 1681984653,
 | 
			
		||||
          nextRetargetHeight: 788256,
 | 
			
		||||
          timeAvg: 609129,
 | 
			
		||||
          timeOffset: 0,
 | 
			
		||||
          expectedBlocks: 2045.66,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    ] as [[number, number, number, number, string, number], DifficultyAdjustment][];
 | 
			
		||||
 | 
			
		||||
    for (const vector of vectors) {
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        AUDIT: false,
 | 
			
		||||
        ADVANCED_GBT_AUDIT: false,
 | 
			
		||||
        ADVANCED_GBT_MEMPOOL: false,
 | 
			
		||||
        RUST_GBT: false,
 | 
			
		||||
        CPFP_INDEXING: false,
 | 
			
		||||
        MAX_BLOCKS_BULK_QUERY: 0,
 | 
			
		||||
        DISK_CACHE_BLOCK_INTERVAL: 6,
 | 
			
		||||
@ -152,4 +153,94 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
      expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('should ensure the docker start.sh script has default values', () => {
 | 
			
		||||
    jest.isolateModules(() => {
 | 
			
		||||
      const startSh = fs.readFileSync(`${__dirname}/../../../docker/backend/start.sh`, 'utf-8');
 | 
			
		||||
      const fixture = JSON.parse(fs.readFileSync(`${__dirname}/../__fixtures__/mempool-config.template.json`, 'utf8'));
 | 
			
		||||
 | 
			
		||||
      function parseJson(jsonObj, root?) {
 | 
			
		||||
        for (const [key, value] of Object.entries(jsonObj)) {
 | 
			
		||||
          // We have a few cases where we can't follow the pattern
 | 
			
		||||
          if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
 | 
			
		||||
            console.log('skipping check for MEMPOOL_HTTP_PORT');
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          switch (typeof value) {
 | 
			
		||||
            case 'object': {
 | 
			
		||||
              if (Array.isArray(value)) {
 | 
			
		||||
                return;
 | 
			
		||||
              } else {
 | 
			
		||||
                parseJson(value, key);
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
            default: {
 | 
			
		||||
              //The flattened string, i.e, __MEMPOOL_ENABLED__
 | 
			
		||||
              const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
 | 
			
		||||
 | 
			
		||||
              //The string used as the environment variable, i.e, MEMPOOL_ENABLED
 | 
			
		||||
              const envVarStr = `${root ? root : ''}_${key}`;
 | 
			
		||||
 | 
			
		||||
              //The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
 | 
			
		||||
              const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
 | 
			
		||||
 | 
			
		||||
              console.log(`looking for ${defaultEntry} in the start.sh script`);
 | 
			
		||||
              const re = new RegExp(defaultEntry);
 | 
			
		||||
              expect(startSh).toMatch(re);
 | 
			
		||||
 | 
			
		||||
              //The string that actually replaces the values in the config file
 | 
			
		||||
              const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json';
 | 
			
		||||
              console.log(`looking for ${sedStr} in the start.sh script`);
 | 
			
		||||
              expect(startSh).toContain(sedStr);
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      parseJson(fixture);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('should ensure that the mempool-config.json Docker template has all the keys', () => {
 | 
			
		||||
    jest.isolateModules(() => {
 | 
			
		||||
      const fixture = JSON.parse(fs.readFileSync(`${__dirname}/../__fixtures__/mempool-config.template.json`, 'utf8'));
 | 
			
		||||
      const dockerJson = fs.readFileSync(`${__dirname}/../../../docker/backend/mempool-config.json`, 'utf-8');
 | 
			
		||||
 | 
			
		||||
      function parseJson(jsonObj, root?) {
 | 
			
		||||
        for (const [key, value] of Object.entries(jsonObj)) {
 | 
			
		||||
          switch (typeof value) {
 | 
			
		||||
            case 'object': {
 | 
			
		||||
              if (Array.isArray(value)) {
 | 
			
		||||
                // numbers, arrays and booleans won't be enclosed by quotes
 | 
			
		||||
                const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
 | 
			
		||||
                expect(dockerJson).toContain(`"${key}": ${replaceStr}`);
 | 
			
		||||
                break;
 | 
			
		||||
              } else {
 | 
			
		||||
                //Check for top level config keys
 | 
			
		||||
                expect(dockerJson).toContain(`"${key}"`);
 | 
			
		||||
                parseJson(value, key);
 | 
			
		||||
                break;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            case 'string': {
 | 
			
		||||
              // strings should be enclosed by quotes
 | 
			
		||||
              const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
 | 
			
		||||
              expect(dockerJson).toContain(`"${key}": "${replaceStr}"`);
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
            default: {
 | 
			
		||||
              // numbers, arrays and booleans won't be enclosed by quotes
 | 
			
		||||
              const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
 | 
			
		||||
              expect(dockerJson).toContain(`"${key}": ${replaceStr}`);
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      parseJson(fixture);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										68
									
								
								backend/src/__tests__/gbt/gbt-tests.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								backend/src/__tests__/gbt/gbt-tests.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import { GbtGenerator, ThreadTransaction } from '../../../rust-gbt';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
const baseline = require('./test-data/target-template.json');
 | 
			
		||||
const testVector = require('./test-data/test-data-ids.json');
 | 
			
		||||
const vectorUidMap: Map<number, string> = new Map(testVector.map(x => [x[0], x[1]]));
 | 
			
		||||
const vectorTxidMap: Map<string, number>  = new Map(testVector.map(x => [x[1], x[0]]));
 | 
			
		||||
// Note that this test buffer is specially constructed
 | 
			
		||||
// such that uids are assigned in numerical txid order
 | 
			
		||||
// so that ties break the same way as in Core's implementation
 | 
			
		||||
const vectorBuffer: Buffer = fs.readFileSync(path.join(__dirname, './', './test-data/test-buffer.bin'));
 | 
			
		||||
 | 
			
		||||
describe('Rust GBT', () => {
 | 
			
		||||
  test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
 | 
			
		||||
    const rustGbt = new GbtGenerator();
 | 
			
		||||
    const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
 | 
			
		||||
    const result = await rustGbt.make(mempool, maxUid);
 | 
			
		||||
 | 
			
		||||
    const blocks: [string, number][][] = result.blocks.map(block => {
 | 
			
		||||
      return block.map(uid => [vectorUidMap.get(uid) || 'missing', uid]);
 | 
			
		||||
    });
 | 
			
		||||
    const template = baseline.map(tx => [tx.txid, vectorTxidMap.get(tx.txid)]);
 | 
			
		||||
 | 
			
		||||
    expect(blocks[0].length).toEqual(baseline.length);
 | 
			
		||||
    expect(blocks[0]).toEqual(template);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function mempoolFromArrayBuffer(buf: ArrayBuffer): { mempool: ThreadTransaction[], maxUid: number } {
 | 
			
		||||
  let maxUid = 0;
 | 
			
		||||
  const view = new DataView(buf);
 | 
			
		||||
  const count = view.getUint32(0, false);
 | 
			
		||||
  const txs: ThreadTransaction[] = [];
 | 
			
		||||
  let offset = 4;
 | 
			
		||||
  for (let i = 0; i < count; i++) {
 | 
			
		||||
    const uid = view.getUint32(offset, false);
 | 
			
		||||
    maxUid = Math.max(maxUid, uid);
 | 
			
		||||
    const tx: ThreadTransaction = {
 | 
			
		||||
      uid,
 | 
			
		||||
      order: txidToOrdering(vectorUidMap.get(uid) as string),
 | 
			
		||||
      fee: view.getFloat64(offset + 4, false),
 | 
			
		||||
      weight: view.getUint32(offset + 12, false),
 | 
			
		||||
      sigops: view.getUint32(offset + 16, false),
 | 
			
		||||
      // feePerVsize: view.getFloat64(offset + 20, false),
 | 
			
		||||
      effectiveFeePerVsize: view.getFloat64(offset + 28, false),
 | 
			
		||||
      inputs: [],
 | 
			
		||||
    };
 | 
			
		||||
    const numInputs = view.getUint32(offset + 36, false);
 | 
			
		||||
    offset += 40;
 | 
			
		||||
    for (let j = 0; j < numInputs; j++) {
 | 
			
		||||
      tx.inputs.push(view.getUint32(offset, false));
 | 
			
		||||
      offset += 4;
 | 
			
		||||
    }
 | 
			
		||||
    txs.push(tx);
 | 
			
		||||
  }
 | 
			
		||||
  return { mempool: txs, maxUid };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function txidToOrdering(txid: string): number {
 | 
			
		||||
  return parseInt(
 | 
			
		||||
    txid.substr(62, 2) +
 | 
			
		||||
      txid.substr(60, 2) +
 | 
			
		||||
      txid.substr(58, 2) +
 | 
			
		||||
      txid.substr(56, 2),
 | 
			
		||||
    16
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7070
									
								
								backend/src/__tests__/gbt/test-data/target-template.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7070
									
								
								backend/src/__tests__/gbt/test-data/target-template.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/src/__tests__/gbt/test-data/test-buffer.bin
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/src/__tests__/gbt/test-data/test-buffer.bin
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								backend/src/__tests__/gbt/test-data/test-data-ids.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								backend/src/__tests__/gbt/test-data/test-data-ids.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,19 +1,21 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
 | 
			
		||||
import rbfCache from './rbf-cache';
 | 
			
		||||
 | 
			
		||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
 | 
			
		||||
 | 
			
		||||
class Audit {
 | 
			
		||||
  auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
 | 
			
		||||
   : { censored: string[], added: string[], fresh: string[], score: number, similarity: number } {
 | 
			
		||||
   : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } {
 | 
			
		||||
    if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
 | 
			
		||||
      return { censored: [], added: [], fresh: [], score: 0, similarity: 1 };
 | 
			
		||||
      return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const matches: string[] = []; // present in both mined block and template
 | 
			
		||||
    const added: string[] = []; // present in mined block, not in template
 | 
			
		||||
    const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
 | 
			
		||||
    const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
 | 
			
		||||
    const isCensored = {}; // missing, without excuse
 | 
			
		||||
    const isDisplaced = {};
 | 
			
		||||
    let displacedWeight = 0;
 | 
			
		||||
@ -35,7 +37,9 @@ class Audit {
 | 
			
		||||
    for (const txid of projectedBlocks[0].transactionIds) {
 | 
			
		||||
      if (!inBlock[txid]) {
 | 
			
		||||
        // tx is recent, may have reached the miner too late for inclusion
 | 
			
		||||
        if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
 | 
			
		||||
        if (rbfCache.isFullRbf(txid)) {
 | 
			
		||||
          fullrbf.push(txid);
 | 
			
		||||
        } else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
 | 
			
		||||
          fresh.push(txid);
 | 
			
		||||
        } else {
 | 
			
		||||
          isCensored[txid] = true;
 | 
			
		||||
@ -91,7 +95,9 @@ class Audit {
 | 
			
		||||
      if (inTemplate[tx.txid]) {
 | 
			
		||||
        matches.push(tx.txid);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (!isDisplaced[tx.txid]) {
 | 
			
		||||
        if (rbfCache.isFullRbf(tx.txid)) {
 | 
			
		||||
          fullrbf.push(tx.txid);
 | 
			
		||||
        } else if (!isDisplaced[tx.txid]) {
 | 
			
		||||
          added.push(tx.txid);
 | 
			
		||||
        }
 | 
			
		||||
        overflowWeight += tx.weight;
 | 
			
		||||
@ -137,6 +143,8 @@ class Audit {
 | 
			
		||||
      censored: Object.keys(isCensored),
 | 
			
		||||
      added,
 | 
			
		||||
      fresh,
 | 
			
		||||
      sigop: [],
 | 
			
		||||
      fullrbf,
 | 
			
		||||
      score,
 | 
			
		||||
      similarity,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
      weight: block.weight,
 | 
			
		||||
      previousblockhash: block.previousblockhash,
 | 
			
		||||
      mediantime: block.mediantime,
 | 
			
		||||
      stale: block.confirmations === -1,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -415,12 +416,38 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
      vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) {
 | 
			
		||||
      const witnessScript = vin.witness[vin.witness.length - 2];
 | 
			
		||||
      vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
 | 
			
		||||
      const witnessScript = this.witnessToP2TRScript(vin.witness);
 | 
			
		||||
      if (witnessScript !== null) {
 | 
			
		||||
        vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This function must only be called when we know the witness we are parsing
 | 
			
		||||
   * is a taproot witness.
 | 
			
		||||
   * @param witness An array of hex strings that represents the witness stack of
 | 
			
		||||
   *                the input.
 | 
			
		||||
   * @returns null if the witness is not a script spend, and the hex string of
 | 
			
		||||
   *          the script item if it is a script spend.
 | 
			
		||||
   */
 | 
			
		||||
  private witnessToP2TRScript(witness: string[]): string | null {
 | 
			
		||||
    if (witness.length < 2) return null;
 | 
			
		||||
    // Note: see BIP341 for parsing details of witness stack
 | 
			
		||||
 | 
			
		||||
    // If there are at least two witness elements, and the first byte of the
 | 
			
		||||
    // last element is 0x50, this last element is called annex a and
 | 
			
		||||
    // is removed from the witness stack.
 | 
			
		||||
    const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
 | 
			
		||||
    // If there are at least two witness elements left, script path spending is used.
 | 
			
		||||
    // Call the second-to-last stack element s, the script.
 | 
			
		||||
    // (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
 | 
			
		||||
    if (hasAnnex && witness.length < 3) return null;
 | 
			
		||||
    const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
 | 
			
		||||
    return witness[positionOfScript];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoinApi;
 | 
			
		||||
 | 
			
		||||
@ -211,6 +211,8 @@ class BitcoinRoutes {
 | 
			
		||||
          bestDescendant: tx.bestDescendant || null,
 | 
			
		||||
          descendants: tx.descendants || null,
 | 
			
		||||
          effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
 | 
			
		||||
          sigops: tx.sigops,
 | 
			
		||||
          adjustedVsize: tx.adjustedVsize,
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
@ -222,7 +224,12 @@ class BitcoinRoutes {
 | 
			
		||||
    } else {
 | 
			
		||||
      let cpfpInfo;
 | 
			
		||||
      if (config.DATABASE.ENABLED) {
 | 
			
		||||
        cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
 | 
			
		||||
        try {
 | 
			
		||||
          cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          res.status(500).send('failed to get CPFP info');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (cpfpInfo) {
 | 
			
		||||
        res.json(cpfpInfo);
 | 
			
		||||
@ -392,9 +399,13 @@ class BitcoinRoutes {
 | 
			
		||||
 | 
			
		||||
  private async getBlockAuditSummary(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const transactions = await blocks.$getBlockAuditSummary(req.params.hash);
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
      const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
 | 
			
		||||
      if (auditSummary) {
 | 
			
		||||
        res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
 | 
			
		||||
        res.json(auditSummary);
 | 
			
		||||
      } else {
 | 
			
		||||
        return res.status(404).send(`audit not available`);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -89,6 +89,7 @@ export namespace IEsploraApi {
 | 
			
		||||
    weight: number;
 | 
			
		||||
    previousblockhash: string;
 | 
			
		||||
    mediantime: number;
 | 
			
		||||
    stale: boolean;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Address {
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import config from '../config';
 | 
			
		||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary } from '../mempool.interfaces';
 | 
			
		||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import diskCache from './disk-cache';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
@ -34,7 +34,7 @@ class Blocks {
 | 
			
		||||
  private lastDifficultyAdjustmentTime = 0;
 | 
			
		||||
  private previousDifficultyRetarget = 0;
 | 
			
		||||
  private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
 | 
			
		||||
  private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = [];
 | 
			
		||||
  private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = [];
 | 
			
		||||
 | 
			
		||||
  private mainLoopTimeout: number = 120000;
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ class Blocks {
 | 
			
		||||
    this.newBlockCallbacks.push(fn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setNewAsyncBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>) {
 | 
			
		||||
  public setNewAsyncBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>) {
 | 
			
		||||
    this.newAsyncBlockCallbacks.push(fn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,7 @@ class Blocks {
 | 
			
		||||
    blockHeight: number,
 | 
			
		||||
    onlyCoinbase: boolean,
 | 
			
		||||
    quiet: boolean = false,
 | 
			
		||||
    addMempoolData: boolean = false,
 | 
			
		||||
  ): Promise<TransactionExtended[]> {
 | 
			
		||||
    const transactions: TransactionExtended[] = [];
 | 
			
		||||
    const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
 | 
			
		||||
@ -96,14 +97,14 @@ class Blocks {
 | 
			
		||||
          logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
          const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
 | 
			
		||||
          const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
 | 
			
		||||
          transactions.push(tx);
 | 
			
		||||
          transactionsFetched++;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          try {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
              // Try again with core
 | 
			
		||||
              const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true);
 | 
			
		||||
              const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData);
 | 
			
		||||
              transactions.push(tx);
 | 
			
		||||
              transactionsFetched++;
 | 
			
		||||
            } else {
 | 
			
		||||
@ -126,12 +127,6 @@ class Blocks {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    transactions.forEach((tx) => {
 | 
			
		||||
      if (!tx.cpfpChecked) {
 | 
			
		||||
        Common.setRelativesAndGetCpfpInfo(tx, mempool); // Child Pay For Parent
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!quiet) {
 | 
			
		||||
      logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
 | 
			
		||||
    }
 | 
			
		||||
@ -163,6 +158,13 @@ class Blocks {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
 | 
			
		||||
    return {
 | 
			
		||||
      id: hash,
 | 
			
		||||
      transactions: Common.stripTransactions(transactions),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
 | 
			
		||||
    block.tx.forEach(tx => {
 | 
			
		||||
      tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
 | 
			
		||||
@ -279,10 +281,14 @@ class Blocks {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      extras.matchRate = null;
 | 
			
		||||
      extras.expectedFees = null;
 | 
			
		||||
      extras.expectedWeight = null;
 | 
			
		||||
      if (config.MEMPOOL.AUDIT) {
 | 
			
		||||
        const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
 | 
			
		||||
        if (auditScore != null) {
 | 
			
		||||
          extras.matchRate = auditScore.matchRate;
 | 
			
		||||
          extras.expectedFees = auditScore.expectedFees;
 | 
			
		||||
          extras.expectedWeight = auditScore.expectedWeight;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -306,7 +312,7 @@ class Blocks {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
 | 
			
		||||
    const address = txMinerInfo.vout[0].scriptpubkey_address;
 | 
			
		||||
    const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter((address) => address);
 | 
			
		||||
 | 
			
		||||
    let pools: PoolTag[] = [];
 | 
			
		||||
    if (config.DATABASE.ENABLED === true) {
 | 
			
		||||
@ -316,11 +322,13 @@ class Blocks {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < pools.length; ++i) {
 | 
			
		||||
      if (address !== undefined) {
 | 
			
		||||
        const addresses: string[] = typeof pools[i].addresses === 'string' ?
 | 
			
		||||
      if (addresses.length) {
 | 
			
		||||
        const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
 | 
			
		||||
          JSON.parse(pools[i].addresses) : pools[i].addresses;
 | 
			
		||||
        if (addresses.indexOf(address) !== -1) {
 | 
			
		||||
          return pools[i];
 | 
			
		||||
        for (let y = 0; y < poolAddresses.length; y++) {
 | 
			
		||||
          if (addresses.indexOf(poolAddresses[y]) !== -1) {
 | 
			
		||||
            return pools[i];
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -450,6 +458,46 @@ class Blocks {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * [INDEXING] Index expected fees & weight for all audited blocks
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateAuditStats(): Promise<void> {
 | 
			
		||||
    const blockIds = await BlocksAuditsRepository.$getBlocksWithoutSummaries();
 | 
			
		||||
    if (!blockIds?.length) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    let timer = Date.now();
 | 
			
		||||
    let indexedThisRun = 0;
 | 
			
		||||
    let indexedTotal = 0;
 | 
			
		||||
    logger.debug(`Indexing ${blockIds.length} block audit details`);
 | 
			
		||||
    for (const hash of blockIds) {
 | 
			
		||||
      const summary = await BlocksSummariesRepository.$getTemplate(hash);
 | 
			
		||||
      let totalFees = 0;
 | 
			
		||||
      let totalWeight = 0;
 | 
			
		||||
      for (const tx of summary?.transactions || []) {
 | 
			
		||||
        totalFees += tx.fee;
 | 
			
		||||
        totalWeight += (tx.vsize * 4);
 | 
			
		||||
      }
 | 
			
		||||
      await BlocksAuditsRepository.$setSummary(hash, totalFees, totalWeight);
 | 
			
		||||
      const cachedBlock = this.blocks.find(block => block.id === hash);
 | 
			
		||||
      if (cachedBlock) {
 | 
			
		||||
        cachedBlock.extras.expectedFees = totalFees;
 | 
			
		||||
        cachedBlock.extras.expectedWeight = totalWeight;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      indexedThisRun++;
 | 
			
		||||
      indexedTotal++;
 | 
			
		||||
      const elapsedSeconds = (Date.now() - timer) / 1000;
 | 
			
		||||
      if (elapsedSeconds > 5) {
 | 
			
		||||
        const blockPerSeconds = indexedThisRun / elapsedSeconds;
 | 
			
		||||
        logger.debug(`Indexed ${indexedTotal} / ${blockIds.length} block audit details (${blockPerSeconds.toFixed(1)}/s)`);
 | 
			
		||||
        timer = Date.now();
 | 
			
		||||
        indexedThisRun = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    logger.debug(`Indexing block audit details completed`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * [INDEXING] Index all blocks metadata for the mining dashboard
 | 
			
		||||
   */
 | 
			
		||||
@ -594,16 +642,20 @@ class Blocks {
 | 
			
		||||
      const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
 | 
			
		||||
      const block = BitcoinApi.convertBlock(verboseBlock);
 | 
			
		||||
      const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
 | 
			
		||||
      const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
 | 
			
		||||
      const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, false, true) as MempoolTransactionExtended[];
 | 
			
		||||
      if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
        // fill in missing transaction fee data from verboseBlock
 | 
			
		||||
        for (let i = 0; i < transactions.length; i++) {
 | 
			
		||||
          if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
 | 
			
		||||
            transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
 | 
			
		||||
      const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
 | 
			
		||||
      const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
 | 
			
		||||
      const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
 | 
			
		||||
      this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
 | 
			
		||||
 | 
			
		||||
      // start async callbacks
 | 
			
		||||
      this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
 | 
			
		||||
      const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
 | 
			
		||||
 | 
			
		||||
      if (Common.indexingEnabled()) {
 | 
			
		||||
        if (!fastForwarded) {
 | 
			
		||||
          const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
 | 
			
		||||
@ -615,16 +667,19 @@ class Blocks {
 | 
			
		||||
            await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
 | 
			
		||||
            await HashratesRepository.$deleteLastEntries();
 | 
			
		||||
            await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
 | 
			
		||||
            this.blocks = this.blocks.slice(0, -10);
 | 
			
		||||
            this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
 | 
			
		||||
            for (let i = 10; i >= 0; --i) {
 | 
			
		||||
              const newBlock = await this.$indexBlock(lastBlock.height - i);
 | 
			
		||||
              this.blocks.push(newBlock);
 | 
			
		||||
              this.updateTimerProgress(timer, `reindexed block`);
 | 
			
		||||
              await this.$getStrippedBlockTransactions(newBlock.id, true, true);
 | 
			
		||||
              this.updateTimerProgress(timer, `reindexed block summary`);
 | 
			
		||||
              let cpfpSummary;
 | 
			
		||||
              if (config.MEMPOOL.CPFP_INDEXING) {
 | 
			
		||||
                await this.$indexCPFP(newBlock.id, lastBlock.height - i);
 | 
			
		||||
                cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
 | 
			
		||||
                this.updateTimerProgress(timer, `reindexed block cpfp`);
 | 
			
		||||
              }
 | 
			
		||||
              await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
 | 
			
		||||
              this.updateTimerProgress(timer, `reindexed block summary`);
 | 
			
		||||
            }
 | 
			
		||||
            await mining.$indexDifficultyAdjustments();
 | 
			
		||||
            await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
 | 
			
		||||
@ -632,9 +687,12 @@ class Blocks {
 | 
			
		||||
            logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
 | 
			
		||||
            indexer.reindex();
 | 
			
		||||
          }
 | 
			
		||||
          await blocksRepository.$saveBlockInDatabase(blockExtended);
 | 
			
		||||
          this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await blocksRepository.$saveBlockInDatabase(blockExtended);
 | 
			
		||||
        this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
 | 
			
		||||
 | 
			
		||||
        if (!fastForwarded) {
 | 
			
		||||
          const lastestPriceId = await PricesRepository.$getLatestPriceId();
 | 
			
		||||
          this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
 | 
			
		||||
          if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
 | 
			
		||||
@ -652,7 +710,7 @@ class Blocks {
 | 
			
		||||
 | 
			
		||||
          // Save blocks summary for visualization if it's enabled
 | 
			
		||||
          if (Common.blocksSummariesIndexingEnabled() === true) {
 | 
			
		||||
            await this.$getStrippedBlockTransactions(blockExtended.id, true);
 | 
			
		||||
            await this.$getStrippedBlockTransactions(blockExtended.id, true, false, cpfpSummary, blockExtended.height);
 | 
			
		||||
            this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`);
 | 
			
		||||
          }
 | 
			
		||||
          if (config.MEMPOOL.CPFP_INDEXING) {
 | 
			
		||||
@ -662,6 +720,10 @@ class Blocks {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // start async callbacks
 | 
			
		||||
      this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
 | 
			
		||||
      const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
 | 
			
		||||
 | 
			
		||||
      if (block.height % 2016 === 0) {
 | 
			
		||||
        if (Common.indexingEnabled()) {
 | 
			
		||||
          await DifficultyAdjustmentsRepository.$saveAdjustments({
 | 
			
		||||
@ -678,6 +740,11 @@ class Blocks {
 | 
			
		||||
        this.currentDifficulty = block.difficulty;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // wait for pending async callbacks to finish
 | 
			
		||||
      this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
 | 
			
		||||
      await Promise.all(callbackPromises);
 | 
			
		||||
      this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
 | 
			
		||||
 | 
			
		||||
      this.blocks.push(blockExtended);
 | 
			
		||||
      if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
 | 
			
		||||
        this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
 | 
			
		||||
@ -694,11 +761,6 @@ class Blocks {
 | 
			
		||||
        diskCache.$saveCacheToDisk();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // wait for pending async callbacks to finish
 | 
			
		||||
      this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
 | 
			
		||||
      await Promise.all(callbackPromises);
 | 
			
		||||
      this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
 | 
			
		||||
 | 
			
		||||
      handledBlocks++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -754,6 +816,16 @@ class Blocks {
 | 
			
		||||
    return blockExtended;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
 | 
			
		||||
    const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
 | 
			
		||||
    const transactions = await this.$getTransactionsExtended(hash, block.height, true);
 | 
			
		||||
    const blockExtended = await this.$getBlockExtended(block, transactions);
 | 
			
		||||
 | 
			
		||||
    blockExtended.canonical = await bitcoinApi.$getBlockHash(block.height);
 | 
			
		||||
 | 
			
		||||
    return blockExtended;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get one block by its hash
 | 
			
		||||
   */
 | 
			
		||||
@ -771,11 +843,15 @@ class Blocks {
 | 
			
		||||
 | 
			
		||||
    // Bitcoin network, add our custom data on top
 | 
			
		||||
    const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
 | 
			
		||||
    return await this.$indexBlock(block.height);
 | 
			
		||||
    if (block.stale) {
 | 
			
		||||
      return await this.$indexStaleBlock(hash);
 | 
			
		||||
    } else {
 | 
			
		||||
      return await this.$indexBlock(block.height);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false,
 | 
			
		||||
    skipDBLookup = false): Promise<TransactionStripped[]>
 | 
			
		||||
    skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise<TransactionStripped[]>
 | 
			
		||||
  {
 | 
			
		||||
    if (skipMemoryCache === false) {
 | 
			
		||||
      // Check the memory cache
 | 
			
		||||
@ -793,13 +869,35 @@ class Blocks {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Call Core RPC
 | 
			
		||||
    const block = await bitcoinClient.getBlock(hash, 2);
 | 
			
		||||
    const summary = this.summarizeBlock(block);
 | 
			
		||||
    let height = blockHeight;
 | 
			
		||||
    let summary: BlockSummary;
 | 
			
		||||
    if (cpfpSummary) {
 | 
			
		||||
      summary = {
 | 
			
		||||
        id: hash,
 | 
			
		||||
        transactions: cpfpSummary.transactions.map(tx => {
 | 
			
		||||
          return {
 | 
			
		||||
            txid: tx.txid,
 | 
			
		||||
            fee: tx.fee,
 | 
			
		||||
            vsize: tx.vsize,
 | 
			
		||||
            value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
 | 
			
		||||
            rate: tx.effectiveFeePerVsize
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      // Call Core RPC
 | 
			
		||||
      const block = await bitcoinClient.getBlock(hash, 2);
 | 
			
		||||
      summary = this.summarizeBlock(block);
 | 
			
		||||
      height = block.height;
 | 
			
		||||
    }
 | 
			
		||||
    if (height == null) {
 | 
			
		||||
      const block = await bitcoinApi.$getBlock(hash);
 | 
			
		||||
      height = block.height;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Index the response if needed
 | 
			
		||||
    if (Common.blocksSummariesIndexingEnabled() === true) {
 | 
			
		||||
      await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions);
 | 
			
		||||
      await BlocksSummariesRepository.$saveTransactions(height, hash, summary.transactions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return summary.transactions;
 | 
			
		||||
@ -955,19 +1053,11 @@ class Blocks {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockAuditSummary(hash: string): Promise<any> {
 | 
			
		||||
    let summary;
 | 
			
		||||
    if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
 | 
			
		||||
      summary = await BlocksAuditsRepository.$getBlockAudit(hash);
 | 
			
		||||
      return BlocksAuditsRepository.$getBlockAudit(hash);
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // fallback to non-audited transaction summary
 | 
			
		||||
    if (!summary?.transactions?.length) {
 | 
			
		||||
      const strippedTransactions = await this.$getStrippedBlockTransactions(hash);
 | 
			
		||||
      summary = {
 | 
			
		||||
        transactions: strippedTransactions
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return summary;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getLastDifficultyAdjustmentTime(): number {
 | 
			
		||||
@ -998,9 +1088,13 @@ class Blocks {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
 | 
			
		||||
    const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);
 | 
			
		||||
    if (!result) {
 | 
			
		||||
      await cpfpRepository.$insertProgressMarker(height);
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);
 | 
			
		||||
      if (!result) {
 | 
			
		||||
        await cpfpRepository.$insertProgressMarker(height);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // not a fatal error, we'll try again next time the indexer runs
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
 | 
			
		||||
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
 | 
			
		||||
import { isIP } from 'net';
 | 
			
		||||
@ -57,15 +57,15 @@ export class Common {
 | 
			
		||||
    return arr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended[] } {
 | 
			
		||||
    const matches: { [txid: string]: TransactionExtended[] } = {};
 | 
			
		||||
  static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended[] } {
 | 
			
		||||
    const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
 | 
			
		||||
    added
 | 
			
		||||
      .forEach((addedTx) => {
 | 
			
		||||
        const foundMatches = deleted.filter((deletedTx) => {
 | 
			
		||||
          // The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
 | 
			
		||||
          return addedTx.fee > deletedTx.fee
 | 
			
		||||
            // The new transaction must pay more fee per kB than the replaced tx.
 | 
			
		||||
            && addedTx.feePerVsize > deletedTx.feePerVsize
 | 
			
		||||
            && addedTx.adjustedFeePerVsize > deletedTx.adjustedFeePerVsize
 | 
			
		||||
            // Spends one or more of the same inputs
 | 
			
		||||
            && deletedTx.vin.some((deletedVin) =>
 | 
			
		||||
              addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
 | 
			
		||||
@ -77,6 +77,32 @@ export class Common {
 | 
			
		||||
    return matches;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static findMinedRbfTransactions(minedTransactions: TransactionExtended[], spendMap: Map<string, MempoolTransactionExtended>): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
 | 
			
		||||
    const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
 | 
			
		||||
    for (const tx of minedTransactions) {
 | 
			
		||||
      const replaced: Set<MempoolTransactionExtended> = new Set();
 | 
			
		||||
      for (let i = 0; i < tx.vin.length; i++) {
 | 
			
		||||
        const vin = tx.vin[i];
 | 
			
		||||
        const match = spendMap.get(`${vin.txid}:${vin.vout}`);
 | 
			
		||||
        if (match && match.txid !== tx.txid) {
 | 
			
		||||
          replaced.add(match);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (replaced.size) {
 | 
			
		||||
        matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx };
 | 
			
		||||
      }
 | 
			
		||||
      // remove this tx from the spendMap
 | 
			
		||||
      // prevents the same tx being replaced more than once
 | 
			
		||||
      for (const vin of tx.vin) {
 | 
			
		||||
        const key = `${vin.txid}:${vin.vout}`;
 | 
			
		||||
        if (spendMap.get(key)?.txid === tx.txid) {
 | 
			
		||||
          spendMap.delete(key);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return matches;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static stripTransaction(tx: TransactionExtended): TransactionStripped {
 | 
			
		||||
    return {
 | 
			
		||||
      txid: tx.txid,
 | 
			
		||||
@ -87,6 +113,10 @@ export class Common {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static stripTransactions(txs: TransactionExtended[]): TransactionStripped[] {
 | 
			
		||||
    return txs.map(this.stripTransaction);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static sleep$(ms: number): Promise<void> {
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
       setTimeout(() => {
 | 
			
		||||
@ -102,18 +132,18 @@ export class Common {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static setRelativesAndGetCpfpInfo(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): CpfpInfo {
 | 
			
		||||
  static setRelativesAndGetCpfpInfo(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
 | 
			
		||||
    const parents = this.findAllParents(tx, memPool);
 | 
			
		||||
    const lowerFeeParents = parents.filter((parent) => parent.feePerVsize < tx.effectiveFeePerVsize);
 | 
			
		||||
    const lowerFeeParents = parents.filter((parent) => parent.adjustedFeePerVsize < tx.effectiveFeePerVsize);
 | 
			
		||||
 | 
			
		||||
    let totalWeight = tx.weight + lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
 | 
			
		||||
    let totalWeight = (tx.adjustedVsize * 4) + lowerFeeParents.reduce((prev, val) => prev + (val.adjustedVsize * 4), 0);
 | 
			
		||||
    let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
 | 
			
		||||
 | 
			
		||||
    tx.ancestors = parents
 | 
			
		||||
      .map((t) => {
 | 
			
		||||
        return {
 | 
			
		||||
          txid: t.txid,
 | 
			
		||||
          weight: t.weight,
 | 
			
		||||
          weight: (t.adjustedVsize * 4),
 | 
			
		||||
          fee: t.fee,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
@ -134,8 +164,8 @@ export class Common {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  private static findAllParents(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): TransactionExtended[] {
 | 
			
		||||
    let parents: TransactionExtended[] = [];
 | 
			
		||||
  private static findAllParents(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): MempoolTransactionExtended[] {
 | 
			
		||||
    let parents: MempoolTransactionExtended[] = [];
 | 
			
		||||
    tx.vin.forEach((parent) => {
 | 
			
		||||
      if (parents.find((p) => p.txid === parent.txid)) {
 | 
			
		||||
        return;
 | 
			
		||||
@ -143,17 +173,17 @@ export class Common {
 | 
			
		||||
 | 
			
		||||
      const parentTx = memPool[parent.txid];
 | 
			
		||||
      if (parentTx) {
 | 
			
		||||
        if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.feePerVsize) {
 | 
			
		||||
        if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.adjustedFeePerVsize) {
 | 
			
		||||
          if (parentTx.bestDescendant && parentTx.bestDescendant.fee < tx.fee + tx.bestDescendant.fee) {
 | 
			
		||||
            parentTx.bestDescendant = {
 | 
			
		||||
              weight: tx.weight + tx.bestDescendant.weight,
 | 
			
		||||
              weight: (tx.adjustedVsize * 4) + tx.bestDescendant.weight,
 | 
			
		||||
              fee: tx.fee + tx.bestDescendant.fee,
 | 
			
		||||
              txid: tx.txid,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        } else if (tx.feePerVsize > parentTx.feePerVsize) {
 | 
			
		||||
        } else if (tx.adjustedFeePerVsize > parentTx.adjustedFeePerVsize) {
 | 
			
		||||
          parentTx.bestDescendant = {
 | 
			
		||||
            weight: tx.weight,
 | 
			
		||||
            weight: (tx.adjustedVsize * 4),
 | 
			
		||||
            fee: tx.fee,
 | 
			
		||||
            txid: tx.txid
 | 
			
		||||
          };
 | 
			
		||||
@ -348,40 +378,80 @@ export class Common {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static calculateCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary {
 | 
			
		||||
    const clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[] = [];
 | 
			
		||||
    let cluster: TransactionExtended[] = [];
 | 
			
		||||
    let ancestors: { [txid: string]: boolean } = {};
 | 
			
		||||
    const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
 | 
			
		||||
    const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
 | 
			
		||||
    let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
 | 
			
		||||
    let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
 | 
			
		||||
    const txMap = {};
 | 
			
		||||
    // initialize the txMap
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
      txMap[tx.txid] = tx;
 | 
			
		||||
    }
 | 
			
		||||
    // reverse pass to identify CPFP clusters
 | 
			
		||||
    for (let i = transactions.length - 1; i >= 0; i--) {
 | 
			
		||||
      const tx = transactions[i];
 | 
			
		||||
      txMap[tx.txid] = tx;
 | 
			
		||||
      if (!ancestors[tx.txid]) {
 | 
			
		||||
        let totalFee = 0;
 | 
			
		||||
        let totalVSize = 0;
 | 
			
		||||
        cluster.forEach(tx => {
 | 
			
		||||
        clusterTxs.forEach(tx => {
 | 
			
		||||
          totalFee += tx?.fee || 0;
 | 
			
		||||
          totalVSize += (tx.weight / 4);
 | 
			
		||||
        });
 | 
			
		||||
        const effectiveFeePerVsize = totalFee / totalVSize;
 | 
			
		||||
        if (cluster.length > 1) {
 | 
			
		||||
          clusters.push({
 | 
			
		||||
            root: cluster[0].txid,
 | 
			
		||||
        let cluster: CpfpCluster;
 | 
			
		||||
        if (clusterTxs.length > 1) {
 | 
			
		||||
          cluster = {
 | 
			
		||||
            root: clusterTxs[0].txid,
 | 
			
		||||
            height,
 | 
			
		||||
            txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
 | 
			
		||||
            txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
 | 
			
		||||
            effectiveFeePerVsize,
 | 
			
		||||
          });
 | 
			
		||||
          };
 | 
			
		||||
          clusters.push(cluster);
 | 
			
		||||
        }
 | 
			
		||||
        cluster.forEach(tx => {
 | 
			
		||||
        clusterTxs.forEach(tx => {
 | 
			
		||||
          txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
 | 
			
		||||
          if (cluster) {
 | 
			
		||||
            clusterMap[tx.txid] = cluster;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        cluster = [];
 | 
			
		||||
        // reset working vars
 | 
			
		||||
        clusterTxs = [];
 | 
			
		||||
        ancestors = {};
 | 
			
		||||
      }
 | 
			
		||||
      cluster.push(tx);
 | 
			
		||||
      clusterTxs.push(tx);
 | 
			
		||||
      tx.vin.forEach(vin => {
 | 
			
		||||
        ancestors[vin.txid] = true;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    // forward pass to enforce ancestor rate caps
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
      let minAncestorRate = tx.effectiveFeePerVsize;
 | 
			
		||||
      for (const vin of tx.vin) {
 | 
			
		||||
        if (txMap[vin.txid]?.effectiveFeePerVsize) {
 | 
			
		||||
          minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // check rounded values to skip cases with almost identical fees
 | 
			
		||||
      const roundedMinAncestorRate = Math.ceil(minAncestorRate);
 | 
			
		||||
      const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
 | 
			
		||||
      if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
 | 
			
		||||
        tx.effectiveFeePerVsize = minAncestorRate;
 | 
			
		||||
        if (!clusterMap[tx.txid]) {
 | 
			
		||||
          // add a single-tx cluster to record the dependent rate
 | 
			
		||||
          const cluster = {
 | 
			
		||||
            root: tx.txid,
 | 
			
		||||
            height,
 | 
			
		||||
            txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
 | 
			
		||||
            effectiveFeePerVsize: minAncestorRate,
 | 
			
		||||
          };
 | 
			
		||||
          clusterMap[tx.txid] = cluster;
 | 
			
		||||
          clusters.push(cluster);
 | 
			
		||||
        } else {
 | 
			
		||||
          // update the existing cluster with the dependent rate
 | 
			
		||||
          clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      transactions,
 | 
			
		||||
      clusters,
 | 
			
		||||
@ -442,3 +512,119 @@ export class Common {
 | 
			
		||||
    return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class to calculate average fee rates of a list of transactions
 | 
			
		||||
 * at certain weight percentiles, in a single pass
 | 
			
		||||
 * 
 | 
			
		||||
 * init with:
 | 
			
		||||
 *   maxWeight - the total weight to measure percentiles relative to (e.g. 4MW for a single block)
 | 
			
		||||
 *   percentileBandWidth - how many weight units to average over for each percentile (as a % of maxWeight)
 | 
			
		||||
 *   percentiles - an array of weight percentiles to compute, in %
 | 
			
		||||
 * 
 | 
			
		||||
 * then call .processNext(tx) for each transaction, in descending order
 | 
			
		||||
 * 
 | 
			
		||||
 * retrieve the final results with .getFeeStats()
 | 
			
		||||
 */
 | 
			
		||||
export class OnlineFeeStatsCalculator {
 | 
			
		||||
  private maxWeight: number;
 | 
			
		||||
  private percentiles = [10,25,50,75,90];
 | 
			
		||||
 | 
			
		||||
  private bandWidthPercent = 2;
 | 
			
		||||
  private bandWidth: number = 0;
 | 
			
		||||
  private bandIndex = 0;
 | 
			
		||||
  private leftBound = 0;
 | 
			
		||||
  private rightBound = 0;
 | 
			
		||||
  private inBand = false;
 | 
			
		||||
  private totalBandFee = 0;
 | 
			
		||||
  private totalBandWeight = 0;
 | 
			
		||||
  private minBandRate = Infinity;
 | 
			
		||||
  private maxBandRate = 0;
 | 
			
		||||
 | 
			
		||||
  private feeRange: { avg: number, min: number, max: number }[] = [];
 | 
			
		||||
  private totalWeight: number = 0;
 | 
			
		||||
 | 
			
		||||
  constructor (maxWeight: number, percentileBandWidth?: number, percentiles?: number[]) {
 | 
			
		||||
    this.maxWeight = maxWeight;
 | 
			
		||||
    if (percentiles && percentiles.length) {
 | 
			
		||||
      this.percentiles = percentiles;
 | 
			
		||||
    }
 | 
			
		||||
    if (percentileBandWidth != null) {
 | 
			
		||||
      this.bandWidthPercent = percentileBandWidth;
 | 
			
		||||
    }
 | 
			
		||||
    this.bandWidth = this.maxWeight * (this.bandWidthPercent / 100);
 | 
			
		||||
    // add min/max percentiles aligned to the ends of the range
 | 
			
		||||
    this.percentiles.unshift(this.bandWidthPercent / 2);
 | 
			
		||||
    this.percentiles.push(100 - (this.bandWidthPercent / 2));
 | 
			
		||||
    this.setNextBounds();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  processNext(tx: { weight: number, fee: number, effectiveFeePerVsize?: number, feePerVsize?: number, rate?: number, txid: string }): void {
 | 
			
		||||
    let left = this.totalWeight;
 | 
			
		||||
    const right = this.totalWeight + tx.weight;
 | 
			
		||||
    if (!this.inBand && right <= this.leftBound) {
 | 
			
		||||
      this.totalWeight += tx.weight;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (left < right) {
 | 
			
		||||
      if (right > this.leftBound) {
 | 
			
		||||
        this.inBand = true;
 | 
			
		||||
        const txRate = (tx.rate || tx.effectiveFeePerVsize || tx.feePerVsize || 0);
 | 
			
		||||
        const weight = Math.min(right, this.rightBound) - Math.max(left, this.leftBound);
 | 
			
		||||
        this.totalBandFee += (txRate * weight);
 | 
			
		||||
        this.totalBandWeight += weight;
 | 
			
		||||
        this.maxBandRate = Math.max(this.maxBandRate, txRate);
 | 
			
		||||
        this.minBandRate = Math.min(this.minBandRate, txRate);
 | 
			
		||||
      }
 | 
			
		||||
      left = Math.min(right, this.rightBound);
 | 
			
		||||
 | 
			
		||||
      if (left >= this.rightBound) {
 | 
			
		||||
        this.inBand = false;
 | 
			
		||||
        const avgBandFeeRate = this.totalBandWeight ? (this.totalBandFee / this.totalBandWeight) : 0;
 | 
			
		||||
        this.feeRange.unshift({ avg: avgBandFeeRate, min: this.minBandRate, max: this.maxBandRate });
 | 
			
		||||
        this.bandIndex++;
 | 
			
		||||
        this.setNextBounds();
 | 
			
		||||
        this.totalBandFee = 0;
 | 
			
		||||
        this.totalBandWeight = 0;
 | 
			
		||||
        this.minBandRate = Infinity;
 | 
			
		||||
        this.maxBandRate = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.totalWeight += tx.weight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setNextBounds(): void {
 | 
			
		||||
    const nextPercentile = this.percentiles[this.bandIndex];
 | 
			
		||||
    if (nextPercentile != null) {
 | 
			
		||||
      this.leftBound = ((nextPercentile / 100) * this.maxWeight) - (this.bandWidth / 2);
 | 
			
		||||
      this.rightBound = this.leftBound + this.bandWidth;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.leftBound = Infinity;
 | 
			
		||||
      this.rightBound = Infinity;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawFeeStats(): WorkingEffectiveFeeStats {
 | 
			
		||||
    if (this.totalBandWeight > 0) {
 | 
			
		||||
      const avgBandFeeRate = this.totalBandWeight ? (this.totalBandFee / this.totalBandWeight) : 0;
 | 
			
		||||
      this.feeRange.unshift({ avg: avgBandFeeRate, min: this.minBandRate, max: this.maxBandRate });
 | 
			
		||||
    }
 | 
			
		||||
    while (this.feeRange.length < this.percentiles.length) {
 | 
			
		||||
      this.feeRange.unshift({ avg: 0, min: 0, max: 0 });
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      minFee: this.feeRange[0].min,
 | 
			
		||||
      medianFee: this.feeRange[Math.floor(this.feeRange.length / 2)].avg,
 | 
			
		||||
      maxFee: this.feeRange[this.feeRange.length - 1].max,
 | 
			
		||||
      feeRange: this.feeRange.map(f => f.avg),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getFeeStats(): EffectiveFeeStats {
 | 
			
		||||
    const stats = this.getRawFeeStats();
 | 
			
		||||
    stats.feeRange[0] = stats.minFee;
 | 
			
		||||
    stats.feeRange[stats.feeRange.length - 1] = stats.maxFee;
 | 
			
		||||
    return stats;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 59;
 | 
			
		||||
  private static currentVersion = 63;
 | 
			
		||||
  private queryTimeout = 3600_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -516,6 +516,33 @@ class DatabaseMigration {
 | 
			
		||||
      // https://github.com/mempool/mempool/issues/3360
 | 
			
		||||
      await this.$executeQuery(`TRUNCATE prices`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 60 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
 | 
			
		||||
      await this.updateToSchemaVersion(60);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 61 && isBitcoin === true) {
 | 
			
		||||
      // Break block templates into their own table
 | 
			
		||||
      if (! await this.$checkIfTableExists('blocks_templates')) {
 | 
			
		||||
        await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
 | 
			
		||||
      }
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
 | 
			
		||||
      await this.updateToSchemaVersion(61);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 62 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
 | 
			
		||||
      await this.updateToSchemaVersion(62);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 63 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
 | 
			
		||||
      await this.updateToSchemaVersion(63);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -1034,7 +1061,7 @@ class DatabaseMigration {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $blocksReindexingTruncate(): Promise<void> {
 | 
			
		||||
    logger.warn(`Truncating pools, blocks and hashrates for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`);
 | 
			
		||||
    logger.warn(`Truncating pools, blocks, hashrates and difficulty_adjustments tables for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`);
 | 
			
		||||
    await Common.sleep$(5000);
 | 
			
		||||
 | 
			
		||||
    await this.$executeQuery(`TRUNCATE blocks`);
 | 
			
		||||
 | 
			
		||||
@ -34,11 +34,12 @@ export function calcDifficultyAdjustment(
 | 
			
		||||
  const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
 | 
			
		||||
  const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
 | 
			
		||||
  const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
 | 
			
		||||
  const actualTimespan = (blocksInEpoch === 2015 ? latestBlockTimestamp : nowSeconds) - DATime;
 | 
			
		||||
 | 
			
		||||
  let difficultyChange = 0;
 | 
			
		||||
  let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET;
 | 
			
		||||
 | 
			
		||||
  difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
 | 
			
		||||
  difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
 | 
			
		||||
  // Max increase is x4 (+300%)
 | 
			
		||||
  if (difficultyChange > 300) {
 | 
			
		||||
    difficultyChange = 300;
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ class DiskCache {
 | 
			
		||||
  private static RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/rbfcache.json';
 | 
			
		||||
  private static CHUNK_FILES = 25;
 | 
			
		||||
  private isWritingCache = false;
 | 
			
		||||
  private ignoreBlocksCache = false;
 | 
			
		||||
 | 
			
		||||
  private semaphore: { resume: (() => void)[], locks: number } = {
 | 
			
		||||
    resume: [],
 | 
			
		||||
@ -194,6 +195,7 @@ class DiskCache {
 | 
			
		||||
 | 
			
		||||
        if (data.mempoolArray) {
 | 
			
		||||
          for (const tx of data.mempoolArray) {
 | 
			
		||||
            delete tx.uid;
 | 
			
		||||
            data.mempool[tx.txid] = tx;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
@ -206,6 +208,7 @@ class DiskCache {
 | 
			
		||||
            const cacheData2 = JSON.parse(fs.readFileSync(fileName, 'utf8'));
 | 
			
		||||
            if (cacheData2.mempoolArray) {
 | 
			
		||||
              for (const tx of cacheData2.mempoolArray) {
 | 
			
		||||
                delete tx.uid;
 | 
			
		||||
                data.mempool[tx.txid] = tx;
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
@ -218,8 +221,13 @@ class DiskCache {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await memPool.$setMempool(data.mempool);
 | 
			
		||||
      blocks.setBlocks(data.blocks);
 | 
			
		||||
      blocks.setBlockSummaries(data.blockSummaries || []);
 | 
			
		||||
      if (!this.ignoreBlocksCache) {
 | 
			
		||||
        blocks.setBlocks(data.blocks);
 | 
			
		||||
        blocks.setBlockSummaries(data.blockSummaries || []);
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.info('Re-saving cache with empty recent blocks data');
 | 
			
		||||
        await this.$saveCacheToDisk(true);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
@ -273,6 +281,10 @@ class DiskCache {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setIgnoreBlocksCache(): void {
 | 
			
		||||
    this.ignoreBlocksCache = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new DiskCache();
 | 
			
		||||
 | 
			
		||||
@ -373,7 +373,7 @@ class NodesApi {
 | 
			
		||||
 | 
			
		||||
  public async $searchNodeByPublicKeyOrAlias(search: string) {
 | 
			
		||||
    try {
 | 
			
		||||
      const publicKeySearch = search.replace('%', '') + '%';
 | 
			
		||||
      const publicKeySearch = search.replace(/[^a-zA-Z0-9]/g, '') + '%';
 | 
			
		||||
      const aliasSearch = search
 | 
			
		||||
        .replace(/[-_.]/g, ' ') // Replace all -_. characters with empty space. Eg: "ln.nicehash" becomes "ln nicehash".  
 | 
			
		||||
        .replace(/[^a-zA-Z0-9 ]/g, '') // Remove all special characters and keep just A to Z, 0 to 9.
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,23 @@
 | 
			
		||||
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction } from '../../rust-gbt';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats } from '../mempool.interfaces';
 | 
			
		||||
import { Common, OnlineFeeStatsCalculator } from './common';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { Worker } from 'worker_threads';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
const MAX_UINT32 = Math.pow(2, 32) - 1;
 | 
			
		||||
 | 
			
		||||
class MempoolBlocks {
 | 
			
		||||
  private mempoolBlocks: MempoolBlockWithTransactions[] = [];
 | 
			
		||||
  private mempoolBlockDeltas: MempoolBlockDelta[] = [];
 | 
			
		||||
  private txSelectionWorker: Worker | null = null;
 | 
			
		||||
  private rustInitialized: boolean = false;
 | 
			
		||||
  private rustGbtGenerator: GbtGenerator = new GbtGenerator();
 | 
			
		||||
 | 
			
		||||
  private nextUid: number = 1;
 | 
			
		||||
  private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  public getMempoolBlocks(): MempoolBlock[] {
 | 
			
		||||
    return this.mempoolBlocks.map((block) => {
 | 
			
		||||
      return {
 | 
			
		||||
@ -36,13 +39,11 @@ class MempoolBlocks {
 | 
			
		||||
    return this.mempoolBlockDeltas;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
 | 
			
		||||
  public updateMempoolBlocks(memPool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
 | 
			
		||||
    const latestMempool = memPool;
 | 
			
		||||
    const memPoolArray: TransactionExtended[] = [];
 | 
			
		||||
    const memPoolArray: MempoolTransactionExtended[] = [];
 | 
			
		||||
    for (const i in latestMempool) {
 | 
			
		||||
      if (latestMempool.hasOwnProperty(i)) {
 | 
			
		||||
        memPoolArray.push(latestMempool[i]);
 | 
			
		||||
      }
 | 
			
		||||
      memPoolArray.push(latestMempool[i]);
 | 
			
		||||
    }
 | 
			
		||||
    const start = new Date().getTime();
 | 
			
		||||
 | 
			
		||||
@ -52,17 +53,17 @@ class MempoolBlocks {
 | 
			
		||||
      tx.ancestors = [];
 | 
			
		||||
      tx.cpfpChecked = false;
 | 
			
		||||
      if (!tx.effectiveFeePerVsize) {
 | 
			
		||||
        tx.effectiveFeePerVsize = tx.feePerVsize;
 | 
			
		||||
        tx.effectiveFeePerVsize = tx.adjustedFeePerVsize;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // First sort
 | 
			
		||||
    memPoolArray.sort((a, b) => {
 | 
			
		||||
      if (a.feePerVsize === b.feePerVsize) {
 | 
			
		||||
      if (a.adjustedFeePerVsize === b.adjustedFeePerVsize) {
 | 
			
		||||
        // tie-break by lexicographic txid order for stability
 | 
			
		||||
        return a.txid < b.txid ? -1 : 1;
 | 
			
		||||
      } else {
 | 
			
		||||
        return b.feePerVsize - a.feePerVsize;
 | 
			
		||||
        return b.adjustedFeePerVsize - a.adjustedFeePerVsize;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -102,16 +103,18 @@ class MempoolBlocks {
 | 
			
		||||
    return blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
 | 
			
		||||
  private calculateMempoolBlocks(transactionsSorted: MempoolTransactionExtended[]): MempoolBlockWithTransactions[] {
 | 
			
		||||
    const mempoolBlocks: MempoolBlockWithTransactions[] = [];
 | 
			
		||||
    let feeStatsCalculator: OnlineFeeStatsCalculator = new OnlineFeeStatsCalculator(config.MEMPOOL.BLOCK_WEIGHT_UNITS);
 | 
			
		||||
    let onlineStats = false;
 | 
			
		||||
    let blockSize = 0;
 | 
			
		||||
    let blockWeight = 0;
 | 
			
		||||
    let blockVsize = 0;
 | 
			
		||||
    let blockFees = 0;
 | 
			
		||||
    const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
 | 
			
		||||
    let transactionIds: string[] = [];
 | 
			
		||||
    let transactions: TransactionExtended[] = [];
 | 
			
		||||
    transactionsSorted.forEach((tx) => {
 | 
			
		||||
    let transactions: MempoolTransactionExtended[] = [];
 | 
			
		||||
    transactionsSorted.forEach((tx, index) => {
 | 
			
		||||
      if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
 | 
			
		||||
        || mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
 | 
			
		||||
        tx.position = {
 | 
			
		||||
@ -126,6 +129,9 @@ class MempoolBlocks {
 | 
			
		||||
          transactions.push(tx);
 | 
			
		||||
        }
 | 
			
		||||
        transactionIds.push(tx.txid);
 | 
			
		||||
        if (onlineStats) {
 | 
			
		||||
          feeStatsCalculator.processNext(tx);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
 | 
			
		||||
        blockVsize = 0;
 | 
			
		||||
@ -133,6 +139,16 @@ class MempoolBlocks {
 | 
			
		||||
          block: mempoolBlocks.length,
 | 
			
		||||
          vsize: blockVsize + (tx.vsize / 2),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
 | 
			
		||||
          const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0);
 | 
			
		||||
          if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
 | 
			
		||||
            onlineStats = true;
 | 
			
		||||
            feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
 | 
			
		||||
            feeStatsCalculator.processNext(tx);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        blockVsize += tx.vsize;
 | 
			
		||||
        blockWeight = tx.weight;
 | 
			
		||||
        blockSize = tx.size;
 | 
			
		||||
@ -142,7 +158,8 @@ class MempoolBlocks {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (transactions.length) {
 | 
			
		||||
      mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
 | 
			
		||||
      const feeStats = onlineStats ? feeStatsCalculator.getRawFeeStats() : undefined;
 | 
			
		||||
      mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees, feeStats));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return mempoolBlocks;
 | 
			
		||||
@ -189,7 +206,7 @@ class MempoolBlocks {
 | 
			
		||||
    return mempoolBlockDeltas;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
 | 
			
		||||
  public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
 | 
			
		||||
    const start = Date.now();
 | 
			
		||||
 | 
			
		||||
    // reset mempool short ids
 | 
			
		||||
@ -202,15 +219,17 @@ class MempoolBlocks {
 | 
			
		||||
    // to reduce the overhead of passing this data to the worker thread
 | 
			
		||||
    const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
 | 
			
		||||
    Object.values(newMempool).forEach(entry => {
 | 
			
		||||
      if (entry.uid != null) {
 | 
			
		||||
        strippedMempool.set(entry.uid, {
 | 
			
		||||
      if (entry.uid !== null && entry.uid !== undefined) {
 | 
			
		||||
        const stripped = {
 | 
			
		||||
          uid: entry.uid,
 | 
			
		||||
          fee: entry.fee,
 | 
			
		||||
          weight: entry.weight,
 | 
			
		||||
          feePerVsize: entry.fee / (entry.weight / 4),
 | 
			
		||||
          effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
 | 
			
		||||
          inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
 | 
			
		||||
        });
 | 
			
		||||
          weight: (entry.adjustedVsize * 4),
 | 
			
		||||
          sigops: entry.sigops,
 | 
			
		||||
          feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
 | 
			
		||||
          effectiveFeePerVsize: entry.effectiveFeePerVsize || entry.adjustedFeePerVsize || entry.feePerVsize,
 | 
			
		||||
          inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[],
 | 
			
		||||
        };
 | 
			
		||||
        strippedMempool.set(entry.uid, stripped);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -243,8 +262,10 @@ class MempoolBlocks {
 | 
			
		||||
      // clean up thread error listener
 | 
			
		||||
      this.txSelectionWorker?.removeListener('error', threadErrorListener);
 | 
			
		||||
 | 
			
		||||
      const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
 | 
			
		||||
      const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), saveResults);
 | 
			
		||||
 | 
			
		||||
      logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
 | 
			
		||||
 | 
			
		||||
      return processed;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -252,7 +273,7 @@ class MempoolBlocks {
 | 
			
		||||
    return this.mempoolBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> {
 | 
			
		||||
  public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], saveResults: boolean = false): Promise<void> {
 | 
			
		||||
    if (!this.txSelectionWorker) {
 | 
			
		||||
      // need to reset the worker
 | 
			
		||||
      await this.$makeBlockTemplates(newMempool, saveResults);
 | 
			
		||||
@ -262,19 +283,20 @@ class MempoolBlocks {
 | 
			
		||||
    const start = Date.now();
 | 
			
		||||
 | 
			
		||||
    for (const tx of Object.values(added)) {
 | 
			
		||||
      this.setUid(tx);
 | 
			
		||||
      this.setUid(tx, true);
 | 
			
		||||
    }
 | 
			
		||||
    const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
 | 
			
		||||
    const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
 | 
			
		||||
    // prepare a stripped down version of the mempool with only the minimum necessary data
 | 
			
		||||
    // to reduce the overhead of passing this data to the worker thread
 | 
			
		||||
    const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => {
 | 
			
		||||
    const addedStripped: CompactThreadTransaction[] = added.filter(entry => (entry.uid !== null && entry.uid !== undefined)).map(entry => {
 | 
			
		||||
      return {
 | 
			
		||||
        uid: entry.uid || 0,
 | 
			
		||||
        fee: entry.fee,
 | 
			
		||||
        weight: entry.weight,
 | 
			
		||||
        feePerVsize: entry.fee / (entry.weight / 4),
 | 
			
		||||
        effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
 | 
			
		||||
        inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
 | 
			
		||||
        weight: (entry.adjustedVsize * 4),
 | 
			
		||||
        sigops: entry.sigops,
 | 
			
		||||
        feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
 | 
			
		||||
        effectiveFeePerVsize: entry.effectiveFeePerVsize || entry.adjustedFeePerVsize || entry.feePerVsize,
 | 
			
		||||
        inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[],
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -296,34 +318,165 @@ class MempoolBlocks {
 | 
			
		||||
      // clean up thread error listener
 | 
			
		||||
      this.txSelectionWorker?.removeListener('error', threadErrorListener);
 | 
			
		||||
 | 
			
		||||
      this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
 | 
			
		||||
      this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), saveResults);
 | 
			
		||||
      logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] {
 | 
			
		||||
    for (const txid of Object.keys(rates)) {
 | 
			
		||||
  private resetRustGbt(): void {
 | 
			
		||||
    this.rustInitialized = false;
 | 
			
		||||
    this.rustGbtGenerator = new GbtGenerator();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $rustMakeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
 | 
			
		||||
    const start = Date.now();
 | 
			
		||||
 | 
			
		||||
    // reset mempool short ids
 | 
			
		||||
    if (saveResults) {
 | 
			
		||||
      this.resetUids();
 | 
			
		||||
    }
 | 
			
		||||
    // set missing short ids
 | 
			
		||||
    for (const tx of Object.values(newMempool)) {
 | 
			
		||||
      this.setUid(tx, !saveResults);
 | 
			
		||||
    }
 | 
			
		||||
    // set short ids for transaction inputs
 | 
			
		||||
    for (const tx of Object.values(newMempool)) {
 | 
			
		||||
      tx.inputs = tx.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // run the block construction algorithm in a separate thread, and wait for a result
 | 
			
		||||
    const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
 | 
			
		||||
    try {
 | 
			
		||||
      const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
 | 
			
		||||
        await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], this.nextUid),
 | 
			
		||||
      );
 | 
			
		||||
      if (saveResults) {
 | 
			
		||||
        this.rustInitialized = true;
 | 
			
		||||
      }
 | 
			
		||||
      const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, saveResults);
 | 
			
		||||
      logger.debug(`RUST makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
 | 
			
		||||
      return processed;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('RUST makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      if (saveResults) {
 | 
			
		||||
        this.resetRustGbt();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return this.mempoolBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }): Promise<MempoolBlockWithTransactions[]> {
 | 
			
		||||
    return this.$rustMakeBlockTemplates(newMempool, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $rustUpdateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[]): Promise<void> {
 | 
			
		||||
    // GBT optimization requires that uids never get too sparse
 | 
			
		||||
    // as a sanity check, we should also explicitly prevent uint32 uid overflow
 | 
			
		||||
    if (this.nextUid + added.length >= Math.min(Math.max(262144, 2 * mempoolSize), MAX_UINT32)) {
 | 
			
		||||
      this.resetRustGbt();
 | 
			
		||||
    }
 | 
			
		||||
    if (!this.rustInitialized) {
 | 
			
		||||
      // need to reset the worker
 | 
			
		||||
      await this.$rustMakeBlockTemplates(newMempool, true);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const start = Date.now();
 | 
			
		||||
    // set missing short ids
 | 
			
		||||
    for (const tx of added) {
 | 
			
		||||
      this.setUid(tx, true);
 | 
			
		||||
    }
 | 
			
		||||
    // set short ids for transaction inputs
 | 
			
		||||
    for (const tx of added) {
 | 
			
		||||
      tx.inputs = tx.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[];
 | 
			
		||||
    }
 | 
			
		||||
    const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
 | 
			
		||||
 | 
			
		||||
    // run the block construction algorithm in a separate thread, and wait for a result
 | 
			
		||||
    try {
 | 
			
		||||
      const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
 | 
			
		||||
        await this.rustGbtGenerator.update(
 | 
			
		||||
          added as RustThreadTransaction[],
 | 
			
		||||
          removedUids,
 | 
			
		||||
          this.nextUid,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      const resultMempoolSize = blocks.reduce((total, block) => total + block.length, 0);
 | 
			
		||||
      if (mempoolSize !== resultMempoolSize) {
 | 
			
		||||
        throw new Error('GBT returned wrong number of transactions, cache is probably out of sync');
 | 
			
		||||
      } else {
 | 
			
		||||
        this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, true);
 | 
			
		||||
      }
 | 
			
		||||
      this.removeUids(removedUids);
 | 
			
		||||
      logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      this.resetRustGbt();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], saveResults): MempoolBlockWithTransactions[] {
 | 
			
		||||
    for (const [txid, rate] of rates) {
 | 
			
		||||
      if (txid in mempool) {
 | 
			
		||||
        mempool[txid].effectiveFeePerVsize = rates[txid];
 | 
			
		||||
        mempool[txid].effectiveFeePerVsize = rate;
 | 
			
		||||
        mempool[txid].cpfpChecked = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lastBlockIndex = blocks.length - 1;
 | 
			
		||||
    let hasBlockStack = blocks.length >= 8;
 | 
			
		||||
    let stackWeight;
 | 
			
		||||
    let feeStatsCalculator: OnlineFeeStatsCalculator | void;
 | 
			
		||||
    if (hasBlockStack) {
 | 
			
		||||
      if (blockWeights && blockWeights[7] !== null) {
 | 
			
		||||
        stackWeight = blockWeights[7];
 | 
			
		||||
      } else {
 | 
			
		||||
        stackWeight = blocks[lastBlockIndex].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0);
 | 
			
		||||
      }
 | 
			
		||||
      hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS;
 | 
			
		||||
      feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const cluster of clusters) {
 | 
			
		||||
      for (const memberTxid of cluster) {
 | 
			
		||||
        const mempoolTx = mempool[memberTxid];
 | 
			
		||||
        if (mempoolTx) {
 | 
			
		||||
          const ancestors: Ancestor[] = [];
 | 
			
		||||
          const descendants: Ancestor[] = [];
 | 
			
		||||
          let matched = false;
 | 
			
		||||
          cluster.forEach(txid => {
 | 
			
		||||
            if (txid === memberTxid) {
 | 
			
		||||
              matched = true;
 | 
			
		||||
            } else {
 | 
			
		||||
              const relative = {
 | 
			
		||||
                txid: txid,
 | 
			
		||||
                fee: mempool[txid].fee,
 | 
			
		||||
                weight: (mempool[txid].adjustedVsize * 4),
 | 
			
		||||
              };
 | 
			
		||||
              if (matched) {
 | 
			
		||||
                descendants.push(relative);
 | 
			
		||||
              } else {
 | 
			
		||||
                ancestors.push(relative);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees }[] = [];
 | 
			
		||||
    const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
 | 
			
		||||
    // update this thread's mempool with the results
 | 
			
		||||
    for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
 | 
			
		||||
      const block: string[] = blocks[blockIndex];
 | 
			
		||||
      let txid: string;
 | 
			
		||||
      let mempoolTx: TransactionExtended;
 | 
			
		||||
    let mempoolTx: MempoolTransactionExtended;
 | 
			
		||||
    const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
 | 
			
		||||
      let totalSize = 0;
 | 
			
		||||
      let totalVsize = 0;
 | 
			
		||||
      let totalWeight = 0;
 | 
			
		||||
      let totalFees = 0;
 | 
			
		||||
      const transactions: TransactionExtended[] = [];
 | 
			
		||||
      for (let txIndex = 0; txIndex < block.length; txIndex++) {
 | 
			
		||||
        txid = block[txIndex];
 | 
			
		||||
      const transactions: MempoolTransactionExtended[] = [];
 | 
			
		||||
      for (const txid of block) {
 | 
			
		||||
        if (txid) {
 | 
			
		||||
          mempoolTx = mempool[txid];
 | 
			
		||||
          // save position in projected blocks
 | 
			
		||||
@ -331,7 +484,21 @@ class MempoolBlocks {
 | 
			
		||||
            block: blockIndex,
 | 
			
		||||
            vsize: totalVsize + (mempoolTx.vsize / 2),
 | 
			
		||||
          };
 | 
			
		||||
          mempoolTx.cpfpChecked = true;
 | 
			
		||||
          if (!mempoolTx.cpfpChecked) {
 | 
			
		||||
            if (mempoolTx.ancestors?.length) {
 | 
			
		||||
              mempoolTx.ancestors = [];
 | 
			
		||||
            }
 | 
			
		||||
            if (mempoolTx.descendants?.length) {
 | 
			
		||||
              mempoolTx.descendants = [];
 | 
			
		||||
            }
 | 
			
		||||
            mempoolTx.bestDescendant = null;
 | 
			
		||||
            mempoolTx.cpfpChecked = true;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // online calculation of stack-of-blocks fee stats
 | 
			
		||||
          if (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) {
 | 
			
		||||
            feeStatsCalculator.processNext(mempoolTx);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          totalSize += mempoolTx.size;
 | 
			
		||||
          totalVsize += mempoolTx.vsize;
 | 
			
		||||
@ -343,46 +510,15 @@ class MempoolBlocks {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      readyBlocks.push({
 | 
			
		||||
        transactionIds: block,
 | 
			
		||||
      return this.dataToMempoolBlocks(
 | 
			
		||||
        block,
 | 
			
		||||
        transactions,
 | 
			
		||||
        totalSize,
 | 
			
		||||
        totalWeight,
 | 
			
		||||
        totalFees
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const cluster of Object.values(clusters)) {
 | 
			
		||||
      for (const memberTxid of cluster) {
 | 
			
		||||
        if (memberTxid in mempool) {
 | 
			
		||||
          const mempoolTx = mempool[memberTxid];
 | 
			
		||||
          const ancestors: Ancestor[] = [];
 | 
			
		||||
          const descendants: Ancestor[] = [];
 | 
			
		||||
          let matched = false;
 | 
			
		||||
          cluster.forEach(txid => {
 | 
			
		||||
            if (txid === memberTxid) {
 | 
			
		||||
              matched = true;
 | 
			
		||||
            } else {
 | 
			
		||||
              const relative = {
 | 
			
		||||
                txid: txid,
 | 
			
		||||
                fee: mempool[txid].fee,
 | 
			
		||||
                weight: mempool[txid].weight,
 | 
			
		||||
              };
 | 
			
		||||
              if (matched) {
 | 
			
		||||
                descendants.push(relative);
 | 
			
		||||
              } else {
 | 
			
		||||
                ancestors.push(relative);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          mempoolTx.ancestors = ancestors;
 | 
			
		||||
          mempoolTx.descendants = descendants;
 | 
			
		||||
          mempoolTx.bestDescendant = null;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const mempoolBlocks = readyBlocks.map(b => this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees));
 | 
			
		||||
        totalFees,
 | 
			
		||||
        (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (saveResults) {
 | 
			
		||||
      const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
 | 
			
		||||
@ -393,8 +529,10 @@ class MempoolBlocks {
 | 
			
		||||
    return mempoolBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private dataToMempoolBlocks(transactionIds: string[], transactions: TransactionExtended[], totalSize: number, totalWeight: number, totalFees: number): MempoolBlockWithTransactions {
 | 
			
		||||
    const feeStats = Common.calcEffectiveFeeStatistics(transactions);
 | 
			
		||||
  private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
 | 
			
		||||
    if (!feeStats) {
 | 
			
		||||
      feeStats = Common.calcEffectiveFeeStatistics(transactions);
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      blockSize: totalSize,
 | 
			
		||||
      blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors
 | 
			
		||||
@ -412,16 +550,20 @@ class MempoolBlocks {
 | 
			
		||||
    this.nextUid = 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setUid(tx: TransactionExtended): number {
 | 
			
		||||
    const uid = this.nextUid;
 | 
			
		||||
    this.nextUid++;
 | 
			
		||||
    this.uidMap.set(uid, tx.txid);
 | 
			
		||||
    tx.uid = uid;
 | 
			
		||||
    return uid;
 | 
			
		||||
  private setUid(tx: MempoolTransactionExtended, skipSet = false): number {
 | 
			
		||||
    if (tx.uid === null || tx.uid === undefined || !skipSet) {
 | 
			
		||||
      const uid = this.nextUid;
 | 
			
		||||
      this.nextUid++;
 | 
			
		||||
      this.uidMap.set(uid, tx.txid);
 | 
			
		||||
      tx.uid = uid;
 | 
			
		||||
      return uid;
 | 
			
		||||
    } else {
 | 
			
		||||
      return tx.uid;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getUid(tx: TransactionExtended): number | void {
 | 
			
		||||
    if (tx?.uid != null && this.uidMap.has(tx.uid)) {
 | 
			
		||||
  private getUid(tx: MempoolTransactionExtended): number | void {
 | 
			
		||||
    if (tx?.uid !== null && tx?.uid !== undefined && this.uidMap.has(tx.uid)) {
 | 
			
		||||
      return tx.uid;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -456,6 +598,28 @@ class MempoolBlocks {
 | 
			
		||||
    }
 | 
			
		||||
    return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private convertNapiResultTxids({ blocks, blockWeights, rates, clusters }: GbtResult)
 | 
			
		||||
    : { blocks: string[][], blockWeights: number[], rates: [string, number][], clusters: string[][] } {
 | 
			
		||||
    const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
 | 
			
		||||
      const txid = this.uidMap.get(uid);
 | 
			
		||||
      if (txid !== undefined) {
 | 
			
		||||
        return txid;
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new Error('GBT returned a block containing a transaction with unknown uid');
 | 
			
		||||
      }
 | 
			
		||||
    }));
 | 
			
		||||
    const convertedRates: [string, number][] = [];
 | 
			
		||||
    for (const [rateUid, rate] of rates) {
 | 
			
		||||
      const rateTxid = this.uidMap.get(rateUid) as string;
 | 
			
		||||
      convertedRates.push([rateTxid, rate]);
 | 
			
		||||
    }
 | 
			
		||||
    const convertedClusters: string[][] = [];
 | 
			
		||||
    for (const cluster of clusters) {
 | 
			
		||||
      convertedClusters.push(cluster.map(uid => this.uidMap.get(uid)) as string[]);
 | 
			
		||||
    }
 | 
			
		||||
    return { blocks: convertedBlocks, blockWeights, rates: convertedRates, clusters: convertedClusters };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new MempoolBlocks();
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
 | 
			
		||||
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
@ -13,13 +13,14 @@ import rbfCache from './rbf-cache';
 | 
			
		||||
class Mempool {
 | 
			
		||||
  private inSync: boolean = false;
 | 
			
		||||
  private mempoolCacheDelta: number = -1;
 | 
			
		||||
  private mempoolCache: { [txId: string]: TransactionExtended } = {};
 | 
			
		||||
  private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
 | 
			
		||||
  private spendMap = new Map<string, MempoolTransactionExtended>();
 | 
			
		||||
  private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
 | 
			
		||||
                                                    maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
 | 
			
		||||
  private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
    deletedTransactions: TransactionExtended[]) => void) | undefined;
 | 
			
		||||
  private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
    deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
 | 
			
		||||
  private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
 | 
			
		||||
    deletedTransactions: MempoolTransactionExtended[]) => void) | undefined;
 | 
			
		||||
  private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
 | 
			
		||||
    deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined;
 | 
			
		||||
 | 
			
		||||
  private txPerSecondArray: number[] = [];
 | 
			
		||||
  private txPerSecond: number = 0;
 | 
			
		||||
@ -63,28 +64,43 @@ class Mempool {
 | 
			
		||||
    return this.latestTransactions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) {
 | 
			
		||||
  public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void {
 | 
			
		||||
    this.mempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
 | 
			
		||||
  public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void {
 | 
			
		||||
    this.$asyncMempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getMempool(): { [txid: string]: TransactionExtended } {
 | 
			
		||||
  public getMempool(): { [txid: string]: MempoolTransactionExtended } {
 | 
			
		||||
    return this.mempoolCache;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
 | 
			
		||||
  public getSpendMap(): Map<string, MempoolTransactionExtended> {
 | 
			
		||||
    return this.spendMap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
 | 
			
		||||
    this.mempoolCache = mempoolData;
 | 
			
		||||
    let count = 0;
 | 
			
		||||
    for (const txid of Object.keys(this.mempoolCache)) {
 | 
			
		||||
      if (this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) {
 | 
			
		||||
        this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.mempoolCache[txid].order == null) {
 | 
			
		||||
        this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid);
 | 
			
		||||
      }
 | 
			
		||||
      count++;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.mempoolChangedCallback) {
 | 
			
		||||
      this.mempoolChangedCallback(this.mempoolCache, [], []);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.$asyncMempoolChangedCallback) {
 | 
			
		||||
      await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []);
 | 
			
		||||
      await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], []);
 | 
			
		||||
    }
 | 
			
		||||
    this.addToSpendMap(Object.values(this.mempoolCache));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $updateMemPoolInfo() {
 | 
			
		||||
@ -127,7 +143,7 @@ class Mempool {
 | 
			
		||||
    const currentMempoolSize = Object.keys(this.mempoolCache).length;
 | 
			
		||||
    this.updateTimerProgress(timer, 'got raw mempool');
 | 
			
		||||
    const diff = transactions.length - currentMempoolSize;
 | 
			
		||||
    const newTransactions: TransactionExtended[] = [];
 | 
			
		||||
    const newTransactions: MempoolTransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
    this.mempoolCacheDelta = Math.abs(diff);
 | 
			
		||||
 | 
			
		||||
@ -145,11 +161,11 @@ class Mempool {
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let loggerTimer = new Date().getTime() / 1000;
 | 
			
		||||
    let intervalTimer = Date.now();
 | 
			
		||||
    for (const txid of transactions) {
 | 
			
		||||
      if (!this.mempoolCache[txid]) {
 | 
			
		||||
        try {
 | 
			
		||||
          const transaction = await transactionUtils.$getTransactionExtended(txid);
 | 
			
		||||
          const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
 | 
			
		||||
          this.updateTimerProgress(timer, 'fetched new transaction');
 | 
			
		||||
          this.mempoolCache[txid] = transaction;
 | 
			
		||||
          if (this.inSync) {
 | 
			
		||||
@ -168,12 +184,20 @@ class Mempool {
 | 
			
		||||
          logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
 | 
			
		||||
      if (elapsedSeconds > 4) {
 | 
			
		||||
        const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
 | 
			
		||||
        logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
 | 
			
		||||
        loadingIndicators.setProgress('mempool', progress);
 | 
			
		||||
        loggerTimer = new Date().getTime() / 1000;
 | 
			
		||||
 | 
			
		||||
      if (Date.now() - intervalTimer > 5_000) {
 | 
			
		||||
        
 | 
			
		||||
        if (this.inSync) {
 | 
			
		||||
          // Break and restart mempool loop if we spend too much time processing
 | 
			
		||||
          // new transactions that may lead to falling behind on block height
 | 
			
		||||
          logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
 | 
			
		||||
          break;
 | 
			
		||||
        } else {
 | 
			
		||||
          const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
 | 
			
		||||
          logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
 | 
			
		||||
          loadingIndicators.setProgress('mempool', progress);
 | 
			
		||||
          intervalTimer = Date.now()
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -199,7 +223,7 @@ class Mempool {
 | 
			
		||||
      }, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const deletedTransactions: TransactionExtended[] = [];
 | 
			
		||||
    const deletedTransactions: MempoolTransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
    if (this.mempoolProtection !== 1) {
 | 
			
		||||
      this.mempoolProtection = 0;
 | 
			
		||||
@ -218,23 +242,24 @@ class Mempool {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const newMempoolSize = currentMempoolSize + newTransactions.length - deletedTransactions.length;
 | 
			
		||||
    const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
 | 
			
		||||
    this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
 | 
			
		||||
 | 
			
		||||
    if (!this.inSync && transactions.length === Object.keys(this.mempoolCache).length) {
 | 
			
		||||
    if (!this.inSync && transactions.length === newMempoolSize) {
 | 
			
		||||
      this.inSync = true;
 | 
			
		||||
      logger.notice('The mempool is now in sync!');
 | 
			
		||||
      loadingIndicators.setProgress('mempool', 100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
 | 
			
		||||
    this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
 | 
			
		||||
 | 
			
		||||
    if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
 | 
			
		||||
      this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
 | 
			
		||||
      this.updateTimerProgress(timer, 'running async mempool callback');
 | 
			
		||||
      await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
 | 
			
		||||
      await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions);
 | 
			
		||||
      this.updateTimerProgress(timer, 'completed async mempool callback');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -267,7 +292,7 @@ class Mempool {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended[]; }): void {
 | 
			
		||||
  public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
 | 
			
		||||
    for (const rbfTransaction in rbfTransactions) {
 | 
			
		||||
      if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
 | 
			
		||||
        // Store replaced transactions
 | 
			
		||||
@ -276,6 +301,34 @@ class Mempool {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
 | 
			
		||||
    for (const rbfTransaction in rbfTransactions) {
 | 
			
		||||
      if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
 | 
			
		||||
        // Store replaced transactions
 | 
			
		||||
        rbfCache.add(rbfTransactions[rbfTransaction].replaced, transactionUtils.extendMempoolTransaction(rbfTransactions[rbfTransaction].replacedBy));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public addToSpendMap(transactions: MempoolTransactionExtended[]): void {
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
      for (const vin of tx.vin) {
 | 
			
		||||
        this.spendMap.set(`${vin.txid}:${vin.vout}`, tx);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public removeFromSpendMap(transactions: TransactionExtended[]): void {
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
      for (const vin of tx.vin) {
 | 
			
		||||
        const key = `${vin.txid}:${vin.vout}`;
 | 
			
		||||
        if (this.spendMap.get(key)?.txid === tx.txid) {
 | 
			
		||||
          this.spendMap.delete(key);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateTxPerSecond() {
 | 
			
		||||
    const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
 | 
			
		||||
    this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ class MiningRoutes {
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlocksHealth)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore)
 | 
			
		||||
@ -244,15 +244,15 @@ class MiningRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getHistoricalBlockPrediction(req: Request, res: Response) {
 | 
			
		||||
  private async $getHistoricalBlocksHealth(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval);
 | 
			
		||||
      const blockCount = await BlocksAuditsRepository.$getPredictionsCount();
 | 
			
		||||
      const blocksHealth = await mining.$getBlocksHealthHistory(req.params.interval);
 | 
			
		||||
      const blockCount = await BlocksAuditsRepository.$getBlocksHealthCount();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.header('X-total-count', blockCount.toString());
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate]));
 | 
			
		||||
      res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -19,12 +19,15 @@ class Mining {
 | 
			
		||||
  private blocksPriceIndexingRunning = false;
 | 
			
		||||
  public lastHashrateIndexingDate: number | null = null;
 | 
			
		||||
  public lastWeeklyHashrateIndexingDate: number | null = null;
 | 
			
		||||
  
 | 
			
		||||
  public reindexHashrateRequested = false;
 | 
			
		||||
  public reindexDifficultyAdjustmentRequested = false;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get historical block predictions match rate
 | 
			
		||||
   * Get historical blocks health
 | 
			
		||||
   */
 | 
			
		||||
   public async $getBlockPredictionsHistory(interval: string | null = null): Promise<any> {
 | 
			
		||||
    return await BlocksAuditsRepository.$getBlockPredictionsHistory(
 | 
			
		||||
   public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
 | 
			
		||||
    return await BlocksAuditsRepository.$getBlocksHealthHistory(
 | 
			
		||||
      this.getTimeRange(interval),
 | 
			
		||||
      Common.getSqlInterval(interval)
 | 
			
		||||
    );
 | 
			
		||||
@ -103,6 +106,7 @@ class Mining {
 | 
			
		||||
        emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0,
 | 
			
		||||
        slug: poolInfo.slug,
 | 
			
		||||
        avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
 | 
			
		||||
        avgFeeDelta: poolInfo.avgFeeDelta,
 | 
			
		||||
      };
 | 
			
		||||
      poolsStats.push(poolStat);
 | 
			
		||||
    });
 | 
			
		||||
@ -290,6 +294,14 @@ class Mining {
 | 
			
		||||
   * Generate daily hashrate data
 | 
			
		||||
   */
 | 
			
		||||
  public async $generateNetworkHashrateHistory(): Promise<void> {
 | 
			
		||||
    // If a re-index was requested, truncate first
 | 
			
		||||
    if (this.reindexHashrateRequested === true) {
 | 
			
		||||
      logger.notice(`hashrates will now be re-indexed`);
 | 
			
		||||
      await database.query(`TRUNCATE hashrates`);
 | 
			
		||||
      this.lastHashrateIndexingDate = 0;
 | 
			
		||||
      this.reindexHashrateRequested = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We only run this once a day around midnight
 | 
			
		||||
    const today = new Date().getUTCDate();
 | 
			
		||||
    if (today === this.lastHashrateIndexingDate) {
 | 
			
		||||
@ -395,6 +407,13 @@ class Mining {
 | 
			
		||||
   * Index difficulty adjustments
 | 
			
		||||
   */
 | 
			
		||||
  public async $indexDifficultyAdjustments(): Promise<void> {
 | 
			
		||||
    // If a re-index was requested, truncate first
 | 
			
		||||
    if (this.reindexDifficultyAdjustmentRequested === true) {
 | 
			
		||||
      logger.notice(`difficulty_adjustments will now be re-indexed`);
 | 
			
		||||
      await database.query(`TRUNCATE difficulty_adjustments`);
 | 
			
		||||
      this.reindexDifficultyAdjustmentRequested = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const indexedHeightsArray = await DifficultyAdjustmentsRepository.$getAdjustmentsHeights();
 | 
			
		||||
    const indexedHeights = {};
 | 
			
		||||
    for (const height of indexedHeightsArray) {
 | 
			
		||||
@ -473,11 +492,11 @@ class Mining {
 | 
			
		||||
    }
 | 
			
		||||
    this.blocksPriceIndexingRunning = true;
 | 
			
		||||
 | 
			
		||||
    let totalInserted = 0;
 | 
			
		||||
    try {
 | 
			
		||||
      const prices: any[] = await PricesRepository.$getPricesTimesAndId();    
 | 
			
		||||
      const blocksWithoutPrices: any[] = await BlocksRepository.$getBlocksWithoutPrice();
 | 
			
		||||
 | 
			
		||||
      let totalInserted = 0;
 | 
			
		||||
      const blocksPrices: BlockPrice[] = [];
 | 
			
		||||
 | 
			
		||||
      for (const block of blocksWithoutPrices) {
 | 
			
		||||
@ -522,7 +541,13 @@ class Mining {
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.blocksPriceIndexingRunning = false;
 | 
			
		||||
      throw e;
 | 
			
		||||
      logger.err(`Cannot index block prices. ${e}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (totalInserted > 0) {
 | 
			
		||||
      logger.info(`Indexing blocks prices completed. Indexed ${totalInserted}`, logger.tags.mining);
 | 
			
		||||
    } else {
 | 
			
		||||
      logger.debug(`Indexing blocks prices completed. Indexed 0.`, logger.tags.mining);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.blocksPriceIndexingRunning = false;
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import config from '../config';
 | 
			
		||||
import PoolsRepository from '../repositories/PoolsRepository';
 | 
			
		||||
import { PoolTag } from '../mempool.interfaces';
 | 
			
		||||
import diskCache from './disk-cache';
 | 
			
		||||
import mining from './mining/mining';
 | 
			
		||||
 | 
			
		||||
class PoolsParser {
 | 
			
		||||
  miningPools: any[] = [];
 | 
			
		||||
@ -41,7 +42,7 @@ class PoolsParser {
 | 
			
		||||
  public async migratePoolsJson(): Promise<void> {
 | 
			
		||||
    // We also need to wipe the backend cache to make sure we don't serve blocks with
 | 
			
		||||
    // the wrong mining pool (usually happen with unknown blocks)
 | 
			
		||||
    diskCache.wipeCache();
 | 
			
		||||
    diskCache.setIgnoreBlocksCache();
 | 
			
		||||
 | 
			
		||||
    await this.$insertUnknownPool();
 | 
			
		||||
 | 
			
		||||
@ -73,14 +74,12 @@ class PoolsParser {
 | 
			
		||||
        if (JSON.stringify(pool.addresses) !== poolDB.addresses ||
 | 
			
		||||
          JSON.stringify(pool.regexes) !== poolDB.regexes) {
 | 
			
		||||
          // Pool addresses changed or coinbase tags changed
 | 
			
		||||
          logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool. If 'AUTOMATIC_BLOCK_REINDEXING' is enabled, we will re-index its blocks and 'unknown' blocks`);
 | 
			
		||||
          logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`);
 | 
			
		||||
          await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
 | 
			
		||||
          await this.$deleteBlocksForPool(poolDB);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logger.info('Mining pools-v2.json import completed');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -118,10 +117,6 @@ class PoolsParser {
 | 
			
		||||
   * @param pool 
 | 
			
		||||
   */
 | 
			
		||||
  private async $deleteBlocksForPool(pool: PoolTag): Promise<void> {
 | 
			
		||||
    if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get oldest blocks mined by the pool and assume pools-v2.json updates only concern most recent years
 | 
			
		||||
    // Ignore early days of Bitcoin as there were no mining pool yet
 | 
			
		||||
    const [oldestPoolBlock]: any[] = await DB.query(`
 | 
			
		||||
@ -132,7 +127,15 @@ class PoolsParser {
 | 
			
		||||
      LIMIT 1`,
 | 
			
		||||
      [pool.id]
 | 
			
		||||
    );
 | 
			
		||||
    const oldestBlockHeight = oldestPoolBlock.length ?? 0 > 0 ? oldestPoolBlock[0].height : 130635;
 | 
			
		||||
 | 
			
		||||
    let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
 | 
			
		||||
    if (config.MEMPOOL.NETWORK === 'testnet') {
 | 
			
		||||
      firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
 | 
			
		||||
    } else if (config.MEMPOOL.NETWORK === 'signet') {
 | 
			
		||||
      firstKnownBlockPool = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const oldestBlockHeight = oldestPoolBlock.length ?? 0 > 0 ? oldestPoolBlock[0].height : firstKnownBlockPool;
 | 
			
		||||
    const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
 | 
			
		||||
    this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${oldestBlockHeight} for re-indexing`);
 | 
			
		||||
    await DB.query(`
 | 
			
		||||
@ -146,16 +149,31 @@ class PoolsParser {
 | 
			
		||||
      WHERE pool_id = ?`,
 | 
			
		||||
      [pool.id]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Re-index hashrates and difficulty adjustments later
 | 
			
		||||
    mining.reindexHashrateRequested = true;
 | 
			
		||||
    mining.reindexDifficultyAdjustmentRequested = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $deleteUnknownBlocks(): Promise<void> {
 | 
			
		||||
    let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
 | 
			
		||||
    if (config.MEMPOOL.NETWORK === 'testnet') {
 | 
			
		||||
      firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
 | 
			
		||||
    } else if (config.MEMPOOL.NETWORK === 'signet') {
 | 
			
		||||
      firstKnownBlockPool = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
 | 
			
		||||
    this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height 130635 for re-indexing`);
 | 
			
		||||
    this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${firstKnownBlockPool} for re-indexing`);
 | 
			
		||||
    await DB.query(`
 | 
			
		||||
      DELETE FROM blocks
 | 
			
		||||
      WHERE pool_id = ? AND height >= 130635`,
 | 
			
		||||
      WHERE pool_id = ? AND height >= ${firstKnownBlockPool}`,
 | 
			
		||||
      [unknownPool[0].id]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Re-index hashrates and difficulty adjustments later
 | 
			
		||||
    mining.reindexHashrateRequested = true;
 | 
			
		||||
    mining.reindexDifficultyAdjustmentRequested = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import logger from "../logger";
 | 
			
		||||
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
 | 
			
		||||
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { Common } from "./common";
 | 
			
		||||
 | 
			
		||||
@ -23,20 +23,20 @@ class RbfCache {
 | 
			
		||||
  private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
 | 
			
		||||
  private dirtyTrees: Set<string> = new Set();
 | 
			
		||||
  private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
 | 
			
		||||
  private txs: Map<string, TransactionExtended> = new Map();
 | 
			
		||||
  private txs: Map<string, MempoolTransactionExtended> = new Map();
 | 
			
		||||
  private expiring: Map<string, number> = new Map();
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public add(replaced: TransactionExtended[], newTxExtended: TransactionExtended): void {
 | 
			
		||||
    if (!newTxExtended || !replaced?.length) {
 | 
			
		||||
  public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
 | 
			
		||||
    if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
 | 
			
		||||
    const newTime = newTxExtended.firstSeen || Date.now();
 | 
			
		||||
    const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
 | 
			
		||||
    newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
 | 
			
		||||
    this.txs.set(newTx.txid, newTxExtended);
 | 
			
		||||
 | 
			
		||||
@ -59,7 +59,7 @@ class RbfCache {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        const replacedTime = replacedTxExtended.firstSeen || Date.now();
 | 
			
		||||
        const replacedTime = replacedTxExtended.firstSeen || (Date.now() / 1000);
 | 
			
		||||
        replacedTrees.push({
 | 
			
		||||
          tx: replacedTx,
 | 
			
		||||
          time: replacedTime,
 | 
			
		||||
@ -74,7 +74,7 @@ class RbfCache {
 | 
			
		||||
    const treeId = replacedTrees[0].tx.txid;
 | 
			
		||||
    const newTree = {
 | 
			
		||||
      tx: newTx,
 | 
			
		||||
      time: newTxExtended.firstSeen || Date.now(),
 | 
			
		||||
      time: newTime,
 | 
			
		||||
      fullRbf,
 | 
			
		||||
      replaces: replacedTrees
 | 
			
		||||
    };
 | 
			
		||||
@ -92,7 +92,7 @@ class RbfCache {
 | 
			
		||||
    return this.replaces.get(txId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getTx(txId: string): TransactionExtended | undefined {
 | 
			
		||||
  public getTx(txId: string): MempoolTransactionExtended | undefined {
 | 
			
		||||
    return this.txs.get(txId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -169,6 +169,19 @@ class RbfCache {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // is the transaction involved in a full rbf replacement?
 | 
			
		||||
  public isFullRbf(txid: string): boolean {
 | 
			
		||||
    const treeId = this.treeMap.get(txid);
 | 
			
		||||
    if (!treeId) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    const tree = this.rbfTrees.get(treeId);
 | 
			
		||||
    if (!tree) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return tree?.fullRbf;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private cleanup(): void {
 | 
			
		||||
    const now = Date.now();
 | 
			
		||||
    for (const txid of this.expiring.keys()) {
 | 
			
		||||
@ -272,7 +285,7 @@ class RbfCache {
 | 
			
		||||
    return deflated;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async importTree(root, txid, deflated, txs: Map<string, TransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
 | 
			
		||||
  async importTree(root, txid, deflated, txs: Map<string, MempoolTransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
 | 
			
		||||
    const treeInfo = deflated[txid];
 | 
			
		||||
    const replaces: RbfTree[] = [];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -211,7 +211,7 @@ class StatisticsApi {
 | 
			
		||||
      CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
 | 
			
		||||
      CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \
 | 
			
		||||
      FROM statistics \
 | 
			
		||||
      WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \
 | 
			
		||||
      ${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
 | 
			
		||||
      GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
 | 
			
		||||
      ORDER BY statistics.added DESC;`;
 | 
			
		||||
  }
 | 
			
		||||
@ -259,7 +259,7 @@ class StatisticsApi {
 | 
			
		||||
      vsize_1800,
 | 
			
		||||
      vsize_2000 \
 | 
			
		||||
      FROM statistics \
 | 
			
		||||
      WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \
 | 
			
		||||
      ${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
 | 
			
		||||
      GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
 | 
			
		||||
      ORDER BY statistics.added DESC;`;
 | 
			
		||||
  }
 | 
			
		||||
@ -386,6 +386,17 @@ class StatisticsApi {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $listAll(): Promise<OptimizedStatistic[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = this.getQueryForDays(43200, 'all'); // 12h interval
 | 
			
		||||
      const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
 | 
			
		||||
      return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$listAll() error' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
 | 
			
		||||
    return statistic.map((s) => {
 | 
			
		||||
      return {
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,11 @@ class StatisticsRoutes {
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y'))
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/all', this.$getStatisticsByTime.bind(this, 'all'))
 | 
			
		||||
    ;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y', req: Request, res: Response) {
 | 
			
		||||
  private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y' | 'all', req: Request, res: Response) {
 | 
			
		||||
    res.header('Pragma', 'public');
 | 
			
		||||
    res.header('Cache-control', 'public');
 | 
			
		||||
    res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
 | 
			
		||||
@ -26,10 +27,6 @@ class StatisticsRoutes {
 | 
			
		||||
    try {
 | 
			
		||||
      let result;
 | 
			
		||||
      switch (time as string) {
 | 
			
		||||
        case '2h':
 | 
			
		||||
          result = await statisticsApi.$list2H();
 | 
			
		||||
          res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
          break;
 | 
			
		||||
        case '24h':
 | 
			
		||||
          result = await statisticsApi.$list24H();
 | 
			
		||||
          res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
@ -58,8 +55,13 @@ class StatisticsRoutes {
 | 
			
		||||
        case '4y':
 | 
			
		||||
          result = await statisticsApi.$list4Y();
 | 
			
		||||
          break;
 | 
			
		||||
        case 'all':
 | 
			
		||||
          result = await statisticsApi.$listAll();
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          result = await statisticsApi.$list2H();
 | 
			
		||||
          res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
 | 
			
		||||
import { TransactionExtended, MempoolTransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
 | 
			
		||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import * as bitcoinjs from 'bitcoinjs-lib';
 | 
			
		||||
 | 
			
		||||
class TransactionUtils {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
@ -22,19 +23,27 @@ class TransactionUtils {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param txId 
 | 
			
		||||
   * @param addPrevouts 
 | 
			
		||||
   * @param lazyPrevouts 
 | 
			
		||||
   * @param txId
 | 
			
		||||
   * @param addPrevouts
 | 
			
		||||
   * @param lazyPrevouts
 | 
			
		||||
   * @param forceCore - See https://github.com/mempool/mempool/issues/2904
 | 
			
		||||
   */
 | 
			
		||||
  public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<TransactionExtended> {
 | 
			
		||||
  public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
 | 
			
		||||
    let transaction: IEsploraApi.Transaction;
 | 
			
		||||
    if (forceCore === true) {
 | 
			
		||||
      transaction  = await bitcoinCoreApi.$getRawTransaction(txId, true);
 | 
			
		||||
    } else {
 | 
			
		||||
      transaction  = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
 | 
			
		||||
    }
 | 
			
		||||
    return this.extendTransaction(transaction);
 | 
			
		||||
    if (addMempoolData || !transaction?.status?.confirmed) {
 | 
			
		||||
      return this.extendMempoolTransaction(transaction);
 | 
			
		||||
    } else {
 | 
			
		||||
      return this.extendTransaction(transaction);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getMempoolTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended> {
 | 
			
		||||
    return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
 | 
			
		||||
@ -50,8 +59,33 @@ class TransactionUtils {
 | 
			
		||||
      feePerVsize: feePerVbytes,
 | 
			
		||||
      effectiveFeePerVsize: feePerVbytes,
 | 
			
		||||
    }, transaction);
 | 
			
		||||
    if (!transaction.status.confirmed) {
 | 
			
		||||
      transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
 | 
			
		||||
    if (!transaction?.status?.confirmed && !transactionExtended.firstSeen) {
 | 
			
		||||
      transactionExtended.firstSeen = Math.round((Date.now() / 1000));
 | 
			
		||||
    }
 | 
			
		||||
    return transactionExtended;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
 | 
			
		||||
    const vsize = Math.ceil(transaction.weight / 4);
 | 
			
		||||
    const fractionalVsize = (transaction.weight / 4);
 | 
			
		||||
    const sigops = this.countSigops(transaction);
 | 
			
		||||
    // https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
 | 
			
		||||
    const adjustedVsize = Math.max(fractionalVsize, sigops *  5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
 | 
			
		||||
    const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
 | 
			
		||||
      (transaction.fee || 0) / fractionalVsize);
 | 
			
		||||
    const adjustedFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1,
 | 
			
		||||
      (transaction.fee || 0) / adjustedVsize);
 | 
			
		||||
    const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
 | 
			
		||||
      order: this.txidToOrdering(transaction.txid),
 | 
			
		||||
      vsize: Math.round(transaction.weight / 4),
 | 
			
		||||
      adjustedVsize,
 | 
			
		||||
      sigops,
 | 
			
		||||
      feePerVsize: feePerVbytes,
 | 
			
		||||
      adjustedFeePerVsize: adjustedFeePerVsize,
 | 
			
		||||
      effectiveFeePerVsize: adjustedFeePerVsize,
 | 
			
		||||
    });
 | 
			
		||||
    if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
 | 
			
		||||
      transactionExtended.firstSeen = Math.round((Date.now() / 1000));
 | 
			
		||||
    }
 | 
			
		||||
    return transactionExtended;
 | 
			
		||||
  }
 | 
			
		||||
@ -63,6 +97,75 @@ class TransactionUtils {
 | 
			
		||||
    }
 | 
			
		||||
    return str;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
 | 
			
		||||
    let sigops = 0;
 | 
			
		||||
    // count OP_CHECKSIG and OP_CHECKSIGVERIFY
 | 
			
		||||
    sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
 | 
			
		||||
 | 
			
		||||
    // count OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY
 | 
			
		||||
    if (isRawScript) {
 | 
			
		||||
      // in scriptPubKey or scriptSig, always worth 20
 | 
			
		||||
      sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
 | 
			
		||||
    } else {
 | 
			
		||||
      // in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
 | 
			
		||||
      const matches = script.matchAll(/(?:OP_(\d+))? OP_CHECKMULTISIG/g);
 | 
			
		||||
      for (const match of matches) {
 | 
			
		||||
        const n = parseInt(match[1]);
 | 
			
		||||
        if (Number.isInteger(n)) {
 | 
			
		||||
          sigops += n;
 | 
			
		||||
        } else {
 | 
			
		||||
          sigops += 20;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return witness ? sigops : (sigops * 4);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public countSigops(transaction: IEsploraApi.Transaction): number {
 | 
			
		||||
    let sigops = 0;
 | 
			
		||||
 | 
			
		||||
    for (const input of transaction.vin) {
 | 
			
		||||
      if (input.scriptsig_asm) {
 | 
			
		||||
        sigops += this.countScriptSigops(input.scriptsig_asm, true);
 | 
			
		||||
      }
 | 
			
		||||
      if (input.prevout) {
 | 
			
		||||
        switch (true) {
 | 
			
		||||
          case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'):
 | 
			
		||||
          case input.prevout.scriptpubkey_type === 'v0_p2wpkh':
 | 
			
		||||
            sigops += 1;
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'):
 | 
			
		||||
          case input.prevout.scriptpubkey_type === 'v0_p2wsh':
 | 
			
		||||
            if (input.witness?.length) {
 | 
			
		||||
              sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const output of transaction.vout) {
 | 
			
		||||
      if (output.scriptpubkey_asm) {
 | 
			
		||||
        sigops += this.countScriptSigops(output.scriptpubkey_asm, true);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return sigops;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // returns the most significant 4 bytes of the txid as an integer
 | 
			
		||||
  public txidToOrdering(txid: string): number {
 | 
			
		||||
    return parseInt(
 | 
			
		||||
      txid.substr(62, 2) +
 | 
			
		||||
        txid.substr(60, 2) +
 | 
			
		||||
        txid.substr(58, 2) +
 | 
			
		||||
        txid.substr(56, 2),
 | 
			
		||||
      16
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new TransactionUtils();
 | 
			
		||||
 | 
			
		||||
@ -48,12 +48,14 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
      weight: tx.weight,
 | 
			
		||||
      feePerVsize: tx.feePerVsize,
 | 
			
		||||
      effectiveFeePerVsize: tx.feePerVsize,
 | 
			
		||||
      sigops: tx.sigops,
 | 
			
		||||
      inputs: tx.inputs || [],
 | 
			
		||||
      relativesSet: false,
 | 
			
		||||
      ancestorMap: new Map<number, AuditTransaction>(),
 | 
			
		||||
      children: new Set<AuditTransaction>(),
 | 
			
		||||
      ancestorFee: 0,
 | 
			
		||||
      ancestorWeight: 0,
 | 
			
		||||
      ancestorSigops: 0,
 | 
			
		||||
      score: 0,
 | 
			
		||||
      used: false,
 | 
			
		||||
      modified: false,
 | 
			
		||||
@ -83,6 +85,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
  // (i.e. the package rooted in the transaction with the best ancestor score)
 | 
			
		||||
  const blocks: number[][] = [];
 | 
			
		||||
  let blockWeight = 4000;
 | 
			
		||||
  let blockSigops = 0;
 | 
			
		||||
  let transactions: AuditTransaction[] = [];
 | 
			
		||||
  const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
 | 
			
		||||
    if (a.score === b.score) {
 | 
			
		||||
@ -118,7 +121,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
 | 
			
		||||
    if (nextTx && !nextTx?.used) {
 | 
			
		||||
      // Check if the package fits into this block
 | 
			
		||||
      if (blocks.length >= 7 || (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS)) {
 | 
			
		||||
      if (blocks.length >= 7 || ((blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) && (blockSigops + nextTx.ancestorSigops <= 80000))) {
 | 
			
		||||
        const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
 | 
			
		||||
        // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
 | 
			
		||||
        const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
 | 
			
		||||
@ -127,7 +130,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
          cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
 | 
			
		||||
          isCluster = true;
 | 
			
		||||
        }
 | 
			
		||||
        const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
 | 
			
		||||
        const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / (nextTx.ancestorWeight / 4));
 | 
			
		||||
        const used: AuditTransaction[] = [];
 | 
			
		||||
        while (sortedTxSet.length) {
 | 
			
		||||
          const ancestor = sortedTxSet.pop();
 | 
			
		||||
@ -155,7 +158,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
 | 
			
		||||
        // remove these as valid package ancestors for any descendants remaining in the mempool
 | 
			
		||||
        if (used.length) {
 | 
			
		||||
          used.forEach(tx => {
 | 
			
		||||
            updateDescendants(tx, auditPool, modified);
 | 
			
		||||
            updateDescendants(tx, auditPool, modified, effectiveFeeRate);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -237,9 +240,11 @@ function setRelatives(
 | 
			
		||||
  };
 | 
			
		||||
  tx.ancestorFee = tx.fee || 0;
 | 
			
		||||
  tx.ancestorWeight = tx.weight || 0;
 | 
			
		||||
  tx.ancestorSigops = tx.sigops || 0;
 | 
			
		||||
  tx.ancestorMap.forEach((ancestor) => {
 | 
			
		||||
    tx.ancestorFee += ancestor.fee;
 | 
			
		||||
    tx.ancestorWeight += ancestor.weight;
 | 
			
		||||
    tx.ancestorSigops += ancestor.sigops;
 | 
			
		||||
  });
 | 
			
		||||
  tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1);
 | 
			
		||||
  tx.relativesSet = true;
 | 
			
		||||
@ -251,6 +256,7 @@ function updateDescendants(
 | 
			
		||||
  rootTx: AuditTransaction,
 | 
			
		||||
  mempool: Map<number, AuditTransaction>,
 | 
			
		||||
  modified: PairingHeap<AuditTransaction>,
 | 
			
		||||
  clusterRate: number,
 | 
			
		||||
): void {
 | 
			
		||||
  const descendantSet: Set<AuditTransaction> = new Set();
 | 
			
		||||
  // stack of nodes left to visit
 | 
			
		||||
@ -270,8 +276,10 @@ function updateDescendants(
 | 
			
		||||
      descendantTx.ancestorMap.delete(rootTx.uid);
 | 
			
		||||
      descendantTx.ancestorFee -= rootTx.fee;
 | 
			
		||||
      descendantTx.ancestorWeight -= rootTx.weight;
 | 
			
		||||
      descendantTx.ancestorSigops -= rootTx.sigops;
 | 
			
		||||
      tmpScore = descendantTx.score;
 | 
			
		||||
      descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4);
 | 
			
		||||
      descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
 | 
			
		||||
 | 
			
		||||
      if (!descendantTx.modifiedNode) {
 | 
			
		||||
        descendantTx.modified = true;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
import {
 | 
			
		||||
  BlockExtended, TransactionExtended, WebsocketResponse,
 | 
			
		||||
  BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
 | 
			
		||||
  OptimizedStatistic, ILoadingIndicators
 | 
			
		||||
} from '../mempool.interfaces';
 | 
			
		||||
import blocks from './blocks';
 | 
			
		||||
@ -22,6 +22,14 @@ import { deepClone } from '../utils/clone';
 | 
			
		||||
import priceUpdater from '../tasks/price-updater';
 | 
			
		||||
import { ApiPrice } from '../repositories/PricesRepository';
 | 
			
		||||
 | 
			
		||||
// valid 'want' subscriptions
 | 
			
		||||
const wantable = [
 | 
			
		||||
  'blocks',
 | 
			
		||||
  'mempool-blocks',
 | 
			
		||||
  'live-2h-chart',
 | 
			
		||||
  'stats',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
class WebsocketHandler {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
  private extraInitProperties = {};
 | 
			
		||||
@ -30,7 +38,7 @@ class WebsocketHandler {
 | 
			
		||||
  private numConnected = 0;
 | 
			
		||||
  private numDisconnected = 0;
 | 
			
		||||
 | 
			
		||||
  private initData: { [key: string]: string } = {};
 | 
			
		||||
  private socketData: { [key: string]: string } = {};
 | 
			
		||||
  private serializedInitData: string = '{}';
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
@ -39,28 +47,28 @@ class WebsocketHandler {
 | 
			
		||||
    this.wss = wss;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setExtraInitProperties(property: string, value: any) {
 | 
			
		||||
  setExtraInitData(property: string, value: any) {
 | 
			
		||||
    this.extraInitProperties[property] = value;
 | 
			
		||||
    this.setInitDataFields(this.extraInitProperties);
 | 
			
		||||
    this.updateSocketDataFields(this.extraInitProperties);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setInitDataFields(data: { [property: string]: any }): void {
 | 
			
		||||
  private updateSocketDataFields(data: { [property: string]: any }): void {
 | 
			
		||||
    for (const property of Object.keys(data)) {
 | 
			
		||||
      if (data[property] != null) {
 | 
			
		||||
        this.initData[property] = JSON.stringify(data[property]);
 | 
			
		||||
        this.socketData[property] = JSON.stringify(data[property]);
 | 
			
		||||
      } else {
 | 
			
		||||
        delete this.initData[property];
 | 
			
		||||
        delete this.socketData[property];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.serializedInitData = '{'
 | 
			
		||||
      + Object.keys(this.initData).map(key => `"${key}": ${this.initData[key]}`).join(', ')
 | 
			
		||||
      + '}';
 | 
			
		||||
    + Object.keys(this.socketData).map(key => `"${key}": ${this.socketData[key]}`).join(', ')
 | 
			
		||||
    + '}';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateInitData(): void {
 | 
			
		||||
  private updateSocketData(): void {
 | 
			
		||||
    const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
 | 
			
		||||
    const da = difficultyAdjustment.getDifficultyAdjustment();
 | 
			
		||||
    this.setInitDataFields({
 | 
			
		||||
    this.updateSocketDataFields({
 | 
			
		||||
      'mempoolInfo': memPool.getMempoolInfo(),
 | 
			
		||||
      'vBytesPerSecond': memPool.getVBytesPerSecond(),
 | 
			
		||||
      'blocks': _blocks,
 | 
			
		||||
@ -94,11 +102,33 @@ class WebsocketHandler {
 | 
			
		||||
          const parsedMessage: WebsocketResponse = JSON.parse(message);
 | 
			
		||||
          const response = {};
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage.action === 'want') {
 | 
			
		||||
            client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
 | 
			
		||||
            client['want-mempool-blocks'] = parsedMessage.data.indexOf('mempool-blocks') > -1;
 | 
			
		||||
            client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
 | 
			
		||||
            client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
 | 
			
		||||
          const wantNow = {};
 | 
			
		||||
          if (parsedMessage && parsedMessage.action === 'want' && Array.isArray(parsedMessage.data)) {
 | 
			
		||||
            for (const sub of wantable) {
 | 
			
		||||
              const key = `want-${sub}`;
 | 
			
		||||
              const wants = parsedMessage.data.includes(sub);
 | 
			
		||||
              if (wants && client['wants'] && !client[key]) {
 | 
			
		||||
                wantNow[key] = true;
 | 
			
		||||
              }
 | 
			
		||||
              client[key] = wants;
 | 
			
		||||
            }
 | 
			
		||||
            client['wants'] = true;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // send initial data when a client first starts a subscription
 | 
			
		||||
          if (wantNow['want-blocks'] || (parsedMessage && parsedMessage['refresh-blocks'])) {
 | 
			
		||||
            response['blocks'] = this.socketData['blocks'];
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (wantNow['want-mempool-blocks']) {
 | 
			
		||||
            response['mempool-blocks'] = this.socketData['mempool-blocks'];
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (wantNow['want-stats']) {
 | 
			
		||||
            response['mempoolInfo'] = this.socketData['mempoolInfo'];
 | 
			
		||||
            response['vBytesPerSecond'] = this.socketData['vBytesPerSecond'];
 | 
			
		||||
            response['fees'] = this.socketData['fees'];
 | 
			
		||||
            response['da'] = this.socketData['da'];
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-tx']) {
 | 
			
		||||
@ -109,29 +139,29 @@ class WebsocketHandler {
 | 
			
		||||
              if (parsedMessage['watch-mempool']) {
 | 
			
		||||
                const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
 | 
			
		||||
                if (rbfCacheTxid) {
 | 
			
		||||
                  response['txReplaced'] = {
 | 
			
		||||
                  response['txReplaced'] = JSON.stringify({
 | 
			
		||||
                    txid: rbfCacheTxid,
 | 
			
		||||
                  };
 | 
			
		||||
                  });
 | 
			
		||||
                  client['track-tx'] = null;
 | 
			
		||||
                } else {
 | 
			
		||||
                  // It might have appeared before we had the time to start watching for it
 | 
			
		||||
                  const tx = memPool.getMempool()[trackTxid];
 | 
			
		||||
                  if (tx) {
 | 
			
		||||
                    if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
                      response['tx'] = tx;
 | 
			
		||||
                      response['tx'] = JSON.stringify(tx);
 | 
			
		||||
                    } else {
 | 
			
		||||
                      // tx.prevout is missing from transactions when in bitcoind mode
 | 
			
		||||
                      try {
 | 
			
		||||
                        const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                        response['tx'] = fullTx;
 | 
			
		||||
                        const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                        response['tx'] = JSON.stringify(fullTx);
 | 
			
		||||
                      } catch (e) {
 | 
			
		||||
                        logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  } else {
 | 
			
		||||
                    try {
 | 
			
		||||
                      const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
 | 
			
		||||
                      response['tx'] = fullTx;
 | 
			
		||||
                      const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
 | 
			
		||||
                      response['tx'] = JSON.stringify(fullTx);
 | 
			
		||||
                    } catch (e) {
 | 
			
		||||
                      logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
                      client['track-mempool-tx'] = parsedMessage['track-tx'];
 | 
			
		||||
@ -141,10 +171,10 @@ class WebsocketHandler {
 | 
			
		||||
              }
 | 
			
		||||
              const tx = memPool.getMempool()[trackTxid];
 | 
			
		||||
              if (tx && tx.position) {
 | 
			
		||||
                response['txPosition'] = {
 | 
			
		||||
                response['txPosition'] = JSON.stringify({
 | 
			
		||||
                  txid: trackTxid,
 | 
			
		||||
                  position: tx.position,
 | 
			
		||||
                };
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-tx'] = null;
 | 
			
		||||
@ -177,10 +207,10 @@ class WebsocketHandler {
 | 
			
		||||
              const index = parsedMessage['track-mempool-block'];
 | 
			
		||||
              client['track-mempool-block'] = index;
 | 
			
		||||
              const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
 | 
			
		||||
              response['projected-block-transactions'] = {
 | 
			
		||||
              response['projected-block-transactions'] = JSON.stringify({
 | 
			
		||||
                index: index,
 | 
			
		||||
                blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
 | 
			
		||||
              };
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-mempool-block'] = null;
 | 
			
		||||
            }
 | 
			
		||||
@ -189,23 +219,24 @@ class WebsocketHandler {
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
 | 
			
		||||
            if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
 | 
			
		||||
              client['track-rbf'] = parsedMessage['track-rbf'];
 | 
			
		||||
              response['rbfLatest'] = JSON.stringify(rbfCache.getRbfTrees(parsedMessage['track-rbf'] === 'fullRbf'));
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-rbf'] = false;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage.action === 'init') {
 | 
			
		||||
            if (!this.initData['blocks']?.length || !this.initData['da']) {
 | 
			
		||||
              this.updateInitData();
 | 
			
		||||
            if (!this.socketData['blocks']?.length || !this.socketData['da']) {
 | 
			
		||||
              this.updateSocketData();
 | 
			
		||||
            }
 | 
			
		||||
            if (!this.initData['blocks']?.length) {
 | 
			
		||||
            if (!this.socketData['blocks']?.length) {
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            client.send(this.serializedInitData);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage.action === 'ping') {
 | 
			
		||||
            response['pong'] = true;
 | 
			
		||||
            response['pong'] = JSON.stringify(true);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
 | 
			
		||||
@ -221,7 +252,8 @@ class WebsocketHandler {
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (Object.keys(response).length) {
 | 
			
		||||
            client.send(JSON.stringify(response));
 | 
			
		||||
            const serializedResponse = this.serializeResponse(response);
 | 
			
		||||
            client.send(serializedResponse);
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -250,7 +282,7 @@ class WebsocketHandler {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.setInitDataFields({ 'loadingIndicators': indicators });
 | 
			
		||||
    this.updateSocketDataFields({ 'loadingIndicators': indicators });
 | 
			
		||||
 | 
			
		||||
    const response = JSON.stringify({ loadingIndicators: indicators });
 | 
			
		||||
    this.wss.clients.forEach((client) => {
 | 
			
		||||
@ -266,7 +298,7 @@ class WebsocketHandler {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.setInitDataFields({ 'conversions': conversionRates });
 | 
			
		||||
    this.updateSocketDataFields({ 'conversions': conversionRates });
 | 
			
		||||
 | 
			
		||||
    const response = JSON.stringify({ conversions: conversionRates });
 | 
			
		||||
    this.wss.clients.forEach((client) => {
 | 
			
		||||
@ -301,8 +333,8 @@ class WebsocketHandler {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
 | 
			
		||||
    newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
 | 
			
		||||
  async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
@ -310,7 +342,11 @@ class WebsocketHandler {
 | 
			
		||||
    this.printLogs();
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
 | 
			
		||||
      await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
 | 
			
		||||
      if (config.MEMPOOL.RUST_GBT) {
 | 
			
		||||
        await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions);
 | 
			
		||||
      } else {
 | 
			
		||||
        await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      mempoolBlocks.updateMempoolBlocks(newMempool, true);
 | 
			
		||||
    }
 | 
			
		||||
@ -332,13 +368,25 @@ class WebsocketHandler {
 | 
			
		||||
    for (const deletedTx of deletedTransactions) {
 | 
			
		||||
      rbfCache.evict(deletedTx.txid);
 | 
			
		||||
    }
 | 
			
		||||
    memPool.removeFromSpendMap(deletedTransactions);
 | 
			
		||||
    memPool.addToSpendMap(newTransactions);
 | 
			
		||||
    const recommendedFees = feeApi.getRecommendedFee();
 | 
			
		||||
 | 
			
		||||
    const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
 | 
			
		||||
 | 
			
		||||
    // update init data
 | 
			
		||||
    this.updateInitData();
 | 
			
		||||
    this.updateSocketDataFields({
 | 
			
		||||
      'mempoolInfo': mempoolInfo,
 | 
			
		||||
      'vBytesPerSecond': vBytesPerSecond,
 | 
			
		||||
      'mempool-blocks': mBlocks,
 | 
			
		||||
      'transactions': latestTransactions,
 | 
			
		||||
      'loadingIndicators': loadingIndicators.getLoadingIndicators(),
 | 
			
		||||
      'da': da?.previousTime ? da : undefined,
 | 
			
		||||
      'fees': recommendedFees,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // cache serialized objects to avoid stringify-ing the same thing for every client
 | 
			
		||||
    const responseCache = { ...this.initData };
 | 
			
		||||
    const responseCache = { ...this.socketData };
 | 
			
		||||
    function getCachedResponse(key: string,  data): string {
 | 
			
		||||
      if (!responseCache[key]) {
 | 
			
		||||
        responseCache[key] = JSON.stringify(data);
 | 
			
		||||
@ -369,8 +417,6 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
 | 
			
		||||
 | 
			
		||||
    this.wss.clients.forEach(async (client) => {
 | 
			
		||||
      if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
        return;
 | 
			
		||||
@ -397,7 +443,7 @@ class WebsocketHandler {
 | 
			
		||||
        if (tx) {
 | 
			
		||||
          if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
            try {
 | 
			
		||||
              const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
              const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
              response['tx'] = JSON.stringify(fullTx);
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -417,7 +463,7 @@ class WebsocketHandler {
 | 
			
		||||
          if (someVin) {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                foundTransactions.push(fullTx);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -431,7 +477,7 @@ class WebsocketHandler {
 | 
			
		||||
          if (someVout) {
 | 
			
		||||
            if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
              try {
 | 
			
		||||
                const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
 | 
			
		||||
                const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
 | 
			
		||||
                foundTransactions.push(fullTx);
 | 
			
		||||
              } catch (e) {
 | 
			
		||||
                logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -488,7 +534,7 @@ class WebsocketHandler {
 | 
			
		||||
        if (rbfReplacedBy) {
 | 
			
		||||
          response['rbfTransaction'] = JSON.stringify({
 | 
			
		||||
            txid: rbfReplacedBy,
 | 
			
		||||
          })
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const rbfChange = rbfChanges.map[client['track-tx']];
 | 
			
		||||
@ -522,15 +568,13 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (Object.keys(response).length) {
 | 
			
		||||
        const serializedResponse = '{'
 | 
			
		||||
          + Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
 | 
			
		||||
          + '}';
 | 
			
		||||
        const serializedResponse = this.serializeResponse(response);
 | 
			
		||||
        client.send(serializedResponse);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
  async handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]): Promise<void> {
 | 
			
		||||
  async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
@ -539,6 +583,10 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
    const _memPool = memPool.getMempool();
 | 
			
		||||
 | 
			
		||||
    const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
 | 
			
		||||
    memPool.handleMinedRbfTransactions(rbfTransactions);
 | 
			
		||||
    memPool.removeFromSpendMap(transactions);
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.AUDIT) {
 | 
			
		||||
      let projectedBlocks;
 | 
			
		||||
      let auditMempool = _memPool;
 | 
			
		||||
@ -548,7 +596,11 @@ class WebsocketHandler {
 | 
			
		||||
      if (separateAudit) {
 | 
			
		||||
        auditMempool = deepClone(_memPool);
 | 
			
		||||
        if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
 | 
			
		||||
          projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
 | 
			
		||||
          if (config.MEMPOOL.RUST_GBT) {
 | 
			
		||||
            projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool);
 | 
			
		||||
          } else {
 | 
			
		||||
            projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
 | 
			
		||||
        }
 | 
			
		||||
@ -557,23 +609,23 @@ class WebsocketHandler {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (Common.indexingEnabled() && memPool.isInSync()) {
 | 
			
		||||
        const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
 | 
			
		||||
        const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
 | 
			
		||||
        const matchRate = Math.round(score * 100 * 100) / 100;
 | 
			
		||||
 | 
			
		||||
        const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
 | 
			
		||||
          return {
 | 
			
		||||
            txid: tx.txid,
 | 
			
		||||
            vsize: tx.vsize,
 | 
			
		||||
            fee: tx.fee ? Math.round(tx.fee) : 0,
 | 
			
		||||
            value: tx.value,
 | 
			
		||||
          };
 | 
			
		||||
        }) : [];
 | 
			
		||||
        const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
 | 
			
		||||
 | 
			
		||||
        let totalFees = 0;
 | 
			
		||||
        let totalWeight = 0;
 | 
			
		||||
        for (const tx of stripped) {
 | 
			
		||||
          totalFees += tx.fee;
 | 
			
		||||
          totalWeight += (tx.vsize * 4);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        BlocksSummariesRepository.$saveTemplate({
 | 
			
		||||
          height: block.height,
 | 
			
		||||
          template: {
 | 
			
		||||
            id: block.id,
 | 
			
		||||
            transactions: stripped
 | 
			
		||||
            transactions: stripped,
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -584,11 +636,17 @@ class WebsocketHandler {
 | 
			
		||||
          addedTxs: added,
 | 
			
		||||
          missingTxs: censored,
 | 
			
		||||
          freshTxs: fresh,
 | 
			
		||||
          sigopTxs: sigop,
 | 
			
		||||
          fullrbfTxs: fullrbf,
 | 
			
		||||
          matchRate: matchRate,
 | 
			
		||||
          expectedFees: totalFees,
 | 
			
		||||
          expectedWeight: totalWeight,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (block.extras) {
 | 
			
		||||
          block.extras.matchRate = matchRate;
 | 
			
		||||
          block.extras.expectedFees = totalFees;
 | 
			
		||||
          block.extras.expectedWeight = totalWeight;
 | 
			
		||||
          block.extras.similarity = similarity;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -606,7 +664,11 @@ class WebsocketHandler {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
 | 
			
		||||
      await mempoolBlocks.$makeBlockTemplates(_memPool, true);
 | 
			
		||||
      if (config.MEMPOOL.RUST_GBT) {
 | 
			
		||||
        await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions);
 | 
			
		||||
      } else {
 | 
			
		||||
        await mempoolBlocks.$makeBlockTemplates(_memPool, true);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      mempoolBlocks.updateMempoolBlocks(_memPool, true);
 | 
			
		||||
    }
 | 
			
		||||
@ -615,11 +677,19 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
    const da = difficultyAdjustment.getDifficultyAdjustment();
 | 
			
		||||
    const fees = feeApi.getRecommendedFee();
 | 
			
		||||
    const mempoolInfo = memPool.getMempoolInfo();
 | 
			
		||||
 | 
			
		||||
    // update init data
 | 
			
		||||
    this.updateInitData();
 | 
			
		||||
    this.updateSocketDataFields({
 | 
			
		||||
      'mempoolInfo': mempoolInfo,
 | 
			
		||||
      'blocks': [...blocks.getBlocks(), block].slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT),
 | 
			
		||||
      'mempool-blocks': mBlocks,
 | 
			
		||||
      'loadingIndicators': loadingIndicators.getLoadingIndicators(),
 | 
			
		||||
      'da': da?.previousTime ? da : undefined,
 | 
			
		||||
      'fees': fees,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const responseCache = { ...this.initData };
 | 
			
		||||
    const responseCache = { ...this.socketData };
 | 
			
		||||
    function getCachedResponse(key, data): string {
 | 
			
		||||
      if (!responseCache[key]) {
 | 
			
		||||
        responseCache[key] = JSON.stringify(data);
 | 
			
		||||
@ -627,22 +697,26 @@ class WebsocketHandler {
 | 
			
		||||
      return responseCache[key];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const mempoolInfo = memPool.getMempoolInfo();
 | 
			
		||||
 | 
			
		||||
    this.wss.clients.forEach((client) => {
 | 
			
		||||
      if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!client['want-blocks']) {
 | 
			
		||||
        return;
 | 
			
		||||
      const response = {};
 | 
			
		||||
 | 
			
		||||
      if (client['want-blocks']) {
 | 
			
		||||
        response['block'] = getCachedResponse('block', block);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const response = {};
 | 
			
		||||
      response['block'] = getCachedResponse('block', block);
 | 
			
		||||
      response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
 | 
			
		||||
      response['da'] = getCachedResponse('da', da?.previousTime ? da : undefined);
 | 
			
		||||
      response['fees'] = getCachedResponse('fees', fees);
 | 
			
		||||
      if (client['want-stats']) {
 | 
			
		||||
        response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
 | 
			
		||||
        response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond());
 | 
			
		||||
        response['fees'] = getCachedResponse('fees', fees);
 | 
			
		||||
 | 
			
		||||
        if (da?.previousTime) {
 | 
			
		||||
          response['da'] = getCachedResponse('da', da);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (mBlocks && client['want-mempool-blocks']) {
 | 
			
		||||
        response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
 | 
			
		||||
@ -650,8 +724,8 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
      if (client['track-tx']) {
 | 
			
		||||
        const trackTxid = client['track-tx'];
 | 
			
		||||
        if (txIds.indexOf(trackTxid) > -1) {
 | 
			
		||||
          response['txConfirmed'] = 'true';
 | 
			
		||||
        if (trackTxid && txIds.indexOf(trackTxid) > -1) {
 | 
			
		||||
          response['txConfirmed'] = JSON.stringify(trackTxid);
 | 
			
		||||
        } else {
 | 
			
		||||
          const mempoolTx = _memPool[trackTxid];
 | 
			
		||||
          if (mempoolTx && mempoolTx.position) {
 | 
			
		||||
@ -737,11 +811,19 @@ class WebsocketHandler {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const serializedResponse = '{'
 | 
			
		||||
      if (Object.keys(response).length) {
 | 
			
		||||
        const serializedResponse = this.serializeResponse(response);
 | 
			
		||||
        client.send(serializedResponse);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // takes a dictionary of JSON serialized values
 | 
			
		||||
  // and zips it together into a valid JSON object
 | 
			
		||||
  private serializeResponse(response): string {
 | 
			
		||||
    return '{'
 | 
			
		||||
        + Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
 | 
			
		||||
        + '}';
 | 
			
		||||
      client.send(serializedResponse);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private printLogs(): void {
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@ interface IConfig {
 | 
			
		||||
    AUDIT: boolean;
 | 
			
		||||
    ADVANCED_GBT_AUDIT: boolean;
 | 
			
		||||
    ADVANCED_GBT_MEMPOOL: boolean;
 | 
			
		||||
    RUST_GBT: boolean;
 | 
			
		||||
    CPFP_INDEXING: boolean;
 | 
			
		||||
    MAX_BLOCKS_BULK_QUERY: number;
 | 
			
		||||
    DISK_CACHE_BLOCK_INTERVAL: number;
 | 
			
		||||
@ -160,6 +161,7 @@ const defaults: IConfig = {
 | 
			
		||||
    'AUDIT': false,
 | 
			
		||||
    'ADVANCED_GBT_AUDIT': false,
 | 
			
		||||
    'ADVANCED_GBT_MEMPOOL': false,
 | 
			
		||||
    'RUST_GBT': false,
 | 
			
		||||
    'CPFP_INDEXING': false,
 | 
			
		||||
    'MAX_BLOCKS_BULK_QUERY': 0,
 | 
			
		||||
    'DISK_CACHE_BLOCK_INTERVAL': 6,
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
 | 
			
		||||
    OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]>
 | 
			
		||||
    OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
 | 
			
		||||
  {
 | 
			
		||||
    this.checkDBFlag();
 | 
			
		||||
    let hardTimeout;
 | 
			
		||||
@ -45,7 +45,9 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
 | 
			
		||||
          reject(new Error(`DB query failed to return, reject or time out within ${hardTimeout / 1000}s - ${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}`));
 | 
			
		||||
        }, hardTimeout);
 | 
			
		||||
 | 
			
		||||
        this.getPool().then(pool => {
 | 
			
		||||
        // Use a specific connection if provided, otherwise delegate to the pool
 | 
			
		||||
        const connectionPromise = connection ? Promise.resolve(connection) : this.getPool();
 | 
			
		||||
        connectionPromise.then((pool: PoolConnection | Pool) => {
 | 
			
		||||
          return pool.query(query, params) as Promise<[T, FieldPacket[]]>;
 | 
			
		||||
        }).then(result => {
 | 
			
		||||
          resolve(result);
 | 
			
		||||
@ -61,6 +63,33 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
 | 
			
		||||
    OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
 | 
			
		||||
  {
 | 
			
		||||
    const pool = await this.getPool();
 | 
			
		||||
    const connection = await pool.getConnection();
 | 
			
		||||
    try {
 | 
			
		||||
      await connection.beginTransaction();
 | 
			
		||||
 | 
			
		||||
      const results: [T, FieldPacket[]][]  = [];
 | 
			
		||||
      for (const query of queries) {
 | 
			
		||||
        const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
 | 
			
		||||
        results.push(result);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await connection.commit();
 | 
			
		||||
 | 
			
		||||
      return results;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      connection.rollback();
 | 
			
		||||
      connection.release();
 | 
			
		||||
      throw e;
 | 
			
		||||
    } finally {
 | 
			
		||||
      connection.release();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async checkDbConnection() {
 | 
			
		||||
    this.checkDBFlag();
 | 
			
		||||
    try {
 | 
			
		||||
 | 
			
		||||
@ -150,7 +150,7 @@ class Server {
 | 
			
		||||
 | 
			
		||||
    if (config.BISQ.ENABLED) {
 | 
			
		||||
      bisq.startBisqService();
 | 
			
		||||
      bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
 | 
			
		||||
      bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitData('bsq-price', price));
 | 
			
		||||
      blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
 | 
			
		||||
      bisqMarkets.startBisqService();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import logger from './logger';
 | 
			
		||||
import bitcoinClient from './api/bitcoin/bitcoin-client';
 | 
			
		||||
import priceUpdater from './tasks/price-updater';
 | 
			
		||||
import PricesRepository from './repositories/PricesRepository';
 | 
			
		||||
import config from './config';
 | 
			
		||||
 | 
			
		||||
export interface CoreIndex {
 | 
			
		||||
  name: string;
 | 
			
		||||
@ -72,7 +73,7 @@ class Indexer {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (task === 'blocksPrices' && !this.tasksRunning.includes(task)) {
 | 
			
		||||
    if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
 | 
			
		||||
      this.tasksRunning.push(task);
 | 
			
		||||
      const lastestPriceId = await PricesRepository.$getLatestPriceId();
 | 
			
		||||
      if (priceUpdater.historyInserted === false || lastestPriceId === null) {
 | 
			
		||||
@ -134,6 +135,7 @@ class Indexer {
 | 
			
		||||
      await mining.$generatePoolHashrateHistory();
 | 
			
		||||
      await blocks.$generateBlocksSummariesDatabase();
 | 
			
		||||
      await blocks.$generateCPFPDatabase();
 | 
			
		||||
      await blocks.$generateAuditStats();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.indexerRunning = false;
 | 
			
		||||
      logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ export interface PoolInfo {
 | 
			
		||||
  blockCount: number;
 | 
			
		||||
  slug: string;
 | 
			
		||||
  avgMatchRate: number | null;
 | 
			
		||||
  avgFeeDelta: number | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PoolStats extends PoolInfo {
 | 
			
		||||
@ -32,13 +33,19 @@ export interface BlockAudit {
 | 
			
		||||
  hash: string,
 | 
			
		||||
  missingTxs: string[],
 | 
			
		||||
  freshTxs: string[],
 | 
			
		||||
  sigopTxs: string[],
 | 
			
		||||
  fullrbfTxs: string[],
 | 
			
		||||
  addedTxs: string[],
 | 
			
		||||
  matchRate: number,
 | 
			
		||||
  expectedFees?: number,
 | 
			
		||||
  expectedWeight?: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuditScore {
 | 
			
		||||
  hash: string,
 | 
			
		||||
  matchRate?: number,
 | 
			
		||||
  expectedFees?: number
 | 
			
		||||
  expectedWeight?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
@ -87,32 +94,44 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
 | 
			
		||||
  uid?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolTransactionExtended extends TransactionExtended {
 | 
			
		||||
  order: number;
 | 
			
		||||
  sigops: number;
 | 
			
		||||
  adjustedVsize: number;
 | 
			
		||||
  adjustedFeePerVsize: number;
 | 
			
		||||
  inputs?: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuditTransaction {
 | 
			
		||||
  uid: number;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
  effectiveFeePerVsize: number;
 | 
			
		||||
  sigops: number;
 | 
			
		||||
  inputs: number[];
 | 
			
		||||
  relativesSet: boolean;
 | 
			
		||||
  ancestorMap: Map<number, AuditTransaction>;
 | 
			
		||||
  children: Set<AuditTransaction>;
 | 
			
		||||
  ancestorFee: number;
 | 
			
		||||
  ancestorWeight: number;
 | 
			
		||||
  ancestorSigops: number;
 | 
			
		||||
  score: number;
 | 
			
		||||
  used: boolean;
 | 
			
		||||
  modified: boolean;
 | 
			
		||||
  modifiedNode: HeapNode<AuditTransaction>;
 | 
			
		||||
  dependencyRate?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CompactThreadTransaction {
 | 
			
		||||
  uid: number;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  sigops: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
  effectiveFeePerVsize?: number;
 | 
			
		||||
  effectiveFeePerVsize: number;
 | 
			
		||||
  inputs: number[];
 | 
			
		||||
  cpfpRoot?: string;
 | 
			
		||||
  cpfpRoot?: number;
 | 
			
		||||
  cpfpChecked?: boolean;
 | 
			
		||||
  dirty?: boolean;
 | 
			
		||||
}
 | 
			
		||||
@ -171,6 +190,8 @@ export interface BlockExtension {
 | 
			
		||||
  feeRange: number[]; // fee rate percentiles
 | 
			
		||||
  reward: number;
 | 
			
		||||
  matchRate: number | null;
 | 
			
		||||
  expectedFees: number | null;
 | 
			
		||||
  expectedWeight: number | null;
 | 
			
		||||
  similarity?: number;
 | 
			
		||||
  pool: {
 | 
			
		||||
    id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
 | 
			
		||||
@ -207,6 +228,7 @@ export interface BlockExtension {
 | 
			
		||||
 */
 | 
			
		||||
export interface BlockExtended extends IEsploraApi.Block {
 | 
			
		||||
  extras: BlockExtension;
 | 
			
		||||
  canonical?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BlockSummary {
 | 
			
		||||
@ -237,9 +259,21 @@ export interface EffectiveFeeStats {
 | 
			
		||||
  feeRange: number[]; // 2nd, 10th, 25th, 50th, 75th, 90th, 98th percentiles
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WorkingEffectiveFeeStats extends EffectiveFeeStats {
 | 
			
		||||
  minFee: number;
 | 
			
		||||
  maxFee: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CpfpCluster {
 | 
			
		||||
  root: string,
 | 
			
		||||
  height: number,
 | 
			
		||||
  txs: Ancestor[],
 | 
			
		||||
  effectiveFeePerVsize: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CpfpSummary {
 | 
			
		||||
  transactions: TransactionExtended[];
 | 
			
		||||
  clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[];
 | 
			
		||||
  clusters: CpfpCluster[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Statistic {
 | 
			
		||||
 | 
			
		||||
@ -6,20 +6,32 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
			
		||||
class BlocksAuditRepositories {
 | 
			
		||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, match_rate)
 | 
			
		||||
        VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
 | 
			
		||||
          JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), audit.matchRate]);
 | 
			
		||||
      await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, match_rate, expected_fees, expected_weight)
 | 
			
		||||
        VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
 | 
			
		||||
          JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
 | 
			
		||||
        logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
        throw e;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlockPredictionsHistory(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
  public async $setSummary(hash: string, expectedFees: number, expectedWeight: number) {
 | 
			
		||||
    try {
 | 
			
		||||
      await DB.query(`
 | 
			
		||||
        UPDATE blocks_audits SET
 | 
			
		||||
        expected_fees = ?,
 | 
			
		||||
        expected_weight = ?
 | 
			
		||||
        WHERE hash = ?
 | 
			
		||||
      `, [expectedFees, expectedWeight, hash]);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot update block audit in db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlocksHealthHistory(div: number, interval: string | null): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`;
 | 
			
		||||
 | 
			
		||||
@ -32,17 +44,17 @@ class BlocksAuditRepositories {
 | 
			
		||||
      const [rows] = await DB.query(query);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      logger.err(`Cannot fetch blocks health history. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getPredictionsCount(): Promise<number> {
 | 
			
		||||
  public async $getBlocksHealthCount(): Promise<number> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`);
 | 
			
		||||
      return rows[0].count;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      logger.err(`Cannot fetch blocks health count. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -50,12 +62,18 @@ class BlocksAuditRepositories {
 | 
			
		||||
  public async $getBlockAudit(hash: string): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
 | 
			
		||||
        blocks.weight, blocks.tx_count,
 | 
			
		||||
        transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, match_rate as matchRate
 | 
			
		||||
        `SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
 | 
			
		||||
        template,
 | 
			
		||||
        missing_txs as missingTxs,
 | 
			
		||||
        added_txs as addedTxs,
 | 
			
		||||
        fresh_txs as freshTxs,
 | 
			
		||||
        sigop_txs as sigopTxs,
 | 
			
		||||
        fullrbf_txs as fullrbfTxs,
 | 
			
		||||
        match_rate as matchRate,
 | 
			
		||||
        expected_fees as expectedFees,
 | 
			
		||||
        expected_weight as expectedWeight
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        JOIN blocks ON blocks.hash = blocks_audits.hash
 | 
			
		||||
        JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
 | 
			
		||||
        JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
 | 
			
		||||
        WHERE blocks_audits.hash = "${hash}"
 | 
			
		||||
      `);
 | 
			
		||||
      
 | 
			
		||||
@ -63,12 +81,11 @@ class BlocksAuditRepositories {
 | 
			
		||||
        rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
 | 
			
		||||
        rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
 | 
			
		||||
        rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
 | 
			
		||||
        rows[0].transactions = JSON.parse(rows[0].transactions);
 | 
			
		||||
        rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
 | 
			
		||||
        rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
 | 
			
		||||
        rows[0].template = JSON.parse(rows[0].template);
 | 
			
		||||
 | 
			
		||||
        if (rows[0].transactions.length) {
 | 
			
		||||
          return rows[0];
 | 
			
		||||
        }
 | 
			
		||||
        return rows[0];
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
@ -80,7 +97,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
  public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT hash, match_rate as matchRate
 | 
			
		||||
        `SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        WHERE blocks_audits.hash = "${hash}"
 | 
			
		||||
      `);
 | 
			
		||||
@ -94,7 +111,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
  public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise<AuditScore[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(
 | 
			
		||||
        `SELECT hash, match_rate as matchRate
 | 
			
		||||
        `SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        WHERE blocks_audits.height BETWEEN ? AND ?
 | 
			
		||||
      `, [minHeight, maxHeight]);
 | 
			
		||||
@ -104,6 +121,32 @@ class BlocksAuditRepositories {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBlocksWithoutSummaries(): Promise<string[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [fromRows]: any[] = await DB.query(`
 | 
			
		||||
        SELECT height
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        WHERE expected_fees IS NULL
 | 
			
		||||
        ORDER BY height DESC
 | 
			
		||||
        LIMIT 1
 | 
			
		||||
      `);
 | 
			
		||||
      if (!fromRows?.length) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
      const fromHeight = fromRows[0].height;
 | 
			
		||||
      const [idRows]: any[] = await DB.query(`
 | 
			
		||||
        SELECT hash
 | 
			
		||||
        FROM blocks_audits
 | 
			
		||||
        WHERE height <= ?
 | 
			
		||||
        ORDER BY height DESC
 | 
			
		||||
      `, [fromHeight]);
 | 
			
		||||
      return idRows.map(row => row.hash);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new BlocksAuditRepositories();
 | 
			
		||||
 | 
			
		||||
@ -577,19 +577,6 @@ class BlocksRepository {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return blocks height
 | 
			
		||||
   */
 | 
			
		||||
   public async $getBlocksHeightsAndTimestamp(): Promise<object[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(`SELECT height, blockTimestamp as timestamp FROM blocks`);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Cannot get blocks height and timestamp from the db. Reason: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get general block stats
 | 
			
		||||
   */
 | 
			
		||||
@ -877,7 +864,7 @@ class BlocksRepository {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get all blocks which have not be linked to a price yet
 | 
			
		||||
   */
 | 
			
		||||
   public async $getBlocksWithoutPrice(): Promise<object[]> {
 | 
			
		||||
  public async $getBlocksWithoutPrice(): Promise<object[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(`
 | 
			
		||||
        SELECT UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.height
 | 
			
		||||
@ -889,7 +876,7 @@ class BlocksRepository {
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Cannot get blocks height and timestamp from the db. Reason: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -909,7 +896,6 @@ class BlocksRepository {
 | 
			
		||||
        logger.debug(`Cannot save blocks prices for blocks [${blockPrices[0].height} to ${blockPrices[blockPrices.length - 1].height}] because it has already been indexed, ignoring`);
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.err(`Cannot save blocks prices for blocks [${blockPrices[0].height} to ${blockPrices[blockPrices.length - 1].height}] into db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
        throw e;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -928,7 +914,7 @@ class BlocksRepository {
 | 
			
		||||
      return blocks;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get blocks with missing coinstatsindex. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1032,10 +1018,14 @@ class BlocksRepository {
 | 
			
		||||
 | 
			
		||||
    // Match rate is not part of the blocks table, but it is part of APIs so we must include it
 | 
			
		||||
    extras.matchRate = null;
 | 
			
		||||
    extras.expectedFees = null;
 | 
			
		||||
    extras.expectedWeight = null;
 | 
			
		||||
    if (config.MEMPOOL.AUDIT) {
 | 
			
		||||
      const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(dbBlk.id);
 | 
			
		||||
      if (auditScore != null) {
 | 
			
		||||
        extras.matchRate = auditScore.matchRate;
 | 
			
		||||
        extras.expectedFees = auditScore.expectedFees;
 | 
			
		||||
        extras.expectedWeight = auditScore.expectedWeight;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -36,21 +36,35 @@ class BlocksSummariesRepository {
 | 
			
		||||
    try {
 | 
			
		||||
      const transactions = JSON.stringify(params.template?.transactions || []);
 | 
			
		||||
      await DB.query(`
 | 
			
		||||
        INSERT INTO blocks_summaries (height, id, transactions, template)
 | 
			
		||||
        VALUE (?, ?, ?, ?)
 | 
			
		||||
        INSERT INTO blocks_templates (id, template)
 | 
			
		||||
        VALUE (?, ?)
 | 
			
		||||
        ON DUPLICATE KEY UPDATE
 | 
			
		||||
          template = ?
 | 
			
		||||
      `, [params.height, blockId, '[]', transactions, transactions]);
 | 
			
		||||
      `, [blockId, transactions, transactions]);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
 | 
			
		||||
        logger.debug(`Cannot save block template for ${blockId} because it has already been indexed, ignoring`);
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.debug(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
        throw e;
 | 
			
		||||
        logger.warn(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getTemplate(id: string): Promise<BlockSummary | undefined> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [templates]: any[] = await DB.query(`SELECT * from blocks_templates WHERE id = ?`, [id]);
 | 
			
		||||
      if (templates.length > 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          id: templates[0].id,
 | 
			
		||||
          transactions: JSON.parse(templates[0].template),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get block template for block id ${id}. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getIndexedSummariesId(): Promise<string[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`);
 | 
			
		||||
 | 
			
		||||
@ -1,62 +1,19 @@
 | 
			
		||||
import cluster, { Cluster } from 'cluster';
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { Ancestor } from '../mempool.interfaces';
 | 
			
		||||
import { Ancestor, CpfpCluster } from '../mempool.interfaces';
 | 
			
		||||
import transactionRepository from '../repositories/TransactionRepository';
 | 
			
		||||
 | 
			
		||||
class CpfpRepository {
 | 
			
		||||
  public async $saveCluster(clusterRoot: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<boolean> {
 | 
			
		||||
    if (!txs[0]) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    // skip clusters of transactions with the same fees
 | 
			
		||||
    const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100;
 | 
			
		||||
    const equalFee = txs.reduce((acc, tx) => {
 | 
			
		||||
      return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
 | 
			
		||||
    }, true);
 | 
			
		||||
    if (equalFee) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const packedTxs = Buffer.from(this.pack(txs));
 | 
			
		||||
      await DB.query(
 | 
			
		||||
        `
 | 
			
		||||
          INSERT INTO compact_cpfp_clusters(root, height, txs, fee_rate)
 | 
			
		||||
          VALUE (UNHEX(?), ?, ?, ?)
 | 
			
		||||
          ON DUPLICATE KEY UPDATE
 | 
			
		||||
            height = ?,
 | 
			
		||||
            txs = ?,
 | 
			
		||||
            fee_rate = ?
 | 
			
		||||
        `,
 | 
			
		||||
        [clusterRoot, height, packedTxs, effectiveFeePerVsize, height, packedTxs, effectiveFeePerVsize]
 | 
			
		||||
      );
 | 
			
		||||
      const maxChunk = 10;
 | 
			
		||||
      let chunkIndex = 0;
 | 
			
		||||
      while (chunkIndex < txs.length) {
 | 
			
		||||
        const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk).map(tx => {
 | 
			
		||||
          return { txid: tx.txid, cluster: clusterRoot };
 | 
			
		||||
        });
 | 
			
		||||
        await transactionRepository.$batchSetCluster(chunk);
 | 
			
		||||
        chunkIndex += maxChunk;
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $batchSaveClusters(clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]): Promise<boolean> {
 | 
			
		||||
    try {
 | 
			
		||||
      const clusterValues: any[] = [];
 | 
			
		||||
      const txs: any[] = [];
 | 
			
		||||
      const clusterValues: [string, number, Buffer, number][] = [];
 | 
			
		||||
      const txs: { txid: string, cluster: string }[] = [];
 | 
			
		||||
 | 
			
		||||
      for (const cluster of clusters) {
 | 
			
		||||
        if (cluster.txs?.length > 1) {
 | 
			
		||||
        if (cluster.txs?.length) {
 | 
			
		||||
          const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100;
 | 
			
		||||
          const equalFee = cluster.txs.reduce((acc, tx) => {
 | 
			
		||||
          const equalFee = cluster.txs.length > 1 && cluster.txs.reduce((acc, tx) => {
 | 
			
		||||
            return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
 | 
			
		||||
          }, true);
 | 
			
		||||
          if (!equalFee) {
 | 
			
		||||
@ -77,16 +34,10 @@ class CpfpRepository {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const queries: { query, params }[] = [];
 | 
			
		||||
 | 
			
		||||
      const maxChunk = 100;
 | 
			
		||||
      let chunkIndex = 0;
 | 
			
		||||
      // insert transactions in batches of up to 100 rows
 | 
			
		||||
      while (chunkIndex < txs.length) {
 | 
			
		||||
        const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk);
 | 
			
		||||
        await transactionRepository.$batchSetCluster(chunk);
 | 
			
		||||
        chunkIndex += maxChunk;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      chunkIndex = 0;
 | 
			
		||||
      // insert clusters in batches of up to 100 rows
 | 
			
		||||
      while (chunkIndex < clusterValues.length) {
 | 
			
		||||
        const chunk = clusterValues.slice(chunkIndex, chunkIndex + maxChunk);
 | 
			
		||||
@ -98,12 +49,23 @@ class CpfpRepository {
 | 
			
		||||
          return (' (UNHEX(?), ?, ?, ?)');
 | 
			
		||||
        }) + ';';
 | 
			
		||||
        const values = chunk.flat();
 | 
			
		||||
        await DB.query(
 | 
			
		||||
        queries.push({
 | 
			
		||||
          query,
 | 
			
		||||
          values
 | 
			
		||||
        );
 | 
			
		||||
          params: values,
 | 
			
		||||
        });
 | 
			
		||||
        chunkIndex += maxChunk;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      chunkIndex = 0;
 | 
			
		||||
      // insert transactions in batches of up to 100 rows
 | 
			
		||||
      while (chunkIndex < txs.length) {
 | 
			
		||||
        const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk);
 | 
			
		||||
        queries.push(transactionRepository.buildBatchSetQuery(chunk));
 | 
			
		||||
        chunkIndex += maxChunk;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await DB.$atomicQuery(queries);
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -111,7 +73,7 @@ class CpfpRepository {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getCluster(clusterRoot: string): Promise<Cluster | void> {
 | 
			
		||||
  public async $getCluster(clusterRoot: string): Promise<CpfpCluster | void> {
 | 
			
		||||
    const [clusterRows]: any = await DB.query(
 | 
			
		||||
      `
 | 
			
		||||
        SELECT *
 | 
			
		||||
@ -122,6 +84,7 @@ class CpfpRepository {
 | 
			
		||||
    );
 | 
			
		||||
    const cluster = clusterRows[0];
 | 
			
		||||
    if (cluster?.txs) {
 | 
			
		||||
      cluster.effectiveFeePerVsize = cluster.fee_rate;
 | 
			
		||||
      cluster.txs = this.unpack(cluster.txs);
 | 
			
		||||
      return cluster;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,8 @@ class PoolsRepository {
 | 
			
		||||
          pools.name AS name,
 | 
			
		||||
          pools.link AS link,
 | 
			
		||||
          slug,
 | 
			
		||||
          AVG(blocks_audits.match_rate) AS avgMatchRate
 | 
			
		||||
          AVG(blocks_audits.match_rate) AS avgMatchRate,
 | 
			
		||||
          AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta
 | 
			
		||||
      FROM blocks
 | 
			
		||||
      JOIN pools on pools.id = pool_id
 | 
			
		||||
      LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height
 | 
			
		||||
 | 
			
		||||
@ -25,9 +25,8 @@ class TransactionRepository {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $batchSetCluster(txs): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `
 | 
			
		||||
  public buildBatchSetQuery(txs: { txid: string, cluster: string }[]): { query, params } {
 | 
			
		||||
    let query = `
 | 
			
		||||
          INSERT IGNORE INTO compact_transactions
 | 
			
		||||
          (
 | 
			
		||||
            txid,
 | 
			
		||||
@ -35,13 +34,22 @@ class TransactionRepository {
 | 
			
		||||
          )
 | 
			
		||||
          VALUES
 | 
			
		||||
      `;
 | 
			
		||||
      query += txs.map(tx => {
 | 
			
		||||
        return (' (UNHEX(?), UNHEX(?))');
 | 
			
		||||
      }) + ';';
 | 
			
		||||
      const values = txs.map(tx => [tx.txid, tx.cluster]).flat();
 | 
			
		||||
    query += txs.map(tx => {
 | 
			
		||||
      return (' (UNHEX(?), UNHEX(?))');
 | 
			
		||||
    }) + ';';
 | 
			
		||||
    const values = txs.map(tx => [tx.txid, tx.cluster]).flat();
 | 
			
		||||
    return {
 | 
			
		||||
      query,
 | 
			
		||||
      params: values,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $batchSetCluster(txs): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = this.buildBatchSetQuery(txs);
 | 
			
		||||
      await DB.query(
 | 
			
		||||
        query,
 | 
			
		||||
        values
 | 
			
		||||
        query.query,
 | 
			
		||||
        query.params,
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -105,6 +113,7 @@ class TransactionRepository {
 | 
			
		||||
    return {
 | 
			
		||||
      descendants,
 | 
			
		||||
      ancestors,
 | 
			
		||||
      effectiveFeePerVsize: cluster.effectiveFeePerVsize,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -71,7 +71,7 @@ class PoolsUpdater {
 | 
			
		||||
      poolsParser.setMiningPools(poolsJson);
 | 
			
		||||
 | 
			
		||||
      if (config.DATABASE.ENABLED === false) { // Don't run db operations
 | 
			
		||||
        logger.info('Mining pools-v2.json import completed (no database)');
 | 
			
		||||
        logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -84,7 +84,7 @@ class PoolsUpdater {
 | 
			
		||||
        logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
 | 
			
		||||
        await DB.query('ROLLBACK;');
 | 
			
		||||
      }
 | 
			
		||||
      logger.info('PoolsUpdater completed');
 | 
			
		||||
      logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
 | 
			
		||||
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/0xflicker.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/0xflicker.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 24, 2023.
 | 
			
		||||
 | 
			
		||||
Signed: 0xflicker
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/joostjager.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/joostjager.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
 | 
			
		||||
 | 
			
		||||
Signed: joostjager
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/pfoytik.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/pfoytik.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 15, 2023.
 | 
			
		||||
 | 
			
		||||
Signed pfoytik
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/secondl1ght.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/secondl1ght.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 14, 2023.
 | 
			
		||||
 | 
			
		||||
Signed: secondl1ght
 | 
			
		||||
							
								
								
									
										3
									
								
								contributors/vostrnad.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/vostrnad.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
 | 
			
		||||
 | 
			
		||||
Signed: vostrnad
 | 
			
		||||
@ -144,8 +144,8 @@ Corresponding `docker-compose.yml` overrides:
 | 
			
		||||
      MEMPOOL_ADVANCED_GBT_AUDIT: ""
 | 
			
		||||
      MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
 | 
			
		||||
      MEMPOOL_CPFP_INDEXING: ""
 | 
			
		||||
      MAX_BLOCKS_BULK_QUERY: ""
 | 
			
		||||
      DISK_CACHE_BLOCK_INTERVAL: ""
 | 
			
		||||
      MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
 | 
			
		||||
      MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
 | 
			
		||||
      ...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,12 @@ WORKDIR /build
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
RUN apt-get update
 | 
			
		||||
RUN apt-get install -y build-essential python3 pkg-config
 | 
			
		||||
RUN apt-get install -y build-essential python3 pkg-config curl
 | 
			
		||||
 | 
			
		||||
# Install Rust via rustup
 | 
			
		||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
 | 
			
		||||
ENV PATH="/root/.cargo/bin:$PATH"
 | 
			
		||||
 | 
			
		||||
RUN npm install --omit=dev --omit=optional
 | 
			
		||||
RUN npm run package
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,9 +25,12 @@
 | 
			
		||||
    "AUDIT": __MEMPOOL_AUDIT__,
 | 
			
		||||
    "ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
 | 
			
		||||
    "ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
 | 
			
		||||
    "RUST_GBT": __MEMPOOL_RUST_GBT__,
 | 
			
		||||
    "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
 | 
			
		||||
    "MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
 | 
			
		||||
    "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
 | 
			
		||||
    "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "__CORE_RPC_HOST__",
 | 
			
		||||
@ -61,7 +64,7 @@
 | 
			
		||||
    "DATABASE": "__DATABASE_DATABASE__",
 | 
			
		||||
    "USERNAME": "__DATABASE_USERNAME__",
 | 
			
		||||
    "PASSWORD": "__DATABASE_PASSWORD__",
 | 
			
		||||
    "TIMEOUT": "__DATABASE_TIMEOUT__"
 | 
			
		||||
    "TIMEOUT": __DATABASE_TIMEOUT__
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG": {
 | 
			
		||||
    "ENABLED": __SYSLOG_ENABLED__,
 | 
			
		||||
@ -84,13 +87,15 @@
 | 
			
		||||
    "STATS_REFRESH_INTERVAL": __LIGHTNING_STATS_REFRESH_INTERVAL__,
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": __LIGHTNING_GRAPH_REFRESH_INTERVAL__,
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": __LIGHTNING_LOGGER_UPDATE_INTERVAL__,
 | 
			
		||||
    "TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__"
 | 
			
		||||
    "TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
 | 
			
		||||
    "FORENSICS_INTERVAL": __LIGHTNING_FORENSICS_INTERVAL__,
 | 
			
		||||
    "FORENSICS_RATE_LIMIT": __LIGHTNING_FORENSICS_RATE_LIMIT__
 | 
			
		||||
  },
 | 
			
		||||
  "LND": {
 | 
			
		||||
    "TLS_CERT_PATH": "__LND_TLS_CERT_PATH__",
 | 
			
		||||
    "MACAROON_PATH": "__LND_MACAROON_PATH__",
 | 
			
		||||
    "REST_API_URL": "__LND_REST_API_URL__",
 | 
			
		||||
    "TIMEOUT": "__LND_TIMEOUT__"
 | 
			
		||||
    "TIMEOUT": __LND_TIMEOUT__
 | 
			
		||||
  },
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "__CLIGHTNING_SOCKET__"
 | 
			
		||||
@ -121,4 +126,4 @@
 | 
			
		||||
    "GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
 | 
			
		||||
    "GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -28,6 +28,7 @@ __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.githu
 | 
			
		||||
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
 | 
			
		||||
__MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
 | 
			
		||||
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
 | 
			
		||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
 | 
			
		||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
 | 
			
		||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
 | 
			
		||||
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
 | 
			
		||||
@ -46,7 +47,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
 | 
			
		||||
 | 
			
		||||
# ESPLORA
 | 
			
		||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
 | 
			
		||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=null}
 | 
			
		||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
 | 
			
		||||
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
 | 
			
		||||
 | 
			
		||||
# SECOND_CORE_RPC
 | 
			
		||||
@ -108,6 +109,8 @@ __LIGHTNING_TOPOLOGY_FOLDER__=${LIGHTNING_TOPOLOGY_FOLDER:=""}
 | 
			
		||||
__LIGHTNING_STATS_REFRESH_INTERVAL__=${LIGHTNING_STATS_REFRESH_INTERVAL:=600}
 | 
			
		||||
__LIGHTNING_GRAPH_REFRESH_INTERVAL__=${LIGHTNING_GRAPH_REFRESH_INTERVAL:=600}
 | 
			
		||||
__LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
 | 
			
		||||
__LIGHTNING_FORENSICS_INTERVAL__=${LIGHTNING_FORENSICS_INTERVAL:=43200}
 | 
			
		||||
__LIGHTNING_FORENSICS_RATE_LIMIT__=${LIGHTNING_FORENSICS_RATE_LIMIT:=20}
 | 
			
		||||
 | 
			
		||||
# LND
 | 
			
		||||
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
 | 
			
		||||
@ -127,84 +130,85 @@ __MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
 | 
			
		||||
 | 
			
		||||
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_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_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_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
 | 
			
		||||
sed -i "s/__MEMPOOL_POLL_RATE_MS__/${__MEMPOOL_POLL_RATE_MS__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_CLEAR_PROTECTION_MINUTES__/${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__/${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_BLOCK_WEIGHT_UNITS__/${__MEMPOOL_BLOCK_WEIGHT_UNITS__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_INITIAL_BLOCKS_AMOUNT__/${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__/${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_INITIAL_BLOCKS_AMOUNT__!${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__!${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_INDEXING_BLOCKS_AMOUNT__!${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__!${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__!${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__!${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_GBT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__CORE_RPC_USERNAME__/${__CORE_RPC_USERNAME__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__CORE_RPC_PASSWORD__/${__CORE_RPC_PASSWORD__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__CORE_RPC_TIMEOUT__/${__CORE_RPC_TIMEOUT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__ELECTRUM_HOST__/${__ELECTRUM_HOST__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__ELECTRUM_PORT__/${__ELECTRUM_PORT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__ELECTRUM_TLS_ENABLED__/${__ELECTRUM_TLS_ENABLED__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__SECOND_CORE_RPC_HOST__/${__SECOND_CORE_RPC_HOST__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SECOND_CORE_RPC_PORT__/${__SECOND_CORE_RPC_PORT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SECOND_CORE_RPC_USERNAME__/${__SECOND_CORE_RPC_USERNAME__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SECOND_CORE_RPC_PASSWORD__/${__SECOND_CORE_RPC_PASSWORD__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SECOND_CORE_RPC_TIMEOUT__/${__SECOND_CORE_RPC_TIMEOUT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SECOND_CORE_RPC_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__DATABASE_ENABLED__/${__DATABASE_ENABLED__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__DATABASE_HOST__/${__DATABASE_HOST__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__DATABASE_SOCKET__!${__DATABASE_SOCKET__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__DATABASE_PORT__!${__DATABASE_PORT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__DATABASE_DATABASE__!${__DATABASE_DATABASE__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__DATABASE_PORT__/${__DATABASE_PORT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__DATABASE_DATABASE__/${__DATABASE_DATABASE__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__DATABASE_USERNAME__/${__DATABASE_USERNAME__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__DATABASE_PASSWORD__/${__DATABASE_PASSWORD__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SYSLOG_PORT__!${__SYSLOG_PORT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SYSLOG_MIN_PRIORITY__!${__SYSLOG_MIN_PRIORITY__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SYSLOG_FACILITY__!${__SYSLOG_FACILITY__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__SYSLOG_ENABLED__/${__SYSLOG_ENABLED__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SYSLOG_HOST__/${__SYSLOG_HOST__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SYSLOG_PORT__/${__SYSLOG_PORT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SYSLOG_MIN_PRIORITY__/${__SYSLOG_MIN_PRIORITY__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SYSLOG_FACILITY__/${__SYSLOG_FACILITY__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__STATISTICS_ENABLED__!${__STATISTICS_ENABLED__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__!${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__STATISTICS_ENABLED__/${__STATISTICS_ENABLED__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__/${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}/g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__BISQ_ENABLED__!${__BISQ_ENABLED__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s/__SOCKS5PROXY_ENABLED__/${__SOCKS5PROXY_ENABLED__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SOCKS5PROXY_USE_ONION__/${__SOCKS5PROXY_USE_ONION__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SOCKS5PROXY_HOST__/${__SOCKS5PROXY_HOST__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json
 | 
			
		||||
sed -i "s/__SOCKS5PROXY_PASSWORD__/${__SOCKS5PROXY_PASSWORD__}/g" mempool-config.json
 | 
			
		||||
sed -i "s!__SOCKS5PROXY_ENABLED__!${__SOCKS5PROXY_ENABLED__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SOCKS5PROXY_USE_ONION__!${__SOCKS5PROXY_USE_ONION__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SOCKS5PROXY_HOST__!${__SOCKS5PROXY_HOST__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SOCKS5PROXY_PORT__!${__SOCKS5PROXY_PORT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SOCKS5PROXY_USERNAME__!${__SOCKS5PROXY_USERNAME__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__SOCKS5PROXY_PASSWORD__!${__SOCKS5PROXY_PASSWORD__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
 | 
			
		||||
@ -223,6 +227,8 @@ sed -i "s!__LIGHTNING_TOPOLOGY_FOLDER__!${__LIGHTNING_TOPOLOGY_FOLDER__}!g" memp
 | 
			
		||||
sed -i "s!__LIGHTNING_STATS_REFRESH_INTERVAL__!${__LIGHTNING_STATS_REFRESH_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_GRAPH_REFRESH_INTERVAL__!${__LIGHTNING_GRAPH_REFRESH_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_FORENSICS_INTERVAL__!${__LIGHTNING_FORENSICS_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_FORENSICS_RATE_LIMIT__!${__LIGHTNING_FORENSICS_RATE_LIMIT__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
# LND
 | 
			
		||||
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ Get the latest Mempool code:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone https://github.com/mempool/mempool
 | 
			
		||||
cd mempool
 | 
			
		||||
cd mempool/frontend
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. Specify Website
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
describe('Liquid', () => {
 | 
			
		||||
describe.skip('Liquid', () => {
 | 
			
		||||
  const baseModule = Cypress.env('BASE_MODULE');
 | 
			
		||||
  const basePath = '';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
describe('Liquid Testnet', () => {
 | 
			
		||||
describe.skip('Liquid Testnet', () => {
 | 
			
		||||
  const baseModule = Cypress.env('BASE_MODULE');
 | 
			
		||||
  const basePath = '/testnet';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -504,9 +504,17 @@ describe('Mainnet', () => {
 | 
			
		||||
 | 
			
		||||
    describe('RBF transactions', () => {
 | 
			
		||||
      it('shows RBF transactions properly (mobile)', () => {
 | 
			
		||||
        cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
 | 
			
		||||
          fixture: 'mainnet_tx_cached.json'
 | 
			
		||||
        }).as('cached_tx');
 | 
			
		||||
 | 
			
		||||
        cy.intercept('/api/v1/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5/rbf', {
 | 
			
		||||
          fixture: 'mainnet_rbf_new.json'
 | 
			
		||||
        }).as('rbf');
 | 
			
		||||
 | 
			
		||||
        cy.viewport('iphone-xr');
 | 
			
		||||
        cy.mockMempoolSocket();
 | 
			
		||||
        cy.visit('/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5');
 | 
			
		||||
        cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
 | 
			
		||||
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
 | 
			
		||||
@ -524,22 +532,30 @@ describe('Mainnet', () => {
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        cy.get('.alert-mempool').should('be.visible');
 | 
			
		||||
        cy.get('.alert-mempool').invoke('css', 'width').then((alertWidth) => {
 | 
			
		||||
        cy.get('.alert').should('be.visible');
 | 
			
		||||
        cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
 | 
			
		||||
          cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        cy.get('.btn-success').then(getRectangle).then((rectA) => {
 | 
			
		||||
          cy.get('.alert-mempool').then(getRectangle).then((rectB) => {
 | 
			
		||||
        cy.get('.btn-warning').then(getRectangle).then((rectA) => {
 | 
			
		||||
          cy.get('.alert').then(getRectangle).then((rectB) => {
 | 
			
		||||
            expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('shows RBF transactions properly (desktop)', () => {
 | 
			
		||||
        cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
 | 
			
		||||
          fixture: 'mainnet_tx_cached.json'
 | 
			
		||||
        }).as('cached_tx');
 | 
			
		||||
 | 
			
		||||
        cy.intercept('/api/v1/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5/rbf', {
 | 
			
		||||
          fixture: 'mainnet_rbf_new.json'
 | 
			
		||||
        }).as('rbf');
 | 
			
		||||
 | 
			
		||||
        cy.viewport('macbook-16');
 | 
			
		||||
        cy.mockMempoolSocket();
 | 
			
		||||
        cy.visit('/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5');
 | 
			
		||||
        cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
 | 
			
		||||
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
 | 
			
		||||
@ -557,17 +573,17 @@ describe('Mainnet', () => {
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        cy.get('.alert-mempool').should('be.visible');
 | 
			
		||||
        cy.get('.alert').should('be.visible');
 | 
			
		||||
 | 
			
		||||
        const alertLocator = '.alert-mempool';
 | 
			
		||||
        const alertLocator = '.alert';
 | 
			
		||||
        const tableLocator = '.container-xl > :nth-child(3)';
 | 
			
		||||
 | 
			
		||||
        cy.get(tableLocator).invoke('css', 'width').then((firstWidth) => {
 | 
			
		||||
          cy.get(alertLocator).invoke('css', 'width').should('equal', firstWidth);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        cy.get('.btn-success').then(getRectangle).then((rectA) => {
 | 
			
		||||
          cy.get('.alert-mempool').then(getRectangle).then((rectB) => {
 | 
			
		||||
        cy.get('.btn-warning').then(getRectangle).then((rectA) => {
 | 
			
		||||
          cy.get('.alert').then(getRectangle).then((rectB) => {
 | 
			
		||||
            expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -1,52 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
  "rbfTransaction": {
 | 
			
		||||
    "txid": "8913ec7ba0ede285dbd120e46f6d61a28f2903c10814a6f6c4f97d0edf3e1f46",
 | 
			
		||||
    "version": 2,
 | 
			
		||||
    "locktime": 632699,
 | 
			
		||||
    "vin": [
 | 
			
		||||
      {
 | 
			
		||||
        "txid": "02238126a63ea2669c5f378012180ef8b54402a949316f9b2f1352c51730a086",
 | 
			
		||||
        "vout": 0,
 | 
			
		||||
        "prevout": {
 | 
			
		||||
          "scriptpubkey": "a914f8e495456956c833e5e8c69b9a9dc041aa14c72f87",
 | 
			
		||||
          "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 f8e495456956c833e5e8c69b9a9dc041aa14c72f OP_EQUAL",
 | 
			
		||||
          "scriptpubkey_type": "p2sh",
 | 
			
		||||
          "scriptpubkey_address": "3QP3LMD8veT5GtWV83Nosif2Bhr73857VB",
 | 
			
		||||
          "value": 25000000
 | 
			
		||||
        },
 | 
			
		||||
        "scriptsig": "22002043288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
 | 
			
		||||
        "scriptsig_asm": "OP_PUSHBYTES_34 002043288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
 | 
			
		||||
        "witness": [
 | 
			
		||||
          "",
 | 
			
		||||
          "3044022009e2d3a8e645f65bc89c8492cd9c08e6fb02609fd402214884a754a1970145340220575bb325429def59f3a3f1e22d9740a3feecbe97438ff3bb5796b2c46b3c477f01",
 | 
			
		||||
          "3044022039c34372882da8fc1c1243bd72b5e7e5e6870301ef56bdebb87bc647fb50f9b5022071a704ee77d742f78b10e45be675d4c45a5f31e884139e75c975144fde70e41701",
 | 
			
		||||
          "522102346eb7133f11e0dc279bc592d5ac948a91676372a6144c9ae2085625d7fbf70421021b9508a458f9d59be4eb8cc87ad582c3b494106fb1d4ec22801569be0700eb7b52ae"
 | 
			
		||||
        ],
 | 
			
		||||
        "is_coinbase": false,
 | 
			
		||||
        "sequence": 4294967293,
 | 
			
		||||
        "inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_32 43288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
 | 
			
		||||
        "inner_witnessscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 02346eb7133f11e0dc279bc592d5ac948a91676372a6144c9ae2085625d7fbf704 OP_PUSHBYTES_33 021b9508a458f9d59be4eb8cc87ad582c3b494106fb1d4ec22801569be0700eb7b OP_PUSHNUM_2 OP_CHECKMULTISIG"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "vout": [
 | 
			
		||||
      {
 | 
			
		||||
        "scriptpubkey": "a914fd4e5e59dd5cf2dc48eaedf1a2a1650ca1ce9d7f87",
 | 
			
		||||
        "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 fd4e5e59dd5cf2dc48eaedf1a2a1650ca1ce9d7f OP_EQUAL",
 | 
			
		||||
        "scriptpubkey_type": "p2sh",
 | 
			
		||||
        "scriptpubkey_address": "3QnNmDhZS7toHA7bhhbTPBdtpLJoeecq5c",
 | 
			
		||||
        "value": 13986350
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "scriptpubkey": "76a914edc93d0446deec1c2d514f3a490f050096e74e0e88ac",
 | 
			
		||||
        "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 edc93d0446deec1c2d514f3a490f050096e74e0e OP_EQUALVERIFY OP_CHECKSIG",
 | 
			
		||||
        "scriptpubkey_type": "p2pkh",
 | 
			
		||||
        "scriptpubkey_address": "1NgJDkTUqJxxCAAZrrsC87kWag5kphrRtM",
 | 
			
		||||
        "value": 11000000
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "size": 372,
 | 
			
		||||
    "weight": 828,
 | 
			
		||||
    "fee": 1.5,
 | 
			
		||||
    "status": { "confirmed": false }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
"txReplaced": {
 | 
			
		||||
  "txid": "8913ec7ba0ede285dbd120e46f6d61a28f2903c10814a6f6c4f97d0edf3e1f46"
 | 
			
		||||
}}
 | 
			
		||||
							
								
								
									
										31
									
								
								frontend/cypress/fixtures/mainnet_rbf_new.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								frontend/cypress/fixtures/mainnet_rbf_new.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
{
 | 
			
		||||
  "replacements": {
 | 
			
		||||
    "tx": {
 | 
			
		||||
      "txid": "f22735aaa8eb84bcae3e7705f78609c6f5f0cd7dfc34ae03094e61f2dab0cc64",
 | 
			
		||||
      "fee": 13843,
 | 
			
		||||
      "vsize": 109.25,
 | 
			
		||||
      "value": 253003805,
 | 
			
		||||
      "rate": 36.04666732302845,
 | 
			
		||||
      "rbf": true
 | 
			
		||||
    },
 | 
			
		||||
    "time": 1683865345,
 | 
			
		||||
    "fullRbf": false,
 | 
			
		||||
    "replaces": [
 | 
			
		||||
      {
 | 
			
		||||
        "tx": {
 | 
			
		||||
          "txid": "21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f",
 | 
			
		||||
          "fee": 8794,
 | 
			
		||||
          "vsize": 109.25,
 | 
			
		||||
          "value": 253008854,
 | 
			
		||||
          "rate": 35.05247612484001,
 | 
			
		||||
          "rbf": true
 | 
			
		||||
        },
 | 
			
		||||
        "time": 1683864993,
 | 
			
		||||
        "interval": 352,
 | 
			
		||||
        "fullRbf": false,
 | 
			
		||||
        "replaces": []
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "replaces": null
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								frontend/cypress/fixtures/mainnet_tx_cached.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								frontend/cypress/fixtures/mainnet_tx_cached.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
			
		||||
{
 | 
			
		||||
  "vsize": 109,
 | 
			
		||||
  "feePerVsize": 80.49427917620137,
 | 
			
		||||
  "effectiveFeePerVsize": 35.05247612484001,
 | 
			
		||||
  "txid": "21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f",
 | 
			
		||||
  "version": 2,
 | 
			
		||||
  "locktime": 0,
 | 
			
		||||
  "vin": [
 | 
			
		||||
    {
 | 
			
		||||
      "txid": "1e3bd5c634781a6ba8bb3d3385b14739bf38cad5332d5fbc5c0ab775e54b9aef",
 | 
			
		||||
      "vout": 144,
 | 
			
		||||
      "prevout": {
 | 
			
		||||
        "scriptpubkey": "0014d98654186b90d95da7e31a30929f5b5b6a0af250",
 | 
			
		||||
        "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d98654186b90d95da7e31a30929f5b5b6a0af250",
 | 
			
		||||
        "scriptpubkey_type": "v0_p2wpkh",
 | 
			
		||||
        "scriptpubkey_address": "bc1qmxr9gxrtjrv4mflrrgcf986mtd4q4ujss432tk",
 | 
			
		||||
        "value": 253017648
 | 
			
		||||
      },
 | 
			
		||||
      "scriptsig": "",
 | 
			
		||||
      "scriptsig_asm": "",
 | 
			
		||||
      "witness": [
 | 
			
		||||
        "30440220448e8f58fcdea87c1969d58438b49da5b43712380bc4c68b02d22cf6b164907302207b2ed660f1a5b3b74f712961ffb3f3a7d1ac6e48b269ea6ff15df985042211f301",
 | 
			
		||||
        "02e39a1f3583e382cec1a1fab6a3f5950b6403c953fada58d809127a497f502ebe"
 | 
			
		||||
      ],
 | 
			
		||||
      "is_coinbase": false,
 | 
			
		||||
      "sequence": 4294967293
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "vout": [
 | 
			
		||||
    {
 | 
			
		||||
      "scriptpubkey": "0014edb5167da7e97c73d7931eb2130ac3e34e6845a9",
 | 
			
		||||
      "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 edb5167da7e97c73d7931eb2130ac3e34e6845a9",
 | 
			
		||||
      "scriptpubkey_type": "v0_p2wpkh",
 | 
			
		||||
      "scriptpubkey_address": "bc1qak63vld8a97884unr6epxzkrud8xs3dfdqswy2",
 | 
			
		||||
      "value": 253008854
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "size": 191,
 | 
			
		||||
  "weight": 437,
 | 
			
		||||
  "fee": 8794,
 | 
			
		||||
  "status": {
 | 
			
		||||
    "confirmed": false
 | 
			
		||||
  },
 | 
			
		||||
  "firstSeen": 1683864993,
 | 
			
		||||
  "uid": 298353,
 | 
			
		||||
  "position": {
 | 
			
		||||
    "block": 0,
 | 
			
		||||
    "vsize": 886207.5
 | 
			
		||||
  },
 | 
			
		||||
  "cpfpChecked": true,
 | 
			
		||||
  "ancestors": [
 | 
			
		||||
    {
 | 
			
		||||
      "txid": "1e3bd5c634781a6ba8bb3d3385b14739bf38cad5332d5fbc5c0ab775e54b9aef",
 | 
			
		||||
      "fee": 169220,
 | 
			
		||||
      "weight": 19877
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "descendants": [],
 | 
			
		||||
  "bestDescendant": null
 | 
			
		||||
}
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
    "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
 | 
			
		||||
    "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
 | 
			
		||||
    "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
 | 
			
		||||
    "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
 | 
			
		||||
    "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources'",
 | 
			
		||||
    "sync-assets-dev": "node sync-assets.js 'src/resources/'",
 | 
			
		||||
    "generate-config": "node generate-config.js",
 | 
			
		||||
    "build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
 | 
			
		||||
import { StartComponent } from './components/start/start.component';
 | 
			
		||||
import { TransactionComponent } from './components/transaction/transaction.component';
 | 
			
		||||
import { BlockComponent } from './components/block/block.component';
 | 
			
		||||
import { ClockMinedComponent as ClockMinedComponent } from './components/clock/clock-mined.component';
 | 
			
		||||
import { ClockMempoolComponent as ClockMempoolComponent } from './components/clock/clock-mempool.component';
 | 
			
		||||
import { ClockComponent } from './components/clock/clock.component';
 | 
			
		||||
import { AddressComponent } from './components/address/address.component';
 | 
			
		||||
import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
			
		||||
import { AboutComponent } from './components/about/about.component';
 | 
			
		||||
@ -358,12 +357,16 @@ let routes: Routes = [
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'clock-mined',
 | 
			
		||||
    component: ClockMinedComponent,
 | 
			
		||||
    path: 'clock',
 | 
			
		||||
    redirectTo: 'clock/mempool/0'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'clock-mempool',
 | 
			
		||||
    component: ClockMempoolComponent,
 | 
			
		||||
    path: 'clock/:mode',
 | 
			
		||||
    redirectTo: 'clock/:mode/0'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'clock/:mode/:index',
 | 
			
		||||
    component: ClockComponent,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'status',
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,6 @@
 | 
			
		||||
.pagination-container {
 | 
			
		||||
  float: none;
 | 
			
		||||
  margin-bottom: 200px;
 | 
			
		||||
  @media(min-width: 400px){
 | 
			
		||||
    float: right;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container-xl {
 | 
			
		||||
    padding-bottom: 110px;
 | 
			
		||||
}
 | 
			
		||||
@ -36,7 +36,7 @@
 | 
			
		||||
          <h5 class="card-title">US Dollar - BTC/USD</h5>
 | 
			
		||||
          <div class="chart-container">
 | 
			
		||||
            <ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
 | 
			
		||||
              <app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>      
 | 
			
		||||
              <app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@ -84,7 +84,7 @@
 | 
			
		||||
                    </ng-template>
 | 
			
		||||
                  </td>
 | 
			
		||||
                  <td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
 | 
			
		||||
                </tr> 
 | 
			
		||||
                </tr>
 | 
			
		||||
              </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
          </div>
 | 
			
		||||
@ -105,9 +105,6 @@
 | 
			
		||||
  </ng-container>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <app-language-selector></app-language-selector>
 | 
			
		||||
 | 
			
		||||
  <app-global-footer></app-global-footer>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingTmpl>
 | 
			
		||||
@ -124,4 +121,4 @@
 | 
			
		||||
 | 
			
		||||
<ng-template #loading>
 | 
			
		||||
  <div class="skeleton-loader shorter"></div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -15,11 +15,9 @@
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="grow"></span>
 | 
			
		||||
      <div class="container-buttons">
 | 
			
		||||
        <button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right">
 | 
			
		||||
          <ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container>
 | 
			
		||||
          <ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
 | 
			
		||||
          <ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
 | 
			
		||||
        </button>
 | 
			
		||||
        <div *ngIf="(latestBlock$ | async) as latestBlock">
 | 
			
		||||
          <app-confirmations [chainTip]="latestBlock?.height" [height]="bisqTx.blockHeight" [hideUnconfirmed]="true" buttonClass="float-right"></app-confirmations>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
@ -66,9 +64,10 @@
 | 
			
		||||
                    {{ bisqTx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
 | 
			
		||||
                  <td *only-vsize i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
 | 
			
		||||
                  <td *only-weight i18n="transaction.fee-per-wu|Transaction fee">Fee per weight unit</td>
 | 
			
		||||
                  <td *ngIf="!isLoadingTx; else loadingTxFee">
 | 
			
		||||
                    {{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol">sat/vB</span>
 | 
			
		||||
                    <app-fee-rate [fee]="tx.fee" [weight]="tx.weight"></app-fee-rate>
 | 
			
		||||
                     
 | 
			
		||||
                    <app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
 | 
			
		||||
                  </td>
 | 
			
		||||
 | 
			
		||||
@ -70,11 +70,7 @@
 | 
			
		||||
 | 
			
		||||
    <div class="btn-container">
 | 
			
		||||
      <span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
 | 
			
		||||
        <button type="button" class="btn btn-sm btn-success mt-2">
 | 
			
		||||
          <ng-container *ngTemplateOutlet="latestBlock.height - tx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.blockHeight + 1}"></ng-container>
 | 
			
		||||
          <ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
 | 
			
		||||
          <ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
 | 
			
		||||
        </button>
 | 
			
		||||
        <app-confirmations [chainTip]="latestBlock?.height" [height]="tx.blockHeight" [hideUnconfirmed]="true" buttonClass="mt-2"></app-confirmations>
 | 
			
		||||
         
 | 
			
		||||
      </span>
 | 
			
		||||
      <button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<div class="container-xl about-page">
 | 
			
		||||
 | 
			
		||||
  <div class="intro">
 | 
			
		||||
    <span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">™</span>
 | 
			
		||||
    <span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">®</span>
 | 
			
		||||
    <img class="logo" src="/resources/mempool-logo-bigger.png" />
 | 
			
		||||
    <div class="version">
 | 
			
		||||
      v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
 | 
			
		||||
@ -13,7 +13,7 @@
 | 
			
		||||
    <p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <video src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
 | 
			
		||||
  <video #promoVideo (click)="unmutePromoVideo()" (touchstart)="unmutePromoVideo()" src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
 | 
			
		||||
    <track label="English" kind="captions" srclang="en" src="/resources/promo-video/en.vtt" [attr.default]="showSubtitles('en') ? '' : null">
 | 
			
		||||
    <track label="日本語" kind="captions" srclang="ja" src="/resources/promo-video/ja.vtt" [attr.default]="showSubtitles('ja') ? '' : null">
 | 
			
		||||
    <track label="中文" kind="captions" srclang="zh" src="/resources/promo-video/zh.vtt" [attr.default]="showSubtitles('zh') ? '' : null">
 | 
			
		||||
@ -119,6 +119,18 @@
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Gemini</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
 | 
			
		||||
        <svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
          <g clip-path="url(#a)" fill-rule="evenodd" clip-rule="evenodd" fill="#e21924">
 | 
			
		||||
            <path d="M21.92 14.59a1.18 1.18 0 0 0-1.18-1.18h-1.82v2.36h1.82a1.18 1.18 0 0 0 1.18-1.18ZM21 17.07h-2v2.45h2a1.23 1.23 0 1 0 0-2.45Z"/>
 | 
			
		||||
            <path d="M36.43 0 35 5.59l-8 2.64-2.43-3.61-4.74 2.05-4.74-2.05-2.43 3.61-8-2.64L3.21 0 0 7.86l7.89 5.86-5.56 4 5.56 1.12 2.69-.49v3.17l3.59 4.38.68 3.19 5 2.87 5-2.87.68-3.19 3.59-4.38v-3.17l2.7.49 5.56-1.12-5.56-4 7.89-5.86zM24.69 18.45a2.5 2.5 0 0 1-2.5 2.5h-1.11v1.56h-1.26V21h-.9v1.56h-1.27V21H15.3v-1.42h.64a.9.9 0 0 0 .9-.9V14.3a.901.901 0 0 0-.9-.91h-.64V12h2.35v-1.5h1.27V12h.9v-1.5h1.26V12h.68A2.269 2.269 0 0 1 24 14.31a2.25 2.25 0 0 1-.92 1.82 2.52 2.52 0 0 1 1.58 2.32z"/>
 | 
			
		||||
          </g>
 | 
			
		||||
          <defs>
 | 
			
		||||
            <clipPath id="a"><path fill="#fff" d="M0 0h160v32H0z"/></clipPath>
 | 
			
		||||
          </defs>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Bull Bitcoin</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://exodus.com/" target="_blank" title="Exodus">
 | 
			
		||||
        <svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
          <circle cx="250" cy="250" r="250" fill="#1F2033"/>
 | 
			
		||||
@ -161,6 +173,21 @@
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Exodus</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://www.luminex.io" target="_blank" title="Luminex">
 | 
			
		||||
        <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="66.95" height="80" viewBox="0 0 300.43 385" style="padding-top: 10px;">
 | 
			
		||||
          <defs>
 | 
			
		||||
            <style>
 | 
			
		||||
              .lum-cls-1 {
 | 
			
		||||
                fill: #f2ea25;
 | 
			
		||||
              }
 | 
			
		||||
            </style>
 | 
			
		||||
          </defs>
 | 
			
		||||
          <path class="lum-cls-1" d="m309.02,90.04c0,49.65-38.73,90.04-95.34,90.04s-95.34-40.39-95.34-90.04S153.77,0,213.69,0c56.28,0,95.34,40.39,95.34,90.04Zm-63.56,0c0-20.52-14.23-37.07-31.78-37.07s-31.78,16.55-31.78,37.07,14.23,37.07,31.78,37.07,31.78-16.55,31.78-37.07Z"/>
 | 
			
		||||
          <path class="lum-cls-1" d="m311.87,372.67h-66.34l-31.84-47.76-31.84,47.76h-66.34l58.38-90.22-53.07-79.61h66.34l26.54,42.46,26.53-42.46h66.34l-53.07,79.61,58.38,90.22Z"/>
 | 
			
		||||
          <rect class="lum-cls-1" width="60.69" height="372.67"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <span>Luminex</span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -193,7 +220,7 @@
 | 
			
		||||
        <img class="image" src="/resources/profile/mynodebtc.png" />
 | 
			
		||||
        <span>myNode</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo">
 | 
			
		||||
      <a href="https://code.samourai.io/ronindojo/RoninDojo" target="_blank" title="RoninDojo">
 | 
			
		||||
        <img class="image" src="/resources/profile/ronindojo.png" />
 | 
			
		||||
        <span>RoninDojo</span>
 | 
			
		||||
      </a>
 | 
			
		||||
@ -205,9 +232,9 @@
 | 
			
		||||
        <img class="image" src="/resources/profile/nix-bitcoin.png" />
 | 
			
		||||
        <span>NixOS</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://github.com/Start9Labs/embassy-os" target="_blank" title="EmbassyOS">
 | 
			
		||||
      <a href="https://github.com/Start9Labs/start-os" target="_blank" title="StartOS">
 | 
			
		||||
        <img class="image" src="/resources/profile/start9.png" />
 | 
			
		||||
        <span>EmbassyOS</span>
 | 
			
		||||
        <span>StartOS</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
 | 
			
		||||
        <img class="image not-rounded" src="/resources/profile/btcpayserver.svg" />
 | 
			
		||||
@ -384,7 +411,7 @@
 | 
			
		||||
      Trademark Notice<br>
 | 
			
		||||
    </div>
 | 
			
		||||
    <p>
 | 
			
		||||
      The Mempool Open Source Project™, mempool.space™, the mempool logo™, the mempool.space logos™, the mempool square logo™, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
 | 
			
		||||
      The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
 | 
			
		||||
    </p>
 | 
			
		||||
    <p>
 | 
			
		||||
      While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
 | 
			
		||||
@ -393,27 +420,6 @@
 | 
			
		||||
 | 
			
		||||
  <div class="footer-links">
 | 
			
		||||
    <a href="/3rdpartylicenses.txt">Third-party Licenses</a>
 | 
			
		||||
    <div class="social-icons">
 | 
			
		||||
      <a target="_blank" href="https://github.com/mempool/mempool">
 | 
			
		||||
        <svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a target="_blank" href="https://twitter.com/mempool">
 | 
			
		||||
        <svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
 | 
			
		||||
        <svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a target="_blank" href="https://youtube.com/@mempool">
 | 
			
		||||
        <svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="youtube" class="svg-inline--fa fa-youtube fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/></svg>
 | 
			
		||||
      </a>
 | 
			
		||||
      <a target="_blank" href="https://bitcointv.com/c/mempool/videos" class="bitcointv">
 | 
			
		||||
        <svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 440 440"><path d="M225.57,2.08l-.69-.45a4.22,4.22,0,0,0-5.72,1.23L182.33,46.09a4,4,0,0,0,.88,5.81l9.38,6.38L173.48,97.49a4.22,4.22,0,0,0,2.45,4.19s3.55.7,4.53-1l41.92-40.56a3.62,3.62,0,0,0-1.51-5.1l-10.55-6.12L227.44,6.79A4.26,4.26,0,0,0,225.57,2.08Z" fill="currentColor"></path><path d="M118.52,401.83c-62.51,0-113.37-51-113.37-113.67V214.68C5.15,152,56,101,118.52,101H342.08a24.82,24.82,0,0,1,24.76,24.83V377a24.81,24.81,0,0,1-24.76,24.82Z"></path><path d="M342.08,105.18a20.65,20.65,0,0,1,20.61,20.66V377a20.66,20.66,0,0,1-20.61,20.66H118.52C58.3,397.67,9.31,348.55,9.31,288.16V214.68c0-60.38,49-109.5,109.21-109.5H342.08m0-8.34H118.52C53.62,96.84,1,149.6,1,214.68v73.48C1,353.24,53.62,406,118.52,406H342.08A29,29,0,0,0,371,377V125.84a29,29,0,0,0-28.92-29Z" fill="currentColor"></path><path fill="currentColor" d="M344.69,346.23A25.84,25.84,0,1,0,335,369.87l-10.22-10.2a11.69,11.69,0,1,1,4.77-5.12l10.31,10.28A25.84,25.84,0,0,0,344.69,346.23Z"></path><path fill="currentColor" d="M315.82,257.61a25.67,25.67,0,0,0-12.53,5.22L315,274.49a9.58,9.58,0,0,1,2.11-.73A9.72,9.72,0,1,1,309.4,283a9.4,9.4,0,0,1,.75-3.41L298.4,267.84a25.77,25.77,0,1,0,17.42-10.23Z"></path><path fill="currentColor" d="M313,214a7.76,7.76,0,1,1,1.41,10.91,7.62,7.62,0,0,1-2.19-2.69l-18.67-.14a25.94,25.94,0,1,0,.05-7l18.64.14A7.4,7.4,0,0,1,313,214Z"></path><path fill="currentColor" d="M341.2,144.08h-6.32c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.32c1.67,0,3.61-1.87,3.61-4.29S342.87,144.08,341.2,144.08Z"></path><path fill="currentColor" d="M301.75,144.08h-6.44c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.44c1.67,0,3.61-1.87,3.61-4.29S303.42,144.08,301.75,144.08Z"></path><path fill="currentColor" d="M321.77,144.08h-7c-1.67,0-3.62,1.87-3.62,4.29s1.95,4.29,3.62,4.29h7c1.67,0,3.62-1.87,3.62-4.29S323.44,144.08,321.77,144.08Z"></path><ellipse fill="currentColor" cx="295.97" cy="127.61" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,131.9a4.29,4.29,0,1,0-4.27-4.29A4.28,4.28,0,0,0,340.54,131.9Z"></path><path fill="currentColor" d="M318.26,131.9a4.29,4.29,0,1,0-4.27-4.29A4.29,4.29,0,0,0,318.26,131.9Z"></path><ellipse fill="currentColor" cx="295.97" cy="169.13" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,164.84a4.3,4.3,0,1,0,4.27,4.29A4.29,4.29,0,0,0,340.54,164.84Z"></path><path fill="currentColor" d="M318.26,164.84a4.3,4.3,0,1,0,4.28,4.29A4.29,4.29,0,0,0,318.26,164.84Z"></path><path d="M108.62,256.87c8.36-1,7.68-7.76,3.14-17-3.64-7.4-9.74-16.39-15.75-25.36-14.23-21.23-27.69-42.23-5.35-41.07,19.55,1,42.9,18.63,68.22,36.74,31.1,22.24,65.16,45.21,98.81,39.11a151.19,151.19,0,0,1,20-2.37V221a92,92,0,0,0-91.91-92.16H124.33A92,92,0,0,0,32.42,221v17.59c17.71,3.81,31,9.94,43.8,14.15C86.6,256.16,96.69,258.31,108.62,256.87Z"></path><path d="M273.37,310.79c-35-15.26-76.67-32.1-104-23.59-3.15,1-5,2.3-6,3.85-3.35,5.31,4.67,13.57,14.89,22.17,7.17,6,15.36,12.21,21.44,17.64,11.47,10.26,15.35,17.84-9.89,16.62-29.75-1.44-49.18-13.75-71.18-24l-.29-.14a165.84,165.84,0,0,0-22.93-8.91c-15.74-4.67-34.22-6.79-58.51-3.28A91.93,91.93,0,0,0,124.33,375h61.45A92,92,0,0,0,273.37,310.79Z"></path><path fill="currentColor" d="M257.69,249.31C224,255.41,190,232.44,158.88,210.2c-25.32-18.11-48.67-35.72-68.22-36.74C68.32,172.3,81.78,193.3,96,214.53c6,9,12.11,18,15.75,25.36,4.54,9.22,5.22,16-3.14,17-11.93,1.44-22-.71-32.4-4.13-12.8-4.21-26.09-10.34-43.8-14.15v44.26c0,1.26.14,2.48.19,3.72a91.8,91.8,0,0,0,2.9,19.62c.43,1.67.84,3.34,1.37,5,24.29-3.51,42.77-1.39,58.51,3.28a165.84,165.84,0,0,1,22.93,8.91c.39-.12.76-.26,1.14-.39l-.85.53c22,10.25,41.43,22.56,71.18,24,25.24,1.22,21.36-6.36,9.89-16.62-6.08-5.43-14.27-11.61-21.44-17.64-10.22-8.6-18.24-16.86-14.89-22.17,1-1.55,2.87-2.87,6-3.85,27.33-8.51,69,8.33,104,23.59.32-1,.56-2.05.84-3.07a92.33,92.33,0,0,0,3.48-24.87V246.94A151.19,151.19,0,0,0,257.69,249.31Z"></path><path fill="currentColor" d="M192,137a78,78,0,0,1,77.78,78v73.91a78,78,0,0,1-77.78,78H118.51a78,78,0,0,1-77.78-78V215a78,78,0,0,1,77.78-78H192m0-8.33H118.51A86.21,86.21,0,0,0,32.42,215v73.91a86.21,86.21,0,0,0,86.09,86.33H192a86.21,86.21,0,0,0,86.09-86.33V215A86.21,86.21,0,0,0,192,128.64Z"></path></svg>
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="footer-version" *ngIf="officialMempoolSpace">
 | 
			
		||||
    {{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
@ -17,6 +17,7 @@ import { DOCUMENT } from '@angular/common';
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class AboutComponent implements OnInit {
 | 
			
		||||
  @ViewChild('promoVideo') promoVideo: ElementRef;
 | 
			
		||||
  backendInfo$: Observable<IBackendInfo>;
 | 
			
		||||
  sponsors$: Observable<any>;
 | 
			
		||||
  translators$: Observable<ITranslators>;
 | 
			
		||||
@ -91,7 +92,11 @@ export class AboutComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  showSubtitles(language) {
 | 
			
		||||
  showSubtitles(language): boolean {
 | 
			
		||||
    return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  unmutePromoVideo(): void {
 | 
			
		||||
    this.promoVideo.nativeElement.muted = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -82,6 +82,10 @@
 | 
			
		||||
 | 
			
		||||
<br />
 | 
			
		||||
 | 
			
		||||
<router-outlet></router-outlet>
 | 
			
		||||
<main>
 | 
			
		||||
  <router-outlet></router-outlet>
 | 
			
		||||
</main>
 | 
			
		||||
 | 
			
		||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
 | 
			
		||||
 | 
			
		||||
<br>
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,12 @@ li.nav-item {
 | 
			
		||||
  padding-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 992px) {
 | 
			
		||||
  footer > .container-fluid {
 | 
			
		||||
    padding-bottom: 35px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 992px) {
 | 
			
		||||
  .navbar {
 | 
			
		||||
    padding: 0rem 2rem;
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ export class BisqMasterPageComponent implements OnInit {
 | 
			
		||||
  isMobile = window.innerWidth <= 767.98;
 | 
			
		||||
  urlLanguage: string;
 | 
			
		||||
  networkPaths: { [network: string]: string };
 | 
			
		||||
  footerVisible = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
@ -31,6 +32,11 @@ export class BisqMasterPageComponent implements OnInit {
 | 
			
		||||
    this.urlLanguage = this.languageService.getLanguageForUrl();
 | 
			
		||||
    this.navigationService.subnetPaths.subscribe((paths) => {
 | 
			
		||||
      this.networkPaths = paths;
 | 
			
		||||
      if (paths.mainnet.indexOf('docs') > -1) {
 | 
			
		||||
        this.footerVisible = false;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.footerVisible = true;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
 | 
			
		||||
import { EChartsOption } from 'echarts';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { Observable, Subscription, combineLatest } from 'rxjs';
 | 
			
		||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
@ -76,10 +76,11 @@ export class BlockFeeRatesGraphComponent implements OnInit {
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
 | 
			
		||||
      .pipe(
 | 
			
		||||
        startWith(this.radioGroupForm.controls.dateSpan.value),
 | 
			
		||||
        switchMap((timespan) => {
 | 
			
		||||
    this.statsObservable$ = combineLatest([
 | 
			
		||||
        this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)),
 | 
			
		||||
        this.stateService.rateUnits$
 | 
			
		||||
    ]).pipe(
 | 
			
		||||
        switchMap(([timespan, rateUnits]) => {
 | 
			
		||||
          this.storageService.setValue('miningWindowPreference', timespan);
 | 
			
		||||
          this.timespan = timespan;
 | 
			
		||||
          this.isLoading = true;
 | 
			
		||||
@ -135,8 +136,8 @@ export class BlockFeeRatesGraphComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
                this.prepareChartOptions({
 | 
			
		||||
                  legends: legends,
 | 
			
		||||
                  series: series,
 | 
			
		||||
                });
 | 
			
		||||
                  series: series
 | 
			
		||||
                }, rateUnits === 'wu');
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
              }),
 | 
			
		||||
              map((response) => {
 | 
			
		||||
@ -150,7 +151,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(data) {
 | 
			
		||||
  prepareChartOptions(data, weightMode) {
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      color: ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'],
 | 
			
		||||
      animation: false,
 | 
			
		||||
@ -181,7 +182,11 @@ export class BlockFeeRatesGraphComponent implements OnInit {
 | 
			
		||||
          let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}</b><br>`;
 | 
			
		||||
 | 
			
		||||
          for (const rate of data.reverse()) {
 | 
			
		||||
            tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1]} sats/vByte<br>`;
 | 
			
		||||
            if (weightMode) {
 | 
			
		||||
              tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1] / 4} sats/WU<br>`;
 | 
			
		||||
            } else {
 | 
			
		||||
              tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1]} sats/vByte<br>`;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (['24h', '3d'].includes(this.timespan)) {
 | 
			
		||||
@ -231,9 +236,12 @@ export class BlockFeeRatesGraphComponent implements OnInit {
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          color: 'rgb(110, 112, 121)',
 | 
			
		||||
          formatter: (val) => {
 | 
			
		||||
            if (weightMode) {
 | 
			
		||||
              val /= 4;
 | 
			
		||||
            }
 | 
			
		||||
            const selectedPowerOfTen: any = selectPowerOfTen(val);
 | 
			
		||||
            const newVal = Math.round(val / selectedPowerOfTen.divider);
 | 
			
		||||
            return `${newVal}${selectedPowerOfTen.unit} s/vB`;
 | 
			
		||||
            return `${newVal}${selectedPowerOfTen.unit} s/${weightMode ? 'WU': 'vB'}`;
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline">
 | 
			
		||||
      <span i18n="mining.block-prediction-accuracy">Block Prediction Accuracy</span>
 | 
			
		||||
      <span i18n="mining.blocks-health">Block Health</span>
 | 
			
		||||
      <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
        <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
      </button>
 | 
			
		||||
@ -12,34 +12,34 @@
 | 
			
		||||
    <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
 | 
			
		||||
          <input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 24h
 | 
			
		||||
          <input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 24h
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
 | 
			
		||||
          <input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3D
 | 
			
		||||
          <input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3D
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
 | 
			
		||||
          <input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1W
 | 
			
		||||
          <input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1W
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
 | 
			
		||||
          <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1M
 | 
			
		||||
          <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
 | 
			
		||||
          <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3M
 | 
			
		||||
          <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
 | 
			
		||||
          <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 6M
 | 
			
		||||
          <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 6M
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
 | 
			
		||||
          <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1Y
 | 
			
		||||
          <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
 | 
			
		||||
          <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 2Y
 | 
			
		||||
          <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 2Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
 | 
			
		||||
          <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3Y
 | 
			
		||||
          <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3Y
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount > 157680" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
 | 
			
		||||
          <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> ALL
 | 
			
		||||
          <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> ALL
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
@ -13,9 +13,9 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block-prediction-graph',
 | 
			
		||||
  templateUrl: './block-prediction-graph.component.html',
 | 
			
		||||
  styleUrls: ['./block-prediction-graph.component.scss'],
 | 
			
		||||
  selector: 'app-block-health-graph',
 | 
			
		||||
  templateUrl: './block-health-graph.component.html',
 | 
			
		||||
  styleUrls: ['./block-health-graph.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
@ -26,7 +26,7 @@ import { StateService } from '../../services/state.service';
 | 
			
		||||
  `],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class BlockPredictionGraphComponent implements OnInit {
 | 
			
		||||
export class BlockHealthGraphComponent implements OnInit {
 | 
			
		||||
  @Input() right: number | string = 45;
 | 
			
		||||
  @Input() left: number | string = 75;
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ export class BlockPredictionGraphComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Prediction Accuracy`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`);
 | 
			
		||||
    this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
    this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
 | 
			
		||||
@ -80,7 +80,7 @@ export class BlockPredictionGraphComponent implements OnInit {
 | 
			
		||||
          this.storageService.setValue('miningWindowPreference', timespan);
 | 
			
		||||
          this.timespan = timespan;
 | 
			
		||||
          this.isLoading = true;
 | 
			
		||||
          return this.apiService.getHistoricalBlockPrediction$(timespan)
 | 
			
		||||
          return this.apiService.getHistoricalBlocksHealth$(timespan)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap((response) => {
 | 
			
		||||
                this.prepareChartOptions(response.body);
 | 
			
		||||
@ -163,7 +163,7 @@ export class BlockPredictionGraphComponent implements OnInit {
 | 
			
		||||
          hideOverlap: true,
 | 
			
		||||
          padding: [0, 5],
 | 
			
		||||
        },
 | 
			
		||||
        data: data.map(prediction => prediction[0])
 | 
			
		||||
        data: data.map(health => health[0])
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: data.length === 0 ? undefined : [
 | 
			
		||||
        {
 | 
			
		||||
@ -186,12 +186,12 @@ export class BlockPredictionGraphComponent implements OnInit {
 | 
			
		||||
      series: data.length === 0 ? undefined : [
 | 
			
		||||
        {
 | 
			
		||||
          zlevel: 0,
 | 
			
		||||
          name: $localize`Match rate`,
 | 
			
		||||
          data: data.map(prediction => ({
 | 
			
		||||
            value: prediction[2],
 | 
			
		||||
            block: prediction[1],
 | 
			
		||||
          name: $localize`Health`,
 | 
			
		||||
          data: data.map(health => ({
 | 
			
		||||
            value: health[2],
 | 
			
		||||
            block: health[1],
 | 
			
		||||
            itemStyle: {
 | 
			
		||||
              color: this.getPredictionColor(prediction[2])
 | 
			
		||||
              color: this.getHealthColor(health[2])
 | 
			
		||||
            }
 | 
			
		||||
          })),
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
@ -257,7 +257,7 @@ export class BlockPredictionGraphComponent implements OnInit {
 | 
			
		||||
    return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getPredictionColor(matchRate) {
 | 
			
		||||
  getHealthColor(matchRate) {
 | 
			
		||||
    return this.colorGradient(
 | 
			
		||||
      Math.pow((100 - matchRate) / 100, 0.5),
 | 
			
		||||
      {red: 67, green: 171, blue: 71},
 | 
			
		||||
@ -294,7 +294,7 @@ export class BlockPredictionGraphComponent implements OnInit {
 | 
			
		||||
    download(this.chartInstance.getDataURL({
 | 
			
		||||
      pixelRatio: 2,
 | 
			
		||||
      excludeComponents: ['dataZoom'],
 | 
			
		||||
    }), `block-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
 | 
			
		||||
    }), `block-health-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    this.chartOptions.grid.bottom = prevBottom;
 | 
			
		||||
    this.chartOptions.backgroundColor = 'none';
 | 
			
		||||
@ -37,7 +37,7 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
  value: number;
 | 
			
		||||
  feerate: number;
 | 
			
		||||
  rate?: number;
 | 
			
		||||
  status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
 | 
			
		||||
  status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
 | 
			
		||||
  context?: 'projected' | 'actual';
 | 
			
		||||
  scene?: BlockScene;
 | 
			
		||||
 | 
			
		||||
@ -171,6 +171,8 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
      case 'censored':
 | 
			
		||||
        return auditColors.censored;
 | 
			
		||||
      case 'missing':
 | 
			
		||||
      case 'sigop':
 | 
			
		||||
      case 'fullrbf':
 | 
			
		||||
        return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
 | 
			
		||||
      case 'fresh':
 | 
			
		||||
        return auditColors.missing;
 | 
			
		||||
 | 
			
		||||
@ -25,28 +25,34 @@
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td class="td-width" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          {{ feeRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
          <app-fee-rate [fee]="feeRate"></app-fee-rate>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr *ngIf="effectiveRate && effectiveRate !== feeRate">
 | 
			
		||||
        <td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          {{ effectiveRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
          <app-fee-rate [fee]="effectiveRate"></app-fee-rate>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
      <tr *only-vsize>
 | 
			
		||||
        <td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
 | 
			
		||||
        <td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr *only-weight>
 | 
			
		||||
        <td class="td-width" i18n="transaction.weight|Transaction Weight">Weight</td>
 | 
			
		||||
        <td [innerHTML]="'‎' + ((vsize * 4) | wuBytes: 2)"></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr *ngIf="auditEnabled && tx && tx.status && tx.status.length">
 | 
			
		||||
        <td class="td-width" i18n="transaction.audit-status">Audit status</td>
 | 
			
		||||
        <ng-container [ngSwitch]="tx?.status">
 | 
			
		||||
          <td *ngSwitchCase="'found'"><span class="badge badge-success" i18n="transaction.audit.match">Match</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'censored'"><span class="badge badge-danger" i18n="transaction.audit.removed">Removed</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'sigop'"><span class="badge badge-warning" i18n="transaction.audit.sigop">High sigop count</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
 | 
			
		||||
          <td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr *ngIf="block?.extras?.medianFee != undefined">
 | 
			
		||||
            <td class="td-width" i18n="block.median-fee">Median fee</td>
 | 
			
		||||
            <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
 | 
			
		||||
            <td>~<app-fee-rate [fee]="block?.extras?.medianFee" rounding="1.0-0"></app-fee-rate></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <ng-template [ngIf]="fees !== undefined">
 | 
			
		||||
            <tr>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,10 @@
 | 
			
		||||
<div class="container-xl" (window:resize)="onResize($event)">
 | 
			
		||||
 | 
			
		||||
  <div class="title-block" [class.time-ltr]="timeLtr" id="block">
 | 
			
		||||
    <div *ngIf="block?.stale" class="alert alert-mempool" role="alert">
 | 
			
		||||
      <span i18n="block.reorged|Block reorg" class="alert-text">This block does not belong to the main chain, it has been replaced by:</span>
 | 
			
		||||
      <app-truncate [text]="block.canonical" [lastChars]="12" [link]="['/block/' | relativeUrl, block.canonical]" [maxWidth]="480"></app-truncate>
 | 
			
		||||
    </div>
 | 
			
		||||
    <h1>
 | 
			
		||||
      <ng-container *ngIf="blockHeight == null || blockHeight > 0; else genesis" i18n="shared.block-title">Block</ng-container>
 | 
			
		||||
      <ng-template #genesis i18n="@@2303359202781425764">Genesis</ng-template>
 | 
			
		||||
@ -23,6 +27,8 @@
 | 
			
		||||
 | 
			
		||||
    <div class="grow"></div>
 | 
			
		||||
 | 
			
		||||
    <button *ngIf="block?.stale" type="button" class="btn btn-sm btn-danger container-button" i18n="block.stale|Stale block state">Stale</button>
 | 
			
		||||
 | 
			
		||||
    <button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -41,7 +47,7 @@
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="block.timestamp">Timestamp</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <app-timestamp [unixTime]="block.timestamp"></app-timestamp>
 | 
			
		||||
                  <app-timestamp [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
@ -63,7 +69,7 @@
 | 
			
		||||
                    *ngIf="blockAudit?.matchRate != null; else nullHealth"
 | 
			
		||||
                  >{{ blockAudit?.matchRate }}%</span>
 | 
			
		||||
                  <ng-template #nullHealth>
 | 
			
		||||
                    <ng-container *ngIf="!isLoadingAudit; else loadingHealth">
 | 
			
		||||
                    <ng-container *ngIf="!isLoadingOverview; else loadingHealth">
 | 
			
		||||
                      <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
@ -121,11 +127,11 @@
 | 
			
		||||
    <ng-container *ngIf="!isLoadingBlock; else loadingRest">
 | 
			
		||||
      <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
 | 
			
		||||
        <td i18n="mempool-block.fee-span">Fee span</td>
 | 
			
		||||
        <td><span>{{ block.extras.feeRange[1] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
 | 
			
		||||
        <td><app-fee-rate [fee]="block?.extras?.minFee" [showUnit]="false" rounding="1.0-0"></app-fee-rate> - <app-fee-rate [fee]="block?.extras?.maxFee" rounding="1.0-0"></app-fee-rate></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr *ngIf="block?.extras?.medianFee != undefined">
 | 
			
		||||
        <td class="td-width" i18n="block.median-fee">Median fee</td>
 | 
			
		||||
        <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
        <td>~<app-fee-rate [fee]="block?.extras?.medianFee" rounding="1.0-0"></app-fee-rate>
 | 
			
		||||
          <span class="fiat">
 | 
			
		||||
            <app-fiat [blockConversion]="blockConversion" [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2"
 | 
			
		||||
              i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes"
 | 
			
		||||
@ -226,6 +232,9 @@
 | 
			
		||||
            (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
 | 
			
		||||
          <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ng-container *ngIf="network !== 'liquid'">
 | 
			
		||||
          <ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-sm" *ngIf="!isMobile">
 | 
			
		||||
        <h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
 | 
			
		||||
@ -235,6 +244,9 @@
 | 
			
		||||
            (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
 | 
			
		||||
          <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ng-container *ngIf="network !== 'liquid'">
 | 
			
		||||
          <ng-container *ngTemplateOutlet="actualDetails"></ng-container>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
@ -385,5 +397,60 @@
 | 
			
		||||
  </a>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #expectedDetails>
 | 
			
		||||
  <table *ngIf="block && blockAudit && blockAudit.expectedFees != null" class="table table-borderless table-striped audit-details-table">
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="block.total-fees|Total fees in a block">Total fees</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          <app-amount [satoshis]="blockAudit.expectedFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="block.weight">Weight</td>
 | 
			
		||||
        <td [innerHTML]="'‎' + (blockAudit.expectedWeight | wuBytes: 2)"></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="mempool-block.transactions">Transactions</td>
 | 
			
		||||
        <td>{{ blockAudit.template?.length || 0 }}</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #actualDetails>
 | 
			
		||||
  <table *ngIf="block && blockAudit && blockAudit.expectedFees != null" class="table table-borderless table-striped audit-details-table">
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="block.total-fees|Total fees in a block">Total fees</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
 | 
			
		||||
          <span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
 | 
			
		||||
            {{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}%
 | 
			
		||||
          </span>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="block.weight">Weight</td>
 | 
			
		||||
        <td [innerHTML]>
 | 
			
		||||
          <span [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></span>
 | 
			
		||||
          <span *ngIf="blockAudit.weightDelta" class="difference" [class.positive]="blockAudit.weightDelta <= 0" [class.negative]="blockAudit.weightDelta > 0">
 | 
			
		||||
            {{ blockAudit.weightDelta < 0 ? '+' : '' }}{{ (-blockAudit.weightDelta * 100) | amountShortener: 2 }}%
 | 
			
		||||
          </span>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="mempool-block.transactions">Transactions</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          {{ block.tx_count }}
 | 
			
		||||
          <span *ngIf="blockAudit.txDelta" class="difference" [class.positive]="blockAudit.txDelta <= 0" [class.negative]="blockAudit.txDelta > 0">
 | 
			
		||||
            {{ blockAudit.txDelta < 0 ? '+' : '' }}{{ (-blockAudit.txDelta * 100) | amountShortener: 2 }}%
 | 
			
		||||
          </span>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<br>
 | 
			
		||||
<br>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,26 @@
 | 
			
		||||
.title-block {
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  @media (min-width: 650px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  h1 {
 | 
			
		||||
    margin: 0rem;
 | 
			
		||||
    margin-right: 15px;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .alert-mempool {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .container-button {
 | 
			
		||||
    align-self: center;
 | 
			
		||||
    margin-right: 1em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.qr-wrapper {
 | 
			
		||||
  background-color: #FFF;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
@ -38,6 +61,17 @@
 | 
			
		||||
      color: rgba(255, 255, 255, 0.4);
 | 
			
		||||
      margin-left: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .difference {
 | 
			
		||||
      margin-left: 0.5em;
 | 
			
		||||
  
 | 
			
		||||
      &.positive {
 | 
			
		||||
        color: rgb(66, 183, 71);
 | 
			
		||||
      }
 | 
			
		||||
      &.negative {
 | 
			
		||||
        color: rgb(183, 66, 66);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -252,3 +286,10 @@ h1 {
 | 
			
		||||
  top: 11px;
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.audit-details-table {
 | 
			
		||||
  margin-top: 1.25rem;
 | 
			
		||||
  @media (max-width: 767.98px) {
 | 
			
		||||
    margin-top: 0.75rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/
 | 
			
		||||
import { Location } from '@angular/common';
 | 
			
		||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
 | 
			
		||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith } from 'rxjs/operators';
 | 
			
		||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs';
 | 
			
		||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
@ -44,7 +44,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  strippedTransactions: TransactionStripped[];
 | 
			
		||||
  overviewTransitionDirection: string;
 | 
			
		||||
  isLoadingOverview = true;
 | 
			
		||||
  isLoadingAudit = true;
 | 
			
		||||
  error: any;
 | 
			
		||||
  blockSubsidy: number;
 | 
			
		||||
  fees: number;
 | 
			
		||||
@ -138,6 +137,8 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        if (block.id === this.blockHash) {
 | 
			
		||||
          this.block = block;
 | 
			
		||||
          block.extras.minFee = this.getMinBlockFee(block);
 | 
			
		||||
          block.extras.maxFee = this.getMaxBlockFee(block);
 | 
			
		||||
          if (block?.extras?.reward != undefined) {
 | 
			
		||||
            this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
 | 
			
		||||
          }
 | 
			
		||||
@ -234,6 +235,8 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
        this.updateAuditAvailableFromBlockHeight(block.height);
 | 
			
		||||
        this.block = block;
 | 
			
		||||
        block.extras.minFee = this.getMinBlockFee(block);
 | 
			
		||||
        block.extras.maxFee = this.getMaxBlockFee(block);
 | 
			
		||||
        this.blockHeight = block.height;
 | 
			
		||||
        this.lastBlockHeight = this.blockHeight;
 | 
			
		||||
        this.nextBlockHeight = block.height + 1;
 | 
			
		||||
@ -277,134 +280,125 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      this.isLoadingOverview = false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!this.auditSupported) {
 | 
			
		||||
      this.overviewSubscription = block$.pipe(
 | 
			
		||||
        startWith(null),
 | 
			
		||||
        pairwise(),
 | 
			
		||||
        switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
 | 
			
		||||
          .pipe(
 | 
			
		||||
            catchError((err) => {
 | 
			
		||||
              this.overviewError = err;
 | 
			
		||||
              return of([]);
 | 
			
		||||
            }),
 | 
			
		||||
            switchMap((transactions) => {
 | 
			
		||||
              if (prevBlock) {
 | 
			
		||||
                return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
 | 
			
		||||
              } else {
 | 
			
		||||
                return of({ transactions, direction: 'down' });
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
 | 
			
		||||
        this.strippedTransactions = transactions;
 | 
			
		||||
        this.isLoadingOverview = false;
 | 
			
		||||
        this.setupBlockGraphs();
 | 
			
		||||
      },
 | 
			
		||||
      (error) => {
 | 
			
		||||
        this.error = error;
 | 
			
		||||
        this.isLoadingOverview = false;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.auditSupported) {
 | 
			
		||||
      this.auditSubscription = block$.pipe(
 | 
			
		||||
        startWith(null),
 | 
			
		||||
        pairwise(),
 | 
			
		||||
        switchMap(([prevBlock, block]) => {
 | 
			
		||||
          this.isLoadingAudit = true;
 | 
			
		||||
          this.blockAudit = null;
 | 
			
		||||
          return this.apiService.getBlockAudit$(block.id)
 | 
			
		||||
    this.overviewSubscription = block$.pipe(
 | 
			
		||||
      switchMap((block) => {
 | 
			
		||||
        return forkJoin([
 | 
			
		||||
          this.apiService.getStrippedBlockTransactions$(block.id)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              catchError((err) => {
 | 
			
		||||
                this.overviewError = err;
 | 
			
		||||
                this.isLoadingAudit = false;
 | 
			
		||||
                return of([]);
 | 
			
		||||
                return of(null);
 | 
			
		||||
              })
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        ),
 | 
			
		||||
        filter((response) => response != null),
 | 
			
		||||
        map((response) => {
 | 
			
		||||
          const blockAudit = response.body;
 | 
			
		||||
          const inTemplate = {};
 | 
			
		||||
          const inBlock = {};
 | 
			
		||||
          const isAdded = {};
 | 
			
		||||
          const isCensored = {};
 | 
			
		||||
          const isMissing = {};
 | 
			
		||||
          const isSelected = {};
 | 
			
		||||
          const isFresh = {};
 | 
			
		||||
          this.numMissing = 0;
 | 
			
		||||
          this.numUnexpected = 0;
 | 
			
		||||
            ),
 | 
			
		||||
          !this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              catchError((err) => {
 | 
			
		||||
                this.overviewError = err;
 | 
			
		||||
                return of(null);
 | 
			
		||||
              })
 | 
			
		||||
            )
 | 
			
		||||
        ]);
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
    .subscribe(([transactions, blockAudit]) => {      
 | 
			
		||||
      if (transactions) {
 | 
			
		||||
        this.strippedTransactions = transactions;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.strippedTransactions = [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
          if (blockAudit?.template) {
 | 
			
		||||
            for (const tx of blockAudit.template) {
 | 
			
		||||
              inTemplate[tx.txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            for (const tx of blockAudit.transactions) {
 | 
			
		||||
              inBlock[tx.txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            for (const txid of blockAudit.addedTxs) {
 | 
			
		||||
              isAdded[txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            for (const txid of blockAudit.missingTxs) {
 | 
			
		||||
              isCensored[txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            for (const txid of blockAudit.freshTxs || []) {
 | 
			
		||||
              isFresh[txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            // set transaction statuses
 | 
			
		||||
            for (const tx of blockAudit.template) {
 | 
			
		||||
              tx.context = 'projected';
 | 
			
		||||
              if (isCensored[tx.txid]) {
 | 
			
		||||
                tx.status = 'censored';
 | 
			
		||||
              } else if (inBlock[tx.txid]) {
 | 
			
		||||
                tx.status = 'found';
 | 
			
		||||
              } else {
 | 
			
		||||
                tx.status = isFresh[tx.txid] ? 'fresh' : 'missing';
 | 
			
		||||
                isMissing[tx.txid] = true;
 | 
			
		||||
                this.numMissing++;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            for (const [index, tx] of blockAudit.transactions.entries()) {
 | 
			
		||||
              tx.context = 'actual';
 | 
			
		||||
              if (index === 0) {
 | 
			
		||||
                tx.status = null;
 | 
			
		||||
              } else if (isAdded[tx.txid]) {
 | 
			
		||||
                tx.status = 'added';
 | 
			
		||||
              } else if (inTemplate[tx.txid]) {
 | 
			
		||||
                tx.status = 'found';
 | 
			
		||||
              } else {
 | 
			
		||||
                tx.status = 'selected';
 | 
			
		||||
                isSelected[tx.txid] = true;
 | 
			
		||||
                this.numUnexpected++;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            for (const tx of blockAudit.transactions) {
 | 
			
		||||
              inBlock[tx.txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
            this.setAuditAvailable(true);
 | 
			
		||||
          } else {
 | 
			
		||||
            this.setAuditAvailable(false);
 | 
			
		||||
      this.blockAudit = null;
 | 
			
		||||
      if (transactions && blockAudit) {
 | 
			
		||||
        const inTemplate = {};
 | 
			
		||||
        const inBlock = {};
 | 
			
		||||
        const isAdded = {};
 | 
			
		||||
        const isCensored = {};
 | 
			
		||||
        const isMissing = {};
 | 
			
		||||
        const isSelected = {};
 | 
			
		||||
        const isFresh = {};
 | 
			
		||||
        const isSigop = {};
 | 
			
		||||
        const isFullRbf = {};
 | 
			
		||||
        this.numMissing = 0;
 | 
			
		||||
        this.numUnexpected = 0;
 | 
			
		||||
 | 
			
		||||
        if (blockAudit?.template) {
 | 
			
		||||
          for (const tx of blockAudit.template) {
 | 
			
		||||
            inTemplate[tx.txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          return blockAudit;
 | 
			
		||||
        }),
 | 
			
		||||
        catchError((err) => {
 | 
			
		||||
          console.log(err);
 | 
			
		||||
          this.error = err;
 | 
			
		||||
          this.isLoadingOverview = false;
 | 
			
		||||
          this.isLoadingAudit = false;
 | 
			
		||||
          for (const tx of transactions) {
 | 
			
		||||
            inBlock[tx.txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          for (const txid of blockAudit.addedTxs) {
 | 
			
		||||
            isAdded[txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          for (const txid of blockAudit.missingTxs) {
 | 
			
		||||
            isCensored[txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          for (const txid of blockAudit.freshTxs || []) {
 | 
			
		||||
            isFresh[txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          for (const txid of blockAudit.sigopTxs || []) {
 | 
			
		||||
            isSigop[txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          for (const txid of blockAudit.fullrbfTxs || []) {
 | 
			
		||||
            isFullRbf[txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          // set transaction statuses
 | 
			
		||||
          for (const tx of blockAudit.template) {
 | 
			
		||||
            tx.context = 'projected';
 | 
			
		||||
            if (isCensored[tx.txid]) {
 | 
			
		||||
              tx.status = 'censored';
 | 
			
		||||
            } else if (inBlock[tx.txid]) {
 | 
			
		||||
              tx.status = 'found';
 | 
			
		||||
            } else {
 | 
			
		||||
              if (isFresh[tx.txid]) {
 | 
			
		||||
                tx.status = 'fresh';
 | 
			
		||||
              } else if (isSigop[tx.txid]) {
 | 
			
		||||
                tx.status = 'sigop';
 | 
			
		||||
              } else if (isFullRbf[tx.txid]) {
 | 
			
		||||
                tx.status = 'fullrbf';
 | 
			
		||||
              } else {
 | 
			
		||||
                tx.status = 'missing';
 | 
			
		||||
              }
 | 
			
		||||
              isMissing[tx.txid] = true;
 | 
			
		||||
              this.numMissing++;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          for (const [index, tx] of transactions.entries()) {
 | 
			
		||||
            tx.context = 'actual';
 | 
			
		||||
            if (index === 0) {
 | 
			
		||||
              tx.status = null;
 | 
			
		||||
            } else if (isAdded[tx.txid]) {
 | 
			
		||||
              tx.status = 'added';
 | 
			
		||||
            } else if (inTemplate[tx.txid]) {
 | 
			
		||||
              tx.status = 'found';
 | 
			
		||||
            } else if (isFullRbf[tx.txid]) {
 | 
			
		||||
              tx.status = 'fullrbf';
 | 
			
		||||
            } else {
 | 
			
		||||
              tx.status = 'selected';
 | 
			
		||||
              isSelected[tx.txid] = true;
 | 
			
		||||
              this.numUnexpected++;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          for (const tx of transactions) {
 | 
			
		||||
            inBlock[tx.txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
 | 
			
		||||
          blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
 | 
			
		||||
          blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
 | 
			
		||||
          this.blockAudit = blockAudit;
 | 
			
		||||
          this.setAuditAvailable(true);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.setAuditAvailable(false);
 | 
			
		||||
          return of(null);
 | 
			
		||||
        }),
 | 
			
		||||
      ).subscribe((blockAudit) => {
 | 
			
		||||
        this.blockAudit = blockAudit;
 | 
			
		||||
        this.setupBlockGraphs();
 | 
			
		||||
        this.isLoadingOverview = false;
 | 
			
		||||
        this.isLoadingAudit = false;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this.setAuditAvailable(false);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.isLoadingOverview = false;
 | 
			
		||||
      this.setupBlockGraphs();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.networkChangedSubscription = this.stateService.networkChanged$
 | 
			
		||||
      .subscribe((network) => this.network = network);
 | 
			
		||||
@ -639,24 +633,50 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateAuditAvailableFromBlockHeight(blockHeight: number): void {
 | 
			
		||||
    if (!this.auditSupported) {
 | 
			
		||||
    if (!this.isAuditAvailableFromBlockHeight(blockHeight)) {
 | 
			
		||||
      this.setAuditAvailable(false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
 | 
			
		||||
    if (!this.auditSupported) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    switch (this.stateService.network) {
 | 
			
		||||
      case 'testnet':
 | 
			
		||||
        if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
 | 
			
		||||
          this.setAuditAvailable(false);
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 'signet':
 | 
			
		||||
        if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
 | 
			
		||||
          this.setAuditAvailable(false);
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
 | 
			
		||||
          this.setAuditAvailable(false);
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMinBlockFee(block: BlockExtended): number {
 | 
			
		||||
    if (block?.extras?.feeRange) {
 | 
			
		||||
      // heuristic to check if feeRange is adjusted for effective rates
 | 
			
		||||
      if (block.extras.medianFee === block.extras.feeRange[3]) {
 | 
			
		||||
        return block.extras.feeRange[1];
 | 
			
		||||
      } else {
 | 
			
		||||
        return block.extras.feeRange[0];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMaxBlockFee(block: BlockExtended): number {
 | 
			
		||||
    if (block?.extras?.feeRange) {
 | 
			
		||||
      return block.extras.feeRange[block.extras.feeRange.length - 1];
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -13,17 +13,16 @@
 | 
			
		||||
        [class.offscreen]="!static && count && i >= count"
 | 
			
		||||
        id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
 | 
			
		||||
        [class.blink-bg]="isSpecial(block.height)">
 | 
			
		||||
        <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
 | 
			
		||||
        <a draggable="false" [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }"
 | 
			
		||||
          class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
 | 
			
		||||
        <div *ngIf="!minimal" [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
 | 
			
		||||
          <a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
 | 
			
		||||
          <a [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }">{{ block.height
 | 
			
		||||
            }}</a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="block-body">
 | 
			
		||||
          <ng-container *ngIf="!minimal">
 | 
			
		||||
            <div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
 | 
			
		||||
              ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
 | 
			
		||||
                i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
 | 
			
		||||
              ~<app-fee-rate [fee]="block?.extras?.medianFee" unitClass="" rounding="1.0-0"></app-fee-rate>
 | 
			
		||||
            </div>
 | 
			
		||||
            <ng-template #emptyfees>
 | 
			
		||||
              <div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
 | 
			
		||||
@ -31,10 +30,10 @@
 | 
			
		||||
              </div>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
 | 
			
		||||
              *ngIf="block?.extras?.feeRange; else emptyfeespan">
 | 
			
		||||
              {{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{
 | 
			
		||||
              block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
 | 
			
		||||
                i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
 | 
			
		||||
              *ngIf="block?.extras?.minFee != null && block?.extras?.maxFee != null; else emptyfeespan">
 | 
			
		||||
              <app-fee-rate [fee]="block?.extras?.minFee" [showUnit]="false" rounding="1.0-0" unitClass=""></app-fee-rate>
 | 
			
		||||
              -
 | 
			
		||||
              <app-fee-rate [fee]="block?.extras?.maxFee" rounding="1.0-0" unitClass=""></app-fee-rate>
 | 
			
		||||
            </div>
 | 
			
		||||
            <ng-template #emptyfeespan>
 | 
			
		||||
              <div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
 | 
			
		||||
@ -54,7 +53,7 @@
 | 
			
		||||
              <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
 | 
			
		||||
              <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
 | 
			
		||||
              <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">
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user