mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-28 06:51:10 -07:00
Compare commits
55 Commits
v0.2.5
...
v0.3.0-beta.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a4cb0601f | |||
| 861e29277c | |||
| c76b149ef9 | |||
| 4c4c6fc840 | |||
| 0c14dfe924 | |||
| 17e531b4ee | |||
| f022f62cce | |||
| e91f1386b1 | |||
| 02f543af38 | |||
| 20c96fb551 | |||
| acd3d6f425 | |||
| 2b15a24b6d | |||
| 7fac0bc613 | |||
| 62f51761ee | |||
| 5340cc288e | |||
| befe3c8fb7 | |||
| 41ec24c81e | |||
| 42b497ff65 | |||
| 01d908a560 | |||
| 42debcce80 | |||
| 8bc993eceb | |||
| 366ac33e23 | |||
| b5a7023bd3 | |||
| 883b38c77c | |||
| 59c767a9e2 | |||
| 9b5bb848f7 | |||
| 5bf06530ce | |||
| 768e6870cb | |||
| 79829ddd53 | |||
| 78082801c6 | |||
| 50771ddccc | |||
| 3a8a9ddecc | |||
| 6cd45c1f1f | |||
| 1a2db43cf5 | |||
| 4840e564f4 | |||
| 744dce932c | |||
| 8dfc1bc932 | |||
| d92cf43c57 | |||
| 099699872e | |||
| 5099903043 | |||
| 982fe47a33 | |||
| 65d5fadd13 | |||
| b55f5255ad | |||
| 83edef4806 | |||
| d4936d889a | |||
| c938cc8eae | |||
| 0558834eef | |||
| 098950fdde | |||
| 91e68a1d1e | |||
| 7172ddb247 | |||
| 96f2e058f7 | |||
| 8782944191 | |||
| ae26db6df2 | |||
| d038141a8a | |||
| f6960c61d6 |
Generated
+113
-116
@@ -338,7 +338,7 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "brk"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_bencher",
|
||||
"brk_bindgen",
|
||||
@@ -399,7 +399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_alloc"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
"mimalloc",
|
||||
@@ -407,7 +407,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_bencher"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_logger",
|
||||
@@ -417,14 +417,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_bencher_visualizer"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"plotters",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brk_bindgen"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_cohort",
|
||||
"brk_query",
|
||||
@@ -437,14 +437,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_cli"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brk_alloc",
|
||||
"brk_computer",
|
||||
"brk_error",
|
||||
"brk_indexer",
|
||||
"brk_iterator",
|
||||
"brk_logger",
|
||||
"brk_mempool",
|
||||
"brk_query",
|
||||
@@ -463,7 +462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_client"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_cohort",
|
||||
"brk_types",
|
||||
@@ -474,7 +473,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_cohort"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_traversable",
|
||||
@@ -486,7 +485,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_computer"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_alloc",
|
||||
@@ -494,12 +493,10 @@ dependencies = [
|
||||
"brk_cohort",
|
||||
"brk_error",
|
||||
"brk_indexer",
|
||||
"brk_iterator",
|
||||
"brk_logger",
|
||||
"brk_oracle",
|
||||
"brk_reader",
|
||||
"brk_rpc",
|
||||
"brk_store",
|
||||
"brk_traversable",
|
||||
"brk_types",
|
||||
"color-eyre",
|
||||
@@ -517,7 +514,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_error"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoincore-rpc",
|
||||
@@ -534,7 +531,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_fetcher"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_logger",
|
||||
@@ -546,14 +543,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_indexer"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_alloc",
|
||||
"brk_bencher",
|
||||
"brk_cohort",
|
||||
"brk_error",
|
||||
"brk_iterator",
|
||||
"brk_logger",
|
||||
"brk_reader",
|
||||
"brk_rpc",
|
||||
@@ -562,8 +558,8 @@ dependencies = [
|
||||
"brk_types",
|
||||
"color-eyre",
|
||||
"fjall",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rlimit",
|
||||
"rustc-hash",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -573,7 +569,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_iterator"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_reader",
|
||||
@@ -583,7 +579,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_logger"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"jiff",
|
||||
"owo-colors",
|
||||
@@ -594,7 +590,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_mempool"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_logger",
|
||||
@@ -609,7 +605,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_oracle"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_indexer",
|
||||
"brk_types",
|
||||
@@ -619,7 +615,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_query"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_computer",
|
||||
@@ -641,7 +637,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_reader"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_error",
|
||||
@@ -651,12 +647,13 @@ dependencies = [
|
||||
"derive_more",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rlimit",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brk_rpc"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoincore-rpc",
|
||||
@@ -673,7 +670,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_server"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"aide",
|
||||
"axum",
|
||||
@@ -708,7 +705,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_store"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_error",
|
||||
"brk_types",
|
||||
@@ -719,7 +716,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_traversable"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"brk_traversable_derive",
|
||||
"brk_types",
|
||||
@@ -732,7 +729,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_traversable_derive"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -741,7 +738,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_types"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"brk_error",
|
||||
@@ -764,7 +761,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brk_website"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-beta.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"brk_logger",
|
||||
@@ -1308,9 +1305,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fjall"
|
||||
version = "3.1.2"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a9530ff159bc3ad3a15da746da0f6e95375c2ac64708cbb85ec1ebd26761a84"
|
||||
checksum = "0ebf22b812878dcd767879cb19e03124fd62563dce6410f96538175fba0c132d"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"byteview",
|
||||
@@ -1318,7 +1315,7 @@ dependencies = [
|
||||
"flume",
|
||||
"log",
|
||||
"lsm-tree",
|
||||
"lz4_flex",
|
||||
"lz4_flex 0.11.6",
|
||||
"tempfile",
|
||||
"xxhash-rust",
|
||||
]
|
||||
@@ -1672,9 +1669,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -1686,7 +1683,6 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
@@ -1732,12 +1728,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"utf8_iter",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
@@ -1745,9 +1742,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
@@ -1758,9 +1755,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
@@ -1772,15 +1769,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
@@ -1792,15 +1789,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
@@ -1967,9 +1964,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.92"
|
||||
version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995"
|
||||
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -2007,9 +2004,9 @@ checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -2060,9 +2057,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
@@ -2087,9 +2084,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lsm-tree"
|
||||
version = "3.1.2"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d67f95fd716870329c30aaeedf87f23d426564e6ce46efa045a91444faf2a19"
|
||||
checksum = "e9bfd2a6ea0c1d430c13643002f35800a87f200fc8ac4827f18a2db9d9fd0644"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"byteview",
|
||||
@@ -2097,7 +2094,7 @@ dependencies = [
|
||||
"enum_dispatch",
|
||||
"interval-heap",
|
||||
"log",
|
||||
"lz4_flex",
|
||||
"lz4_flex 0.11.6",
|
||||
"quick_cache",
|
||||
"rustc-hash",
|
||||
"self_cell",
|
||||
@@ -2109,13 +2106,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.13.0"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a"
|
||||
checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
|
||||
dependencies = [
|
||||
"twox-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
@@ -2324,12 +2327,6 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@@ -2412,9 +2409,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
@@ -2545,9 +2542,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rawdb"
|
||||
version = "0.9.0"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fddb06a11fcc5f7f44d9b5bee4ab61b5a1135232b2fd239253428abd192ba504"
|
||||
checksum = "f23b5d5fae99af33e8d0c82763b890c469dcf18b48600ed78b0d70fce4dbe189"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@@ -2916,9 +2913,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -3134,9 +3131,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
@@ -3182,9 +3179,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
@@ -3197,27 +3194,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
|
||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
@@ -3439,14 +3436,14 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
|
||||
|
||||
[[package]]
|
||||
name = "vecdb"
|
||||
version = "0.9.0"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a33f1cbef9bf38048ee1b51328366f0a734e06bcc0b9739d68fef9ecce43d0b8"
|
||||
checksum = "a5fe60956ddba8c141ca8020aaf5bea55683475b83d19006c5f44b85c71bf974"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"libc",
|
||||
"log",
|
||||
"lz4_flex",
|
||||
"lz4_flex 0.13.0",
|
||||
"parking_lot",
|
||||
"pco",
|
||||
"rawdb",
|
||||
@@ -3462,9 +3459,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vecdb_derive"
|
||||
version = "0.9.0"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d31f03d1c7269d65195fb4d54c1d510b124807871bd11af7d10a08700d7590"
|
||||
checksum = "789897c1999d5d74f977020ad3d449846df046194103a4afcbac6d49baeaaffc"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -3512,9 +3509,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.115"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a"
|
||||
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -3525,9 +3522,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.115"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67"
|
||||
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -3535,9 +3532,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.115"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf"
|
||||
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -3548,9 +3545,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.115"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93"
|
||||
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -3591,9 +3588,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.92"
|
||||
version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94"
|
||||
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -3788,9 +3785,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||
|
||||
[[package]]
|
||||
name = "wio"
|
||||
@@ -3891,9 +3888,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "xxhash-rust"
|
||||
@@ -3914,9 +3911,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
@@ -3925,9 +3922,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3957,18 +3954,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3984,9 +3981,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
@@ -3995,9 +3992,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
@@ -4006,9 +4003,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+27
-26
@@ -4,7 +4,7 @@ members = ["crates/*"]
|
||||
package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node"
|
||||
package.license = "MIT"
|
||||
package.edition = "2024"
|
||||
package.version = "0.2.5"
|
||||
package.version = "0.3.0-beta.0"
|
||||
package.homepage = "https://bitcoinresearchkit.org"
|
||||
package.repository = "https://github.com/bitcoinresearchkit/brk"
|
||||
package.readme = "README.md"
|
||||
@@ -40,35 +40,35 @@ aide = { version = "0.16.0-alpha.3", features = ["axum-json", "axum-query"] }
|
||||
axum = { version = "0.8.8", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
|
||||
bitcoin = { version = "0.32.8", features = ["serde"] }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
brk_alloc = { version = "0.2.5", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.2.5", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.2.5", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.2.5", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.2.5", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.2.5", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.2.5", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.2.5", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.2.5", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.2.5", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.2.5", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.2.5", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.2.5", path = "crates/brk_mempool" }
|
||||
brk_oracle = { version = "0.2.5", path = "crates/brk_oracle" }
|
||||
brk_query = { version = "0.2.5", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.2.5", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.2.5", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.2.5", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.2.5", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.2.5", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.2.5", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.2.5", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.2.5", path = "crates/brk_website" }
|
||||
brk_alloc = { version = "0.3.0-beta.0", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.3.0-beta.0", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.3.0-beta.0", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.3.0-beta.0", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.3.0-beta.0", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.3.0-beta.0", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.3.0-beta.0", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.3.0-beta.0", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.3.0-beta.0", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.3.0-beta.0", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.3.0-beta.0", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.3.0-beta.0", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.3.0-beta.0", path = "crates/brk_mempool" }
|
||||
brk_oracle = { version = "0.3.0-beta.0", path = "crates/brk_oracle" }
|
||||
brk_query = { version = "0.3.0-beta.0", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.3.0-beta.0", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.3.0-beta.0", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.3.0-beta.0", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.3.0-beta.0", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.3.0-beta.0", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.3.0-beta.0", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.3.0-beta.0", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.3.0-beta.0", path = "crates/brk_website" }
|
||||
byteview = "0.10.1"
|
||||
color-eyre = "0.6.5"
|
||||
corepc-client = { package = "brk-corepc-client", version = "0.11.0", features = ["client-sync"] }
|
||||
corepc-jsonrpc = { package = "brk-corepc-jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = "3.1.2"
|
||||
fjall = "=3.0.4"
|
||||
indexmap = { version = "2.13.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.23", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
owo-colors = "4.3.0"
|
||||
@@ -87,7 +87,7 @@ tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", "
|
||||
tower-layer = "0.3"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
ureq = { version = "3.3.0", features = ["json"] }
|
||||
vecdb = { version = "0.9.0", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
vecdb = { version = "0.9.3", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
|
||||
[workspace.metadata.release]
|
||||
@@ -95,6 +95,7 @@ shared-version = true
|
||||
tag-name = "v{{version}}"
|
||||
pre-release-commit-message = "release: v{{version}}"
|
||||
tag-message = "release: v{{version}}"
|
||||
allow-branch = ["main", "next"]
|
||||
|
||||
[workspace.metadata.dist]
|
||||
cargo-dist-version = "0.30.2"
|
||||
|
||||
@@ -69,31 +69,48 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{{ signal?: AbortSignal, onUpdate?: (value: {}) => void }}}} [options]",
|
||||
return_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(output, " async {}({}) {{", method_name, params).unwrap();
|
||||
let params_with_opts = if params.is_empty() {
|
||||
"{ signal, onUpdate } = {}".to_string()
|
||||
} else {
|
||||
format!("{}, {{ signal, onUpdate }} = {{}}", params)
|
||||
};
|
||||
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
|
||||
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(output, " return this.getJson(`{}`);", path).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return this.getJson(`{}`, {{ signal, onUpdate }});",
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(output, " const params = new URLSearchParams();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
let ident = sanitize_ident(¶m.name);
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" params.set('{}', String({}));",
|
||||
param.name, param.name
|
||||
param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if ({} !== undefined) params.set('{}', String({}));",
|
||||
param.name, param.name, param.name
|
||||
ident, param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -108,11 +125,11 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if (format === 'csv') {{").unwrap();
|
||||
writeln!(output, " return this.getText(path);").unwrap();
|
||||
writeln!(output, " return this.getText(path, {{ signal }});").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " return this.getJson(path);").unwrap();
|
||||
writeln!(output, " return this.getJson(path, {{ signal, onUpdate }});").unwrap();
|
||||
} else {
|
||||
writeln!(output, " return this.getJson(path);").unwrap();
|
||||
writeln!(output, " return this.getJson(path, {{ signal, onUpdate }});").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,14 +144,19 @@ fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
params.push(param.name.clone());
|
||||
params.push(sanitize_ident(¶m.name));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
params.push(param.name.clone());
|
||||
params.push(sanitize_ident(¶m.name));
|
||||
}
|
||||
params.join(", ")
|
||||
}
|
||||
|
||||
/// Strip characters invalid in JS identifiers (e.g. `[]` from `txId[]`).
|
||||
fn sanitize_ident(name: &str) -> String {
|
||||
name.replace(['[', ']'], "")
|
||||
}
|
||||
|
||||
fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
|
||||
let mut result = path.to_string();
|
||||
for param in path_params {
|
||||
|
||||
@@ -22,6 +22,20 @@ pub fn generate_base_client(output: &mut String) {
|
||||
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
|
||||
const _runIdle = (/** @type {{VoidFunction}} */ fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
|
||||
const _defaultCacheName = '__BRK_CLIENT__';
|
||||
/** @param {{*}} v */
|
||||
const _addCamelGetters = (v) => {{
|
||||
if (Array.isArray(v)) {{ v.forEach(_addCamelGetters); return v; }}
|
||||
if (v && typeof v === 'object' && v.constructor === Object) {{
|
||||
for (const k in v) {{
|
||||
if (k.includes('_')) {{
|
||||
const c = k.replace(/_([a-z])/g, (_, l) => l.toUpperCase());
|
||||
if (!(c in v)) Object.defineProperty(v, c, {{ get() {{ return this[k]; }} }});
|
||||
}}
|
||||
_addCamelGetters(v[k]);
|
||||
}}
|
||||
}}
|
||||
return v;
|
||||
}};
|
||||
|
||||
/**
|
||||
* @param {{string|boolean|undefined}} cache
|
||||
@@ -390,11 +404,14 @@ class BrkClientBase {{
|
||||
|
||||
/**
|
||||
* @param {{string}} path
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<Response>}}
|
||||
*/
|
||||
async get(path) {{
|
||||
async get(path, {{ signal }} = {{}}) {{
|
||||
const url = `${{this.baseUrl}}${{path}}`;
|
||||
const res = await fetch(url, {{ signal: AbortSignal.timeout(this.timeout) }});
|
||||
const signals = [AbortSignal.timeout(this.timeout)];
|
||||
if (signal) signals.push(signal);
|
||||
const res = await fetch(url, {{ signal: AbortSignal.any(signals) }});
|
||||
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
|
||||
return res;
|
||||
}}
|
||||
@@ -403,10 +420,10 @@ class BrkClientBase {{
|
||||
* Make a GET request - races cache vs network, first to resolve calls onUpdate
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available (may be called twice: cache then network)
|
||||
* @param {{{{ onUpdate?: (value: T) => void, signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
async getJson(path, onUpdate) {{
|
||||
async getJson(path, {{ onUpdate, signal }} = {{}}) {{
|
||||
const url = `${{this.baseUrl}}${{path}}`;
|
||||
const cache = this._cache ?? await this._cachePromise;
|
||||
|
||||
@@ -418,7 +435,7 @@ class BrkClientBase {{
|
||||
const cachePromise = cache?.match(url).then(async (res) => {{
|
||||
cachedRes = res ?? null;
|
||||
if (!res) return null;
|
||||
const json = await res.json();
|
||||
const json = _addCamelGetters(await res.json());
|
||||
if (!resolved && onUpdate) {{
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
@@ -426,9 +443,9 @@ class BrkClientBase {{
|
||||
return json;
|
||||
}});
|
||||
|
||||
const networkPromise = this.get(path).then(async (res) => {{
|
||||
const networkPromise = this.get(path, {{ signal }}).then(async (res) => {{
|
||||
const cloned = res.clone();
|
||||
const json = await res.json();
|
||||
const json = _addCamelGetters(await res.json());
|
||||
// Skip update if ETag matches and cache already delivered
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) {{
|
||||
if (!resolved && onUpdate) {{
|
||||
@@ -458,10 +475,11 @@ class BrkClientBase {{
|
||||
/**
|
||||
* Make a GET request and return raw text (for CSV responses)
|
||||
* @param {{string}} path
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<string>}}
|
||||
*/
|
||||
async getText(path) {{
|
||||
const res = await this.get(path);
|
||||
async getText(path, {{ signal }} = {{}}) {{
|
||||
const res = await this.get(path, {{ signal }});
|
||||
return res.text();
|
||||
}}
|
||||
|
||||
@@ -474,7 +492,7 @@ class BrkClientBase {{
|
||||
*/
|
||||
async _fetchSeriesData(path, onUpdate) {{
|
||||
const wrappedOnUpdate = onUpdate ? (/** @type {{SeriesData<T>}} */ raw) => onUpdate(_wrapSeriesData(raw)) : undefined;
|
||||
const raw = await this.getJson(path, wrappedOnUpdate);
|
||||
const raw = await this.getJson(path, {{ onUpdate: wrappedOnUpdate }});
|
||||
return _wrapSeriesData(raw);
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -101,7 +101,7 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_python)
|
||||
.unwrap_or_else(|| "Any".to_string()),
|
||||
.unwrap_or_else(|| "str".to_string()),
|
||||
);
|
||||
|
||||
let return_type = if endpoint.supports_csv {
|
||||
@@ -159,11 +159,17 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
// Build path
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
let fetch_method = if endpoint.returns_json() {
|
||||
"get_json"
|
||||
} else {
|
||||
"get_text"
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
if endpoint.path_params.is_empty() {
|
||||
writeln!(output, " return self.get_json('{}')", path).unwrap();
|
||||
writeln!(output, " return self.{}('{}')", fetch_method, path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " return self.get_json(f'{}')", path).unwrap();
|
||||
writeln!(output, " return self.{}(f'{}')", fetch_method, path).unwrap();
|
||||
}
|
||||
} else {
|
||||
writeln!(output, " params = []").unwrap();
|
||||
@@ -197,9 +203,9 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if format == 'csv':").unwrap();
|
||||
writeln!(output, " return self.get_text(path)").unwrap();
|
||||
writeln!(output, " return self.get_json(path)").unwrap();
|
||||
writeln!(output, " return self.{}(path)", fetch_method).unwrap();
|
||||
} else {
|
||||
writeln!(output, " return self.get_json(path)").unwrap();
|
||||
writeln!(output, " return self.{}(path)", fetch_method).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_rust)
|
||||
.unwrap_or_else(|| "serde_json::Value".to_string());
|
||||
.unwrap_or_else(|| "String".to_string());
|
||||
|
||||
let return_type = if endpoint.supports_csv {
|
||||
format!("FormatResponse<{}>", base_return_type)
|
||||
@@ -132,29 +132,43 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.unwrap();
|
||||
|
||||
let (path, index_arg) = build_path_template(endpoint);
|
||||
let fetch_method = if endpoint.returns_json() {
|
||||
"get_json"
|
||||
} else {
|
||||
"get_text"
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.get_json(&format!(\"{}\"{}))",
|
||||
path, index_arg
|
||||
" self.base.{}(&format!(\"{}\"{}))",
|
||||
fetch_method, path, index_arg
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(output, " let mut query = Vec::new();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
if param.required {
|
||||
let ident = sanitize_ident(¶m.name);
|
||||
let is_array = param.param_type.ends_with("[]");
|
||||
if is_array {
|
||||
writeln!(
|
||||
output,
|
||||
" for v in {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
ident, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" query.push(format!(\"{}={{}}\", {}));",
|
||||
param.name, param.name
|
||||
param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
param.name, param.name
|
||||
ident, param.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -177,12 +191,13 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
writeln!(output, " }} else {{").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.get_json(&path).map(FormatResponse::Json)"
|
||||
" self.base.{}(&path).map(FormatResponse::Json)",
|
||||
fetch_method
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
} else {
|
||||
writeln!(output, " self.base.get_json(&path)").unwrap();
|
||||
writeln!(output, " self.base.{}(&path)", fetch_method).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,26 +213,35 @@ fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
let rust_type = param_type_to_rust(¶m.param_type);
|
||||
params.push(format!(", {}: {}", param.name, rust_type));
|
||||
params.push(format!(", {}: {}", sanitize_ident(¶m.name), rust_type));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
let rust_type = param_type_to_rust(¶m.param_type);
|
||||
let name = sanitize_ident(¶m.name);
|
||||
if param.required {
|
||||
params.push(format!(", {}: {}", param.name, rust_type));
|
||||
params.push(format!(", {}: {}", name, rust_type));
|
||||
} else {
|
||||
params.push(format!(", {}: Option<{}>", param.name, rust_type));
|
||||
params.push(format!(", {}: Option<{}>", name, rust_type));
|
||||
}
|
||||
}
|
||||
params.join("")
|
||||
}
|
||||
|
||||
/// Strip characters invalid in Rust identifiers (e.g. `[]` from `txId[]`).
|
||||
fn sanitize_ident(name: &str) -> String {
|
||||
name.replace(['[', ']'], "")
|
||||
}
|
||||
|
||||
/// Convert parameter type to Rust type for function signatures.
|
||||
fn param_type_to_rust(param_type: &str) -> String {
|
||||
if let Some(inner) = param_type.strip_suffix("[]") {
|
||||
return format!("&[{}]", param_type_to_rust(inner));
|
||||
}
|
||||
match param_type {
|
||||
"string" | "*" => "&str".to_string(),
|
||||
"integer" | "number" => "i64".to_string(),
|
||||
"boolean" => "bool".to_string(),
|
||||
other => other.to_string(), // Domain types like Index, SeriesName, Format
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ impl Endpoint {
|
||||
self.method == "GET" && !self.deprecated
|
||||
}
|
||||
|
||||
/// Returns true if this endpoint returns JSON (has a response_type extracted from application/json).
|
||||
pub fn returns_json(&self) -> bool {
|
||||
self.response_type.is_some()
|
||||
}
|
||||
|
||||
/// Returns the operation ID or generates one from the path.
|
||||
/// The returned string uses the raw case from the spec (typically camelCase).
|
||||
pub fn operation_name(&self) -> String {
|
||||
|
||||
@@ -74,6 +74,9 @@ pub fn escape_python_keyword(name: &str) -> String {
|
||||
"try", "while", "with", "yield",
|
||||
];
|
||||
|
||||
// Strip characters invalid in identifiers (e.g. `[]` from `txId[]`)
|
||||
let name = name.replace(['[', ']'], "");
|
||||
|
||||
// Prefix with underscore if starts with digit
|
||||
let name = if name.starts_with(|c: char| c.is_ascii_digit()) {
|
||||
format!("_{}", name)
|
||||
|
||||
@@ -13,7 +13,6 @@ brk_alloc = { workspace = true }
|
||||
brk_computer = { workspace = true }
|
||||
brk_error = { workspace = true, features = ["tokio", "vecdb"] }
|
||||
brk_indexer = { workspace = true }
|
||||
brk_iterator = { workspace = true }
|
||||
brk_logger = { workspace = true }
|
||||
brk_mempool = { workspace = true }
|
||||
brk_query = { workspace = true }
|
||||
@@ -26,7 +25,7 @@ owo-colors = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = "1.1.0"
|
||||
toml = "1.1.2"
|
||||
vecdb = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
# BRK CLI
|
||||
|
||||
Command-line interface for running a Bitcoin Research Kit instance.
|
||||
Run your own Bitcoin Research Kit instance. One binary, one command. Full sync in ~4-7h depending on hardware. ~44% disk overhead vs 250% for mempool/electrs.
|
||||
|
||||
## Demo
|
||||
|
||||
- [bitview.space](https://bitview.space) - web interface
|
||||
- [bitview.space/api](https://bitview.space/api) - API docs
|
||||
[bitview.space](https://bitview.space) is the official free hosted instance.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ use brk_alloc::Mimalloc;
|
||||
use brk_computer::Computer;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_mempool::Mempool;
|
||||
use brk_query::AsyncQuery;
|
||||
use brk_reader::Reader;
|
||||
@@ -37,8 +36,6 @@ pub fn main() -> anyhow::Result<()> {
|
||||
|
||||
let reader = Reader::new(config.blocksdir(), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&config.brkdir())?;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -52,7 +49,7 @@ pub fn main() -> anyhow::Result<()> {
|
||||
info!("Indexing {blocks_behind} blocks before starting server...");
|
||||
info!("---");
|
||||
sleep(Duration::from_secs(10));
|
||||
indexer.index(&blocks, &client, &exit)?;
|
||||
indexer.index(&reader, &client, &exit)?;
|
||||
drop(indexer);
|
||||
Mimalloc::collect();
|
||||
indexer = Indexer::forced_import(&config.brkdir())?;
|
||||
@@ -102,14 +99,14 @@ pub fn main() -> anyhow::Result<()> {
|
||||
let total_start = Instant::now();
|
||||
|
||||
let starting_indexes = if cfg!(debug_assertions) {
|
||||
indexer.checked_index(&blocks, &client, &exit)?
|
||||
indexer.checked_index(&reader, &client, &exit)?
|
||||
} else {
|
||||
indexer.index(&blocks, &client, &exit)?
|
||||
indexer.index(&reader, &client, &exit)?
|
||||
};
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, starting_indexes, &exit)?;
|
||||
|
||||
info!("Total time: {:?}", total_start.elapsed());
|
||||
info!("Waiting for new blocks...");
|
||||
|
||||
+1356
-4814
File diff suppressed because it is too large
Load Diff
@@ -14,3 +14,6 @@ brk_traversable = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["vecdb"]
|
||||
|
||||
@@ -153,6 +153,6 @@ impl<T> Loss<T> {
|
||||
.into_iter()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.map(move |(n, threshold)| (threshold, &ranges[len - 1 - n..]))
|
||||
.map(move |(n, threshold)| (threshold, &ranges[len - 2 - n..]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,6 @@ impl<T> Profit<T> {
|
||||
.into_iter()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.map(move |(n, threshold)| (threshold, &ranges[..n + 1]))
|
||||
.map(move |(n, threshold)| (threshold, &ranges[..n + 2]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,8 @@ brk_error = { workspace = true, features = ["vecdb"] }
|
||||
brk_cohort = { workspace = true }
|
||||
brk_indexer = { workspace = true }
|
||||
brk_oracle = { workspace = true }
|
||||
brk_iterator = { workspace = true }
|
||||
brk_logger = { workspace = true }
|
||||
brk_reader = { workspace = true }
|
||||
brk_rpc = { workspace = true, features = ["corepc"] }
|
||||
brk_store = { workspace = true }
|
||||
brk_traversable = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
@@ -33,6 +30,7 @@ smallvec = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
brk_reader = { workspace = true }
|
||||
brk_alloc = { workspace = true }
|
||||
brk_bencher = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::{
|
||||
use brk_alloc::Mimalloc;
|
||||
use brk_computer::Computer;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use vecdb::Exit;
|
||||
@@ -31,8 +30,6 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
let exit = Exit::new();
|
||||
@@ -42,7 +39,7 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
let chain_height = client.get_last_height()?;
|
||||
let indexed_height = indexer.vecs.starting_height();
|
||||
if u32::from(chain_height).saturating_sub(u32::from(indexed_height)) > 1000 {
|
||||
indexer.checked_index(&blocks, &client, &exit)?;
|
||||
indexer.checked_index(&reader, &client, &exit)?;
|
||||
drop(indexer);
|
||||
Mimalloc::collect();
|
||||
indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
@@ -52,11 +49,11 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
loop {
|
||||
let i = Instant::now();
|
||||
let starting_indexes = indexer.checked_index(&blocks, &client, &exit)?;
|
||||
let starting_indexes = indexer.checked_index(&reader, &client, &exit)?;
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, starting_indexes, &exit)?;
|
||||
dbg!(i.elapsed());
|
||||
sleep(Duration::from_secs(10));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use brk_bencher::Bencher;
|
||||
use brk_computer::Computer;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use tracing::{debug, info};
|
||||
@@ -28,8 +27,6 @@ pub fn main() -> Result<()> {
|
||||
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
let mut computer = Computer::forced_import(&outputs_benches_dir, &indexer)?;
|
||||
@@ -47,13 +44,13 @@ pub fn main() -> Result<()> {
|
||||
});
|
||||
|
||||
let i = Instant::now();
|
||||
let starting_indexes = indexer.index(&blocks, &client, &exit)?;
|
||||
let starting_indexes = indexer.index(&reader, &client, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
let i = Instant::now();
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, starting_indexes, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
// We want to benchmark the drop too
|
||||
|
||||
@@ -9,7 +9,6 @@ use brk_alloc::Mimalloc;
|
||||
use brk_bencher::Bencher;
|
||||
use brk_computer::Computer;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use tracing::{debug, info};
|
||||
@@ -45,15 +44,13 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
// Pre-run indexer if too far behind, then drop and reimport to reduce memory
|
||||
let chain_height = client.get_last_height()?;
|
||||
let indexed_height = indexer.vecs.starting_height();
|
||||
if chain_height.saturating_sub(*indexed_height) > 1000 {
|
||||
indexer.index(&blocks, &client, &exit)?;
|
||||
indexer.index(&reader, &client, &exit)?;
|
||||
drop(indexer);
|
||||
Mimalloc::collect();
|
||||
indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
@@ -63,13 +60,13 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
loop {
|
||||
let i = Instant::now();
|
||||
let starting_indexes = indexer.index(&blocks, &client, &exit)?;
|
||||
let starting_indexes = indexer.index(&reader, &client, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
let i = Instant::now();
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, starting_indexes, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
sleep(Duration::from_secs(60));
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Vecs {
|
||||
|
||||
self.hodl_bank.compute_cumulative_transformed_binary(
|
||||
starting_indexes.height,
|
||||
&prices.spot.usd.height,
|
||||
&prices.cached_spot_usd,
|
||||
&self.vocdd_median_1y,
|
||||
|price, median| StoredF64::from(f64::from(price) - f64::from(median)),
|
||||
exit,
|
||||
@@ -31,7 +31,7 @@ impl Vecs {
|
||||
|
||||
self.value.height.compute_divide(
|
||||
starting_indexes.height,
|
||||
&prices.spot.usd.height,
|
||||
&prices.cached_spot_usd,
|
||||
&self.hodl_bank,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -24,7 +24,7 @@ impl Vecs {
|
||||
.compute(starting_indexes.height, exit, |vec| {
|
||||
vec.compute_multiply(
|
||||
starting_indexes.height,
|
||||
&prices.spot.usd.height,
|
||||
&prices.cached_spot_usd,
|
||||
&coinblocks_destroyed.block,
|
||||
exit,
|
||||
)?;
|
||||
@@ -34,7 +34,7 @@ impl Vecs {
|
||||
self.created.compute(starting_indexes.height, exit, |vec| {
|
||||
vec.compute_multiply(
|
||||
starting_indexes.height,
|
||||
&prices.spot.usd.height,
|
||||
&prices.cached_spot_usd,
|
||||
&activity.coinblocks_created.block,
|
||||
exit,
|
||||
)?;
|
||||
@@ -44,7 +44,7 @@ impl Vecs {
|
||||
self.stored.compute(starting_indexes.height, exit, |vec| {
|
||||
vec.compute_multiply(
|
||||
starting_indexes.height,
|
||||
&prices.spot.usd.height,
|
||||
&prices.cached_spot_usd,
|
||||
&activity.coinblocks_stored.block,
|
||||
exit,
|
||||
)?;
|
||||
@@ -57,7 +57,7 @@ impl Vecs {
|
||||
self.vocdd.compute(starting_indexes.height, exit, |vec| {
|
||||
vec.compute_transform3(
|
||||
starting_indexes.height,
|
||||
&prices.spot.usd.height,
|
||||
&prices.cached_spot_usd,
|
||||
&coindays_destroyed.block,
|
||||
circulating_supply,
|
||||
|(i, price, cdd, supply, _): (_, Dollars, StoredF64, Bitcoin, _)| {
|
||||
|
||||
@@ -7,7 +7,7 @@ use brk_types::{
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::FxHashSet;
|
||||
use tracing::{debug, info};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec, unlikely};
|
||||
|
||||
use crate::{
|
||||
distribution::{
|
||||
@@ -243,7 +243,11 @@ pub(crate) fn process_blocks(
|
||||
for height in starting_height.to_usize()..=last_height.to_usize() {
|
||||
let height = Height::from(height);
|
||||
|
||||
info!("Processing chain at {}...", height);
|
||||
if unlikely(height.is_multiple_of(100)) {
|
||||
info!("Processing chain at {}...", height);
|
||||
} else {
|
||||
debug!("Processing chain at {}...", height);
|
||||
}
|
||||
|
||||
// Get block metadata from pre-collected vecs
|
||||
let offset = height.to_usize() - start_usize;
|
||||
|
||||
@@ -119,7 +119,7 @@ impl AllCohortMetrics {
|
||||
|
||||
self.unrealized.compute(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized.price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
@@ -139,7 +139,7 @@ impl AllCohortMetrics {
|
||||
|
||||
self.cost_basis.compute_prices(
|
||||
starting_indexes,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.unrealized.invested_capital.in_profit.cents.height,
|
||||
&self.unrealized.invested_capital.in_loss.cents.height,
|
||||
&self.supply.in_profit.sats.height,
|
||||
@@ -150,7 +150,7 @@ impl AllCohortMetrics {
|
||||
)?;
|
||||
|
||||
self.unrealized
|
||||
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
|
||||
.compute_sentiment(starting_indexes, &prices.cached_spot_cents, exit)?;
|
||||
|
||||
self.relative.compute(
|
||||
starting_indexes.height,
|
||||
|
||||
@@ -82,7 +82,7 @@ impl BasicCohortMetrics {
|
||||
|
||||
self.unrealized.compute(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized.price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -140,7 +140,7 @@ impl CoreCohortMetrics {
|
||||
|
||||
self.unrealized.compute(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized.price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -108,14 +108,14 @@ impl ExtendedCohortMetrics {
|
||||
|
||||
self.unrealized.compute(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized.price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.cost_basis.compute_prices(
|
||||
starting_indexes,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.unrealized.invested_capital.in_profit.cents.height,
|
||||
&self.unrealized.invested_capital.in_loss.cents.height,
|
||||
&self.supply.in_profit.sats.height,
|
||||
@@ -126,7 +126,7 @@ impl ExtendedCohortMetrics {
|
||||
)?;
|
||||
|
||||
self.unrealized
|
||||
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
|
||||
.compute_sentiment(starting_indexes, &prices.cached_spot_cents, exit)?;
|
||||
|
||||
self.relative.compute(
|
||||
starting_indexes.height,
|
||||
|
||||
@@ -124,7 +124,7 @@ impl MinimalCohortMetrics {
|
||||
|
||||
self.unrealized.compute(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized.price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -86,7 +86,7 @@ impl TypeCohortMetrics {
|
||||
|
||||
self.unrealized.compute(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized.price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -126,7 +126,7 @@ impl ProfitabilityBucket {
|
||||
|
||||
self.unrealized_pnl.all.height.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized_cap.all.height,
|
||||
&self.supply.all.sats.height,
|
||||
|(i, spot, cap, supply, ..)| {
|
||||
@@ -139,7 +139,7 @@ impl ProfitabilityBucket {
|
||||
)?;
|
||||
self.unrealized_pnl.sth.height.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized_cap.sth.height,
|
||||
&self.supply.sth.sats.height,
|
||||
|(i, spot, cap, supply, ..)| {
|
||||
@@ -153,7 +153,7 @@ impl ProfitabilityBucket {
|
||||
|
||||
self.nupl.bps.height.compute_transform3(
|
||||
max_from,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.realized_cap.all.height,
|
||||
&self.supply.all.sats.height,
|
||||
|(i, spot, cap_dollars, supply_sats, ..)| {
|
||||
@@ -273,7 +273,7 @@ impl ProfitabilityMetrics {
|
||||
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
|
||||
})?;
|
||||
|
||||
let aggregate_version = version + Version::ONE;
|
||||
let aggregate_version = version + Version::TWO;
|
||||
|
||||
let profit = Profit::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
|
||||
|
||||
@@ -122,7 +122,7 @@ impl UnrealizedFull {
|
||||
.compute_transform3(
|
||||
starting_indexes.height,
|
||||
supply_in_profit_sats,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.inner.basic.profit.cents.height,
|
||||
|(h, supply_sats, spot, profit, ..): (_, Sats, Cents, Cents, _)| {
|
||||
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
|
||||
@@ -142,7 +142,7 @@ impl UnrealizedFull {
|
||||
.compute_transform3(
|
||||
starting_indexes.height,
|
||||
supply_in_loss_sats,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.inner.basic.loss.cents.height,
|
||||
|(h, supply_sats, spot, loss, ..): (_, Sats, Cents, Cents, _)| {
|
||||
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Height, Indexes, StoredI8, Version};
|
||||
use vecdb::{AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
use vecdb::{AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
cointime, distribution, indexes,
|
||||
@@ -123,7 +123,7 @@ impl RealizedEnvelope {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
let spot = &prices.spot.cents.height;
|
||||
let spot = &prices.cached_spot_cents;
|
||||
|
||||
// Zone: spot vs own envelope bands (-4 to +4)
|
||||
self.compute_index(spot, starting_indexes, exit)?;
|
||||
@@ -136,7 +136,7 @@ impl RealizedEnvelope {
|
||||
|
||||
fn compute_index(
|
||||
&mut self,
|
||||
spot: &EagerVec<PcoVec<Height, Cents>>,
|
||||
spot: &impl ReadableVec<Height, Cents>,
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
@@ -214,7 +214,7 @@ impl RealizedEnvelope {
|
||||
fn compute_score(
|
||||
&mut self,
|
||||
models: &[&RatioPerBlockPercentiles; 10],
|
||||
spot: &EagerVec<PcoVec<Height, Cents>>,
|
||||
spot: &impl ReadableVec<Height, Cents>,
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
|
||||
@@ -59,7 +59,7 @@ impl AmountPerBlock {
|
||||
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
|
||||
max_from,
|
||||
&self.sats.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
|
||||
@@ -50,7 +50,7 @@ impl AmountBlock {
|
||||
self.cents.compute_binary::<Sats, Cents, SatsToCents>(
|
||||
max_from,
|
||||
&self.sats,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
|
||||
@@ -98,6 +98,7 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
|
||||
let count_indexes_batch: Vec<brk_types::StoredU64> =
|
||||
count_indexes.collect_range_at(start, fi_len);
|
||||
|
||||
let zero = T::from(0_usize);
|
||||
let mut values: Vec<T> = Vec::new();
|
||||
|
||||
first_indexes_batch
|
||||
@@ -114,8 +115,11 @@ impl<T: NumericValue + JsonSchema> PerBlockDistribution<T> {
|
||||
&mut values,
|
||||
);
|
||||
|
||||
if skip_count > 0 {
|
||||
values.retain(|v| *v > zero);
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
let zero = T::from(0_usize);
|
||||
for vec in [
|
||||
&mut *min,
|
||||
&mut *max,
|
||||
|
||||
@@ -71,7 +71,7 @@ impl PriceWithRatioPerBlock {
|
||||
F: FnMut(&mut EagerVec<PcoVec<Height, Cents>>) -> Result<()>,
|
||||
{
|
||||
compute_price(&mut self.cents.height)?;
|
||||
self.compute_ratio(starting_indexes, &prices.spot.cents.height, exit)
|
||||
self.compute_ratio(starting_indexes, &prices.cached_spot_cents, exit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ impl PriceWithRatioExtendedPerBlock {
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let close_price = &prices.spot.cents.height;
|
||||
let close_price = &prices.cached_spot_cents;
|
||||
self.base
|
||||
.compute_ratio(starting_indexes, close_price, exit)?;
|
||||
self.percentiles.compute(
|
||||
|
||||
@@ -102,7 +102,7 @@ impl Vecs {
|
||||
{
|
||||
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&average_price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
@@ -163,7 +163,7 @@ impl Vecs {
|
||||
{
|
||||
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&lookback_price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
@@ -266,7 +266,7 @@ impl Vecs {
|
||||
{
|
||||
returns.compute_binary::<Cents, Cents, RatioDiffCentsBps32>(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&average_price.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::{fs, path::Path, thread, time::Instant};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_reader::Reader;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use tracing::info;
|
||||
@@ -23,7 +22,6 @@ mod market;
|
||||
mod mining;
|
||||
mod outputs;
|
||||
mod pools;
|
||||
mod positions;
|
||||
pub mod prices;
|
||||
mod scripts;
|
||||
mod supply;
|
||||
@@ -35,7 +33,6 @@ pub struct Computer<M: StorageMode = Rw> {
|
||||
pub mining: Box<mining::Vecs<M>>,
|
||||
pub transactions: Box<transactions::Vecs<M>>,
|
||||
pub scripts: Box<scripts::Vecs<M>>,
|
||||
pub positions: Box<positions::Vecs<M>>,
|
||||
pub cointime: Box<cointime::Vecs<M>>,
|
||||
pub constants: Box<constants::Vecs>,
|
||||
pub indexes: Box<indexes::Vecs<M>>,
|
||||
@@ -63,24 +60,12 @@ impl Computer {
|
||||
const STACK_SIZE: usize = 8 * 1024 * 1024;
|
||||
let big_thread = || thread::Builder::new().stack_size(STACK_SIZE);
|
||||
|
||||
let (indexes, positions) = timed("Imported indexes/positions", || {
|
||||
thread::scope(|s| -> Result<_> {
|
||||
let positions_handle = big_thread().spawn_scoped(s, || -> Result<_> {
|
||||
Ok(Box::new(positions::Vecs::forced_import(
|
||||
&computed_path,
|
||||
VERSION,
|
||||
)?))
|
||||
})?;
|
||||
|
||||
let indexes = Box::new(indexes::Vecs::forced_import(
|
||||
&computed_path,
|
||||
VERSION,
|
||||
indexer,
|
||||
)?);
|
||||
let positions = positions_handle.join().unwrap()?;
|
||||
|
||||
Ok((indexes, positions))
|
||||
})
|
||||
let indexes = timed("Imported indexes", || -> Result<_> {
|
||||
Ok(Box::new(indexes::Vecs::forced_import(
|
||||
&computed_path,
|
||||
VERSION,
|
||||
indexer,
|
||||
)?))
|
||||
})?;
|
||||
|
||||
let (constants, prices) = timed("Imported prices/constants", || -> Result<_> {
|
||||
@@ -257,7 +242,6 @@ impl Computer {
|
||||
market,
|
||||
distribution,
|
||||
supply,
|
||||
positions,
|
||||
pools,
|
||||
cointime,
|
||||
indexes,
|
||||
@@ -278,7 +262,6 @@ impl Computer {
|
||||
mining::DB_NAME,
|
||||
transactions::DB_NAME,
|
||||
scripts::DB_NAME,
|
||||
positions::DB_NAME,
|
||||
cointime::DB_NAME,
|
||||
indicators::DB_NAME,
|
||||
indexes::DB_NAME,
|
||||
@@ -319,7 +302,6 @@ impl Computer {
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
starting_indexes: brk_indexer::Indexes,
|
||||
reader: &Reader,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
internal::cache_clear_all();
|
||||
@@ -387,13 +369,6 @@ impl Computer {
|
||||
)
|
||||
})?;
|
||||
|
||||
let positions = scope.spawn(|| {
|
||||
timed("Computed positions", || {
|
||||
self.positions
|
||||
.compute(indexer, &starting_indexes, reader, exit)
|
||||
})
|
||||
});
|
||||
|
||||
timed("Computed transactions", || {
|
||||
self.transactions.compute(
|
||||
indexer,
|
||||
@@ -419,7 +394,6 @@ impl Computer {
|
||||
)
|
||||
})?;
|
||||
|
||||
positions.join().unwrap()?;
|
||||
market.join().unwrap()?;
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -561,7 +535,6 @@ impl_iter_named!(
|
||||
mining,
|
||||
transactions,
|
||||
scripts,
|
||||
positions,
|
||||
cointime,
|
||||
constants,
|
||||
indicators,
|
||||
|
||||
@@ -15,7 +15,7 @@ impl Vecs {
|
||||
) -> Result<()> {
|
||||
self.high.cents.height.compute_all_time_high(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Vecs {
|
||||
self.days_since.height.compute_transform3(
|
||||
starting_indexes.height,
|
||||
&self.high.cents.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&indexes.timestamp.monotonic,
|
||||
|(i, ath, price, ts, slf)| {
|
||||
if ath_ts.is_none() {
|
||||
@@ -68,7 +68,7 @@ impl Vecs {
|
||||
|
||||
self.drawdown.compute_drawdown(
|
||||
starting_indexes.height,
|
||||
&prices.spot.cents.height,
|
||||
&prices.cached_spot_cents,
|
||||
&self.high.cents.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -13,7 +13,7 @@ impl Vecs {
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let price = &prices.spot.cents.height;
|
||||
let price = &prices.cached_spot_cents;
|
||||
|
||||
for (price_past, days) in self.price_past.iter_mut_with_days() {
|
||||
let window_starts = blocks.lookback.start_vec(days as usize);
|
||||
|
||||
@@ -13,7 +13,7 @@ impl Vecs {
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let close = &prices.spot.cents.height;
|
||||
let close = &prices.cached_spot_cents;
|
||||
|
||||
for (sma, period) in [
|
||||
(&mut self.sma._1w, 7),
|
||||
|
||||
@@ -13,7 +13,7 @@ impl Vecs {
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let price = &prices.spot.cents.height;
|
||||
let price = &prices.cached_spot_cents;
|
||||
|
||||
for (min_vec, max_vec, starts) in [
|
||||
(
|
||||
|
||||
@@ -24,7 +24,7 @@ impl Vecs {
|
||||
{
|
||||
returns.compute_binary::<Dollars, Dollars, RatioDiffDollarsBps32>(
|
||||
starting_indexes.height,
|
||||
&prices.spot.usd.height,
|
||||
&prices.cached_spot_usd,
|
||||
&lookback_price.usd.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -16,7 +16,7 @@ pub(super) fn compute(
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let close = &prices.spot.usd.height;
|
||||
let close = &prices.cached_spot_usd;
|
||||
let ws_fast = blocks.lookback.start_vec(fast_days);
|
||||
let ws_slow = blocks.lookback.start_vec(slow_days);
|
||||
let ws_signal = blocks.lookback.start_vec(signal_days);
|
||||
|
||||
@@ -25,7 +25,7 @@ impl Vecs {
|
||||
indexer,
|
||||
indexes,
|
||||
&blocks.lookback,
|
||||
&transactions.fees,
|
||||
transactions,
|
||||
prices,
|
||||
starting_indexes,
|
||||
exit,
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Vecs {
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
lookback: &blocks::LookbackVecs,
|
||||
transactions_fees: &transactions::FeesVecs,
|
||||
transactions: &transactions::Vecs,
|
||||
prices: &prices::Vecs,
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
@@ -67,7 +67,7 @@ impl Vecs {
|
||||
starting_indexes.height,
|
||||
&indexer.vecs.transactions.first_tx_index,
|
||||
&indexes.height.tx_index_count,
|
||||
&transactions_fees.fee.tx_index,
|
||||
&transactions.fees.fee.tx_index,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -95,6 +95,13 @@ impl Vecs {
|
||||
self.subsidy
|
||||
.compute_rest(starting_indexes.height, prices, exit)?;
|
||||
|
||||
self.output_volume.compute_subtract(
|
||||
starting_indexes.height,
|
||||
&transactions.volume.transfer_volume.block.sats,
|
||||
&self.fees.block.sats,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.unclaimed.block.sats.compute_transform(
|
||||
starting_indexes.height,
|
||||
&self.subsidy.block.sats,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
use vecdb::{Database, EagerVec, ImportableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
@@ -44,6 +44,7 @@ impl Vecs {
|
||||
cached_starts,
|
||||
)?,
|
||||
fees: AmountPerBlockFull::forced_import(db, "fees", version, indexes, cached_starts)?,
|
||||
output_volume: EagerVec::forced_import(db, "output_volume", version)?,
|
||||
unclaimed: AmountPerBlockCumulative::forced_import(
|
||||
db,
|
||||
"unclaimed_rewards",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, BasisPoints32};
|
||||
use vecdb::{Rw, StorageMode};
|
||||
use brk_types::{BasisPoints16, BasisPoints32, Height, Sats};
|
||||
use vecdb::{EagerVec, PcoVec, Rw, StorageMode};
|
||||
|
||||
use crate::internal::{
|
||||
AmountPerBlockCumulative, AmountPerBlockCumulativeRolling, AmountPerBlockFull,
|
||||
@@ -12,6 +12,7 @@ pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub coinbase: AmountPerBlockCumulativeRolling<M>,
|
||||
pub subsidy: AmountPerBlockCumulativeRolling<M>,
|
||||
pub fees: AmountPerBlockFull<M>,
|
||||
pub output_volume: M::Stored<EagerVec<PcoVec<Height, Sats>>>,
|
||||
pub unclaimed: AmountPerBlockCumulative<M>,
|
||||
#[traversable(wrap = "fees", rename = "dominance")]
|
||||
pub fee_dominance: PercentPerBlock<BasisPoints16, M>,
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::{collections::BTreeMap, path::Path};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_store::AnyStore;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Addr, AddrBytes, Height, Indexes, OutputType, PoolSlug, Pools, TxOutIndex, pools};
|
||||
use rayon::prelude::*;
|
||||
@@ -45,7 +44,7 @@ impl Vecs {
|
||||
let db = open_db(parent_path, DB_NAME, 100_000)?;
|
||||
let pools = pools();
|
||||
|
||||
let version = parent_version + Version::new(3) + Version::new(pools.len() as u32);
|
||||
let version = parent_version + Version::new(4) + Version::new(pools.len() as u32);
|
||||
|
||||
let mut major_map = BTreeMap::new();
|
||||
let mut minor_map = BTreeMap::new();
|
||||
@@ -114,8 +113,17 @@ impl Vecs {
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.pool
|
||||
.validate_computed_version_or_reset(indexer.stores.height_to_coinbase_tag.version())?;
|
||||
let dep_version = indexer.vecs.blocks.coinbase_tag.version();
|
||||
let pool_vec_version = self.pool.header().vec_version();
|
||||
let pool_computed = self.pool.header().computed_version();
|
||||
let expected = pool_vec_version + dep_version;
|
||||
if expected != pool_computed {
|
||||
tracing::warn!(
|
||||
"Pool version mismatch: vec_version={pool_vec_version:?} + dep={dep_version:?} = {expected:?}, stored computed={pool_computed:?}, len={}",
|
||||
self.pool.len()
|
||||
);
|
||||
}
|
||||
self.pool.validate_computed_version_or_reset(dep_version)?;
|
||||
|
||||
let first_txout_index = indexer.vecs.transactions.first_txout_index.reader();
|
||||
let output_type = indexer.vecs.outputs.output_type.reader();
|
||||
@@ -142,12 +150,12 @@ impl Vecs {
|
||||
|
||||
self.pool.truncate_if_needed_at(min)?;
|
||||
|
||||
indexer
|
||||
.stores
|
||||
.height_to_coinbase_tag
|
||||
.iter()
|
||||
.skip(min)
|
||||
.try_for_each(|(_, coinbase_tag)| -> Result<()> {
|
||||
let len = indexer.vecs.blocks.coinbase_tag.len();
|
||||
|
||||
indexer.vecs.blocks.coinbase_tag.try_for_each_range_at(
|
||||
min,
|
||||
len,
|
||||
|coinbase_tag| -> Result<()> {
|
||||
let tx_index = first_tx_index_cursor.next().unwrap();
|
||||
let out_start = first_txout_index.get(tx_index.to_usize());
|
||||
|
||||
@@ -174,12 +182,13 @@ impl Vecs {
|
||||
.map(|bytes| Addr::try_from(&bytes).unwrap())
|
||||
.and_then(|addr| self.pools.find_from_addr(&addr))
|
||||
})
|
||||
.or_else(|| self.pools.find_from_coinbase_tag(&coinbase_tag))
|
||||
.or_else(|| self.pools.find_from_coinbase_tag(&coinbase_tag.as_str()))
|
||||
.unwrap_or(unknown);
|
||||
|
||||
self.pool.push(pool.slug);
|
||||
Ok(())
|
||||
})?;
|
||||
},
|
||||
)?;
|
||||
|
||||
let _lock = exit.lock();
|
||||
self.pool.write()?;
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_reader::{Reader, XOR_LEN, XORBytes};
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BlkPosition, Height, Indexes, TxIndex, Version};
|
||||
use tracing::info;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, Database, Exit, ImportableVec, PcoVec, ReadableVec, Rw, StorageMode,
|
||||
WritableVec,
|
||||
};
|
||||
|
||||
use crate::internal::db_utils::{finalize_db, open_db};
|
||||
|
||||
pub const DB_NAME: &str = "positions";
|
||||
|
||||
#[derive(Traversable)]
|
||||
#[traversable(hidden)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
db: Database,
|
||||
|
||||
pub block: M::Stored<PcoVec<Height, BlkPosition>>,
|
||||
pub tx: M::Stored<PcoVec<TxIndex, BlkPosition>>,
|
||||
}
|
||||
|
||||
impl Vecs {
|
||||
pub(crate) fn forced_import(parent_path: &Path, parent_version: Version) -> Result<Self> {
|
||||
let db = open_db(parent_path, DB_NAME, 1_000_000)?;
|
||||
let version = parent_version;
|
||||
|
||||
let this = Self {
|
||||
block: PcoVec::forced_import(&db, "position", version + Version::TWO)?,
|
||||
tx: PcoVec::forced_import(&db, "position", version + Version::TWO)?,
|
||||
db,
|
||||
};
|
||||
finalize_db(&this.db, &this)?;
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
starting_indexes: &Indexes,
|
||||
reader: &Reader,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.db.sync_bg_tasks()?;
|
||||
|
||||
self.compute_(indexer, starting_indexes, reader, exit)?;
|
||||
let exit = exit.clone();
|
||||
self.db.run_bg(move |db| {
|
||||
let _lock = exit.lock();
|
||||
db.compact_deferred_default()
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_xor_bytes(&mut self, reader: &Reader) -> Result<()> {
|
||||
let xor_path = self.db.path().join("xor.dat");
|
||||
let current = reader.xor_bytes();
|
||||
let cached = fs::read(&xor_path)
|
||||
.ok()
|
||||
.and_then(|b| <[u8; XOR_LEN]>::try_from(b).ok())
|
||||
.map(XORBytes::from);
|
||||
|
||||
match cached {
|
||||
Some(c) if c == current => return Ok(()),
|
||||
Some(_) => {
|
||||
info!("XOR bytes changed, resetting positions...");
|
||||
self.block.reset()?;
|
||||
self.tx.reset()?;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
fs::write(&xor_path, *current)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
starting_indexes: &Indexes,
|
||||
parser: &Reader,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.check_xor_bytes(parser)?;
|
||||
|
||||
// Validate computed versions against dependencies
|
||||
let dep_version = indexer.vecs.transactions.first_tx_index.version()
|
||||
+ indexer.vecs.transactions.height.version();
|
||||
self.block.validate_computed_version_or_reset(dep_version)?;
|
||||
self.tx.validate_computed_version_or_reset(dep_version)?;
|
||||
|
||||
let min_tx_index = TxIndex::from(self.tx.len()).min(starting_indexes.tx_index);
|
||||
|
||||
let Some(min_height) = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.height
|
||||
.collect_one(min_tx_index)
|
||||
.map(|h: Height| h.min(starting_indexes.height))
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let first_tx_at_min_height = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(min_height)
|
||||
.unwrap();
|
||||
|
||||
self.block.truncate_if_needed(min_height)?;
|
||||
self.tx.truncate_if_needed(first_tx_at_min_height)?;
|
||||
|
||||
parser
|
||||
.read(
|
||||
Some(min_height),
|
||||
Some((indexer.vecs.transactions.first_tx_index.len() - 1).into()),
|
||||
)
|
||||
.iter()
|
||||
.try_for_each(|block| -> Result<()> {
|
||||
self.block.push(block.metadata().position());
|
||||
|
||||
block.tx_metadata().iter().for_each(|metadata| {
|
||||
self.tx.push(metadata.position());
|
||||
});
|
||||
|
||||
if *block.height() % 1_000 == 0 {
|
||||
let _lock = exit.lock();
|
||||
self.block.write()?;
|
||||
self.tx.write()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _lock = exit.lock();
|
||||
self.block.write()?;
|
||||
self.tx.write()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ use std::path::Path;
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use vecdb::{Database, ReadableCloneableVec, Rw, StorageMode};
|
||||
use brk_types::{Cents, Dollars, Height};
|
||||
use vecdb::{CachedVec, Database, LazyVecFrom1, ReadableCloneableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
@@ -27,6 +28,11 @@ pub struct Vecs<M: StorageMode = Rw> {
|
||||
#[traversable(skip)]
|
||||
pub db: Database,
|
||||
|
||||
#[traversable(skip)]
|
||||
pub cached_spot_cents: CachedVec<Height, Cents>,
|
||||
#[traversable(skip)]
|
||||
pub cached_spot_usd: LazyVecFrom1<Height, Dollars, Height, Cents>,
|
||||
|
||||
pub split: SplitByUnit<M>,
|
||||
pub ohlc: OhlcByUnit<M>,
|
||||
pub spot: PriceByUnit<M>,
|
||||
@@ -169,6 +175,13 @@ impl Vecs {
|
||||
sats: ohlc_sats,
|
||||
};
|
||||
|
||||
let cached_spot_cents = CachedVec::new(&price_cents.height);
|
||||
let cached_spot_usd = LazyVecFrom1::transformed::<CentsUnsignedToDollars>(
|
||||
"price",
|
||||
version,
|
||||
cached_spot_cents.read_only_boxed_clone(),
|
||||
);
|
||||
|
||||
let spot = PriceByUnit {
|
||||
usd: price_usd,
|
||||
cents: price_cents,
|
||||
@@ -177,6 +190,8 @@ impl Vecs {
|
||||
|
||||
Ok(Self {
|
||||
db: db.clone(),
|
||||
cached_spot_cents,
|
||||
cached_spot_usd,
|
||||
split,
|
||||
ohlc,
|
||||
spot,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{FeeRate, Indexes, Sats};
|
||||
use brk_types::{FeeRate, Indexes, OutPoint, Sats, TxInIndex, VSize};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec, unlikely};
|
||||
|
||||
use super::super::size;
|
||||
@@ -33,26 +33,47 @@ impl Vecs {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.compute_fee_and_fee_rate(size_vecs, starting_indexes, exit)?;
|
||||
self.compute_fees(indexer, indexes, size_vecs, starting_indexes, exit)?;
|
||||
|
||||
let (r3, r4) = rayon::join(
|
||||
let (r1, (r2, r3)) = rayon::join(
|
||||
|| {
|
||||
self.fee
|
||||
.derive_from_with_skip(indexer, indexes, starting_indexes, exit, 1)
|
||||
},
|
||||
|| {
|
||||
self.fee_rate
|
||||
.derive_from_with_skip(indexer, indexes, starting_indexes, exit, 1)
|
||||
rayon::join(
|
||||
|| {
|
||||
self.fee_rate.derive_from_with_skip(
|
||||
indexer,
|
||||
indexes,
|
||||
starting_indexes,
|
||||
exit,
|
||||
1,
|
||||
)
|
||||
},
|
||||
|| {
|
||||
self.effective_fee_rate.derive_from_with_skip(
|
||||
indexer,
|
||||
indexes,
|
||||
starting_indexes,
|
||||
exit,
|
||||
1,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
r1?;
|
||||
r2?;
|
||||
r3?;
|
||||
r4?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_fee_and_fee_rate(
|
||||
fn compute_fees(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
size_vecs: &size::Vecs,
|
||||
starting_indexes: &Indexes,
|
||||
exit: &Exit,
|
||||
@@ -67,6 +88,9 @@ impl Vecs {
|
||||
self.fee_rate
|
||||
.tx_index
|
||||
.validate_computed_version_or_reset(dep_version)?;
|
||||
self.effective_fee_rate
|
||||
.tx_index
|
||||
.validate_computed_version_or_reset(dep_version)?;
|
||||
|
||||
let target = self
|
||||
.input_value
|
||||
@@ -78,6 +102,7 @@ impl Vecs {
|
||||
.tx_index
|
||||
.len()
|
||||
.min(self.fee_rate.tx_index.len())
|
||||
.min(self.effective_fee_rate.tx_index.len())
|
||||
.min(starting_indexes.tx_index.to_usize());
|
||||
|
||||
if min >= target {
|
||||
@@ -90,39 +115,171 @@ impl Vecs {
|
||||
self.fee_rate
|
||||
.tx_index
|
||||
.truncate_if_needed(starting_indexes.tx_index)?;
|
||||
self.effective_fee_rate
|
||||
.tx_index
|
||||
.truncate_if_needed(starting_indexes.tx_index)?;
|
||||
|
||||
loop {
|
||||
let skip = self.fee.tx_index.len();
|
||||
let end = self.fee.tx_index.batch_end(target);
|
||||
if skip >= end {
|
||||
let start_tx = self.fee.tx_index.len();
|
||||
let max_height = indexer.vecs.transactions.first_tx_index.len();
|
||||
|
||||
let start_height = if start_tx == 0 {
|
||||
0
|
||||
} else {
|
||||
indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.height
|
||||
.collect_one_at(start_tx)
|
||||
.unwrap()
|
||||
.to_usize()
|
||||
};
|
||||
|
||||
for h in start_height..max_height {
|
||||
let first_tx: usize = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one_at(h)
|
||||
.unwrap()
|
||||
.to_usize();
|
||||
let n = *indexes.height.tx_index_count.collect_one_at(h).unwrap() as usize;
|
||||
|
||||
if first_tx + n > target {
|
||||
break;
|
||||
}
|
||||
|
||||
let input_batch = self.input_value.collect_range_at(skip, end);
|
||||
let output_batch = self.output_value.collect_range_at(skip, end);
|
||||
let vsize_batch = size_vecs.vsize.tx_index.collect_range_at(skip, end);
|
||||
// Batch read all per-tx data for this block
|
||||
let input_values = self.input_value.collect_range_at(first_tx, first_tx + n);
|
||||
let output_values = self.output_value.collect_range_at(first_tx, first_tx + n);
|
||||
let vsizes: Vec<VSize> = size_vecs
|
||||
.vsize
|
||||
.tx_index
|
||||
.collect_range_at(first_tx, first_tx + n);
|
||||
let txin_starts: Vec<TxInIndex> = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txin_index
|
||||
.collect_range_at(first_tx, first_tx + n);
|
||||
let input_begin = txin_starts[0].to_usize();
|
||||
let input_end = if h + 1 < max_height {
|
||||
indexer
|
||||
.vecs
|
||||
.inputs
|
||||
.first_txin_index
|
||||
.collect_one_at(h + 1)
|
||||
.unwrap()
|
||||
.to_usize()
|
||||
} else {
|
||||
indexer.vecs.inputs.outpoint.len()
|
||||
};
|
||||
let outpoints: Vec<OutPoint> = indexer
|
||||
.vecs
|
||||
.inputs
|
||||
.outpoint
|
||||
.collect_range_at(input_begin, input_end);
|
||||
|
||||
for j in 0..input_batch.len() {
|
||||
let fee = if unlikely(input_batch[j].is_max()) {
|
||||
// Compute fee + fee_rate per tx
|
||||
let mut fees = Vec::with_capacity(n);
|
||||
for j in 0..n {
|
||||
let fee = if unlikely(input_values[j].is_max()) {
|
||||
Sats::ZERO
|
||||
} else {
|
||||
input_batch[j] - output_batch[j]
|
||||
input_values[j] - output_values[j]
|
||||
};
|
||||
self.fee.tx_index.push(fee);
|
||||
self.fee_rate
|
||||
.tx_index
|
||||
.push(FeeRate::from((fee, vsize_batch[j])));
|
||||
self.fee_rate.tx_index.push(FeeRate::from((fee, vsizes[j])));
|
||||
fees.push(fee);
|
||||
}
|
||||
|
||||
let _lock = exit.lock();
|
||||
let (r1, r2) = rayon::join(
|
||||
|| self.fee.tx_index.write(),
|
||||
|| self.fee_rate.tx_index.write(),
|
||||
// Effective fee rate via same-block CPFP clustering
|
||||
let effective = cluster_fee_rates(
|
||||
&txin_starts,
|
||||
&outpoints,
|
||||
input_begin,
|
||||
first_tx,
|
||||
&fees,
|
||||
&vsizes,
|
||||
);
|
||||
r1?;
|
||||
r2?;
|
||||
for rate in effective {
|
||||
self.effective_fee_rate.tx_index.push(rate);
|
||||
}
|
||||
|
||||
if h % 1_000 == 0 {
|
||||
let _lock = exit.lock();
|
||||
self.fee.tx_index.write()?;
|
||||
self.fee_rate.tx_index.write()?;
|
||||
self.effective_fee_rate.tx_index.write()?;
|
||||
}
|
||||
}
|
||||
|
||||
let _lock = exit.lock();
|
||||
self.fee.tx_index.write()?;
|
||||
self.fee_rate.tx_index.write()?;
|
||||
self.effective_fee_rate.tx_index.write()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Clusters same-block parent-child txs and computes effective fee rate per cluster.
|
||||
fn cluster_fee_rates(
|
||||
txin_starts: &[TxInIndex],
|
||||
outpoints: &[OutPoint],
|
||||
outpoint_base: usize,
|
||||
first_tx: usize,
|
||||
fees: &[Sats],
|
||||
vsizes: &[VSize],
|
||||
) -> Vec<FeeRate> {
|
||||
let n = fees.len();
|
||||
let mut parent: Vec<usize> = (0..n).collect();
|
||||
|
||||
for j in 1..n {
|
||||
let start = txin_starts[j].to_usize() - outpoint_base;
|
||||
let end = if j + 1 < txin_starts.len() {
|
||||
txin_starts[j + 1].to_usize() - outpoint_base
|
||||
} else {
|
||||
outpoints.len()
|
||||
};
|
||||
|
||||
for op in &outpoints[start..end] {
|
||||
if op.is_coinbase() {
|
||||
continue;
|
||||
}
|
||||
let parent_tx = op.tx_index().to_usize();
|
||||
if parent_tx >= first_tx && parent_tx < first_tx + n {
|
||||
union(&mut parent, j, parent_tx - first_tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cluster_fee = vec![Sats::ZERO; n];
|
||||
let mut cluster_vsize = vec![VSize::from(0u64); n];
|
||||
for j in 0..n {
|
||||
let root = find(&mut parent, j);
|
||||
cluster_fee[root] += fees[j];
|
||||
cluster_vsize[root] += vsizes[j];
|
||||
}
|
||||
|
||||
(0..n)
|
||||
.map(|j| {
|
||||
let root = find(&mut parent, j);
|
||||
FeeRate::from((cluster_fee[root], cluster_vsize[root]))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn find(parent: &mut [usize], mut i: usize) -> usize {
|
||||
while parent[i] != i {
|
||||
parent[i] = parent[parent[i]];
|
||||
i = parent[i];
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
fn union(parent: &mut [usize], a: usize, b: usize) {
|
||||
let ra = find(parent, a);
|
||||
let rb = find(parent, b);
|
||||
if ra != rb {
|
||||
parent[ra] = rb;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use vecdb::{Database, EagerVec, ImportableVec};
|
||||
use super::Vecs;
|
||||
use crate::{indexes, internal::PerTxDistribution};
|
||||
|
||||
/// Bump this when fee/feerate aggregation logic changes (e.g., skip coinbase).
|
||||
const VERSION: Version = Version::new(2);
|
||||
/// Bump this when fee/feerate aggregation logic changes (e.g., skip coinbase, skip zero-fee).
|
||||
const VERSION: Version = Version::new(3);
|
||||
|
||||
impl Vecs {
|
||||
pub(crate) fn forced_import(
|
||||
@@ -20,6 +20,12 @@ impl Vecs {
|
||||
output_value: EagerVec::forced_import(db, "output_value", version)?,
|
||||
fee: PerTxDistribution::forced_import(db, "fee", v, indexes)?,
|
||||
fee_rate: PerTxDistribution::forced_import(db, "fee_rate", v, indexes)?,
|
||||
effective_fee_rate: PerTxDistribution::forced_import(
|
||||
db,
|
||||
"effective_fee_rate",
|
||||
v,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub output_value: M::Stored<EagerVec<PcoVec<TxIndex, Sats>>>,
|
||||
pub fee: PerTxDistribution<Sats, M>,
|
||||
pub fee_rate: PerTxDistribution<FeeRate, M>,
|
||||
pub effective_fee_rate: PerTxDistribution<FeeRate, M>,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ exclude = ["examples/"]
|
||||
bitcoin = { workspace = true }
|
||||
brk_error = { workspace = true, features = ["fjall", "vecdb"] }
|
||||
brk_cohort = { workspace = true }
|
||||
brk_iterator = { workspace = true }
|
||||
brk_logger = { workspace = true }
|
||||
brk_reader = { workspace = true }
|
||||
brk_rpc = { workspace = true, features = ["corepc"] }
|
||||
@@ -20,11 +19,11 @@ brk_store = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
brk_traversable = { workspace = true }
|
||||
fjall = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rlimit = "0.11.0"
|
||||
rustc-hash = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::{
|
||||
|
||||
use brk_alloc::Mimalloc;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use tracing::{debug, info};
|
||||
@@ -33,9 +32,6 @@ fn main() -> color_eyre::Result<()> {
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
debug!("Reader created.");
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
debug!("Blocks created.");
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
debug!("Indexer imported.");
|
||||
|
||||
@@ -44,7 +40,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
loop {
|
||||
let i = Instant::now();
|
||||
indexer.checked_index(&blocks, &client, &exit)?;
|
||||
indexer.checked_index(&reader, &client, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
@@ -9,7 +9,6 @@ use brk_alloc::Mimalloc;
|
||||
use brk_bencher::Bencher;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use tracing::{debug, info};
|
||||
@@ -33,8 +32,6 @@ fn main() -> Result<()> {
|
||||
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
let mut bencher =
|
||||
@@ -50,7 +47,7 @@ fn main() -> Result<()> {
|
||||
});
|
||||
|
||||
let i = Instant::now();
|
||||
indexer.index(&blocks, &client, &exit)?;
|
||||
indexer.index(&reader, &client, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
// We want to benchmark the drop too
|
||||
|
||||
@@ -9,7 +9,6 @@ use brk_alloc::Mimalloc;
|
||||
use brk_bencher::Bencher;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use tracing::{debug, info};
|
||||
@@ -33,8 +32,6 @@ fn main() -> Result<()> {
|
||||
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
let mut bencher =
|
||||
@@ -51,7 +48,7 @@ fn main() -> Result<()> {
|
||||
|
||||
loop {
|
||||
let i = Instant::now();
|
||||
indexer.index(&blocks, &client, &exit)?;
|
||||
indexer.index(&reader, &client, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
@@ -4,7 +4,7 @@ use brk_types::{TxIndex, Txid, TxidPrefix, Version};
|
||||
|
||||
// One version for all data sources
|
||||
// Increment on **change _OR_ addition**
|
||||
pub const VERSION: Version = Version::new(25);
|
||||
pub const VERSION: Version = Version::new(26);
|
||||
pub const SNAPSHOT_BLOCK_RANGE: usize = 1_000;
|
||||
|
||||
/// Known duplicate Bitcoin transactions (BIP30)
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
thread::{self, sleep},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::{Reader, XORBytes};
|
||||
use brk_rpc::Client;
|
||||
use brk_types::Height;
|
||||
use brk_types::{BlockHash, Height};
|
||||
use fjall::PersistMode;
|
||||
use parking_lot::RwLock;
|
||||
use tracing::{debug, info};
|
||||
use vecdb::{Exit, RawDBError, ReadOnlyClone, ReadableVec, Ro, Rw, StorageMode};
|
||||
use vecdb::{
|
||||
Exit, RawDBError, ReadOnlyClone, ReadableVec, Ro, Rw, StorageMode, WritableVec, unlikely,
|
||||
};
|
||||
mod constants;
|
||||
mod indexes;
|
||||
mod processor;
|
||||
@@ -31,8 +35,16 @@ pub use stores::Stores;
|
||||
pub use vecs::*;
|
||||
|
||||
pub struct Indexer<M: StorageMode = Rw> {
|
||||
path: PathBuf,
|
||||
pub vecs: Vecs<M>,
|
||||
pub stores: Stores,
|
||||
tip_blockhash: Arc<RwLock<BlockHash>>,
|
||||
}
|
||||
|
||||
impl<M: StorageMode> Indexer<M> {
|
||||
pub fn tip_blockhash(&self) -> BlockHash {
|
||||
self.tip_blockhash.read().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadOnlyClone for Indexer {
|
||||
@@ -40,8 +52,10 @@ impl ReadOnlyClone for Indexer {
|
||||
|
||||
fn read_only_clone(&self) -> Indexer<Ro> {
|
||||
Indexer {
|
||||
path: self.path.clone(),
|
||||
vecs: self.vecs.read_only_clone(),
|
||||
stores: self.stores.clone(),
|
||||
tip_blockhash: self.tip_blockhash.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,14 +66,6 @@ impl Indexer {
|
||||
}
|
||||
|
||||
fn forced_import_inner(outputs_dir: &Path, can_retry: bool) -> Result<Self> {
|
||||
info!("Increasing number of open files limit...");
|
||||
let no_file_limit = rlimit::getrlimit(rlimit::Resource::NOFILE)?;
|
||||
rlimit::setrlimit(
|
||||
rlimit::Resource::NOFILE,
|
||||
no_file_limit.0.max(10_000),
|
||||
no_file_limit.1,
|
||||
)?;
|
||||
|
||||
info!("Importing indexer...");
|
||||
|
||||
let indexed_path = outputs_dir.join("indexed");
|
||||
@@ -73,7 +79,14 @@ impl Indexer {
|
||||
let stores = Stores::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported stores in {:?}", i.elapsed());
|
||||
|
||||
Ok(Self { vecs, stores })
|
||||
let tip_blockhash = vecs.blocks.blockhash.collect_last().unwrap_or_default();
|
||||
|
||||
Ok(Self {
|
||||
path: indexed_path.clone(),
|
||||
vecs,
|
||||
stores,
|
||||
tip_blockhash: Arc::new(RwLock::new(tip_blockhash)),
|
||||
})
|
||||
};
|
||||
|
||||
match try_import() {
|
||||
@@ -93,28 +106,57 @@ impl Indexer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(&mut self, blocks: &Blocks, client: &Client, exit: &Exit) -> Result<Indexes> {
|
||||
self.index_(blocks, client, exit, false)
|
||||
/// Fully resets the indexer by deleting stores from disk and reimporting.
|
||||
/// Unlike stores.reset() which uses keyspace.clear() (leaving a journal
|
||||
/// record that gets replayed on every recovery), this cleanly recreates.
|
||||
fn full_reset(&mut self) -> Result<()> {
|
||||
info!("Full reset...");
|
||||
self.vecs.reset()?;
|
||||
let stores_path = self.path.join("stores");
|
||||
fs::remove_dir_all(&stores_path).ok();
|
||||
self.stores = Stores::forced_import(&self.path, VERSION)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn index(&mut self, reader: &Reader, client: &Client, exit: &Exit) -> Result<Indexes> {
|
||||
self.index_(reader, client, exit, false)
|
||||
}
|
||||
|
||||
pub fn checked_index(
|
||||
&mut self,
|
||||
blocks: &Blocks,
|
||||
reader: &Reader,
|
||||
client: &Client,
|
||||
exit: &Exit,
|
||||
) -> Result<Indexes> {
|
||||
self.index_(blocks, client, exit, true)
|
||||
self.index_(reader, client, exit, true)
|
||||
}
|
||||
|
||||
fn check_xor_bytes(&mut self, reader: &Reader) -> Result<()> {
|
||||
let current = reader.xor_bytes();
|
||||
let cached = XORBytes::from(self.path.as_path());
|
||||
|
||||
if cached == current {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.full_reset()?;
|
||||
|
||||
fs::write(self.path.join("xor.dat"), *current)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn index_(
|
||||
&mut self,
|
||||
blocks: &Blocks,
|
||||
reader: &Reader,
|
||||
client: &Client,
|
||||
exit: &Exit,
|
||||
check_collisions: bool,
|
||||
) -> Result<Indexes> {
|
||||
self.vecs.db.sync_bg_tasks()?;
|
||||
|
||||
self.check_xor_bytes(reader)?;
|
||||
|
||||
debug!("Starting indexing...");
|
||||
|
||||
let last_blockhash = self.vecs.blocks.blockhash.collect_last();
|
||||
@@ -139,8 +181,7 @@ impl Indexer {
|
||||
}
|
||||
None => {
|
||||
info!("Data inconsistency detected, resetting indexer...");
|
||||
self.vecs.reset()?;
|
||||
self.stores.reset()?;
|
||||
self.full_reset()?;
|
||||
(Indexes::default(), None)
|
||||
}
|
||||
}
|
||||
@@ -172,13 +213,13 @@ impl Indexer {
|
||||
let stores_res = s.spawn(|| -> Result<()> {
|
||||
let i = Instant::now();
|
||||
stores.commit(height)?;
|
||||
info!("Stores exported in {:?}", i.elapsed());
|
||||
debug!("Stores exported in {:?}", i.elapsed());
|
||||
Ok(())
|
||||
});
|
||||
let vecs_res = s.spawn(|| -> Result<()> {
|
||||
let i = Instant::now();
|
||||
vecs.flush(height)?;
|
||||
info!("Vecs exported in {:?}", i.elapsed());
|
||||
debug!("Vecs exported in {:?}", i.elapsed());
|
||||
Ok(())
|
||||
});
|
||||
stores_res.join().unwrap()?;
|
||||
@@ -195,13 +236,22 @@ impl Indexer {
|
||||
let vecs = &mut self.vecs;
|
||||
let stores = &mut self.stores;
|
||||
|
||||
for block in blocks.after(prev_hash)? {
|
||||
for block in reader.after(prev_hash)?.iter() {
|
||||
let height = block.height();
|
||||
|
||||
info!("Indexing block {height}...");
|
||||
if unlikely(height.is_multiple_of(100)) {
|
||||
info!("Indexing block {height}...");
|
||||
} else {
|
||||
debug!("Indexing block {height}...");
|
||||
}
|
||||
|
||||
indexes.height = height;
|
||||
|
||||
vecs.blocks.position.push(block.metadata().position());
|
||||
block.tx_metadata().iter().for_each(|m| {
|
||||
vecs.transactions.position.push(m.position());
|
||||
});
|
||||
|
||||
let mut processor = BlockProcessor {
|
||||
block: &block,
|
||||
height,
|
||||
@@ -252,6 +302,8 @@ impl Indexer {
|
||||
export(stores, vecs, height)?;
|
||||
readers = Readers::new(vecs);
|
||||
}
|
||||
|
||||
*self.tip_blockhash.write() = block.block_hash().into();
|
||||
}
|
||||
|
||||
drop(readers);
|
||||
@@ -266,21 +318,26 @@ impl Indexer {
|
||||
|
||||
sleep(Duration::from_secs(5));
|
||||
|
||||
info!("Exporting...");
|
||||
let i = Instant::now();
|
||||
|
||||
if !tasks.is_empty() {
|
||||
let i = Instant::now();
|
||||
for task in tasks {
|
||||
task().map_err(vecdb::RawDBError::other)?;
|
||||
}
|
||||
info!("Stores committed in {:?}", i.elapsed());
|
||||
debug!("Stores committed in {:?}", i.elapsed());
|
||||
|
||||
let i = Instant::now();
|
||||
fjall_db
|
||||
.persist(PersistMode::SyncData)
|
||||
.map_err(RawDBError::other)?;
|
||||
info!("Stores persisted in {:?}", i.elapsed());
|
||||
debug!("Stores persisted in {:?}", i.elapsed());
|
||||
}
|
||||
|
||||
db.compact()?;
|
||||
|
||||
info!("Exported in {:?}", i.elapsed());
|
||||
Ok(())
|
||||
});
|
||||
|
||||
|
||||
@@ -28,14 +28,14 @@ impl BlockProcessor<'_> {
|
||||
.blockhash_prefix_to_height
|
||||
.insert(blockhash_prefix, height);
|
||||
|
||||
self.stores
|
||||
.height_to_coinbase_tag
|
||||
.insert(height, self.block.coinbase_tag().into());
|
||||
|
||||
self.vecs
|
||||
.blocks
|
||||
.blockhash
|
||||
.checked_push(height, blockhash.clone())?;
|
||||
self.vecs
|
||||
.blocks
|
||||
.coinbase_tag
|
||||
.checked_push(height, self.block.coinbase_tag())?;
|
||||
self.vecs
|
||||
.blocks
|
||||
.difficulty
|
||||
@@ -53,21 +53,28 @@ impl BlockProcessor<'_> {
|
||||
pub fn push_block_size_and_weight(&mut self, txs: &[ComputedTx]) -> Result<()> {
|
||||
let overhead = bitcoin::block::Header::SIZE + bitcoin::VarInt::from(txs.len()).size();
|
||||
let mut total_size = overhead;
|
||||
let mut weight_wu = overhead * 4;
|
||||
for ct in txs {
|
||||
let base = ct.base_size as usize;
|
||||
let total = ct.total_size as usize;
|
||||
total_size += total;
|
||||
weight_wu += base * 3 + total;
|
||||
let mut weight = overhead * 4;
|
||||
let mut sw_txs = 0u32;
|
||||
let mut sw_size = 0usize;
|
||||
let mut sw_weight = 0usize;
|
||||
|
||||
for (i, tx) in txs.iter().enumerate() {
|
||||
total_size += tx.total_size as usize;
|
||||
weight += tx.weight();
|
||||
if i > 0 && tx.is_segwit() {
|
||||
sw_txs += 1;
|
||||
sw_size += tx.total_size as usize;
|
||||
sw_weight += tx.weight();
|
||||
}
|
||||
}
|
||||
self.vecs
|
||||
.blocks
|
||||
.total
|
||||
.checked_push(self.height, total_size.into())?;
|
||||
self.vecs
|
||||
.blocks
|
||||
.weight
|
||||
.checked_push(self.height, weight_wu.into())?;
|
||||
|
||||
let h = self.height;
|
||||
let blocks = &mut self.vecs.blocks;
|
||||
blocks.total.checked_push(h, total_size.into())?;
|
||||
blocks.weight.checked_push(h, weight.into())?;
|
||||
blocks.segwit_txs.checked_push(h, sw_txs.into())?;
|
||||
blocks.segwit_size.checked_push(h, sw_size.into())?;
|
||||
blocks.segwit_weight.checked_push(h, sw_weight.into())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,18 @@ pub struct ComputedTx<'a> {
|
||||
pub total_size: u32,
|
||||
}
|
||||
|
||||
impl ComputedTx<'_> {
|
||||
#[inline]
|
||||
pub fn is_segwit(&self) -> bool {
|
||||
self.base_size != self.total_size
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn weight(&self) -> usize {
|
||||
self.base_size as usize * 3 + self.total_size as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Reusable buffers cleared and refilled each block to avoid allocation churn.
|
||||
#[derive(Default)]
|
||||
pub struct BlockBuffers {
|
||||
|
||||
@@ -7,11 +7,11 @@ use brk_error::Result;
|
||||
use brk_store::{AnyStore, Kind, Mode, Store};
|
||||
use brk_types::{
|
||||
AddrHash, AddrIndexOutPoint, AddrIndexTxIndex, BlockHashPrefix, Height, OutPoint, OutputType,
|
||||
StoredString, TxIndex, TxOutIndex, TxidPrefix, TypeIndex, Unit, Version, Vout,
|
||||
TxIndex, TxOutIndex, TxidPrefix, TypeIndex, Unit, Version, Vout,
|
||||
};
|
||||
use fjall::{Database, PersistMode};
|
||||
use rayon::prelude::*;
|
||||
use tracing::info;
|
||||
use tracing::{debug, info};
|
||||
use vecdb::{AnyVec, ReadableVec, VecIndex};
|
||||
|
||||
use crate::{Indexes, constants::DUPLICATE_TXID_PREFIXES};
|
||||
@@ -26,7 +26,6 @@ pub struct Stores {
|
||||
pub addr_type_to_addr_index_and_tx_index: ByAddrType<Store<AddrIndexTxIndex, Unit>>,
|
||||
pub addr_type_to_addr_index_and_unspent_outpoint: ByAddrType<Store<AddrIndexOutPoint, Unit>>,
|
||||
pub blockhash_prefix_to_height: Store<BlockHashPrefix, Height>,
|
||||
pub height_to_coinbase_tag: Store<Height, StoredString>,
|
||||
pub txid_prefix_to_tx_index: Store<TxidPrefix, TxIndex>,
|
||||
}
|
||||
|
||||
@@ -43,7 +42,8 @@ impl Stores {
|
||||
|
||||
let database = match brk_store::open_database(path) {
|
||||
Ok(database) => database,
|
||||
Err(_) if can_retry => {
|
||||
Err(err) if can_retry => {
|
||||
info!("Failed to open stores at {path:?}: {err:?}, deleting and retrying");
|
||||
fs::remove_dir_all(path)?;
|
||||
return Self::forced_import_inner(parent, version, false);
|
||||
}
|
||||
@@ -85,17 +85,9 @@ impl Stores {
|
||||
)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
let stores = Self {
|
||||
db: database.clone(),
|
||||
|
||||
height_to_coinbase_tag: Store::import(
|
||||
database_ref,
|
||||
path,
|
||||
"height_to_coinbase_tag",
|
||||
version,
|
||||
Mode::PushOnly,
|
||||
Kind::Sequential,
|
||||
)?,
|
||||
addr_type_to_addr_hash_to_addr_index: ByAddrType::new_with_index(
|
||||
create_addr_hash_to_addr_index_store,
|
||||
)?,
|
||||
@@ -122,7 +114,9 @@ impl Stores {
|
||||
Kind::Recent,
|
||||
5,
|
||||
)?,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(stores)
|
||||
}
|
||||
|
||||
pub fn starting_height(&self) -> Height {
|
||||
@@ -135,7 +129,6 @@ impl Stores {
|
||||
fn iter_any(&self) -> impl Iterator<Item = &dyn AnyStore> {
|
||||
[
|
||||
&self.blockhash_prefix_to_height as &dyn AnyStore,
|
||||
&self.height_to_coinbase_tag,
|
||||
&self.txid_prefix_to_tx_index,
|
||||
]
|
||||
.into_iter()
|
||||
@@ -159,7 +152,6 @@ impl Stores {
|
||||
fn par_iter_any_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStore> {
|
||||
[
|
||||
&mut self.blockhash_prefix_to_height as &mut dyn AnyStore,
|
||||
&mut self.height_to_coinbase_tag,
|
||||
&mut self.txid_prefix_to_tx_index,
|
||||
]
|
||||
.into_par_iter()
|
||||
@@ -184,11 +176,11 @@ impl Stores {
|
||||
let i = Instant::now();
|
||||
self.par_iter_any_mut()
|
||||
.try_for_each(|store| store.commit(height))?;
|
||||
info!("Stores committed in {:?}", i.elapsed());
|
||||
debug!("Stores committed in {:?}", i.elapsed());
|
||||
|
||||
let i = Instant::now();
|
||||
self.db.persist(PersistMode::SyncData)?;
|
||||
info!("Stores persisted in {:?}", i.elapsed());
|
||||
debug!("Stores persisted in {:?}", i.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -210,7 +202,6 @@ impl Stores {
|
||||
}
|
||||
|
||||
take!(self.blockhash_prefix_to_height);
|
||||
take!(self.height_to_coinbase_tag);
|
||||
take!(self.txid_prefix_to_tx_index);
|
||||
|
||||
for store in self.addr_type_to_addr_hash_to_addr_index.values_mut() {
|
||||
@@ -257,7 +248,6 @@ impl Stores {
|
||||
fn is_empty(&self) -> Result<bool> {
|
||||
Ok(self.blockhash_prefix_to_height.is_empty()?
|
||||
&& self.txid_prefix_to_tx_index.is_empty()?
|
||||
&& self.height_to_coinbase_tag.is_empty()?
|
||||
&& self
|
||||
.addr_type_to_addr_hash_to_addr_index
|
||||
.values()
|
||||
@@ -286,12 +276,6 @@ impl Stores {
|
||||
},
|
||||
);
|
||||
|
||||
(starting_indexes.height.to_usize()..vecs.blocks.blockhash.len())
|
||||
.map(Height::from)
|
||||
.for_each(|h| {
|
||||
self.height_to_coinbase_tag.remove(h);
|
||||
});
|
||||
|
||||
for addr_type in OutputType::ADDR_TYPES {
|
||||
for hash in vecs.iter_addr_hashes_from(addr_type, starting_indexes.height)? {
|
||||
self.addr_type_to_addr_hash_to_addr_index
|
||||
@@ -418,16 +402,5 @@ impl Stores {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<()> {
|
||||
info!("Resetting stores...");
|
||||
|
||||
// Clear all stores (both in-memory buffers and on-disk keyspaces)
|
||||
self.par_iter_any_mut()
|
||||
.try_for_each(|store| store.reset())?;
|
||||
|
||||
// Persist the cleared state
|
||||
self.db.persist(PersistMode::SyncAll)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BlockHash, Height, StoredF64, StoredU64, Timestamp, Version, Weight};
|
||||
use brk_types::{
|
||||
BlkPosition, BlockHash, CoinbaseTag, Height, StoredF64, StoredU32, StoredU64, Timestamp,
|
||||
Version, Weight,
|
||||
};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, BytesVec, Database, ImportableVec, PcoVec, Rw, Stamp, StorageMode, WritableVec,
|
||||
@@ -11,6 +14,7 @@ use crate::parallel_import;
|
||||
#[derive(Traversable)]
|
||||
pub struct BlocksVecs<M: StorageMode = Rw> {
|
||||
pub blockhash: M::Stored<BytesVec<Height, BlockHash>>,
|
||||
pub coinbase_tag: M::Stored<BytesVec<Height, CoinbaseTag>>,
|
||||
#[traversable(wrap = "difficulty", rename = "value")]
|
||||
pub difficulty: M::Stored<PcoVec<Height, StoredF64>>,
|
||||
/// Doesn't guarantee continuity due to possible reorgs and more generally the nature of mining
|
||||
@@ -20,45 +24,85 @@ pub struct BlocksVecs<M: StorageMode = Rw> {
|
||||
pub total: M::Stored<PcoVec<Height, StoredU64>>,
|
||||
#[traversable(wrap = "weight", rename = "base")]
|
||||
pub weight: M::Stored<PcoVec<Height, Weight>>,
|
||||
#[traversable(hidden)]
|
||||
pub position: M::Stored<PcoVec<Height, BlkPosition>>,
|
||||
pub segwit_txs: M::Stored<PcoVec<Height, StoredU32>>,
|
||||
pub segwit_size: M::Stored<PcoVec<Height, StoredU64>>,
|
||||
pub segwit_weight: M::Stored<PcoVec<Height, Weight>>,
|
||||
}
|
||||
|
||||
impl BlocksVecs {
|
||||
pub fn forced_import(db: &Database, version: Version) -> Result<Self> {
|
||||
let (blockhash, difficulty, timestamp, total, weight) = parallel_import! {
|
||||
blockhash = BytesVec::forced_import(db, "blockhash", version),
|
||||
difficulty = PcoVec::forced_import(db, "difficulty", version),
|
||||
timestamp = PcoVec::forced_import(db, "timestamp", version),
|
||||
total_size = PcoVec::forced_import(db, "total_size", version),
|
||||
weight = PcoVec::forced_import(db, "block_weight", version),
|
||||
};
|
||||
Ok(Self {
|
||||
let (
|
||||
blockhash,
|
||||
coinbase_tag,
|
||||
difficulty,
|
||||
timestamp,
|
||||
total,
|
||||
weight,
|
||||
position,
|
||||
segwit_txs,
|
||||
segwit_size,
|
||||
segwit_weight,
|
||||
) = parallel_import! {
|
||||
blockhash = BytesVec::forced_import(db, "blockhash", version),
|
||||
coinbase_tag = BytesVec::forced_import(db, "coinbase_tag", version),
|
||||
difficulty = PcoVec::forced_import(db, "difficulty", version),
|
||||
timestamp = PcoVec::forced_import(db, "timestamp", version),
|
||||
total_size = PcoVec::forced_import(db, "total_size", version),
|
||||
weight = PcoVec::forced_import(db, "block_weight", version),
|
||||
position = PcoVec::forced_import(db, "block_position", version),
|
||||
segwit_txs = PcoVec::forced_import(db, "segwit_txs", version),
|
||||
segwit_size = PcoVec::forced_import(db, "segwit_size", version),
|
||||
segwit_weight = PcoVec::forced_import(db, "segwit_weight", version),
|
||||
};
|
||||
Ok(Self {
|
||||
blockhash,
|
||||
coinbase_tag,
|
||||
difficulty,
|
||||
timestamp,
|
||||
total,
|
||||
weight,
|
||||
position,
|
||||
segwit_txs,
|
||||
segwit_size,
|
||||
segwit_weight,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn truncate(&mut self, height: Height, stamp: Stamp) -> Result<()> {
|
||||
self.blockhash
|
||||
.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.coinbase_tag
|
||||
.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.difficulty
|
||||
.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.timestamp
|
||||
.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.total.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.weight.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.position.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.segwit_txs
|
||||
.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.segwit_size
|
||||
.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
self.segwit_weight
|
||||
.truncate_if_needed_with_stamp(height, stamp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn par_iter_mut_any(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
[
|
||||
&mut self.blockhash as &mut dyn AnyStoredVec,
|
||||
&mut self.coinbase_tag,
|
||||
&mut self.difficulty,
|
||||
&mut self.timestamp,
|
||||
&mut self.total,
|
||||
&mut self.weight,
|
||||
&mut self.position,
|
||||
&mut self.segwit_txs,
|
||||
&mut self.segwit_size,
|
||||
&mut self.segwit_weight,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{
|
||||
Height, RawLockTime, StoredBool, StoredU32, TxInIndex, TxIndex, TxOutIndex, TxVersion, Txid,
|
||||
Version,
|
||||
BlkPosition, Height, RawLockTime, StoredBool, StoredU32, TxInIndex, TxIndex, TxOutIndex,
|
||||
TxVersion, Txid, Version,
|
||||
};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
@@ -23,6 +23,8 @@ pub struct TransactionsVecs<M: StorageMode = Rw> {
|
||||
pub is_explicitly_rbf: M::Stored<PcoVec<TxIndex, StoredBool>>,
|
||||
pub first_txin_index: M::Stored<PcoVec<TxIndex, TxInIndex>>,
|
||||
pub first_txout_index: M::Stored<BytesVec<TxIndex, TxOutIndex>>,
|
||||
#[traversable(hidden)]
|
||||
pub position: M::Stored<PcoVec<TxIndex, BlkPosition>>,
|
||||
}
|
||||
|
||||
pub struct TxMetadataVecs<'a> {
|
||||
@@ -70,6 +72,7 @@ impl TransactionsVecs {
|
||||
is_explicitly_rbf,
|
||||
first_txin_index,
|
||||
first_txout_index,
|
||||
position,
|
||||
) = parallel_import! {
|
||||
first_tx_index = PcoVec::forced_import(db, "first_tx_index", version),
|
||||
height = PcoVec::forced_import(db, "height", version),
|
||||
@@ -81,6 +84,7 @@ impl TransactionsVecs {
|
||||
is_explicitly_rbf = PcoVec::forced_import(db, "is_explicitly_rbf", version),
|
||||
first_txin_index = PcoVec::forced_import(db, "first_txin_index", version),
|
||||
first_txout_index = BytesVec::forced_import(db, "first_txout_index", version),
|
||||
position = PcoVec::forced_import(db, "tx_position", version),
|
||||
};
|
||||
Ok(Self {
|
||||
first_tx_index,
|
||||
@@ -93,6 +97,7 @@ impl TransactionsVecs {
|
||||
is_explicitly_rbf,
|
||||
first_txin_index,
|
||||
first_txout_index,
|
||||
position,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -115,6 +120,8 @@ impl TransactionsVecs {
|
||||
.truncate_if_needed_with_stamp(tx_index, stamp)?;
|
||||
self.first_txout_index
|
||||
.truncate_if_needed_with_stamp(tx_index, stamp)?;
|
||||
self.position
|
||||
.truncate_if_needed_with_stamp(tx_index, stamp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -130,6 +137,7 @@ impl TransactionsVecs {
|
||||
&mut self.is_explicitly_rbf,
|
||||
&mut self.first_txin_index,
|
||||
&mut self.first_txout_index,
|
||||
&mut self.position,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn init(path: Option<&Path>) -> io::Result<()> {
|
||||
|
||||
let directives = std::env::var("RUST_LOG").unwrap_or_else(|_| {
|
||||
format!(
|
||||
"{level},bitcoin=off,bitcoincore_rpc=off,corepc=off,fjall=off,brk_fjall=off,lsm_tree=off,brk_rolldown=off,rolldown=off,tracing=off,aide=off,rustls=off,notify=off,oxc_resolver=off,tower_http=off"
|
||||
"{level},bitcoin=off,bitcoincore_rpc=off,corepc=off,tracing=off,aide=off,fjall=off,lsm_tree=off,tower_http=off"
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_types::{FeeRate, MempoolEntryInfo, Sats, Txid, TxidPrefix, VSize};
|
||||
use brk_types::{FeeRate, MempoolEntryInfo, Sats, Timestamp, Txid, TxidPrefix, VSize};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A mempool transaction entry.
|
||||
@@ -16,6 +16,8 @@ pub struct Entry {
|
||||
pub ancestor_vsize: VSize,
|
||||
/// Parent txid prefixes (most txs have 0-2 parents)
|
||||
pub depends: SmallVec<[TxidPrefix; 2]>,
|
||||
/// When this tx was first seen in the mempool
|
||||
pub first_seen: Timestamp,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
@@ -27,6 +29,7 @@ impl Entry {
|
||||
ancestor_fee: info.ancestor_fee,
|
||||
ancestor_vsize: VSize::from(info.ancestor_size),
|
||||
depends: info.depends.iter().map(TxidPrefix::from).collect(),
|
||||
first_seen: Timestamp::now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use brk_types::TxidPrefix;
|
||||
use rustc_hash::FxHashMap;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{entry::Entry, types::TxIndex};
|
||||
|
||||
@@ -11,12 +12,20 @@ use crate::{entry::Entry, types::TxIndex};
|
||||
pub struct EntryPool {
|
||||
entries: Vec<Option<Entry>>,
|
||||
prefix_to_idx: FxHashMap<TxidPrefix, TxIndex>,
|
||||
parent_to_children: FxHashMap<TxidPrefix, SmallVec<[TxidPrefix; 2]>>,
|
||||
free_slots: Vec<TxIndex>,
|
||||
}
|
||||
|
||||
impl EntryPool {
|
||||
/// Insert an entry, returning its index.
|
||||
pub fn insert(&mut self, prefix: TxidPrefix, entry: Entry) -> TxIndex {
|
||||
for parent in &entry.depends {
|
||||
self.parent_to_children
|
||||
.entry(*parent)
|
||||
.or_default()
|
||||
.push(prefix);
|
||||
}
|
||||
|
||||
let idx = match self.free_slots.pop() {
|
||||
Some(idx) => {
|
||||
self.entries[idx.as_usize()] = Some(entry);
|
||||
@@ -39,9 +48,28 @@ impl EntryPool {
|
||||
self.entries.get(idx.as_usize())?.as_ref()
|
||||
}
|
||||
|
||||
/// Get direct children of a transaction (txs that depend on it).
|
||||
pub fn children(&self, prefix: &TxidPrefix) -> &[TxidPrefix] {
|
||||
self.parent_to_children
|
||||
.get(prefix)
|
||||
.map(SmallVec::as_slice)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Remove an entry by its txid prefix.
|
||||
pub fn remove(&mut self, prefix: &TxidPrefix) {
|
||||
if let Some(idx) = self.prefix_to_idx.remove(prefix) {
|
||||
if let Some(entry) = self.entries.get(idx.as_usize()).and_then(|e| e.as_ref()) {
|
||||
for parent in &entry.depends {
|
||||
if let Some(children) = self.parent_to_children.get_mut(parent) {
|
||||
children.retain(|c| c != prefix);
|
||||
if children.is_empty() {
|
||||
self.parent_to_children.remove(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.parent_to_children.remove(prefix);
|
||||
if let Some(slot) = self.entries.get_mut(idx.as_usize()) {
|
||||
*slot = None;
|
||||
}
|
||||
|
||||
@@ -106,6 +106,10 @@ impl MempoolInner {
|
||||
self.txs.read()
|
||||
}
|
||||
|
||||
pub fn get_entries(&self) -> RwLockReadGuard<'_, EntryPool> {
|
||||
self.entries.read()
|
||||
}
|
||||
|
||||
pub fn get_addrs(&self) -> RwLockReadGuard<'_, AddrTracker> {
|
||||
self.addrs.read()
|
||||
}
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
use brk_types::{TxWithHex, Txid};
|
||||
use brk_types::{MempoolRecentTx, TxWithHex, Txid};
|
||||
use derive_more::Deref;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
const RECENT_CAP: usize = 10;
|
||||
|
||||
/// Store of full transaction data for API access.
|
||||
#[derive(Default, Deref)]
|
||||
pub struct TxStore(FxHashMap<Txid, TxWithHex>);
|
||||
pub struct TxStore {
|
||||
#[deref]
|
||||
txs: FxHashMap<Txid, TxWithHex>,
|
||||
recent: Vec<MempoolRecentTx>,
|
||||
}
|
||||
|
||||
impl TxStore {
|
||||
/// Check if a transaction exists.
|
||||
pub fn contains(&self, txid: &Txid) -> bool {
|
||||
self.0.contains_key(txid)
|
||||
self.txs.contains_key(txid)
|
||||
}
|
||||
|
||||
/// Add transactions in bulk.
|
||||
pub fn extend(&mut self, txs: FxHashMap<Txid, TxWithHex>) {
|
||||
self.0.extend(txs);
|
||||
let mut new: Vec<_> = txs
|
||||
.iter()
|
||||
.take(RECENT_CAP)
|
||||
.map(|(txid, tx_hex)| MempoolRecentTx::from((txid, tx_hex.tx())))
|
||||
.collect();
|
||||
let keep = RECENT_CAP.saturating_sub(new.len());
|
||||
new.extend(self.recent.drain(..keep.min(self.recent.len())));
|
||||
self.recent = new;
|
||||
self.txs.extend(txs);
|
||||
}
|
||||
|
||||
/// Last 10 transactions to enter the mempool.
|
||||
pub fn recent(&self) -> &[MempoolRecentTx] {
|
||||
&self.recent
|
||||
}
|
||||
|
||||
/// Keep items matching predicate, call `on_remove` for each removed item.
|
||||
@@ -23,7 +42,7 @@ impl TxStore {
|
||||
K: FnMut(&Txid) -> bool,
|
||||
R: FnMut(&Txid, &TxWithHex),
|
||||
{
|
||||
self.0.retain(|txid, tx| {
|
||||
self.txs.retain(|txid, tx| {
|
||||
if keep(txid) {
|
||||
true
|
||||
} else {
|
||||
|
||||
@@ -4,8 +4,8 @@ use bitcoin::{Network, PublicKey, ScriptBuf};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{
|
||||
Addr, AddrBytes, AddrChainStats, AddrHash, AddrIndexOutPoint, AddrIndexTxIndex, AddrStats,
|
||||
AnyAddrDataIndexEnum, OutputType, Sats, Transaction, TxIndex, TxStatus, Txid, TypeIndex, Unit,
|
||||
Utxo, Vout,
|
||||
AnyAddrDataIndexEnum, BlockHash, Dollars, Height, OutputType, Timestamp, Transaction, TxIndex,
|
||||
TxStatus, Txid, TypeIndex, Unit, Utxo, Vout,
|
||||
};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
|
||||
@@ -69,8 +69,14 @@ impl Query {
|
||||
.into(),
|
||||
};
|
||||
|
||||
let realized_price = match &any_addr_index.to_enum() {
|
||||
AnyAddrDataIndexEnum::Funded(_) => addr_data.realized_price().to_dollars(),
|
||||
AnyAddrDataIndexEnum::Empty(_) => Dollars::default(),
|
||||
};
|
||||
|
||||
Ok(AddrStats {
|
||||
addr,
|
||||
addr_type,
|
||||
chain_stats: AddrChainStats {
|
||||
type_index,
|
||||
funded_txo_count: addr_data.funded_txo_count,
|
||||
@@ -78,6 +84,7 @@ impl Query {
|
||||
spent_txo_count: addr_data.spent_txo_count,
|
||||
spent_txo_sum: addr_data.sent,
|
||||
tx_count: addr_data.tx_count,
|
||||
realized_price,
|
||||
},
|
||||
mempool_stats: self.mempool().map(|mempool| {
|
||||
mempool
|
||||
@@ -97,10 +104,7 @@ impl Query {
|
||||
limit: usize,
|
||||
) -> Result<Vec<Transaction>> {
|
||||
let txindices = self.addr_txindices(&addr, after_txid, limit)?;
|
||||
txindices
|
||||
.into_iter()
|
||||
.map(|tx_index| self.transaction_by_index(tx_index))
|
||||
.collect()
|
||||
self.transactions_by_indices(&txindices)
|
||||
}
|
||||
|
||||
pub fn addr_txids(
|
||||
@@ -134,33 +138,33 @@ impl Query {
|
||||
.get(output_type)
|
||||
.unwrap();
|
||||
|
||||
let prefix = u32::from(type_index).to_be_bytes();
|
||||
|
||||
let after_tx_index = if let Some(after_txid) = after_txid {
|
||||
let tx_index = stores
|
||||
if let Some(after_txid) = after_txid {
|
||||
let after_tx_index = stores
|
||||
.txid_prefix_to_tx_index
|
||||
.get(&after_txid.into())
|
||||
.map_err(|_| Error::UnknownTxid)?
|
||||
.ok_or(Error::UnknownTxid)?
|
||||
.into_owned();
|
||||
Some(tx_index)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(store
|
||||
.prefix(prefix)
|
||||
.rev()
|
||||
.filter(|(key, _): &(AddrIndexTxIndex, Unit)| {
|
||||
if let Some(after) = after_tx_index {
|
||||
key.tx_index() < after
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.take(limit)
|
||||
.map(|(key, _)| key.tx_index())
|
||||
.collect())
|
||||
// Seek directly to after_tx_index and iterate backward — O(limit)
|
||||
let min = AddrIndexTxIndex::min_for_addr(type_index);
|
||||
let bound = AddrIndexTxIndex::from((type_index, after_tx_index));
|
||||
Ok(store
|
||||
.range(min..bound)
|
||||
.rev()
|
||||
.take(limit)
|
||||
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
|
||||
.collect())
|
||||
} else {
|
||||
// No pagination — scan from end of prefix
|
||||
let prefix = u32::from(type_index).to_be_bytes();
|
||||
Ok(store
|
||||
.prefix(prefix)
|
||||
.rev()
|
||||
.take(limit)
|
||||
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr_utxos(&self, addr: Addr) -> Result<Vec<Utxo>> {
|
||||
@@ -186,39 +190,41 @@ impl Query {
|
||||
let first_txout_index_reader = vecs.transactions.first_txout_index.reader();
|
||||
let value_reader = vecs.outputs.value.reader();
|
||||
let blockhash_reader = vecs.blocks.blockhash.reader();
|
||||
let mut height_cursor = vecs.transactions.height.cursor();
|
||||
let mut block_ts_cursor = vecs.blocks.timestamp.cursor();
|
||||
|
||||
let utxos: Vec<Utxo> = outpoints
|
||||
.into_iter()
|
||||
.map(|(tx_index, vout)| {
|
||||
let txid: Txid = txid_reader.get(tx_index.to_usize());
|
||||
let height = vecs
|
||||
.transactions
|
||||
.height
|
||||
.collect_one_at(tx_index.to_usize())
|
||||
.unwrap();
|
||||
let first_txout_index = first_txout_index_reader.get(tx_index.to_usize());
|
||||
let txout_index = first_txout_index + vout;
|
||||
let value: Sats = value_reader.get(usize::from(txout_index));
|
||||
let block_hash = blockhash_reader.get(usize::from(height));
|
||||
let block_time = vecs
|
||||
.blocks
|
||||
.timestamp
|
||||
.collect_one_at(usize::from(height))
|
||||
.unwrap();
|
||||
let mut cached_block: Option<(Height, BlockHash, Timestamp)> = None;
|
||||
let mut utxos = Vec::with_capacity(outpoints.len());
|
||||
|
||||
Utxo {
|
||||
txid,
|
||||
vout,
|
||||
status: TxStatus {
|
||||
confirmed: true,
|
||||
block_height: Some(height),
|
||||
block_hash: Some(block_hash),
|
||||
block_time: Some(block_time),
|
||||
},
|
||||
value,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for (tx_index, vout) in outpoints {
|
||||
let txid = txid_reader.get(tx_index.to_usize());
|
||||
let height: Height = height_cursor.get(tx_index.to_usize()).unwrap();
|
||||
let first_txout_index = first_txout_index_reader.get(tx_index.to_usize());
|
||||
let value = value_reader.get(usize::from(first_txout_index + vout));
|
||||
|
||||
let (block_hash, block_time) = if let Some((h, ref bh, bt)) = cached_block
|
||||
&& h == height
|
||||
{
|
||||
(bh.clone(), bt)
|
||||
} else {
|
||||
let bh = blockhash_reader.get(height.to_usize());
|
||||
let bt = block_ts_cursor.get(height.to_usize()).unwrap();
|
||||
cached_block = Some((height, bh.clone(), bt));
|
||||
(bh, bt)
|
||||
};
|
||||
|
||||
utxos.push(Utxo {
|
||||
txid,
|
||||
vout,
|
||||
status: TxStatus {
|
||||
confirmed: true,
|
||||
block_height: Some(height),
|
||||
block_hash: Some(block_hash),
|
||||
block_time: Some(block_time),
|
||||
},
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(utxos)
|
||||
}
|
||||
@@ -247,6 +253,29 @@ impl Query {
|
||||
Ok(txids)
|
||||
}
|
||||
|
||||
/// Height of the last on-chain activity for an address (last tx_index → height).
|
||||
pub fn addr_last_activity_height(&self, addr: &Addr) -> Result<Height> {
|
||||
let (output_type, type_index) = self.resolve_addr(addr)?;
|
||||
let store = self
|
||||
.indexer()
|
||||
.stores
|
||||
.addr_type_to_addr_index_and_tx_index
|
||||
.get(output_type)
|
||||
.unwrap();
|
||||
let prefix = u32::from(type_index).to_be_bytes();
|
||||
let last_tx_index = store
|
||||
.prefix(prefix)
|
||||
.next_back()
|
||||
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
|
||||
.ok_or(Error::UnknownAddr)?;
|
||||
self.indexer()
|
||||
.vecs
|
||||
.transactions
|
||||
.height
|
||||
.collect_one(last_tx_index)
|
||||
.ok_or(Error::UnknownAddr)
|
||||
}
|
||||
|
||||
/// Resolve an address string to its output type and type_index
|
||||
fn resolve_addr(&self, addr: &Addr) -> Result<(OutputType, TypeIndex)> {
|
||||
let stores = &self.indexer().stores;
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
use std::io::Read;
|
||||
|
||||
use bitcoin::consensus::Decodable;
|
||||
use bitcoin::hex::DisplayHex;
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{BlockHash, BlockHashPrefix, BlockInfo, Height, TxIndex};
|
||||
use brk_types::{
|
||||
BlockExtras, BlockHash, BlockHashPrefix, BlockHeader, BlockInfo, BlockInfoV1, BlockPool,
|
||||
FeeRate, Height, PoolSlug, Sats, Timestamp, TxIndex, VSize, pools,
|
||||
};
|
||||
use vecdb::{AnyVec, ReadableVec, VecIndex};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
const DEFAULT_BLOCK_COUNT: u32 = 10;
|
||||
const DEFAULT_V1_BLOCK_COUNT: u32 = 15;
|
||||
const HEADER_SIZE: usize = 80;
|
||||
|
||||
impl Query {
|
||||
pub fn block(&self, hash: &BlockHash) -> Result<BlockInfo> {
|
||||
@@ -13,58 +22,70 @@ impl Query {
|
||||
}
|
||||
|
||||
pub fn block_by_height(&self, height: Height) -> Result<BlockInfo> {
|
||||
let indexer = self.indexer();
|
||||
|
||||
let max_height = self.max_height();
|
||||
let max_height = self.indexed_height();
|
||||
if height > max_height {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
self.blocks_range(height.to_usize(), height.to_usize() + 1)?
|
||||
.pop()
|
||||
.ok_or(Error::NotFound("Block not found".into()))
|
||||
}
|
||||
|
||||
let blockhash = indexer.vecs.blocks.blockhash.read_once(height)?;
|
||||
let difficulty = indexer.vecs.blocks.difficulty.collect_one(height).unwrap();
|
||||
let timestamp = indexer.vecs.blocks.timestamp.collect_one(height).unwrap();
|
||||
let size = indexer.vecs.blocks.total.collect_one(height).unwrap();
|
||||
let weight = indexer.vecs.blocks.weight.collect_one(height).unwrap();
|
||||
let tx_count = self.tx_count_at_height(height, max_height)?;
|
||||
pub fn block_by_height_v1(&self, height: Height) -> Result<BlockInfoV1> {
|
||||
let max_height = self.height();
|
||||
if height > max_height {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
self.blocks_v1_range(height.to_usize(), height.to_usize() + 1)?
|
||||
.pop()
|
||||
.ok_or(Error::NotFound("Block not found".into()))
|
||||
}
|
||||
|
||||
Ok(BlockInfo {
|
||||
id: blockhash,
|
||||
height,
|
||||
tx_count,
|
||||
size: *size,
|
||||
weight,
|
||||
timestamp,
|
||||
difficulty: *difficulty,
|
||||
})
|
||||
pub fn block_header_hex(&self, hash: &BlockHash) -> Result<String> {
|
||||
let height = self.height_by_hash(hash)?;
|
||||
let header = self.read_block_header(height)?;
|
||||
Ok(bitcoin::consensus::encode::serialize_hex(&header))
|
||||
}
|
||||
|
||||
pub fn block_hash_by_height(&self, height: Height) -> Result<BlockHash> {
|
||||
let max_height = self.indexed_height();
|
||||
if height > max_height {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
Ok(self.indexer().vecs.blocks.blockhash.read_once(height)?)
|
||||
}
|
||||
|
||||
pub fn blocks(&self, start_height: Option<Height>) -> Result<Vec<BlockInfo>> {
|
||||
let max_height = self.indexed_height();
|
||||
let (begin, end) = self.resolve_block_range(start_height, DEFAULT_BLOCK_COUNT);
|
||||
self.blocks_range(begin, end)
|
||||
}
|
||||
|
||||
let start = start_height.unwrap_or(max_height);
|
||||
let start = start.min(max_height);
|
||||
pub fn blocks_v1(&self, start_height: Option<Height>) -> Result<Vec<BlockInfoV1>> {
|
||||
let (begin, end) = self.resolve_block_range(start_height, DEFAULT_V1_BLOCK_COUNT);
|
||||
self.blocks_v1_range(begin, end)
|
||||
}
|
||||
|
||||
let start_u32: u32 = start.into();
|
||||
let count = DEFAULT_BLOCK_COUNT.min(start_u32 + 1) as usize;
|
||||
// === Range queries (bulk reads) ===
|
||||
|
||||
if count == 0 {
|
||||
fn blocks_range(&self, begin: usize, end: usize) -> Result<Vec<BlockInfo>> {
|
||||
if begin >= end {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let indexer = self.indexer();
|
||||
let computer = self.computer();
|
||||
let reader = self.reader();
|
||||
|
||||
// Batch-read all PcoVec data for the contiguous range (avoids
|
||||
// per-block page decompression — 4 reads instead of 4*count).
|
||||
let end = start_u32 as usize + 1;
|
||||
let begin = end - count;
|
||||
|
||||
// Bulk read all indexed data
|
||||
let blockhashes = indexer.vecs.blocks.blockhash.collect_range_at(begin, end);
|
||||
let difficulties = indexer.vecs.blocks.difficulty.collect_range_at(begin, end);
|
||||
let timestamps = indexer.vecs.blocks.timestamp.collect_range_at(begin, end);
|
||||
let sizes = indexer.vecs.blocks.total.collect_range_at(begin, end);
|
||||
let weights = indexer.vecs.blocks.weight.collect_range_at(begin, end);
|
||||
let positions = indexer.vecs.blocks.position.collect_range_at(begin, end);
|
||||
|
||||
// Batch-read first_tx_index for tx_count computation (need one extra for next boundary)
|
||||
// Bulk read tx indexes for tx_count
|
||||
let max_height = self.indexed_height();
|
||||
let tx_index_end = if end <= max_height.to_usize() {
|
||||
end + 1
|
||||
} else {
|
||||
@@ -77,38 +98,310 @@ impl Query {
|
||||
.collect_range_at(begin, tx_index_end);
|
||||
let total_txs = computer.indexes.tx_index.identity.len();
|
||||
|
||||
// Bulk read median time window
|
||||
let median_start = begin.saturating_sub(10);
|
||||
let median_timestamps: Vec<Timestamp> = indexer
|
||||
.vecs
|
||||
.blocks
|
||||
.timestamp
|
||||
.collect_range_at(median_start, end);
|
||||
|
||||
let count = end - begin;
|
||||
let mut blocks = Vec::with_capacity(count);
|
||||
|
||||
for i in (0..count).rev() {
|
||||
let height = Height::from(begin + i);
|
||||
let blockhash = indexer.vecs.blocks.blockhash.read_once(height)?;
|
||||
let raw_header = reader.read_raw_bytes(positions[i], HEADER_SIZE)?;
|
||||
let header = Self::decode_header(&raw_header)?;
|
||||
|
||||
let tx_count = if i + 1 < first_tx_indexes.len() {
|
||||
first_tx_indexes[i + 1].to_usize() - first_tx_indexes[i].to_usize()
|
||||
(first_tx_indexes[i + 1].to_usize() - first_tx_indexes[i].to_usize()) as u32
|
||||
} else {
|
||||
total_txs - first_tx_indexes[i].to_usize()
|
||||
(total_txs - first_tx_indexes[i].to_usize()) as u32
|
||||
};
|
||||
|
||||
let median_time =
|
||||
Self::compute_median_time(&median_timestamps, begin + i, median_start);
|
||||
|
||||
blocks.push(BlockInfo {
|
||||
id: blockhash,
|
||||
height,
|
||||
tx_count: tx_count as u32,
|
||||
id: blockhashes[i].clone(),
|
||||
height: Height::from(begin + i),
|
||||
version: header.version,
|
||||
timestamp: timestamps[i],
|
||||
bits: header.bits,
|
||||
nonce: header.nonce,
|
||||
difficulty: *difficulties[i],
|
||||
merkle_root: header.merkle_root,
|
||||
tx_count,
|
||||
size: *sizes[i],
|
||||
weight: weights[i],
|
||||
timestamp: timestamps[i],
|
||||
difficulty: *difficulties[i],
|
||||
previous_block_hash: header.previous_block_hash,
|
||||
median_time,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(blocks)
|
||||
}
|
||||
|
||||
pub(crate) fn blocks_v1_range(&self, begin: usize, end: usize) -> Result<Vec<BlockInfoV1>> {
|
||||
if begin >= end {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let count = end - begin;
|
||||
let indexer = self.indexer();
|
||||
let computer = self.computer();
|
||||
let reader = self.reader();
|
||||
let all_pools = pools();
|
||||
|
||||
// Bulk read all indexed data
|
||||
let blockhashes = indexer.vecs.blocks.blockhash.collect_range_at(begin, end);
|
||||
let difficulties = indexer.vecs.blocks.difficulty.collect_range_at(begin, end);
|
||||
let timestamps = indexer.vecs.blocks.timestamp.collect_range_at(begin, end);
|
||||
let sizes = indexer.vecs.blocks.total.collect_range_at(begin, end);
|
||||
let weights = indexer.vecs.blocks.weight.collect_range_at(begin, end);
|
||||
let positions = indexer.vecs.blocks.position.collect_range_at(begin, end);
|
||||
let pool_slugs = computer.pools.pool.collect_range_at(begin, end);
|
||||
|
||||
// Bulk read tx indexes
|
||||
let max_height = self.indexed_height();
|
||||
let tx_index_end = if end <= max_height.to_usize() {
|
||||
end + 1
|
||||
} else {
|
||||
end
|
||||
};
|
||||
let first_tx_indexes: Vec<TxIndex> = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_range_at(begin, tx_index_end);
|
||||
let total_txs = computer.indexes.tx_index.identity.len();
|
||||
|
||||
// Bulk read segwit stats
|
||||
let segwit_txs = indexer.vecs.blocks.segwit_txs.collect_range_at(begin, end);
|
||||
let segwit_sizes = indexer.vecs.blocks.segwit_size.collect_range_at(begin, end);
|
||||
let segwit_weights = indexer
|
||||
.vecs
|
||||
.blocks
|
||||
.segwit_weight
|
||||
.collect_range_at(begin, end);
|
||||
|
||||
// Bulk read extras data
|
||||
let fee_sats = computer
|
||||
.mining
|
||||
.rewards
|
||||
.fees
|
||||
.block
|
||||
.sats
|
||||
.collect_range_at(begin, end);
|
||||
let subsidy_sats = computer
|
||||
.mining
|
||||
.rewards
|
||||
.subsidy
|
||||
.block
|
||||
.sats
|
||||
.collect_range_at(begin, end);
|
||||
let input_counts = computer.inputs.count.sum.collect_range_at(begin, end);
|
||||
let output_counts = computer
|
||||
.outputs
|
||||
.count
|
||||
.total
|
||||
.sum
|
||||
.collect_range_at(begin, end);
|
||||
let utxo_set_sizes = computer
|
||||
.outputs
|
||||
.count
|
||||
.unspent
|
||||
.height
|
||||
.collect_range_at(begin, end);
|
||||
let input_volumes = computer
|
||||
.transactions
|
||||
.volume
|
||||
.transfer_volume
|
||||
.block
|
||||
.sats
|
||||
.collect_range_at(begin, end);
|
||||
let prices = computer.prices.cached_spot_usd.collect_range_at(begin, end);
|
||||
let output_volumes = computer
|
||||
.mining
|
||||
.rewards
|
||||
.output_volume
|
||||
.collect_range_at(begin, end);
|
||||
|
||||
// Bulk read effective fee rate distribution (accounts for CPFP)
|
||||
let frd = &computer
|
||||
.transactions
|
||||
.fees
|
||||
.effective_fee_rate
|
||||
.distribution
|
||||
.block;
|
||||
let fr_min = frd.min.height.collect_range_at(begin, end);
|
||||
let fr_pct10 = frd.pct10.height.collect_range_at(begin, end);
|
||||
let fr_pct25 = frd.pct25.height.collect_range_at(begin, end);
|
||||
let fr_median = frd.median.height.collect_range_at(begin, end);
|
||||
let fr_pct75 = frd.pct75.height.collect_range_at(begin, end);
|
||||
let fr_pct90 = frd.pct90.height.collect_range_at(begin, end);
|
||||
let fr_max = frd.max.height.collect_range_at(begin, end);
|
||||
|
||||
// Bulk read fee amount distribution (sats)
|
||||
let fad = &computer.transactions.fees.fee.distribution.block;
|
||||
let fa_min = fad.min.height.collect_range_at(begin, end);
|
||||
let fa_pct10 = fad.pct10.height.collect_range_at(begin, end);
|
||||
let fa_pct25 = fad.pct25.height.collect_range_at(begin, end);
|
||||
let fa_median = fad.median.height.collect_range_at(begin, end);
|
||||
let fa_pct75 = fad.pct75.height.collect_range_at(begin, end);
|
||||
let fa_pct90 = fad.pct90.height.collect_range_at(begin, end);
|
||||
let fa_max = fad.max.height.collect_range_at(begin, end);
|
||||
|
||||
|
||||
// Bulk read median time window
|
||||
let median_start = begin.saturating_sub(10);
|
||||
let median_timestamps = indexer
|
||||
.vecs
|
||||
.blocks
|
||||
.timestamp
|
||||
.collect_range_at(median_start, end);
|
||||
|
||||
let mut blocks = Vec::with_capacity(count);
|
||||
|
||||
for i in (0..count).rev() {
|
||||
let tx_count = if i + 1 < first_tx_indexes.len() {
|
||||
(first_tx_indexes[i + 1].to_usize() - first_tx_indexes[i].to_usize()) as u32
|
||||
} else {
|
||||
(total_txs - first_tx_indexes[i].to_usize()) as u32
|
||||
};
|
||||
|
||||
// Single reader for header + coinbase (adjacent in blk file)
|
||||
let varint_len = Self::compact_size_len(tx_count) as usize;
|
||||
let (raw_header, coinbase_raw, coinbase_address, coinbase_addresses, coinbase_signature, coinbase_signature_ascii, scriptsig_bytes) = match reader.reader_at(positions[i]) {
|
||||
Ok(mut blk) => {
|
||||
let mut header_buf = [0u8; HEADER_SIZE];
|
||||
if blk.read_exact(&mut header_buf).is_err() {
|
||||
([0u8; HEADER_SIZE], String::new(), None, vec![], String::new(), String::new(), vec![])
|
||||
} else {
|
||||
// Skip tx count varint
|
||||
let mut skip = [0u8; 5];
|
||||
let _ = blk.read_exact(&mut skip[..varint_len]);
|
||||
let coinbase = Self::parse_coinbase_from_read(blk);
|
||||
(header_buf, coinbase.0, coinbase.1, coinbase.2, coinbase.3, coinbase.4, coinbase.5)
|
||||
}
|
||||
}
|
||||
Err(_) => ([0u8; HEADER_SIZE], String::new(), None, vec![], String::new(), String::new(), vec![]),
|
||||
};
|
||||
let header = Self::decode_header(&raw_header)?;
|
||||
|
||||
let weight = weights[i];
|
||||
let size = *sizes[i];
|
||||
let total_fees = fee_sats[i];
|
||||
let subsidy = subsidy_sats[i];
|
||||
let total_inputs = (*input_counts[i]).saturating_sub(1);
|
||||
let total_outputs = *output_counts[i];
|
||||
let vsize = weight.to_vbytes_ceil();
|
||||
let total_fees_u64 = u64::from(total_fees);
|
||||
let non_coinbase = tx_count.saturating_sub(1) as u64;
|
||||
|
||||
let pool_slug = pool_slugs[i];
|
||||
let pool = all_pools.get(pool_slug);
|
||||
|
||||
let miner_names = if pool_slug == PoolSlug::Ocean {
|
||||
Self::parse_datum_miner_names(&scriptsig_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let median_time =
|
||||
Self::compute_median_time(&median_timestamps, begin + i, median_start);
|
||||
|
||||
let info = BlockInfo {
|
||||
id: blockhashes[i].clone(),
|
||||
height: Height::from(begin + i),
|
||||
version: header.version,
|
||||
timestamp: timestamps[i],
|
||||
bits: header.bits,
|
||||
nonce: header.nonce,
|
||||
difficulty: *difficulties[i],
|
||||
merkle_root: header.merkle_root,
|
||||
tx_count,
|
||||
size,
|
||||
weight,
|
||||
previous_block_hash: header.previous_block_hash,
|
||||
median_time,
|
||||
};
|
||||
|
||||
let total_input_amt = input_volumes[i];
|
||||
let total_output_amt = output_volumes[i];
|
||||
|
||||
let extras = BlockExtras {
|
||||
total_fees,
|
||||
median_fee: fr_median[i],
|
||||
fee_range: [
|
||||
fr_min[i],
|
||||
fr_pct10[i],
|
||||
fr_pct25[i],
|
||||
fr_median[i],
|
||||
fr_pct75[i],
|
||||
fr_pct90[i],
|
||||
fr_max[i],
|
||||
],
|
||||
reward: subsidy + total_fees,
|
||||
pool: BlockPool {
|
||||
id: pool.mempool_unique_id(),
|
||||
name: pool.name.to_string(),
|
||||
slug: pool_slug,
|
||||
miner_names,
|
||||
},
|
||||
avg_fee: Sats::from(if non_coinbase > 0 {
|
||||
total_fees_u64 / non_coinbase
|
||||
} else {
|
||||
0
|
||||
}),
|
||||
avg_fee_rate: FeeRate::from((total_fees, VSize::from(vsize))),
|
||||
coinbase_raw,
|
||||
coinbase_address,
|
||||
coinbase_addresses,
|
||||
coinbase_signature,
|
||||
coinbase_signature_ascii,
|
||||
avg_tx_size: if tx_count > 0 {
|
||||
size as f64 / tx_count as f64
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
total_inputs,
|
||||
total_outputs,
|
||||
total_output_amt,
|
||||
median_fee_amt: fa_median[i],
|
||||
fee_percentiles: [
|
||||
fa_min[i],
|
||||
fa_pct10[i],
|
||||
fa_pct25[i],
|
||||
fa_median[i],
|
||||
fa_pct75[i],
|
||||
fa_pct90[i],
|
||||
fa_max[i],
|
||||
],
|
||||
segwit_total_txs: *segwit_txs[i],
|
||||
segwit_total_size: *segwit_sizes[i],
|
||||
segwit_total_weight: segwit_weights[i],
|
||||
header: raw_header.to_lower_hex_string(),
|
||||
utxo_set_change: total_outputs as i64 - total_inputs as i64,
|
||||
utxo_set_size: *utxo_set_sizes[i],
|
||||
total_input_amt,
|
||||
virtual_size: vsize as f64,
|
||||
price: prices[i],
|
||||
orphans: vec![],
|
||||
first_seen: None,
|
||||
};
|
||||
|
||||
blocks.push(BlockInfoV1 { info, extras });
|
||||
}
|
||||
|
||||
Ok(blocks)
|
||||
}
|
||||
|
||||
// === Helper methods ===
|
||||
|
||||
pub fn height_by_hash(&self, hash: &BlockHash) -> Result<Height> {
|
||||
let indexer = self.indexer();
|
||||
|
||||
let prefix = BlockHashPrefix::from(hash);
|
||||
|
||||
indexer
|
||||
.stores
|
||||
.blockhash_prefix_to_height
|
||||
@@ -117,31 +410,160 @@ impl Query {
|
||||
.ok_or(Error::NotFound("Block not found".into()))
|
||||
}
|
||||
|
||||
fn max_height(&self) -> Height {
|
||||
Height::from(self.indexer().vecs.blocks.blockhash.len().saturating_sub(1))
|
||||
}
|
||||
|
||||
fn tx_count_at_height(&self, height: Height, max_height: Height) -> Result<u32> {
|
||||
let indexer = self.indexer();
|
||||
let computer = self.computer();
|
||||
|
||||
let first_tx_index = indexer
|
||||
pub fn read_block_header(&self, height: Height) -> Result<bitcoin::block::Header> {
|
||||
let position = self
|
||||
.indexer()
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.blocks
|
||||
.position
|
||||
.collect_one(height)
|
||||
.unwrap();
|
||||
let next_first_tx_index = if height < max_height {
|
||||
indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(height.incremented())
|
||||
.unwrap()
|
||||
let raw = self.reader().read_raw_bytes(position, HEADER_SIZE)?;
|
||||
bitcoin::block::Header::consensus_decode(&mut raw.as_slice())
|
||||
.map_err(|_| Error::Internal("Failed to decode block header"))
|
||||
}
|
||||
|
||||
fn resolve_block_range(&self, start_height: Option<Height>, count: u32) -> (usize, usize) {
|
||||
let max_height = self.height();
|
||||
let start = start_height.unwrap_or(max_height).min(max_height);
|
||||
let start_u32: u32 = start.into();
|
||||
let count = count.min(start_u32 + 1) as usize;
|
||||
let end = start_u32 as usize + 1;
|
||||
let begin = end - count;
|
||||
(begin, end)
|
||||
}
|
||||
|
||||
fn decode_header(bytes: &[u8]) -> Result<BlockHeader> {
|
||||
let raw = bitcoin::block::Header::consensus_decode(&mut &bytes[..])
|
||||
.map_err(|_| Error::Internal("Failed to decode block header"))?;
|
||||
Ok(BlockHeader::from(raw))
|
||||
}
|
||||
|
||||
fn compute_median_time(
|
||||
all_timestamps: &[Timestamp],
|
||||
height: usize,
|
||||
window_start: usize,
|
||||
) -> Timestamp {
|
||||
let rel_start = height.saturating_sub(10) - window_start;
|
||||
let rel_end = height + 1 - window_start;
|
||||
let mut sorted: Vec<usize> = all_timestamps[rel_start..rel_end]
|
||||
.iter()
|
||||
.map(|t| usize::from(*t))
|
||||
.collect();
|
||||
sorted.sort_unstable();
|
||||
Timestamp::from(sorted[sorted.len() / 2])
|
||||
}
|
||||
|
||||
fn compact_size_len(tx_count: u32) -> u32 {
|
||||
if tx_count <= 0xFC {
|
||||
1
|
||||
} else if tx_count <= 0xFFFF {
|
||||
3
|
||||
} else {
|
||||
TxIndex::from(computer.indexes.tx_index.identity.len())
|
||||
5
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse OCEAN DATUM protocol miner names from coinbase scriptsig.
|
||||
/// Skips BIP34 height push, reads tag payload, splits on 0x0F delimiter.
|
||||
fn parse_datum_miner_names(scriptsig: &[u8]) -> Option<Vec<String>> {
|
||||
if scriptsig.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip BIP34 height push: first byte is length of height data
|
||||
let height_len = scriptsig[0] as usize;
|
||||
let mut tag_len_idx = 1 + height_len;
|
||||
if tag_len_idx >= scriptsig.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Read tags payload length (may use OP_PUSHDATA1 for >75 bytes)
|
||||
let mut tags_len = scriptsig[tag_len_idx] as usize;
|
||||
if tags_len == 0x4c {
|
||||
tag_len_idx += 1;
|
||||
if tag_len_idx >= scriptsig.len() {
|
||||
return None;
|
||||
}
|
||||
tags_len = scriptsig[tag_len_idx] as usize;
|
||||
}
|
||||
|
||||
let tag_start = tag_len_idx + 1;
|
||||
if tag_start + tags_len > scriptsig.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Decode tag bytes, strip nulls, split on 0x0F, keep only alphanumeric + space
|
||||
let tag_bytes = &scriptsig[tag_start..tag_start + tags_len];
|
||||
let tag_string: String = tag_bytes
|
||||
.iter()
|
||||
.filter(|&&b| b != 0x00)
|
||||
.map(|&b| b as char)
|
||||
.collect();
|
||||
|
||||
let names: Vec<String> = tag_string
|
||||
.split('\x0f')
|
||||
.map(|s| {
|
||||
s.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric() || *c == ' ')
|
||||
.collect::<String>()
|
||||
})
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.collect();
|
||||
|
||||
if names.is_empty() { None } else { Some(names) }
|
||||
}
|
||||
|
||||
fn parse_coinbase_from_read(
|
||||
reader: impl Read,
|
||||
) -> (String, Option<String>, Vec<String>, String, String, Vec<u8>) {
|
||||
let empty = (String::new(), None, vec![], String::new(), String::new(), vec![]);
|
||||
|
||||
let tx = match bitcoin::Transaction::consensus_decode(&mut bitcoin::io::FromStd::new(reader)) {
|
||||
Ok(tx) => tx,
|
||||
Err(_) => return empty,
|
||||
};
|
||||
|
||||
Ok((next_first_tx_index.to_usize() - first_tx_index.to_usize()) as u32)
|
||||
let scriptsig_bytes: Vec<u8> = tx
|
||||
.input
|
||||
.first()
|
||||
.map(|input| input.script_sig.as_bytes().to_vec())
|
||||
.unwrap_or_default();
|
||||
|
||||
let coinbase_raw = scriptsig_bytes.to_lower_hex_string();
|
||||
|
||||
let coinbase_signature_ascii: String = scriptsig_bytes
|
||||
.iter()
|
||||
.map(|&b| b as char)
|
||||
.collect();
|
||||
|
||||
let mut coinbase_addresses: Vec<String> = tx
|
||||
.output
|
||||
.iter()
|
||||
.filter_map(|output| {
|
||||
bitcoin::Address::from_script(&output.script_pubkey, bitcoin::Network::Bitcoin)
|
||||
.ok()
|
||||
.map(|a| a.to_string())
|
||||
})
|
||||
.collect();
|
||||
coinbase_addresses.dedup();
|
||||
let coinbase_address = coinbase_addresses.first().cloned();
|
||||
|
||||
let coinbase_signature = tx
|
||||
.output
|
||||
.iter()
|
||||
.find(|output| !output.script_pubkey.is_op_return())
|
||||
.or(tx.output.first())
|
||||
.map(|output| output.script_pubkey.to_asm_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
(
|
||||
coinbase_raw,
|
||||
coinbase_address,
|
||||
coinbase_addresses,
|
||||
coinbase_signature,
|
||||
coinbase_signature_ascii,
|
||||
scriptsig_bytes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ impl Query {
|
||||
|
||||
fn block_raw_by_height(&self, height: Height) -> Result<Vec<u8>> {
|
||||
let indexer = self.indexer();
|
||||
let computer = self.computer();
|
||||
let reader = self.reader();
|
||||
|
||||
let max_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1));
|
||||
@@ -20,7 +19,7 @@ impl Query {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
|
||||
let position = computer.positions.block.collect_one(height).unwrap();
|
||||
let position = indexer.vecs.blocks.position.collect_one(height).unwrap();
|
||||
let size = indexer.vecs.blocks.total.collect_one(height).unwrap();
|
||||
|
||||
reader.read_raw_bytes(position, *size as usize)
|
||||
|
||||
@@ -31,14 +31,14 @@ impl Query {
|
||||
|
||||
let start: usize = usize::from(first_height_of_day).min(max_height_usize);
|
||||
|
||||
let timestamps = &indexer.vecs.blocks.timestamp;
|
||||
let mut ts_cursor = indexer.vecs.blocks.timestamp.cursor();
|
||||
|
||||
// Search forward from start to find the last block <= target timestamp
|
||||
let mut best_height = start;
|
||||
let mut best_ts = timestamps.collect_one_at(start).unwrap();
|
||||
let mut best_ts = ts_cursor.get(start).unwrap();
|
||||
|
||||
for h in (start + 1)..=max_height_usize {
|
||||
let block_ts = timestamps.collect_one_at(h).unwrap();
|
||||
let block_ts = ts_cursor.get(h).unwrap();
|
||||
if block_ts <= target {
|
||||
best_height = h;
|
||||
best_ts = block_ts;
|
||||
@@ -49,7 +49,7 @@ impl Query {
|
||||
|
||||
// Check one block before start in case we need to go backward
|
||||
if start > 0 && best_ts > target {
|
||||
let prev_ts = timestamps.collect_one_at(start - 1).unwrap();
|
||||
let prev_ts = ts_cursor.get(start - 1).unwrap();
|
||||
if prev_ts <= target {
|
||||
best_height = start - 1;
|
||||
best_ts = prev_ts;
|
||||
@@ -67,7 +67,7 @@ impl Query {
|
||||
// Convert timestamp to ISO 8601 format
|
||||
let ts_secs: i64 = (*best_ts).into();
|
||||
let iso_timestamp = JiffTimestamp::from_second(ts_secs)
|
||||
.map(|t| t.to_string())
|
||||
.map(|t| t.strftime("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
|
||||
.unwrap_or_else(|_| best_ts.to_string());
|
||||
|
||||
Ok(BlockTimestamp {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use bitcoin::{consensus::Decodable, hex::DisplayHex};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{BlockHash, Height, Transaction, TxIndex, Txid};
|
||||
use vecdb::{AnyVec, ReadableVec};
|
||||
use brk_types::{
|
||||
BlockHash, Height, OutputType, Sats, Timestamp, Transaction, TxIn, TxIndex, TxOut, TxStatus,
|
||||
Txid, Vout, Weight,
|
||||
};
|
||||
use vecdb::{AnyVec, ReadableVec, VecIndex};
|
||||
|
||||
use super::BLOCK_TXS_PAGE_SIZE;
|
||||
use crate::Query;
|
||||
@@ -13,7 +19,16 @@ impl Query {
|
||||
|
||||
pub fn block_txs(&self, hash: &BlockHash, start_index: TxIndex) -> Result<Vec<Transaction>> {
|
||||
let height = self.height_by_hash(hash)?;
|
||||
self.block_txs_by_height(height, start_index.into())
|
||||
let (first, tx_count) = self.block_tx_range(height)?;
|
||||
let start: usize = start_index.into();
|
||||
if start >= tx_count {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let count = BLOCK_TXS_PAGE_SIZE.min(tx_count - start);
|
||||
let indices: Vec<TxIndex> = (first + start..first + start + count)
|
||||
.map(TxIndex::from)
|
||||
.collect();
|
||||
self.transactions_by_indices(&indices)
|
||||
}
|
||||
|
||||
pub fn block_txid_at_index(&self, hash: &BlockHash, index: TxIndex) -> Result<Txid> {
|
||||
@@ -23,109 +38,208 @@ impl Query {
|
||||
|
||||
// === Helper methods ===
|
||||
|
||||
fn block_txids_by_height(&self, height: Height) -> Result<Vec<Txid>> {
|
||||
let indexer = self.indexer();
|
||||
|
||||
let max_height = self.indexed_height();
|
||||
if height > max_height {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
|
||||
let first_tx_index = indexer
|
||||
pub(crate) fn block_txids_by_height(&self, height: Height) -> Result<Vec<Txid>> {
|
||||
let (first, tx_count) = self.block_tx_range(height)?;
|
||||
Ok(self
|
||||
.indexer()
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(height)
|
||||
.unwrap();
|
||||
let next_first_tx_index = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(height.incremented())
|
||||
.unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len()));
|
||||
|
||||
let first: usize = first_tx_index.into();
|
||||
let next: usize = next_first_tx_index.into();
|
||||
|
||||
let txids: Vec<Txid> = indexer.vecs.transactions.txid.collect_range_at(first, next);
|
||||
|
||||
Ok(txids)
|
||||
}
|
||||
|
||||
fn block_txs_by_height(&self, height: Height, start_index: usize) -> Result<Vec<Transaction>> {
|
||||
let indexer = self.indexer();
|
||||
|
||||
let max_height = self.indexed_height();
|
||||
if height > max_height {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
|
||||
let first_tx_index = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(height)
|
||||
.unwrap();
|
||||
let next_first_tx_index = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(height.incremented())
|
||||
.unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len()));
|
||||
|
||||
let first: usize = first_tx_index.into();
|
||||
let next: usize = next_first_tx_index.into();
|
||||
let tx_count = next - first;
|
||||
|
||||
if start_index >= tx_count {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let end_index = (start_index + BLOCK_TXS_PAGE_SIZE).min(tx_count);
|
||||
let count = end_index - start_index;
|
||||
|
||||
let mut txs = Vec::with_capacity(count);
|
||||
for i in start_index..end_index {
|
||||
let tx_index = TxIndex::from(first + i);
|
||||
let tx = self.transaction_by_index(tx_index)?;
|
||||
txs.push(tx);
|
||||
}
|
||||
|
||||
Ok(txs)
|
||||
.txid
|
||||
.collect_range_at(first, first + tx_count))
|
||||
}
|
||||
|
||||
fn block_txid_at_index_by_height(&self, height: Height, index: usize) -> Result<Txid> {
|
||||
let indexer = self.indexer();
|
||||
let (first, tx_count) = self.block_tx_range(height)?;
|
||||
if index >= tx_count {
|
||||
return Err(Error::OutOfRange("Transaction index out of range".into()));
|
||||
}
|
||||
Ok(self
|
||||
.indexer()
|
||||
.vecs
|
||||
.transactions
|
||||
.txid
|
||||
.reader()
|
||||
.get(first + index))
|
||||
}
|
||||
|
||||
let max_height = self.indexed_height();
|
||||
if height > max_height {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
/// Batch-read transactions at arbitrary indices.
|
||||
/// Reads in ascending index order for I/O locality, returns in caller's order.
|
||||
pub fn transactions_by_indices(&self, indices: &[TxIndex]) -> Result<Vec<Transaction>> {
|
||||
if indices.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let first_tx_index = indexer
|
||||
let len = indices.len();
|
||||
|
||||
// Sort positions ascending for sequential I/O (O(n) when already sorted)
|
||||
let mut order: Vec<usize> = (0..len).collect();
|
||||
order.sort_unstable_by_key(|&i| indices[i]);
|
||||
|
||||
let indexer = self.indexer();
|
||||
let reader = self.reader();
|
||||
|
||||
let mut txid_cursor = indexer.vecs.transactions.txid.cursor();
|
||||
let mut height_cursor = indexer.vecs.transactions.height.cursor();
|
||||
let mut locktime_cursor = indexer.vecs.transactions.raw_locktime.cursor();
|
||||
let mut total_size_cursor = indexer.vecs.transactions.total_size.cursor();
|
||||
let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor();
|
||||
let mut position_cursor = indexer.vecs.transactions.position.cursor();
|
||||
|
||||
let txid_reader = indexer.vecs.transactions.txid.reader();
|
||||
let first_txout_index_reader = indexer.vecs.transactions.first_txout_index.reader();
|
||||
let value_reader = indexer.vecs.outputs.value.reader();
|
||||
let output_type_reader = indexer.vecs.outputs.output_type.reader();
|
||||
let type_index_reader = indexer.vecs.outputs.type_index.reader();
|
||||
let addr_readers = indexer.vecs.addrs.addr_readers();
|
||||
let blockhash_reader = indexer.vecs.blocks.blockhash.reader();
|
||||
let mut block_ts_cursor = indexer.vecs.blocks.timestamp.cursor();
|
||||
|
||||
let mut cached_block: Option<(Height, BlockHash, Timestamp)> = None;
|
||||
|
||||
// Read in sorted order, write directly to original position
|
||||
let mut txs: Vec<Option<Transaction>> = (0..len).map(|_| None).collect();
|
||||
|
||||
for &pos in &order {
|
||||
let tx_index = indices[pos];
|
||||
let idx = tx_index.to_usize();
|
||||
|
||||
let txid = txid_cursor.get(idx).unwrap();
|
||||
let height = height_cursor.get(idx).unwrap();
|
||||
let lock_time = locktime_cursor.get(idx).unwrap();
|
||||
let total_size = total_size_cursor.get(idx).unwrap();
|
||||
let first_txin_index = first_txin_cursor.get(idx).unwrap();
|
||||
let position = position_cursor.get(idx).unwrap();
|
||||
|
||||
let (block_hash, block_time) = if let Some((h, ref bh, bt)) = cached_block
|
||||
&& h == height
|
||||
{
|
||||
(bh.clone(), bt)
|
||||
} else {
|
||||
let bh = blockhash_reader.get(height.to_usize());
|
||||
let bt = block_ts_cursor.get(height.to_usize()).unwrap();
|
||||
cached_block = Some((height, bh.clone(), bt));
|
||||
(bh, bt)
|
||||
};
|
||||
|
||||
let buffer = reader.read_raw_bytes(position, *total_size as usize)?;
|
||||
let tx = bitcoin::Transaction::consensus_decode(&mut Cursor::new(buffer))
|
||||
.map_err(|_| Error::Parse("Failed to decode transaction".into()))?;
|
||||
|
||||
let outpoints = indexer.vecs.inputs.outpoint.collect_range_at(
|
||||
usize::from(first_txin_index),
|
||||
usize::from(first_txin_index) + tx.input.len(),
|
||||
);
|
||||
|
||||
let input: Vec<TxIn> = tx
|
||||
.input
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, txin)| {
|
||||
let outpoint = outpoints[j];
|
||||
let is_coinbase = outpoint.is_coinbase();
|
||||
|
||||
let (prev_txid, prev_vout, prevout) = if is_coinbase {
|
||||
(Txid::COINBASE, Vout::MAX, None)
|
||||
} else {
|
||||
let prev_tx_index = outpoint.tx_index();
|
||||
let prev_vout = outpoint.vout();
|
||||
let prev_txid = txid_reader.get(prev_tx_index.to_usize());
|
||||
let prev_first_txout_index =
|
||||
first_txout_index_reader.get(prev_tx_index.to_usize());
|
||||
let prev_txout_index = prev_first_txout_index + prev_vout;
|
||||
let prev_value = value_reader.get(usize::from(prev_txout_index));
|
||||
let prev_output_type: OutputType =
|
||||
output_type_reader.get(usize::from(prev_txout_index));
|
||||
let prev_type_index = type_index_reader.get(usize::from(prev_txout_index));
|
||||
let script_pubkey =
|
||||
addr_readers.script_pubkey(prev_output_type, prev_type_index);
|
||||
(
|
||||
prev_txid,
|
||||
prev_vout,
|
||||
Some(TxOut::from((script_pubkey, prev_value))),
|
||||
)
|
||||
};
|
||||
|
||||
let witness = txin
|
||||
.witness
|
||||
.iter()
|
||||
.map(|w| w.to_lower_hex_string())
|
||||
.collect();
|
||||
|
||||
TxIn {
|
||||
txid: prev_txid,
|
||||
vout: prev_vout,
|
||||
prevout,
|
||||
script_sig: txin.script_sig.clone(),
|
||||
script_sig_asm: (),
|
||||
witness,
|
||||
is_coinbase,
|
||||
sequence: txin.sequence.0,
|
||||
inner_redeem_script_asm: (),
|
||||
inner_witness_script_asm: (),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let weight = Weight::from(tx.weight());
|
||||
let total_sigop_cost = tx.total_sigop_cost(|outpoint| {
|
||||
tx.input
|
||||
.iter()
|
||||
.position(|i| i.previous_output == *outpoint)
|
||||
.and_then(|j| input[j].prevout.as_ref())
|
||||
.map(|p| bitcoin::TxOut {
|
||||
value: bitcoin::Amount::from_sat(u64::from(p.value)),
|
||||
script_pubkey: p.script_pubkey.clone(),
|
||||
})
|
||||
});
|
||||
let output: Vec<TxOut> = tx.output.into_iter().map(TxOut::from).collect();
|
||||
|
||||
let mut transaction = Transaction {
|
||||
index: Some(tx_index),
|
||||
txid,
|
||||
version: tx.version.into(),
|
||||
lock_time,
|
||||
total_size: *total_size as usize,
|
||||
weight,
|
||||
total_sigop_cost,
|
||||
fee: Sats::ZERO,
|
||||
input,
|
||||
output,
|
||||
status: TxStatus {
|
||||
confirmed: true,
|
||||
block_height: Some(height),
|
||||
block_hash: Some(block_hash),
|
||||
block_time: Some(block_time),
|
||||
},
|
||||
};
|
||||
|
||||
transaction.compute_fee();
|
||||
txs[pos] = Some(transaction);
|
||||
}
|
||||
|
||||
Ok(txs.into_iter().map(Option::unwrap).collect())
|
||||
}
|
||||
|
||||
/// Returns (first_tx_raw_index, tx_count) for a block at `height`.
|
||||
fn block_tx_range(&self, height: Height) -> Result<(usize, usize)> {
|
||||
let indexer = self.indexer();
|
||||
if height > self.indexed_height() {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
let first: usize = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(height)
|
||||
.unwrap();
|
||||
let next_first_tx_index = indexer
|
||||
.unwrap()
|
||||
.into();
|
||||
let next: usize = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(height.incremented())
|
||||
.unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len()));
|
||||
|
||||
let first: usize = first_tx_index.into();
|
||||
let next: usize = next_first_tx_index.into();
|
||||
let tx_count = next - first;
|
||||
|
||||
if index >= tx_count {
|
||||
return Err(Error::OutOfRange("Transaction index out of range".into()));
|
||||
}
|
||||
|
||||
let tx_index = first + index;
|
||||
let txid = indexer.vecs.transactions.txid.reader().get(tx_index);
|
||||
|
||||
Ok(txid)
|
||||
.unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len()))
|
||||
.into();
|
||||
Ok((first, next - first))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{MempoolBlock, MempoolInfo, RecommendedFees, Txid};
|
||||
use brk_types::{
|
||||
CpfpEntry, CpfpInfo, FeeRate, MempoolBlock, MempoolInfo, MempoolRecentTx, RecommendedFees,
|
||||
Txid, TxidPrefix, Weight,
|
||||
};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
@@ -40,4 +45,78 @@ impl Query {
|
||||
|
||||
Ok(blocks)
|
||||
}
|
||||
|
||||
pub fn mempool_recent(&self) -> Result<Vec<MempoolRecentTx>> {
|
||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
||||
Ok(mempool.get_txs().recent().to_vec())
|
||||
}
|
||||
|
||||
pub fn cpfp(&self, txid: &Txid) -> Result<CpfpInfo> {
|
||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
||||
let entries = mempool.get_entries();
|
||||
let prefix = TxidPrefix::from(txid);
|
||||
|
||||
let Some(entry) = entries.get(&prefix) else {
|
||||
return Ok(CpfpInfo::default());
|
||||
};
|
||||
|
||||
// Ancestors: walk up the depends chain
|
||||
let mut ancestors = Vec::new();
|
||||
let mut stack: Vec<TxidPrefix> = entry.depends.to_vec();
|
||||
while let Some(p) = stack.pop() {
|
||||
if let Some(anc) = entries.get(&p) {
|
||||
ancestors.push(CpfpEntry {
|
||||
txid: anc.txid.clone(),
|
||||
weight: Weight::from(anc.vsize),
|
||||
fee: anc.fee,
|
||||
});
|
||||
stack.extend(anc.depends.iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
let mut descendants = Vec::new();
|
||||
for child_prefix in entries.children(&prefix) {
|
||||
if let Some(e) = entries.get(child_prefix) {
|
||||
descendants.push(CpfpEntry {
|
||||
txid: e.txid.clone(),
|
||||
weight: Weight::from(e.vsize),
|
||||
fee: e.fee,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let effective_fee_per_vsize = entry.effective_fee_rate();
|
||||
|
||||
let best_descendant = descendants
|
||||
.iter()
|
||||
.max_by(|a, b| {
|
||||
FeeRate::from((a.fee, a.weight))
|
||||
.partial_cmp(&FeeRate::from((b.fee, b.weight)))
|
||||
.unwrap_or(Ordering::Equal)
|
||||
})
|
||||
.cloned();
|
||||
|
||||
Ok(CpfpInfo {
|
||||
ancestors,
|
||||
best_descendant,
|
||||
descendants,
|
||||
effective_fee_per_vsize: Some(effective_fee_per_vsize),
|
||||
fee: Some(entry.fee),
|
||||
adjusted_vsize: Some(entry.vsize),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transaction_times(&self, txids: &[Txid]) -> Result<Vec<u64>> {
|
||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
||||
let entries = mempool.get_entries();
|
||||
Ok(txids
|
||||
.iter()
|
||||
.map(|txid| {
|
||||
entries
|
||||
.get(&TxidPrefix::from(txid))
|
||||
.map(|e| usize::from(e.first_seen) as u64)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,57 @@
|
||||
// TODO: INCOMPLETE - indexes_to_fee_rate.day1 doesn't have percentile fields
|
||||
// because from_tx_index.rs calls remove_percentiles() before creating day1.
|
||||
// Need to either:
|
||||
// 1. Use .height instead and convert height to day1 for iteration
|
||||
// 2. Fix from_tx_index.rs to preserve percentiles for day1
|
||||
// 3. Create a separate day1 computation path with percentiles
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::{
|
||||
BlockFeeRatesEntry,
|
||||
// FeeRatePercentiles,
|
||||
TimePeriod,
|
||||
};
|
||||
// use vecdb::{IterableVec, VecIndex};
|
||||
use brk_types::{BlockFeeRatesEntry, FeeRate, FeeRatePercentiles, TimePeriod};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use super::block_window::BlockWindow;
|
||||
use crate::Query;
|
||||
|
||||
impl Query {
|
||||
pub fn block_fee_rates(&self, _time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
|
||||
// Disabled until percentile data is available at day1 level
|
||||
Ok(Vec::new())
|
||||
pub fn block_fee_rates(&self, time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
|
||||
let bw = BlockWindow::new(self, time_period);
|
||||
let computer = self.computer();
|
||||
let frd = &computer.transactions.fees.effective_fee_rate.distribution.block;
|
||||
|
||||
// Original implementation:
|
||||
// let computer = self.computer();
|
||||
// let current_height = self.height();
|
||||
// let start = current_height
|
||||
// .to_usize()
|
||||
// .saturating_sub(time_period.block_count());
|
||||
//
|
||||
// let iter = Day1Iter::new(computer, start, current_height.to_usize());
|
||||
//
|
||||
// let vecs = &computer.transactions.transaction.indexes_to_fee_rate.day1;
|
||||
// let mut min = vecs.unwrap_min().iter();
|
||||
// let mut pct10 = vecs.unwrap_pct10().iter();
|
||||
// let mut pct25 = vecs.unwrap_pct25().iter();
|
||||
// let mut median = vecs.unwrap_median().iter();
|
||||
// let mut pct75 = vecs.unwrap_pct75().iter();
|
||||
// let mut pct90 = vecs.unwrap_pct90().iter();
|
||||
// let mut max = vecs.unwrap_max().iter();
|
||||
//
|
||||
// Ok(iter.collect(|di, ts, h| {
|
||||
// Some(BlockFeeRatesEntry {
|
||||
// avg_height: h,
|
||||
// timestamp: ts,
|
||||
// percentiles: FeeRatePercentiles::new(
|
||||
// min.get(di).unwrap_or_default(),
|
||||
// pct10.get(di).unwrap_or_default(),
|
||||
// pct25.get(di).unwrap_or_default(),
|
||||
// median.get(di).unwrap_or_default(),
|
||||
// pct75.get(di).unwrap_or_default(),
|
||||
// pct90.get(di).unwrap_or_default(),
|
||||
// max.get(di).unwrap_or_default(),
|
||||
// ),
|
||||
// })
|
||||
// }))
|
||||
let min = frd.min.height.collect_range_at(bw.start, bw.end);
|
||||
let pct10 = frd.pct10.height.collect_range_at(bw.start, bw.end);
|
||||
let pct25 = frd.pct25.height.collect_range_at(bw.start, bw.end);
|
||||
let median = frd.median.height.collect_range_at(bw.start, bw.end);
|
||||
let pct75 = frd.pct75.height.collect_range_at(bw.start, bw.end);
|
||||
let pct90 = frd.pct90.height.collect_range_at(bw.start, bw.end);
|
||||
let max = frd.max.height.collect_range_at(bw.start, bw.end);
|
||||
|
||||
let timestamps = bw.timestamps(self);
|
||||
|
||||
let mut results = Vec::with_capacity(timestamps.len());
|
||||
let mut pos = 0;
|
||||
let total = min.len();
|
||||
|
||||
for ts in ×tamps {
|
||||
let window_end = (pos + bw.window).min(total);
|
||||
let count = window_end - pos;
|
||||
if count > 0 {
|
||||
let mid = (pos + window_end) / 2;
|
||||
let avg = |vals: &[FeeRate]| -> FeeRate {
|
||||
let sum: f64 = vals[pos..window_end].iter().map(|f| f64::from(*f)).sum();
|
||||
FeeRate::new(sum / count as f64)
|
||||
};
|
||||
|
||||
results.push(BlockFeeRatesEntry {
|
||||
avg_height: brk_types::Height::from(bw.start + mid),
|
||||
timestamp: *ts,
|
||||
percentiles: FeeRatePercentiles::new(
|
||||
avg(&min),
|
||||
avg(&pct10),
|
||||
avg(&pct25),
|
||||
avg(&median),
|
||||
avg(&pct75),
|
||||
avg(&pct90),
|
||||
avg(&max),
|
||||
),
|
||||
});
|
||||
}
|
||||
pos = window_end;
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,22 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{BlockFeesEntry, Height, Sats, TimePeriod};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
use brk_types::{BlockFeesEntry, TimePeriod};
|
||||
|
||||
use super::day1_iter::Day1Iter;
|
||||
use super::block_window::BlockWindow;
|
||||
use crate::Query;
|
||||
|
||||
impl Query {
|
||||
pub fn block_fees(&self, time_period: TimePeriod) -> Result<Vec<BlockFeesEntry>> {
|
||||
let computer = self.computer();
|
||||
let current_height = self.height();
|
||||
let start = current_height
|
||||
.to_usize()
|
||||
.saturating_sub(time_period.block_count());
|
||||
|
||||
let iter = Day1Iter::new(computer, start, current_height.to_usize());
|
||||
|
||||
let cumulative = &computer.mining.rewards.fees.cumulative.sats.height;
|
||||
let first_height = &computer.indexes.day1.first_height;
|
||||
|
||||
Ok(iter.collect(|di, ts, h| {
|
||||
let h_start = first_height.collect_one(di)?;
|
||||
let h_end = first_height
|
||||
.collect_one(di + 1_usize)
|
||||
.unwrap_or(Height::from(current_height.to_usize() + 1));
|
||||
let block_count = h_end.to_usize() - h_start.to_usize();
|
||||
if block_count == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cumulative_end = cumulative.collect_one_at(h_end.to_usize() - 1)?;
|
||||
let cumulative_start = if h_start.to_usize() > 0 {
|
||||
cumulative
|
||||
.collect_one_at(h_start.to_usize() - 1)
|
||||
.unwrap_or(Sats::ZERO)
|
||||
} else {
|
||||
Sats::ZERO
|
||||
};
|
||||
let daily_sum = cumulative_end - cumulative_start;
|
||||
let avg_fees = Sats::from(*daily_sum / block_count as u64);
|
||||
|
||||
Some(BlockFeesEntry {
|
||||
avg_height: h,
|
||||
timestamp: ts,
|
||||
avg_fees,
|
||||
let bw = BlockWindow::new(self, time_period);
|
||||
let cumulative = &self.computer().mining.rewards.fees.cumulative.sats.height;
|
||||
Ok(bw
|
||||
.cumulative_averages(self, cumulative)
|
||||
.into_iter()
|
||||
.map(|w| BlockFeesEntry {
|
||||
avg_height: w.avg_height,
|
||||
timestamp: w.timestamp,
|
||||
avg_fees: w.avg_value,
|
||||
usd: w.usd,
|
||||
})
|
||||
}))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,29 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{BlockRewardsEntry, Height, Sats, TimePeriod};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
use brk_types::{BlockRewardsEntry, TimePeriod};
|
||||
|
||||
use super::block_window::BlockWindow;
|
||||
use crate::Query;
|
||||
|
||||
impl Query {
|
||||
pub fn block_rewards(&self, time_period: TimePeriod) -> Result<Vec<BlockRewardsEntry>> {
|
||||
let computer = self.computer();
|
||||
let indexer = self.indexer();
|
||||
let current_height = self.height().to_usize();
|
||||
let start = current_height.saturating_sub(time_period.block_count());
|
||||
|
||||
let coinbase_vec = &computer.mining.rewards.coinbase.block.sats;
|
||||
let timestamp_vec = &indexer.vecs.blocks.timestamp;
|
||||
|
||||
match time_period {
|
||||
// Per-block, exact rewards
|
||||
TimePeriod::Day | TimePeriod::ThreeDays => {
|
||||
let rewards: Vec<Sats> = coinbase_vec.collect_range_at(start, current_height + 1);
|
||||
let timestamps: Vec<brk_types::Timestamp> =
|
||||
timestamp_vec.collect_range_at(start, current_height + 1);
|
||||
|
||||
Ok(rewards
|
||||
.iter()
|
||||
.zip(timestamps.iter())
|
||||
.enumerate()
|
||||
.map(|(i, (reward, ts))| BlockRewardsEntry {
|
||||
avg_height: (start + i) as u32,
|
||||
timestamp: **ts,
|
||||
avg_rewards: **reward,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
// Daily averages, sampled to ~200 points
|
||||
_ => {
|
||||
let first_height_vec = &computer.indexes.day1.first_height;
|
||||
let day1_vec = &computer.indexes.height.day1;
|
||||
|
||||
let start_di = day1_vec
|
||||
.collect_one(Height::from(start))
|
||||
.unwrap_or_default();
|
||||
let end_di = day1_vec
|
||||
.collect_one(Height::from(current_height))
|
||||
.unwrap_or_default();
|
||||
|
||||
let total_days = end_di.to_usize().saturating_sub(start_di.to_usize()) + 1;
|
||||
let step = (total_days / 200).max(1);
|
||||
|
||||
let mut entries = Vec::with_capacity(total_days / step + 1);
|
||||
let mut di = start_di.to_usize();
|
||||
|
||||
while di <= end_di.to_usize() {
|
||||
let day = brk_types::Day1::from(di);
|
||||
let next_day = brk_types::Day1::from(di + 1);
|
||||
|
||||
if let Some(first_h) = first_height_vec.collect_one(day) {
|
||||
let next_h = first_height_vec
|
||||
.collect_one(next_day)
|
||||
.unwrap_or(Height::from(current_height + 1));
|
||||
|
||||
let block_count = next_h.to_usize() - first_h.to_usize();
|
||||
if block_count > 0 {
|
||||
let sum =
|
||||
coinbase_vec
|
||||
.fold_range(first_h, next_h, Sats::ZERO, |acc, v| acc + v);
|
||||
let avg = *sum / block_count as u64;
|
||||
|
||||
if let Some(ts) = timestamp_vec.collect_one(first_h) {
|
||||
entries.push(BlockRewardsEntry {
|
||||
avg_height: first_h.to_usize() as u32,
|
||||
timestamp: *ts,
|
||||
avg_rewards: avg,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
di += step;
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
let bw = BlockWindow::new(self, time_period);
|
||||
let cumulative = &self
|
||||
.computer()
|
||||
.mining
|
||||
.rewards
|
||||
.coinbase
|
||||
.cumulative
|
||||
.sats
|
||||
.height;
|
||||
Ok(bw
|
||||
.cumulative_averages(self, cumulative)
|
||||
.into_iter()
|
||||
.map(|w| BlockRewardsEntry {
|
||||
avg_height: w.avg_height,
|
||||
timestamp: w.timestamp,
|
||||
avg_rewards: w.avg_value,
|
||||
usd: w.usd,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod};
|
||||
use vecdb::{ReadableOptionVec, VecIndex};
|
||||
use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod, Weight};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use super::day1_iter::Day1Iter;
|
||||
use super::block_window::BlockWindow;
|
||||
use crate::Query;
|
||||
|
||||
impl Query {
|
||||
pub fn block_sizes_weights(&self, time_period: TimePeriod) -> Result<BlockSizesWeights> {
|
||||
let computer = self.computer();
|
||||
let current_height = self.height();
|
||||
let start = current_height
|
||||
.to_usize()
|
||||
.saturating_sub(time_period.block_count());
|
||||
let bw = BlockWindow::new(self, time_period);
|
||||
let timestamps = bw.timestamps(self);
|
||||
|
||||
let iter = Day1Iter::new(computer, start, current_height.to_usize());
|
||||
|
||||
// Rolling 24h median, sampled at day1 boundaries
|
||||
let sizes_vec = &computer
|
||||
// Batch read per-block rolling 24h medians for the range
|
||||
let all_sizes = computer
|
||||
.blocks
|
||||
.size
|
||||
.size
|
||||
@@ -24,8 +20,9 @@ impl Query {
|
||||
.distribution
|
||||
.median
|
||||
._24h
|
||||
.day1;
|
||||
let weights_vec = &computer
|
||||
.height
|
||||
.collect_range_at(bw.start, bw.end);
|
||||
let all_weights = computer
|
||||
.blocks
|
||||
.weight
|
||||
.weight
|
||||
@@ -33,35 +30,30 @@ impl Query {
|
||||
.distribution
|
||||
.median
|
||||
._24h
|
||||
.day1;
|
||||
.height
|
||||
.collect_range_at(bw.start, bw.end);
|
||||
|
||||
let entries: Vec<_> = iter.collect(|di, ts, h| {
|
||||
let size: Option<u64> = sizes_vec.collect_one_flat(di).map(|s| *s);
|
||||
let weight: Option<u64> = weights_vec.collect_one_flat(di).map(|w| *w);
|
||||
Some((u32::from(h), (*ts), size, weight))
|
||||
});
|
||||
// Sample at window midpoints
|
||||
let mut sizes = Vec::with_capacity(timestamps.len());
|
||||
let mut weights = Vec::with_capacity(timestamps.len());
|
||||
|
||||
let sizes = entries
|
||||
.iter()
|
||||
.filter_map(|(h, ts, size, _)| {
|
||||
size.map(|s| BlockSizeEntry {
|
||||
avg_height: *h,
|
||||
for ((avg_height, start, _end), ts) in bw.iter().zip(×tamps) {
|
||||
let mid = start - bw.start + (bw.window / 2).min(all_sizes.len().saturating_sub(1));
|
||||
if let Some(&size) = all_sizes.get(mid) {
|
||||
sizes.push(BlockSizeEntry {
|
||||
avg_height,
|
||||
timestamp: *ts,
|
||||
avg_size: s,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let weights = entries
|
||||
.iter()
|
||||
.filter_map(|(h, ts, _, weight)| {
|
||||
weight.map(|w| BlockWeightEntry {
|
||||
avg_height: *h,
|
||||
avg_size: *size,
|
||||
});
|
||||
}
|
||||
if let Some(&weight) = all_weights.get(mid) {
|
||||
weights.push(BlockWeightEntry {
|
||||
avg_height,
|
||||
timestamp: *ts,
|
||||
avg_weight: w,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
avg_weight: Weight::from(*weight),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BlockSizesWeights { sizes, weights })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
use brk_types::{Cents, Dollars, Height, Sats, TimePeriod, Timestamp};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
/// Number of blocks per aggregation window, matching mempool.space's granularity.
|
||||
fn block_window(period: TimePeriod) -> usize {
|
||||
match period {
|
||||
TimePeriod::Day | TimePeriod::ThreeDays | TimePeriod::Week => 1,
|
||||
TimePeriod::Month => 3,
|
||||
TimePeriod::ThreeMonths => 12,
|
||||
TimePeriod::SixMonths => 18,
|
||||
TimePeriod::Year | TimePeriod::TwoYears => 48,
|
||||
TimePeriod::ThreeYears => 72,
|
||||
TimePeriod::All => 144,
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-window average with metadata.
|
||||
pub struct WindowAvg {
|
||||
pub avg_height: Height,
|
||||
pub timestamp: Timestamp,
|
||||
pub avg_value: Sats,
|
||||
pub usd: Dollars,
|
||||
}
|
||||
|
||||
/// Block range and window size for a time period.
|
||||
pub struct BlockWindow {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub window: usize,
|
||||
}
|
||||
|
||||
impl BlockWindow {
|
||||
pub fn new(query: &Query, time_period: TimePeriod) -> Self {
|
||||
let current_height = query.height();
|
||||
let computer = query.computer();
|
||||
let lookback = &computer.blocks.lookback;
|
||||
|
||||
// Use pre-computed timestamp-based lookback for accurate time boundaries.
|
||||
// 24h, 1w, 1m, 1y use in-memory CachedVec; others fall back to PcoVec.
|
||||
let cached = &lookback.cached_window_starts.0;
|
||||
let start_height = match time_period {
|
||||
TimePeriod::Day => cached._24h.collect_one(current_height),
|
||||
TimePeriod::ThreeDays => lookback._3d.collect_one(current_height),
|
||||
TimePeriod::Week => cached._1w.collect_one(current_height),
|
||||
TimePeriod::Month => cached._1m.collect_one(current_height),
|
||||
TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height),
|
||||
TimePeriod::SixMonths => lookback._6m.collect_one(current_height),
|
||||
TimePeriod::Year => cached._1y.collect_one(current_height),
|
||||
TimePeriod::TwoYears => lookback._2y.collect_one(current_height),
|
||||
TimePeriod::ThreeYears => lookback._3y.collect_one(current_height),
|
||||
TimePeriod::All => None,
|
||||
}
|
||||
.unwrap_or_default();
|
||||
|
||||
Self {
|
||||
start: start_height.to_usize(),
|
||||
end: current_height.to_usize() + 1,
|
||||
window: block_window(time_period),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute per-window averages from a cumulative sats vec.
|
||||
/// Batch-reads timestamps, prices, and the cumulative in one pass.
|
||||
pub fn cumulative_averages(
|
||||
&self,
|
||||
query: &Query,
|
||||
cumulative: &impl ReadableVec<Height, Sats>,
|
||||
) -> Vec<WindowAvg> {
|
||||
let indexer = query.indexer();
|
||||
let computer = query.computer();
|
||||
|
||||
// Batch read all needed data for the range
|
||||
let all_ts = indexer
|
||||
.vecs
|
||||
.blocks
|
||||
.timestamp
|
||||
.collect_range_at(self.start, self.end);
|
||||
let all_prices: Vec<Cents> = computer
|
||||
.prices
|
||||
.cached_spot_cents
|
||||
.collect_range_at(self.start, self.end);
|
||||
let read_start = self.start.saturating_sub(1).max(0);
|
||||
let all_cum = cumulative.collect_range_at(read_start, self.end);
|
||||
let offset = if self.start > 0 { 1 } else { 0 };
|
||||
|
||||
let mut results = Vec::with_capacity(self.count());
|
||||
let mut pos = 0;
|
||||
let total = all_ts.len();
|
||||
|
||||
while pos < total {
|
||||
let window_end = (pos + self.window).min(total);
|
||||
let block_count = (window_end - pos) as u64;
|
||||
if block_count > 0 {
|
||||
let mid = (pos + window_end) / 2;
|
||||
let cum_end = all_cum[window_end - 1 + offset];
|
||||
let cum_start = if pos + offset > 0 {
|
||||
all_cum[pos + offset - 1]
|
||||
} else {
|
||||
Sats::ZERO
|
||||
};
|
||||
let total_sats = cum_end - cum_start;
|
||||
results.push(WindowAvg {
|
||||
avg_height: Height::from(self.start + mid),
|
||||
timestamp: all_ts[mid],
|
||||
avg_value: Sats::from(*total_sats / block_count),
|
||||
usd: Dollars::from(all_prices[mid]),
|
||||
});
|
||||
}
|
||||
pos = window_end;
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Batch-read timestamps for the midpoint of each window.
|
||||
pub fn timestamps(&self, query: &Query) -> Vec<Timestamp> {
|
||||
let all_ts = query
|
||||
.indexer()
|
||||
.vecs
|
||||
.blocks
|
||||
.timestamp
|
||||
.collect_range_at(self.start, self.end);
|
||||
let mut timestamps = Vec::with_capacity(self.count());
|
||||
let mut pos = 0;
|
||||
while pos < all_ts.len() {
|
||||
let window_end = (pos + self.window).min(all_ts.len());
|
||||
timestamps.push(all_ts[(pos + window_end) / 2]);
|
||||
pos = window_end;
|
||||
}
|
||||
timestamps
|
||||
}
|
||||
|
||||
/// Number of windows in this range.
|
||||
fn count(&self) -> usize {
|
||||
(self.end - self.start).div_ceil(self.window)
|
||||
}
|
||||
|
||||
/// Iterate windows, yielding (avg_height, window_start, window_end) for each.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Height, usize, usize)> + '_ {
|
||||
let mut pos = self.start;
|
||||
std::iter::from_fn(move || {
|
||||
if pos >= self.end {
|
||||
return None;
|
||||
}
|
||||
let window_end = (pos + self.window).min(self.end);
|
||||
let avg_height = Height::from((pos + window_end) / 2);
|
||||
let start = pos;
|
||||
pos = window_end;
|
||||
Some((avg_height, start, window_end))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use brk_computer::Computer;
|
||||
use brk_types::{Day1, Height, Timestamp};
|
||||
use vecdb::{ReadableVec, Ro, VecIndex};
|
||||
|
||||
/// Helper for iterating over day1 ranges with sampling.
|
||||
pub struct Day1Iter<'a> {
|
||||
computer: &'a Computer<Ro>,
|
||||
start_di: Day1,
|
||||
end_di: Day1,
|
||||
step: usize,
|
||||
}
|
||||
|
||||
impl<'a> Day1Iter<'a> {
|
||||
pub fn new(computer: &'a Computer<Ro>, start_height: usize, end_height: usize) -> Self {
|
||||
let start_di = computer
|
||||
.indexes
|
||||
.height
|
||||
.day1
|
||||
.collect_one(Height::from(start_height))
|
||||
.unwrap_or_default();
|
||||
let end_di = computer
|
||||
.indexes
|
||||
.height
|
||||
.day1
|
||||
.collect_one(Height::from(end_height))
|
||||
.unwrap_or_default();
|
||||
|
||||
let total = end_di.to_usize().saturating_sub(start_di.to_usize()) + 1;
|
||||
let step = (total / 200).max(1);
|
||||
|
||||
Self {
|
||||
computer,
|
||||
start_di,
|
||||
end_di,
|
||||
step,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate and collect entries using the provided transform function.
|
||||
pub fn collect<T, F>(&self, mut transform: F) -> Vec<T>
|
||||
where
|
||||
F: FnMut(Day1, Timestamp, Height) -> Option<T>,
|
||||
{
|
||||
let total = self
|
||||
.end_di
|
||||
.to_usize()
|
||||
.saturating_sub(self.start_di.to_usize())
|
||||
+ 1;
|
||||
let timestamps = &self.computer.indexes.timestamp.day1;
|
||||
let heights = &self.computer.indexes.day1.first_height;
|
||||
|
||||
let mut entries = Vec::with_capacity(total / self.step + 1);
|
||||
let mut i = self.start_di.to_usize();
|
||||
|
||||
while i <= self.end_di.to_usize() {
|
||||
let di = Day1::from(i);
|
||||
if let (Some(ts), Some(h)) = (timestamps.collect_one(di), heights.collect_one(di))
|
||||
&& let Some(entry) = transform(di, ts, h)
|
||||
{
|
||||
entries.push(entry);
|
||||
}
|
||||
i += self.step;
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl Query {
|
||||
let time_offset = expected_time as i64 - elapsed_time as i64;
|
||||
|
||||
// Calculate previous retarget using stored difficulty values
|
||||
let previous_retarget = if current_epoch_usize > 0 {
|
||||
let (previous_retarget, previous_time) = if current_epoch_usize > 0 {
|
||||
let prev_epoch = Epoch::from(current_epoch_usize - 1);
|
||||
let prev_epoch_start = computer
|
||||
.indexes
|
||||
@@ -107,26 +107,33 @@ impl Query {
|
||||
.collect_one(epoch_start_height)
|
||||
.unwrap();
|
||||
|
||||
if *prev_difficulty > 0.0 {
|
||||
let retarget = if *prev_difficulty > 0.0 {
|
||||
((*curr_difficulty / *prev_difficulty) - 1.0) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
|
||||
(retarget, epoch_start_timestamp)
|
||||
} else {
|
||||
0.0
|
||||
(0.0, epoch_start_timestamp)
|
||||
};
|
||||
|
||||
// Expected blocks based on wall clock time since epoch start
|
||||
let expected_blocks = elapsed_time as f64 / TARGET_BLOCK_TIME as f64;
|
||||
|
||||
Ok(DifficultyAdjustment {
|
||||
progress_percent,
|
||||
difficulty_change,
|
||||
estimated_retarget_date,
|
||||
estimated_retarget_date: estimated_retarget_date * 1000,
|
||||
remaining_blocks,
|
||||
remaining_time,
|
||||
remaining_time: remaining_time * 1000,
|
||||
previous_retarget,
|
||||
previous_time,
|
||||
next_retarget_height: Height::from(next_retarget_height),
|
||||
time_avg,
|
||||
adjusted_time_avg: time_avg,
|
||||
time_avg: time_avg * 1000,
|
||||
adjusted_time_avg: time_avg * 1000,
|
||||
time_offset,
|
||||
expected_blocks,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_computer::Computer;
|
||||
use brk_types::{DifficultyAdjustmentEntry, Epoch, Height};
|
||||
use brk_types::{DifficultyAdjustmentEntry, Height};
|
||||
use vecdb::{ReadableVec, Ro, VecIndex};
|
||||
|
||||
/// Iterate over difficulty epochs within a height range.
|
||||
@@ -21,28 +21,27 @@ pub fn iter_difficulty_epochs(
|
||||
.collect_one(Height::from(end_height))
|
||||
.unwrap_or_default();
|
||||
|
||||
let epoch_to_height = &computer.indexes.epoch.first_height;
|
||||
let epoch_to_timestamp = &computer.indexes.timestamp.epoch;
|
||||
let epoch_to_difficulty = &computer.blocks.difficulty.value.epoch;
|
||||
let mut height_cursor = computer.indexes.epoch.first_height.cursor();
|
||||
let mut timestamp_cursor = computer.indexes.timestamp.epoch.cursor();
|
||||
let mut difficulty_cursor = computer.blocks.difficulty.value.epoch.cursor();
|
||||
|
||||
let mut results = Vec::with_capacity(end_epoch.to_usize() - start_epoch.to_usize() + 1);
|
||||
let mut prev_difficulty: Option<f64> = None;
|
||||
|
||||
for epoch_usize in start_epoch.to_usize()..=end_epoch.to_usize() {
|
||||
let epoch = Epoch::from(epoch_usize);
|
||||
let epoch_height = epoch_to_height.collect_one(epoch).unwrap_or_default();
|
||||
let epoch_height = height_cursor.get(epoch_usize).unwrap_or_default();
|
||||
|
||||
// Skip epochs before our start height but track difficulty
|
||||
if epoch_height.to_usize() < start_height {
|
||||
prev_difficulty = epoch_to_difficulty.collect_one(epoch).map(|d| *d);
|
||||
prev_difficulty = difficulty_cursor.get(epoch_usize).map(|d| *d);
|
||||
continue;
|
||||
}
|
||||
|
||||
let epoch_timestamp = epoch_to_timestamp.collect_one(epoch).unwrap_or_default();
|
||||
let epoch_difficulty = *epoch_to_difficulty.collect_one(epoch).unwrap_or_default();
|
||||
let epoch_timestamp = timestamp_cursor.get(epoch_usize).unwrap_or_default();
|
||||
let epoch_difficulty = *difficulty_cursor.get(epoch_usize).unwrap_or_default();
|
||||
|
||||
let change_percent = match prev_difficulty {
|
||||
Some(prev) if prev > 0.0 => ((epoch_difficulty / prev) - 1.0) * 100.0,
|
||||
Some(prev) if prev > 0.0 => epoch_difficulty / prev,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{Day1, DifficultyEntry, HashrateEntry, HashrateSummary, Height, TimePeriod};
|
||||
use brk_types::{DifficultyEntry, HashrateEntry, HashrateSummary, Height, TimePeriod};
|
||||
use vecdb::{ReadableOptionVec, ReadableVec, VecIndex};
|
||||
|
||||
use super::epochs::iter_difficulty_epochs;
|
||||
@@ -56,16 +56,15 @@ impl Query {
|
||||
let total_days = end_day1.to_usize().saturating_sub(start_day1.to_usize()) + 1;
|
||||
let step = (total_days / 200).max(1); // Max ~200 data points
|
||||
|
||||
let hashrate_vec = &computer.mining.hashrate.rate.base.day1;
|
||||
let timestamp_vec = &computer.indexes.timestamp.day1;
|
||||
let mut hr_cursor = computer.mining.hashrate.rate.base.day1.cursor();
|
||||
let mut ts_cursor = computer.indexes.timestamp.day1.cursor();
|
||||
|
||||
let mut hashrates = Vec::with_capacity(total_days / step + 1);
|
||||
let mut di = start_day1.to_usize();
|
||||
while di <= end_day1.to_usize() {
|
||||
let day1 = Day1::from(di);
|
||||
if let (Some(hr), Some(timestamp)) = (
|
||||
hashrate_vec.collect_one_flat(day1),
|
||||
timestamp_vec.collect_one(day1),
|
||||
if let (Some(Some(hr)), Some(timestamp)) = (
|
||||
hr_cursor.get(di),
|
||||
ts_cursor.get(di),
|
||||
) {
|
||||
hashrates.push(HashrateEntry {
|
||||
timestamp,
|
||||
@@ -79,9 +78,10 @@ impl Query {
|
||||
let difficulty: Vec<DifficultyEntry> = iter_difficulty_epochs(computer, start, end)
|
||||
.into_iter()
|
||||
.map(|e| DifficultyEntry {
|
||||
timestamp: e.timestamp,
|
||||
difficulty: e.difficulty,
|
||||
time: e.timestamp,
|
||||
height: e.height,
|
||||
difficulty: e.difficulty,
|
||||
adjustment: e.change_percent,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ mod block_fee_rates;
|
||||
mod block_fees;
|
||||
mod block_rewards;
|
||||
mod block_sizes;
|
||||
mod day1_iter;
|
||||
mod block_window;
|
||||
mod difficulty;
|
||||
mod difficulty_adjustments;
|
||||
mod epochs;
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{
|
||||
Height, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo, PoolInfo, PoolSlug,
|
||||
PoolStats, PoolsSummary, TimePeriod, pools,
|
||||
BlockInfoV1, Day1, Height, Pool, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo,
|
||||
PoolHashrateEntry, PoolInfo, PoolSlug, PoolStats, PoolsSummary, StoredF64, StoredU64,
|
||||
TimePeriod, pools,
|
||||
};
|
||||
use vecdb::{AnyVec, ReadableVec, VecIndex};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
/// 7-day lookback for share computation (matching mempool.space)
|
||||
const LOOKBACK_DAYS: usize = 7;
|
||||
/// Weekly sample interval (matching mempool.space's 604800s interval)
|
||||
const SAMPLE_WEEKLY: usize = 7;
|
||||
|
||||
/// Pre-read shared data for hashrate computation.
|
||||
struct HashrateSharedData {
|
||||
start_day: usize,
|
||||
end_day: usize,
|
||||
daily_hashrate: Vec<Option<StoredF64>>,
|
||||
first_heights: Vec<Height>,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub fn mining_pools(&self, time_period: TimePeriod) -> Result<PoolsSummary> {
|
||||
let computer = self.computer();
|
||||
let current_height = self.height();
|
||||
let end = current_height.to_usize();
|
||||
|
||||
// No blocks indexed yet
|
||||
if computer.pools.pool.len() == 0 {
|
||||
@@ -19,14 +32,46 @@ impl Query {
|
||||
pools: vec![],
|
||||
block_count: 0,
|
||||
last_estimated_hashrate: 0,
|
||||
last_estimated_hashrate3d: 0,
|
||||
last_estimated_hashrate1w: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate start height based on time period
|
||||
let start = end.saturating_sub(time_period.block_count());
|
||||
// Use timestamp-based lookback for accurate time boundaries
|
||||
let lookback = &computer.blocks.lookback;
|
||||
let start = match time_period {
|
||||
TimePeriod::Day => lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._24h
|
||||
.collect_one(current_height),
|
||||
TimePeriod::ThreeDays => lookback._3d.collect_one(current_height),
|
||||
TimePeriod::Week => lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._1w
|
||||
.collect_one(current_height),
|
||||
TimePeriod::Month => lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._1m
|
||||
.collect_one(current_height),
|
||||
TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height),
|
||||
TimePeriod::SixMonths => lookback._6m.collect_one(current_height),
|
||||
TimePeriod::Year => lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._1y
|
||||
.collect_one(current_height),
|
||||
TimePeriod::TwoYears => lookback._2y.collect_one(current_height),
|
||||
TimePeriod::ThreeYears => lookback._3y.collect_one(current_height),
|
||||
TimePeriod::All => None,
|
||||
}
|
||||
.unwrap_or_default()
|
||||
.to_usize();
|
||||
|
||||
let pools = pools();
|
||||
let mut pool_data: Vec<(&'static brk_types::Pool, u64)> = Vec::new();
|
||||
let mut pool_data: Vec<(&'static Pool, u64)> = Vec::new();
|
||||
|
||||
// For each pool, get cumulative count at end and start, subtract to get range count
|
||||
for (pool_id, cumulative) in computer
|
||||
@@ -78,13 +123,38 @@ impl Query {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// TODO: Calculate actual hashrate from difficulty
|
||||
let last_estimated_hashrate = 0u128;
|
||||
let hashrate_at = |height: Height| -> u128 {
|
||||
let day = computer
|
||||
.indexes
|
||||
.height
|
||||
.day1
|
||||
.collect_one(height)
|
||||
.unwrap_or_default();
|
||||
computer
|
||||
.mining
|
||||
.hashrate
|
||||
.rate
|
||||
.base
|
||||
.day1
|
||||
.collect_one(day)
|
||||
.flatten()
|
||||
.map(|v| *v as u128)
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
let lookback = &computer.blocks.lookback;
|
||||
let last_estimated_hashrate = hashrate_at(current_height);
|
||||
let last_estimated_hashrate3d =
|
||||
hashrate_at(lookback._3d.collect_one(current_height).unwrap_or_default());
|
||||
let last_estimated_hashrate1w =
|
||||
hashrate_at(lookback._1w.collect_one(current_height).unwrap_or_default());
|
||||
|
||||
Ok(PoolsSummary {
|
||||
pools: pool_stats,
|
||||
block_count: total_blocks,
|
||||
last_estimated_hashrate,
|
||||
last_estimated_hashrate3d,
|
||||
last_estimated_hashrate1w,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,8 +188,15 @@ impl Query {
|
||||
// Get total blocks (all time)
|
||||
let total_all: u64 = *cumulative.collect_one(current_height).unwrap_or_default();
|
||||
|
||||
// Get blocks for 24h (144 blocks)
|
||||
let start_24h = end.saturating_sub(144);
|
||||
// Use timestamp-based lookback for accurate time boundaries
|
||||
let lookback = &computer.blocks.lookback;
|
||||
let start_24h = lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._24h
|
||||
.collect_one(current_height)
|
||||
.unwrap_or_default()
|
||||
.to_usize();
|
||||
let count_before_24h: u64 = if start_24h == 0 {
|
||||
0
|
||||
} else {
|
||||
@@ -129,8 +206,13 @@ impl Query {
|
||||
};
|
||||
let total_24h = total_all.saturating_sub(count_before_24h);
|
||||
|
||||
// Get blocks for 1w (1008 blocks)
|
||||
let start_1w = end.saturating_sub(1008);
|
||||
let start_1w = lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._1w
|
||||
.collect_one(current_height)
|
||||
.unwrap_or_default()
|
||||
.to_usize();
|
||||
let count_before_1w: u64 = if start_1w == 0 {
|
||||
0
|
||||
} else {
|
||||
@@ -173,8 +255,260 @@ impl Query {
|
||||
day: share_24h,
|
||||
week: share_1w,
|
||||
},
|
||||
estimated_hashrate: 0, // TODO: Calculate from share and network hashrate
|
||||
estimated_hashrate: {
|
||||
let day = computer
|
||||
.indexes
|
||||
.height
|
||||
.day1
|
||||
.collect_one(current_height)
|
||||
.unwrap_or_default();
|
||||
let network_hr = computer
|
||||
.mining
|
||||
.hashrate
|
||||
.rate
|
||||
.base
|
||||
.day1
|
||||
.collect_one(day)
|
||||
.flatten()
|
||||
.map(|v| *v as u128)
|
||||
.unwrap_or(0);
|
||||
(share_24h * network_hr as f64) as u128
|
||||
},
|
||||
reported_hashrate: None,
|
||||
total_reward: computer
|
||||
.pools
|
||||
.major
|
||||
.get(&slug)
|
||||
.and_then(|v| v.rewards.cumulative.sats.height.collect_one(current_height)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pool_blocks(
|
||||
&self,
|
||||
slug: PoolSlug,
|
||||
start_height: Option<Height>,
|
||||
) -> Result<Vec<BlockInfoV1>> {
|
||||
let computer = self.computer();
|
||||
let max_height = self.height().to_usize();
|
||||
let start = start_height.map(|h| h.to_usize()).unwrap_or(max_height);
|
||||
|
||||
let reader = computer.pools.pool.reader();
|
||||
let end = start.min(reader.len().saturating_sub(1));
|
||||
|
||||
const POOL_BLOCKS_LIMIT: usize = 100;
|
||||
let mut heights = Vec::with_capacity(POOL_BLOCKS_LIMIT);
|
||||
for h in (0..=end).rev() {
|
||||
if reader.get(h) == slug {
|
||||
heights.push(h);
|
||||
if heights.len() >= POOL_BLOCKS_LIMIT {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group consecutive descending heights into ranges for batch reads
|
||||
let mut blocks = Vec::with_capacity(heights.len());
|
||||
let mut i = 0;
|
||||
while i < heights.len() {
|
||||
let hi = heights[i];
|
||||
while i + 1 < heights.len() && heights[i + 1] + 1 == heights[i] {
|
||||
i += 1;
|
||||
}
|
||||
if let Ok(mut v) = self.blocks_v1_range(heights[i], hi + 1) {
|
||||
blocks.append(&mut v);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Ok(blocks)
|
||||
}
|
||||
|
||||
pub fn pool_hashrate(&self, slug: PoolSlug) -> Result<Vec<PoolHashrateEntry>> {
|
||||
let pool_name = pools().get(slug).name.to_string();
|
||||
let shared = self.hashrate_shared_data(0)?;
|
||||
let pool_cum = self.pool_daily_cumulative(slug, shared.start_day, shared.end_day)?;
|
||||
Ok(Self::compute_hashrate_entries(
|
||||
&shared,
|
||||
&pool_cum,
|
||||
&pool_name,
|
||||
SAMPLE_WEEKLY,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn pools_hashrate(
|
||||
&self,
|
||||
time_period: Option<TimePeriod>,
|
||||
) -> Result<Vec<PoolHashrateEntry>> {
|
||||
let start_height = match time_period {
|
||||
Some(tp) => {
|
||||
let lookback = &self.computer().blocks.lookback;
|
||||
let current_height = self.height();
|
||||
match tp {
|
||||
TimePeriod::Day => lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._24h
|
||||
.collect_one(current_height),
|
||||
TimePeriod::ThreeDays => lookback._3d.collect_one(current_height),
|
||||
TimePeriod::Week => lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._1w
|
||||
.collect_one(current_height),
|
||||
TimePeriod::Month => lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._1m
|
||||
.collect_one(current_height),
|
||||
TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height),
|
||||
TimePeriod::SixMonths => lookback._6m.collect_one(current_height),
|
||||
TimePeriod::Year => lookback
|
||||
.cached_window_starts
|
||||
.0
|
||||
._1y
|
||||
.collect_one(current_height),
|
||||
TimePeriod::TwoYears => lookback._2y.collect_one(current_height),
|
||||
TimePeriod::ThreeYears => lookback._3y.collect_one(current_height),
|
||||
TimePeriod::All => None,
|
||||
}
|
||||
.unwrap_or_default()
|
||||
.to_usize()
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
let shared = self.hashrate_shared_data(start_height)?;
|
||||
let pools_list = pools();
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for pool in pools_list.iter() {
|
||||
let Ok(pool_cum) =
|
||||
self.pool_daily_cumulative(pool.slug, shared.start_day, shared.end_day)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
entries.extend(Self::compute_hashrate_entries(
|
||||
&shared,
|
||||
&pool_cum,
|
||||
pool.name,
|
||||
SAMPLE_WEEKLY,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Shared data needed for hashrate computation (read once, reuse across pools).
|
||||
fn hashrate_shared_data(&self, start_height: usize) -> Result<HashrateSharedData> {
|
||||
let computer = self.computer();
|
||||
let current_height = self.height();
|
||||
let start_day = computer
|
||||
.indexes
|
||||
.height
|
||||
.day1
|
||||
.collect_one_at(start_height)
|
||||
.unwrap_or_default()
|
||||
.to_usize();
|
||||
let end_day = computer
|
||||
.indexes
|
||||
.height
|
||||
.day1
|
||||
.collect_one(current_height)
|
||||
.unwrap_or_default()
|
||||
.to_usize()
|
||||
+ 1;
|
||||
let daily_hashrate = computer
|
||||
.mining
|
||||
.hashrate
|
||||
.rate
|
||||
.base
|
||||
.day1
|
||||
.collect_range_at(start_day, end_day);
|
||||
let first_heights = computer
|
||||
.indexes
|
||||
.day1
|
||||
.first_height
|
||||
.collect_range_at(start_day, end_day);
|
||||
|
||||
Ok(HashrateSharedData {
|
||||
start_day,
|
||||
end_day,
|
||||
daily_hashrate,
|
||||
first_heights,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read daily cumulative blocks mined for a pool.
|
||||
fn pool_daily_cumulative(
|
||||
&self,
|
||||
slug: PoolSlug,
|
||||
start_day: usize,
|
||||
end_day: usize,
|
||||
) -> Result<Vec<Option<StoredU64>>> {
|
||||
let computer = self.computer();
|
||||
computer
|
||||
.pools
|
||||
.major
|
||||
.get(&slug)
|
||||
.map(|v| {
|
||||
v.base
|
||||
.blocks_mined
|
||||
.cumulative
|
||||
.day1
|
||||
.collect_range_at(start_day, end_day)
|
||||
})
|
||||
.or_else(|| {
|
||||
computer.pools.minor.get(&slug).map(|v| {
|
||||
v.blocks_mined
|
||||
.cumulative
|
||||
.day1
|
||||
.collect_range_at(start_day, end_day)
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| Error::NotFound("Pool not found".into()))
|
||||
}
|
||||
|
||||
/// Compute hashrate entries from daily cumulative blocks + shared data.
|
||||
/// Uses 7-day windowed share: pool_blocks_in_week / total_blocks_in_week.
|
||||
fn compute_hashrate_entries(
|
||||
shared: &HashrateSharedData,
|
||||
pool_cum: &[Option<StoredU64>],
|
||||
pool_name: &str,
|
||||
sample_days: usize,
|
||||
) -> Vec<PoolHashrateEntry> {
|
||||
let total = pool_cum.len();
|
||||
if total <= LOOKBACK_DAYS {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let mut i = LOOKBACK_DAYS;
|
||||
while i < total {
|
||||
if let (Some(cum_now), Some(cum_prev)) = (pool_cum[i], pool_cum[i - LOOKBACK_DAYS]) {
|
||||
let pool_blocks = (*cum_now).saturating_sub(*cum_prev);
|
||||
if pool_blocks > 0 {
|
||||
let h_now = shared.first_heights[i].to_usize();
|
||||
let h_prev = shared.first_heights[i - LOOKBACK_DAYS].to_usize();
|
||||
let total_blocks = h_now.saturating_sub(h_prev);
|
||||
|
||||
if total_blocks > 0
|
||||
&& let Some(hr) = shared.daily_hashrate[i].as_ref()
|
||||
{
|
||||
let network_hr = **hr;
|
||||
let share = pool_blocks as f64 / total_blocks as f64;
|
||||
let day = Day1::from(shared.start_day + i);
|
||||
entries.push(PoolHashrateEntry {
|
||||
timestamp: day.to_timestamp(),
|
||||
avg_hashrate: (network_hr * share) as u128,
|
||||
share,
|
||||
pool_name: pool_name.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
i += sample_days;
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Dollars;
|
||||
use brk_types::{Dollars, ExchangeRates, HistoricalPrice, HistoricalPriceEntry, Hour4, Timestamp};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::Query;
|
||||
|
||||
@@ -18,4 +19,43 @@ impl Query {
|
||||
|
||||
Ok(oracle.price_dollars())
|
||||
}
|
||||
|
||||
pub fn historical_price(&self, timestamp: Option<Timestamp>) -> Result<HistoricalPrice> {
|
||||
let prices = match timestamp {
|
||||
Some(ts) => self.price_at(ts)?,
|
||||
None => self.all_prices()?,
|
||||
};
|
||||
Ok(HistoricalPrice {
|
||||
prices,
|
||||
exchange_rates: ExchangeRates {},
|
||||
})
|
||||
}
|
||||
|
||||
fn price_at(&self, target: Timestamp) -> Result<Vec<HistoricalPriceEntry>> {
|
||||
let h4 = Hour4::from_timestamp(target);
|
||||
let cents = self.computer().prices.spot.cents.hour4.collect_one(h4);
|
||||
Ok(vec![HistoricalPriceEntry {
|
||||
time: h4.to_timestamp(),
|
||||
usd: Dollars::from(cents.flatten().unwrap_or_default()),
|
||||
}])
|
||||
}
|
||||
|
||||
fn all_prices(&self) -> Result<Vec<HistoricalPriceEntry>> {
|
||||
let computer = self.computer();
|
||||
Ok(computer
|
||||
.prices
|
||||
.spot
|
||||
.cents
|
||||
.hour4
|
||||
.collect()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, cents)| {
|
||||
Some(HistoricalPriceEntry {
|
||||
time: Hour4::from(i).to_timestamp(),
|
||||
usd: Dollars::from(cents?),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
+262
-273
@@ -1,26 +1,24 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use bitcoin::{consensus::Decodable, hex::DisplayHex};
|
||||
use bitcoin::hex::{DisplayHex, FromHex};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{
|
||||
OutputType, Sats, Transaction, TxIn, TxInIndex, TxIndex, TxOut, TxOutspend, TxStatus, Txid,
|
||||
TxidParam, TxidPrefix, Vin, Vout, Weight,
|
||||
BlockHash, Height, MerkleProof, Timestamp, Transaction, TxInIndex, TxIndex, TxOutIndex,
|
||||
TxOutspend, TxStatus, Txid, TxidPrefix, Vin, Vout,
|
||||
};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
impl Query {
|
||||
pub fn transaction(&self, TxidParam { txid }: TxidParam) -> Result<Transaction> {
|
||||
pub fn transaction(&self, txid: &Txid) -> Result<Transaction> {
|
||||
// First check mempool for unconfirmed transactions
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& let Some(tx_with_hex) = mempool.get_txs().get(&txid)
|
||||
&& let Some(tx_with_hex) = mempool.get_txs().get(txid)
|
||||
{
|
||||
return Ok(tx_with_hex.tx().clone());
|
||||
}
|
||||
|
||||
// Look up confirmed transaction by txid prefix
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let prefix = TxidPrefix::from(txid);
|
||||
let indexer = self.indexer();
|
||||
let Ok(Some(tx_index)) = indexer
|
||||
.stores
|
||||
@@ -34,16 +32,16 @@ impl Query {
|
||||
self.transaction_by_index(tx_index)
|
||||
}
|
||||
|
||||
pub fn transaction_status(&self, TxidParam { txid }: TxidParam) -> Result<TxStatus> {
|
||||
pub fn transaction_status(&self, txid: &Txid) -> Result<TxStatus> {
|
||||
// First check mempool for unconfirmed transactions
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& mempool.get_txs().contains_key(&txid)
|
||||
&& mempool.get_txs().contains_key(txid)
|
||||
{
|
||||
return Ok(TxStatus::UNCONFIRMED);
|
||||
}
|
||||
|
||||
// Look up confirmed transaction by txid prefix
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let prefix = TxidPrefix::from(txid);
|
||||
let indexer = self.indexer();
|
||||
let Ok(Some(tx_index)) = indexer
|
||||
.stores
|
||||
@@ -55,13 +53,8 @@ impl Query {
|
||||
};
|
||||
|
||||
// Get block info for status
|
||||
let height = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.height
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
let block_hash = indexer.vecs.blocks.blockhash.read_once(height)?;
|
||||
let height = indexer.vecs.transactions.height.collect_one(tx_index).unwrap();
|
||||
let block_hash = indexer.vecs.blocks.blockhash.reader().get(height.to_usize());
|
||||
let block_time = indexer.vecs.blocks.timestamp.collect_one(height).unwrap();
|
||||
|
||||
Ok(TxStatus {
|
||||
@@ -72,16 +65,37 @@ impl Query {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transaction_hex(&self, TxidParam { txid }: TxidParam) -> Result<String> {
|
||||
pub fn transaction_raw(&self, txid: &Txid) -> Result<Vec<u8>> {
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& let Some(tx_with_hex) = mempool.get_txs().get(txid)
|
||||
{
|
||||
return Vec::from_hex(tx_with_hex.hex())
|
||||
.map_err(|_| Error::Parse("Failed to decode mempool tx hex".into()));
|
||||
}
|
||||
|
||||
let prefix = TxidPrefix::from(txid);
|
||||
let indexer = self.indexer();
|
||||
let Ok(Some(tx_index)) = indexer
|
||||
.stores
|
||||
.txid_prefix_to_tx_index
|
||||
.get(&prefix)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(Error::UnknownTxid);
|
||||
};
|
||||
self.transaction_raw_by_index(tx_index)
|
||||
}
|
||||
|
||||
pub fn transaction_hex(&self, txid: &Txid) -> Result<String> {
|
||||
// First check mempool for unconfirmed transactions
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& let Some(tx_with_hex) = mempool.get_txs().get(&txid)
|
||||
&& let Some(tx_with_hex) = mempool.get_txs().get(txid)
|
||||
{
|
||||
return Ok(tx_with_hex.hex().to_string());
|
||||
}
|
||||
|
||||
// Look up confirmed transaction by txid prefix
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let prefix = TxidPrefix::from(txid);
|
||||
let indexer = self.indexer();
|
||||
let Ok(Some(tx_index)) = indexer
|
||||
.stores
|
||||
@@ -95,316 +109,291 @@ impl Query {
|
||||
self.transaction_hex_by_index(tx_index)
|
||||
}
|
||||
|
||||
pub fn outspend(&self, TxidParam { txid }: TxidParam, vout: Vout) -> Result<TxOutspend> {
|
||||
// Mempool outputs are unspent in on-chain terms
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& mempool.get_txs().contains_key(&txid)
|
||||
{
|
||||
pub fn outspend(&self, txid: &Txid, vout: Vout) -> Result<TxOutspend> {
|
||||
if self.mempool().is_some_and(|m| m.get_txs().contains_key(txid)) {
|
||||
return Ok(TxOutspend::UNSPENT);
|
||||
}
|
||||
|
||||
// Look up confirmed transaction
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let indexer = self.indexer();
|
||||
let Ok(Some(tx_index)) = indexer
|
||||
.stores
|
||||
.txid_prefix_to_tx_index
|
||||
.get(&prefix)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(Error::UnknownTxid);
|
||||
};
|
||||
|
||||
// Calculate txout_index
|
||||
let first_txout_index = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.read_once(tx_index)?;
|
||||
let txout_index = first_txout_index + vout;
|
||||
|
||||
// Look up spend status
|
||||
let computer = self.computer();
|
||||
let txin_index = computer.outputs.spent.txin_index.read_once(txout_index)?;
|
||||
|
||||
if txin_index == TxInIndex::UNSPENT {
|
||||
let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?;
|
||||
if usize::from(vout) >= output_count {
|
||||
return Ok(TxOutspend::UNSPENT);
|
||||
}
|
||||
|
||||
self.outspend_details(txin_index)
|
||||
self.resolve_outspend(first_txout + vout)
|
||||
}
|
||||
|
||||
pub fn outspends(&self, TxidParam { txid }: TxidParam) -> Result<Vec<TxOutspend>> {
|
||||
// Mempool outputs are unspent in on-chain terms
|
||||
pub fn outspends(&self, txid: &Txid) -> Result<Vec<TxOutspend>> {
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& let Some(tx_with_hex) = mempool.get_txs().get(&txid)
|
||||
&& let Some(tx_with_hex) = mempool.get_txs().get(txid)
|
||||
{
|
||||
let output_count = tx_with_hex.tx().output.len();
|
||||
return Ok(vec![TxOutspend::UNSPENT; output_count]);
|
||||
return Ok(vec![TxOutspend::UNSPENT; tx_with_hex.tx().output.len()]);
|
||||
}
|
||||
let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?;
|
||||
|
||||
// Look up confirmed transaction
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let indexer = self.indexer();
|
||||
let Ok(Some(tx_index)) = indexer
|
||||
.stores
|
||||
.txid_prefix_to_tx_index
|
||||
.get(&prefix)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(Error::UnknownTxid);
|
||||
};
|
||||
let txin_index_reader = self.computer().outputs.spent.txin_index.reader();
|
||||
let txid_reader = indexer.vecs.transactions.txid.reader();
|
||||
let blockhash_reader = indexer.vecs.blocks.blockhash.reader();
|
||||
|
||||
// Get output range
|
||||
let first_txout_index = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.read_once(tx_index)?;
|
||||
let next_first_txout_index = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.read_once(tx_index.incremented())?;
|
||||
let output_count = usize::from(next_first_txout_index) - usize::from(first_txout_index);
|
||||
let mut input_tx_cursor = indexer.vecs.inputs.tx_index.cursor();
|
||||
let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor();
|
||||
let mut height_cursor = indexer.vecs.transactions.height.cursor();
|
||||
let mut block_ts_cursor = indexer.vecs.blocks.timestamp.cursor();
|
||||
|
||||
// Get spend status for each output
|
||||
let computer = self.computer();
|
||||
let txin_index_reader = computer.outputs.spent.txin_index.reader();
|
||||
let mut cached_block: Option<(Height, BlockHash, Timestamp)> = None;
|
||||
|
||||
let mut outspends = Vec::with_capacity(output_count);
|
||||
for i in 0..output_count {
|
||||
let txout_index = first_txout_index + Vout::from(i);
|
||||
let txin_index = txin_index_reader.get(usize::from(txout_index));
|
||||
let txin_index = txin_index_reader.get(usize::from(first_txout + Vout::from(i)));
|
||||
|
||||
if txin_index == TxInIndex::UNSPENT {
|
||||
outspends.push(TxOutspend::UNSPENT);
|
||||
} else {
|
||||
outspends.push(self.outspend_details(txin_index)?);
|
||||
continue;
|
||||
}
|
||||
|
||||
let spending_tx_index = input_tx_cursor.get(usize::from(txin_index)).unwrap();
|
||||
let spending_first_txin =
|
||||
first_txin_cursor.get(spending_tx_index.to_usize()).unwrap();
|
||||
let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin));
|
||||
let spending_txid = txid_reader.get(spending_tx_index.to_usize());
|
||||
let spending_height = height_cursor.get(spending_tx_index.to_usize()).unwrap();
|
||||
|
||||
let (block_hash, block_time) = if let Some((h, ref bh, bt)) = cached_block
|
||||
&& h == spending_height
|
||||
{
|
||||
(bh.clone(), bt)
|
||||
} else {
|
||||
let bh = blockhash_reader.get(spending_height.to_usize());
|
||||
let bt = block_ts_cursor.get(spending_height.to_usize()).unwrap();
|
||||
cached_block = Some((spending_height, bh.clone(), bt));
|
||||
(bh, bt)
|
||||
};
|
||||
|
||||
outspends.push(TxOutspend {
|
||||
spent: true,
|
||||
txid: Some(spending_txid),
|
||||
vin: Some(vin),
|
||||
status: Some(TxStatus {
|
||||
confirmed: true,
|
||||
block_height: Some(spending_height),
|
||||
block_hash: Some(block_hash),
|
||||
block_time: Some(block_time),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(outspends)
|
||||
}
|
||||
|
||||
// === Helper methods ===
|
||||
|
||||
pub fn transaction_by_index(&self, tx_index: TxIndex) -> Result<Transaction> {
|
||||
/// Resolve txid to (tx_index, first_txout_index, output_count).
|
||||
fn resolve_tx_outputs(&self, txid: &Txid) -> Result<(TxIndex, TxOutIndex, usize)> {
|
||||
let prefix = TxidPrefix::from(txid);
|
||||
let indexer = self.indexer();
|
||||
let reader = self.reader();
|
||||
let computer = self.computer();
|
||||
|
||||
// Get tx metadata using collect_one for PcoVec, read_once for BytesVec
|
||||
let txid = indexer.vecs.transactions.txid.read_once(tx_index)?;
|
||||
let height = indexer
|
||||
let tx_index: TxIndex = indexer
|
||||
.stores
|
||||
.txid_prefix_to_tx_index
|
||||
.get(&prefix)?
|
||||
.map(|cow| cow.into_owned())
|
||||
.ok_or(Error::UnknownTxid)?;
|
||||
let first = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.height
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
let version = indexer
|
||||
.first_txout_index
|
||||
.read_once(tx_index)?;
|
||||
let next = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.tx_version
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
let lock_time = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.raw_locktime
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
let total_size = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.total_size
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
let first_txin_index = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txin_index
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
let position = computer.positions.tx.collect_one(tx_index).unwrap();
|
||||
|
||||
// Get block info for status
|
||||
let block_hash = indexer.vecs.blocks.blockhash.read_once(height)?;
|
||||
let block_time = indexer.vecs.blocks.timestamp.collect_one(height).unwrap();
|
||||
|
||||
// Read and decode the raw transaction from blk file
|
||||
let buffer = reader.read_raw_bytes(position, *total_size as usize)?;
|
||||
let mut cursor = Cursor::new(buffer);
|
||||
let tx = bitcoin::Transaction::consensus_decode(&mut cursor)
|
||||
.map_err(|_| Error::Parse("Failed to decode transaction".into()))?;
|
||||
|
||||
// Create readers for random access lookups
|
||||
let txid_reader = indexer.vecs.transactions.txid.reader();
|
||||
let first_txout_index_reader = indexer.vecs.transactions.first_txout_index.reader();
|
||||
let value_reader = indexer.vecs.outputs.value.reader();
|
||||
let output_type_reader = indexer.vecs.outputs.output_type.reader();
|
||||
let type_index_reader = indexer.vecs.outputs.type_index.reader();
|
||||
let addr_readers = indexer.vecs.addrs.addr_readers();
|
||||
|
||||
// Batch-read outpoints for all inputs (avoids per-input PcoVec page decompression)
|
||||
let outpoints: Vec<_> = indexer.vecs.inputs.outpoint.collect_range_at(
|
||||
usize::from(first_txin_index),
|
||||
usize::from(first_txin_index) + tx.input.len(),
|
||||
);
|
||||
|
||||
// Build inputs with prevout information
|
||||
let input: Vec<TxIn> = tx
|
||||
.input
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, txin)| {
|
||||
let outpoint = outpoints[i];
|
||||
|
||||
let is_coinbase = outpoint.is_coinbase();
|
||||
|
||||
// Get prevout info if not coinbase
|
||||
let (prev_txid, prev_vout, prevout) = if is_coinbase {
|
||||
(Txid::COINBASE, Vout::MAX, None)
|
||||
} else {
|
||||
let prev_tx_index = outpoint.tx_index();
|
||||
let prev_vout = outpoint.vout();
|
||||
let prev_txid = txid_reader.get(prev_tx_index.to_usize());
|
||||
|
||||
// Calculate the txout_index for the prevout
|
||||
let prev_first_txout_index =
|
||||
first_txout_index_reader.get(prev_tx_index.to_usize());
|
||||
let prev_txout_index = prev_first_txout_index + prev_vout;
|
||||
|
||||
let prev_value = value_reader.get(usize::from(prev_txout_index));
|
||||
let prev_output_type: OutputType =
|
||||
output_type_reader.get(usize::from(prev_txout_index));
|
||||
let prev_type_index = type_index_reader.get(usize::from(prev_txout_index));
|
||||
let script_pubkey =
|
||||
addr_readers.script_pubkey(prev_output_type, prev_type_index);
|
||||
|
||||
let prevout = Some(TxOut::from((script_pubkey, prev_value)));
|
||||
|
||||
(prev_txid, prev_vout, prevout)
|
||||
};
|
||||
|
||||
TxIn {
|
||||
txid: prev_txid,
|
||||
vout: prev_vout,
|
||||
prevout,
|
||||
script_sig: txin.script_sig.clone(),
|
||||
script_sig_asm: (),
|
||||
is_coinbase,
|
||||
sequence: txin.sequence.0,
|
||||
inner_redeem_script_asm: (),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Calculate weight before consuming tx.output
|
||||
let weight = Weight::from(tx.weight());
|
||||
|
||||
// Calculate sigop cost
|
||||
let total_sigop_cost = tx.total_sigop_cost(|_| None);
|
||||
|
||||
// Build outputs
|
||||
let output: Vec<TxOut> = tx.output.into_iter().map(TxOut::from).collect();
|
||||
|
||||
// Build status
|
||||
let status = TxStatus {
|
||||
confirmed: true,
|
||||
block_height: Some(height),
|
||||
block_hash: Some(block_hash),
|
||||
block_time: Some(block_time),
|
||||
};
|
||||
|
||||
let mut transaction = Transaction {
|
||||
index: Some(tx_index),
|
||||
txid,
|
||||
version,
|
||||
lock_time,
|
||||
total_size: *total_size as usize,
|
||||
weight,
|
||||
total_sigop_cost,
|
||||
fee: Sats::ZERO, // Will be computed below
|
||||
input,
|
||||
output,
|
||||
status,
|
||||
};
|
||||
|
||||
// Compute fee from inputs - outputs
|
||||
transaction.compute_fee();
|
||||
|
||||
Ok(transaction)
|
||||
.first_txout_index
|
||||
.read_once(tx_index.incremented())?;
|
||||
Ok((tx_index, first, usize::from(next) - usize::from(first)))
|
||||
}
|
||||
|
||||
fn transaction_hex_by_index(&self, tx_index: TxIndex) -> Result<String> {
|
||||
/// Resolve spend status for a single output.
|
||||
fn resolve_outspend(&self, txout_index: TxOutIndex) -> Result<TxOutspend> {
|
||||
let indexer = self.indexer();
|
||||
let reader = self.reader();
|
||||
let computer = self.computer();
|
||||
let txin_index = self
|
||||
.computer()
|
||||
.outputs
|
||||
.spent
|
||||
.txin_index
|
||||
.reader()
|
||||
.get(usize::from(txout_index));
|
||||
|
||||
let total_size = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.total_size
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
let position = computer.positions.tx.collect_one(tx_index).unwrap();
|
||||
if txin_index == TxInIndex::UNSPENT {
|
||||
return Ok(TxOutspend::UNSPENT);
|
||||
}
|
||||
|
||||
let buffer = reader.read_raw_bytes(position, *total_size as usize)?;
|
||||
|
||||
Ok(buffer.to_lower_hex_string())
|
||||
}
|
||||
|
||||
fn outspend_details(&self, txin_index: TxInIndex) -> Result<TxOutspend> {
|
||||
let indexer = self.indexer();
|
||||
|
||||
// Look up spending tx_index directly
|
||||
let spending_tx_index = indexer
|
||||
.vecs
|
||||
.inputs
|
||||
.tx_index
|
||||
.collect_one(txin_index)
|
||||
.collect_one_at(usize::from(txin_index))
|
||||
.unwrap();
|
||||
|
||||
// Calculate vin
|
||||
let spending_first_txin_index = indexer
|
||||
let spending_first_txin = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txin_index
|
||||
.collect_one(spending_tx_index)
|
||||
.unwrap();
|
||||
let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin_index));
|
||||
|
||||
// Get spending tx details
|
||||
let spending_txid = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.txid
|
||||
.read_once(spending_tx_index)?;
|
||||
let spending_height = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.height
|
||||
.collect_one(spending_tx_index)
|
||||
.unwrap();
|
||||
let block_hash = indexer.vecs.blocks.blockhash.read_once(spending_height)?;
|
||||
let block_time = indexer
|
||||
.vecs
|
||||
.blocks
|
||||
.timestamp
|
||||
.collect_one(spending_height)
|
||||
.unwrap();
|
||||
|
||||
Ok(TxOutspend {
|
||||
spent: true,
|
||||
txid: Some(spending_txid),
|
||||
vin: Some(vin),
|
||||
txid: Some(
|
||||
indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.txid
|
||||
.reader()
|
||||
.get(spending_tx_index.to_usize()),
|
||||
),
|
||||
vin: Some(Vin::from(
|
||||
usize::from(txin_index) - usize::from(spending_first_txin),
|
||||
)),
|
||||
status: Some(TxStatus {
|
||||
confirmed: true,
|
||||
block_height: Some(spending_height),
|
||||
block_hash: Some(block_hash),
|
||||
block_time: Some(block_time),
|
||||
block_hash: Some(
|
||||
indexer
|
||||
.vecs
|
||||
.blocks
|
||||
.blockhash
|
||||
.reader()
|
||||
.get(spending_height.to_usize()),
|
||||
),
|
||||
block_time: Some(
|
||||
indexer
|
||||
.vecs
|
||||
.blocks
|
||||
.timestamp
|
||||
.collect_one(spending_height)
|
||||
.unwrap(),
|
||||
),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// === Helper methods ===
|
||||
|
||||
pub fn transaction_by_index(&self, tx_index: TxIndex) -> Result<Transaction> {
|
||||
self.transactions_by_indices(&[tx_index])?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Error::NotFound("Transaction not found".into()))
|
||||
}
|
||||
|
||||
fn transaction_raw_by_index(&self, tx_index: TxIndex) -> Result<Vec<u8>> {
|
||||
let indexer = self.indexer();
|
||||
let total_size = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.total_size
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
let position = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.position
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
self.reader().read_raw_bytes(position, *total_size as usize)
|
||||
}
|
||||
|
||||
fn transaction_hex_by_index(&self, tx_index: TxIndex) -> Result<String> {
|
||||
Ok(self
|
||||
.transaction_raw_by_index(tx_index)?
|
||||
.to_lower_hex_string())
|
||||
}
|
||||
|
||||
pub fn resolve_tx(&self, txid: &Txid) -> Result<(TxIndex, Height)> {
|
||||
let indexer = self.indexer();
|
||||
let prefix = TxidPrefix::from(txid);
|
||||
let tx_index: TxIndex = indexer
|
||||
.stores
|
||||
.txid_prefix_to_tx_index
|
||||
.get(&prefix)?
|
||||
.map(|cow| cow.into_owned())
|
||||
.ok_or(Error::UnknownTxid)?;
|
||||
let height: Height = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.height
|
||||
.collect_one(tx_index)
|
||||
.unwrap();
|
||||
Ok((tx_index, height))
|
||||
}
|
||||
|
||||
pub fn broadcast_transaction(&self, hex: &str) -> Result<Txid> {
|
||||
self.client().send_raw_transaction(hex)
|
||||
}
|
||||
|
||||
pub fn merkleblock_proof(&self, txid: &Txid) -> Result<String> {
|
||||
let (_, height) = self.resolve_tx(txid)?;
|
||||
let header = self.read_block_header(height)?;
|
||||
let txids = self.block_txids_by_height(height)?;
|
||||
|
||||
let target: bitcoin::Txid = txid.into();
|
||||
let btxids: Vec<bitcoin::Txid> = txids.iter().map(bitcoin::Txid::from).collect();
|
||||
let mb = bitcoin::MerkleBlock::from_header_txids_with_predicate(&header, &btxids, |t| {
|
||||
*t == target
|
||||
});
|
||||
Ok(bitcoin::consensus::encode::serialize_hex(&mb))
|
||||
}
|
||||
|
||||
pub fn merkle_proof(&self, txid: &Txid) -> Result<MerkleProof> {
|
||||
let (tx_index, height) = self.resolve_tx(txid)?;
|
||||
let first_tx = self
|
||||
.indexer()
|
||||
.vecs
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_one(height)
|
||||
.ok_or(Error::NotFound("Block not found".into()))?;
|
||||
let pos = tx_index.to_usize() - first_tx.to_usize();
|
||||
let txids = self.block_txids_by_height(height)?;
|
||||
|
||||
Ok(MerkleProof {
|
||||
block_height: height,
|
||||
merkle: merkle_path(&txids, pos),
|
||||
pos,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn merkle_path(txids: &[Txid], pos: usize) -> Vec<String> {
|
||||
use bitcoin::hashes::{Hash, sha256d};
|
||||
|
||||
// Txid bytes are in internal order (same layout as bitcoin::Txid)
|
||||
let mut hashes: Vec<[u8; 32]> = txids
|
||||
.iter()
|
||||
.map(|t| bitcoin::Txid::from(t).to_byte_array())
|
||||
.collect();
|
||||
|
||||
let mut proof = Vec::new();
|
||||
let mut idx = pos;
|
||||
|
||||
while hashes.len() > 1 {
|
||||
let sibling = if idx ^ 1 < hashes.len() { idx ^ 1 } else { idx };
|
||||
// Display order: reverse bytes for hex output
|
||||
let mut display = hashes[sibling];
|
||||
display.reverse();
|
||||
proof.push(bitcoin::hex::DisplayHex::to_lower_hex_string(&display));
|
||||
|
||||
hashes = hashes
|
||||
.chunks(2)
|
||||
.map(|pair| {
|
||||
let right = pair.last().unwrap();
|
||||
let mut combined = [0u8; 64];
|
||||
combined[..32].copy_from_slice(&pair[0]);
|
||||
combined[32..].copy_from_slice(right);
|
||||
sha256d::Hash::hash(&combined).to_byte_array()
|
||||
})
|
||||
.collect();
|
||||
idx /= 2;
|
||||
}
|
||||
|
||||
proof
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use brk_indexer::Indexer;
|
||||
use brk_mempool::Mempool;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::Client;
|
||||
use brk_types::{Height, SyncStatus};
|
||||
use brk_types::{BlockHash, BlockHashPrefix, Height, SyncStatus};
|
||||
use vecdb::{AnyVec, ReadOnlyClone, ReadableVec, Ro};
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
@@ -72,6 +72,16 @@ impl Query {
|
||||
self.indexed_height().min(self.computed_height())
|
||||
}
|
||||
|
||||
/// Tip block hash, cached in the indexer.
|
||||
pub fn tip_blockhash(&self) -> BlockHash {
|
||||
self.indexer().tip_blockhash()
|
||||
}
|
||||
|
||||
/// Tip block hash prefix for cache etags.
|
||||
pub fn tip_hash_prefix(&self) -> BlockHashPrefix {
|
||||
BlockHashPrefix::from(&self.tip_blockhash())
|
||||
}
|
||||
|
||||
/// Build sync status with the given tip height
|
||||
pub fn sync_status(&self, tip_height: Height) -> SyncStatus {
|
||||
let indexed_height = self.indexed_height();
|
||||
|
||||
@@ -20,3 +20,4 @@ derive_more = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rlimit = "0.11.0"
|
||||
|
||||
@@ -21,7 +21,7 @@ fn main() -> Result<()> {
|
||||
|
||||
if let Some(block) = reader.read(Some(height), Some(height)).iter().next() {
|
||||
println!(
|
||||
"height={} hash={} txs={} coinbase=\"{}\" ({:?})",
|
||||
"height={} hash={} txs={} coinbase=\"{:?}\" ({:?})",
|
||||
block.height(),
|
||||
block.hash(),
|
||||
block.txdata.len(),
|
||||
|
||||
+194
-80
@@ -5,6 +5,7 @@ use std::{
|
||||
fs::{self, File},
|
||||
io::{Read, Seek, SeekFrom},
|
||||
ops::ControlFlow,
|
||||
os::unix::fs::FileExt,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
thread,
|
||||
@@ -14,7 +15,7 @@ use bitcoin::{block::Header, consensus::Decodable};
|
||||
use blk_index_to_blk_path::*;
|
||||
use brk_error::{Error, Result};
|
||||
use brk_rpc::Client;
|
||||
use brk_types::{BlkMetadata, BlkPosition, BlockHash, Height, ReadBlock};
|
||||
use brk_types::{BlkPosition, BlockHash, Height, ReadBlock};
|
||||
pub use crossbeam::channel::Receiver;
|
||||
use crossbeam::channel::bounded;
|
||||
use derive_more::Deref;
|
||||
@@ -24,28 +25,17 @@ use tracing::{error, warn};
|
||||
|
||||
mod blk_index_to_blk_path;
|
||||
mod decode;
|
||||
mod scan;
|
||||
mod xor_bytes;
|
||||
mod xor_index;
|
||||
|
||||
use decode::*;
|
||||
use scan::*;
|
||||
pub use xor_bytes::*;
|
||||
pub use xor_index::*;
|
||||
|
||||
const MAGIC_BYTES: [u8; 4] = [249, 190, 180, 217];
|
||||
const BOUND_CAP: usize = 50;
|
||||
|
||||
fn find_magic(bytes: &[u8], xor_i: &mut XORIndex, xor_bytes: XORBytes) -> Option<usize> {
|
||||
let mut window = [0u8; 4];
|
||||
for (i, &b) in bytes.iter().enumerate() {
|
||||
window.rotate_left(1);
|
||||
window[3] = xor_i.byte(b, xor_bytes);
|
||||
if window == MAGIC_BYTES {
|
||||
return Some(i + 1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
///
|
||||
/// Bitcoin BLK file reader
|
||||
///
|
||||
@@ -64,6 +54,7 @@ impl Reader {
|
||||
#[derive(Debug)]
|
||||
pub struct ReaderInner {
|
||||
blk_index_to_blk_path: Arc<RwLock<BlkIndexToBlkPath>>,
|
||||
blk_file_cache: RwLock<BTreeMap<u16, File>>,
|
||||
xor_bytes: XORBytes,
|
||||
blocks_dir: PathBuf,
|
||||
client: Client,
|
||||
@@ -71,11 +62,19 @@ pub struct ReaderInner {
|
||||
|
||||
impl ReaderInner {
|
||||
pub fn new(blocks_dir: PathBuf, client: Client) -> Self {
|
||||
let no_file_limit = rlimit::getrlimit(rlimit::Resource::NOFILE).unwrap_or((0, 0));
|
||||
let _ = rlimit::setrlimit(
|
||||
rlimit::Resource::NOFILE,
|
||||
no_file_limit.0.max(15_000),
|
||||
no_file_limit.1,
|
||||
);
|
||||
|
||||
Self {
|
||||
xor_bytes: XORBytes::from(blocks_dir.as_path()),
|
||||
blk_index_to_blk_path: Arc::new(RwLock::new(BlkIndexToBlkPath::scan(
|
||||
blocks_dir.as_path(),
|
||||
))),
|
||||
blk_file_cache: RwLock::new(BTreeMap::new()),
|
||||
blocks_dir,
|
||||
client,
|
||||
}
|
||||
@@ -97,30 +96,89 @@ impl ReaderInner {
|
||||
self.xor_bytes
|
||||
}
|
||||
|
||||
/// Read raw bytes from a blk file at the given position with XOR decoding
|
||||
pub fn read_raw_bytes(&self, position: BlkPosition, size: usize) -> Result<Vec<u8>> {
|
||||
/// Ensure the blk file for `blk_index` is in the file handle cache.
|
||||
fn ensure_blk_cached(&self, blk_index: u16) -> Result<()> {
|
||||
if self.blk_file_cache.read().contains_key(&blk_index) {
|
||||
return Ok(());
|
||||
}
|
||||
let blk_paths = self.blk_index_to_blk_path();
|
||||
let blk_path = blk_paths
|
||||
.get(&position.blk_index())
|
||||
.get(&blk_index)
|
||||
.ok_or(Error::NotFound("Blk file not found".into()))?;
|
||||
let file = File::open(blk_path)?;
|
||||
self.blk_file_cache.write().entry(blk_index).or_insert(file);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut file = File::open(blk_path)?;
|
||||
file.seek(SeekFrom::Start(position.offset() as u64))?;
|
||||
/// Read raw bytes from a blk file at the given position with XOR decoding.
|
||||
pub fn read_raw_bytes(&self, position: BlkPosition, size: usize) -> Result<Vec<u8>> {
|
||||
self.ensure_blk_cached(position.blk_index())?;
|
||||
|
||||
let cache = self.blk_file_cache.read();
|
||||
let file = cache.get(&position.blk_index()).unwrap();
|
||||
let mut buffer = vec![0u8; size];
|
||||
file.read_exact(&mut buffer)?;
|
||||
|
||||
let mut xori = XORIndex::default();
|
||||
xori.add_assign(position.offset() as usize);
|
||||
xori.bytes(&mut buffer, self.xor_bytes);
|
||||
|
||||
file.read_at(&mut buffer, position.offset() as u64)?;
|
||||
XORIndex::decode_at(&mut buffer, position.offset() as usize, self.xor_bytes);
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Returns a `Read` impl positioned at `position` in the blk file.
|
||||
/// Reads only the bytes requested — no upfront allocation.
|
||||
pub fn reader_at(&self, position: BlkPosition) -> Result<BlkRead<'_>> {
|
||||
self.ensure_blk_cached(position.blk_index())?;
|
||||
|
||||
let mut xor_index = XORIndex::default();
|
||||
xor_index.add_assign(position.offset() as usize);
|
||||
|
||||
Ok(BlkRead {
|
||||
cache: self.blk_file_cache.read(),
|
||||
blk_index: position.blk_index(),
|
||||
offset: position.offset() as u64,
|
||||
xor_index,
|
||||
xor_bytes: self.xor_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a receiver streaming `ReadBlock`s from `hash + 1` to the chain tip.
|
||||
/// If `hash` is `None`, starts from genesis.
|
||||
pub fn after(&self, hash: Option<BlockHash>) -> Result<Receiver<ReadBlock>> {
|
||||
let start = if let Some(hash) = hash.as_ref() {
|
||||
let info = self.client.get_block_header_info(hash)?;
|
||||
Height::from(info.height + 1)
|
||||
} else {
|
||||
Height::ZERO
|
||||
};
|
||||
let end = self.client.get_last_height()?;
|
||||
|
||||
if end < start {
|
||||
return Ok(bounded(0).1);
|
||||
}
|
||||
|
||||
if *end - *start < 10 {
|
||||
let mut blocks: Vec<_> = self.read_rev(Some(start), Some(end)).iter().collect();
|
||||
blocks.reverse();
|
||||
|
||||
let (send, recv) = bounded(blocks.len());
|
||||
for block in blocks {
|
||||
let _ = send.send(block);
|
||||
}
|
||||
return Ok(recv);
|
||||
}
|
||||
|
||||
Ok(self.read(Some(start), Some(end)))
|
||||
}
|
||||
|
||||
/// Returns a crossbeam channel receiver that streams `ReadBlock`s in chain order.
|
||||
///
|
||||
/// Both `start` and `end` are inclusive. `None` means unbounded.
|
||||
pub fn read(&self, start: Option<Height>, end: Option<Height>) -> Receiver<ReadBlock> {
|
||||
if let (Some(s), Some(e)) = (start, end)
|
||||
&& s > e
|
||||
{
|
||||
let (_, recv) = bounded(0);
|
||||
return recv;
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
|
||||
let (send_bytes, recv_bytes) = bounded(BOUND_CAP / 2);
|
||||
@@ -151,53 +209,25 @@ impl ReaderInner {
|
||||
thread::spawn(move || {
|
||||
let _ = blk_index_to_blk_path.range(first_blk_index..).try_for_each(
|
||||
move |(blk_index, blk_path)| {
|
||||
let mut xor_i = XORIndex::default();
|
||||
|
||||
let blk_index = *blk_index;
|
||||
|
||||
let Ok(mut blk_bytes_) = fs::read(blk_path) else {
|
||||
let Ok(mut bytes) = fs::read(blk_path) else {
|
||||
error!("Failed to read blk file: {}", blk_path.display());
|
||||
return ControlFlow::Break(());
|
||||
};
|
||||
let blk_bytes = blk_bytes_.as_mut_slice();
|
||||
let mut i = 0;
|
||||
|
||||
loop {
|
||||
let Some(offset) = find_magic(&blk_bytes[i..], &mut xor_i, xor_bytes)
|
||||
else {
|
||||
break;
|
||||
};
|
||||
i += offset;
|
||||
|
||||
if i + 4 > blk_bytes.len() {
|
||||
warn!("Truncated blk file {blk_index}: not enough bytes for block length at offset {i}");
|
||||
break;
|
||||
}
|
||||
let len = u32::from_le_bytes(
|
||||
xor_i
|
||||
.bytes(&mut blk_bytes[i..(i + 4)], xor_bytes)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
) as usize;
|
||||
i += 4;
|
||||
|
||||
if i + len > blk_bytes.len() {
|
||||
warn!("Truncated blk file {blk_index}: block at offset {} claims {len} bytes but only {} remain", i - 4, blk_bytes.len() - i);
|
||||
break;
|
||||
}
|
||||
let position = BlkPosition::new(blk_index, i as u32);
|
||||
let metadata = BlkMetadata::new(position, len as u32);
|
||||
|
||||
let block_bytes = (blk_bytes[i..(i + len)]).to_vec();
|
||||
|
||||
if send_bytes.send((metadata, block_bytes, xor_i)).is_err() {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
|
||||
i += len;
|
||||
xor_i.add_assign(len);
|
||||
let result = scan_bytes(
|
||||
&mut bytes,
|
||||
*blk_index,
|
||||
0,
|
||||
xor_bytes,
|
||||
|metadata, block_bytes, xor_i| {
|
||||
if send_bytes.send((metadata, block_bytes, xor_i)).is_err() {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
},
|
||||
);
|
||||
if result.interrupted {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
|
||||
ControlFlow::Continue(())
|
||||
},
|
||||
);
|
||||
@@ -288,6 +318,83 @@ impl ReaderInner {
|
||||
recv_ordered
|
||||
}
|
||||
|
||||
/// Streams `ReadBlock`s in reverse order (newest first) by scanning
|
||||
/// `.blk` files from the tail. Efficient for reading recent blocks.
|
||||
/// Both `start` and `end` are inclusive. `None` means unbounded.
|
||||
pub fn read_rev(&self, start: Option<Height>, end: Option<Height>) -> Receiver<ReadBlock> {
|
||||
const CHUNK: usize = 5 * 1024 * 1024;
|
||||
|
||||
if let (Some(s), Some(e)) = (start, end)
|
||||
&& s > e
|
||||
{
|
||||
return bounded(0).1;
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
let xor_bytes = self.xor_bytes;
|
||||
let paths = BlkIndexToBlkPath::scan(&self.blocks_dir);
|
||||
*self.blk_index_to_blk_path.write() = paths.clone();
|
||||
let (send, recv) = bounded(BOUND_CAP);
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut head = Vec::new();
|
||||
|
||||
for (&blk_index, path) in paths.iter().rev() {
|
||||
let file_len = fs::metadata(path).map(|m| m.len() as usize).unwrap_or(0);
|
||||
if file_len == 0 {
|
||||
continue;
|
||||
}
|
||||
let Ok(mut file) = File::open(path) else {
|
||||
return;
|
||||
};
|
||||
let mut read_end = file_len;
|
||||
|
||||
while read_end > 0 {
|
||||
let read_start = read_end.saturating_sub(CHUNK);
|
||||
let chunk_len = read_end - read_start;
|
||||
read_end = read_start;
|
||||
|
||||
let _ = file.seek(SeekFrom::Start(read_start as u64));
|
||||
let mut buf = vec![0u8; chunk_len + head.len()];
|
||||
if file.read_exact(&mut buf[..chunk_len]).is_err() {
|
||||
return;
|
||||
}
|
||||
buf[chunk_len..].copy_from_slice(&head);
|
||||
head.clear();
|
||||
|
||||
let mut blocks = Vec::new();
|
||||
let result = scan_bytes(
|
||||
&mut buf,
|
||||
blk_index,
|
||||
read_start,
|
||||
xor_bytes,
|
||||
|metadata, bytes, xor_i| {
|
||||
if let Ok(Some(block)) = decode_block(
|
||||
bytes, metadata, &client, xor_i, xor_bytes, start, end, 0, 0,
|
||||
) {
|
||||
blocks.push(block);
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
},
|
||||
);
|
||||
|
||||
for block in blocks.into_iter().rev() {
|
||||
let done = start.is_some_and(|s| block.height() <= s);
|
||||
if send.send(block).is_err() || done {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if read_start > 0 {
|
||||
head = buf[..result.first_magic.unwrap_or(buf.len())].to_vec();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
recv
|
||||
}
|
||||
|
||||
fn find_start_blk_index(
|
||||
&self,
|
||||
target_start: Option<Height>,
|
||||
@@ -298,18 +405,6 @@ impl ReaderInner {
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
// If start is a very recent block we only look back X blk file before the last
|
||||
if let Ok(height) = self.client.get_last_height()
|
||||
&& (*height).saturating_sub(*target_start) <= 3
|
||||
{
|
||||
return Ok(blk_index_to_blk_path
|
||||
.keys()
|
||||
.rev()
|
||||
.nth(2)
|
||||
.copied()
|
||||
.unwrap_or_default());
|
||||
}
|
||||
|
||||
let blk_indices: Vec<u16> = blk_index_to_blk_path.keys().copied().collect();
|
||||
|
||||
if blk_indices.is_empty() {
|
||||
@@ -379,3 +474,22 @@ impl ReaderInner {
|
||||
Ok(Height::new(height))
|
||||
}
|
||||
}
|
||||
|
||||
/// Streaming reader at a position in a blk file. Reads via pread + XOR on demand.
|
||||
pub struct BlkRead<'a> {
|
||||
cache: RwLockReadGuard<'a, BTreeMap<u16, File>>,
|
||||
blk_index: u16,
|
||||
offset: u64,
|
||||
xor_index: XORIndex,
|
||||
xor_bytes: XORBytes,
|
||||
}
|
||||
|
||||
impl Read for BlkRead<'_> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let file = self.cache.get(&self.blk_index).unwrap();
|
||||
let n = file.read_at(buf, self.offset)?;
|
||||
self.xor_index.bytes(&mut buf[..n], self.xor_bytes);
|
||||
self.offset += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use brk_types::{BlkMetadata, BlkPosition};
|
||||
|
||||
use crate::{XORBytes, XORIndex};
|
||||
|
||||
const MAGIC_BYTES: [u8; 4] = [249, 190, 180, 217];
|
||||
|
||||
pub fn find_magic(bytes: &[u8], xor_i: &mut XORIndex, xor_bytes: XORBytes) -> Option<usize> {
|
||||
let mut window = [0u8; 4];
|
||||
for (i, &b) in bytes.iter().enumerate() {
|
||||
window.rotate_left(1);
|
||||
window[3] = xor_i.byte(b, xor_bytes);
|
||||
if window == MAGIC_BYTES {
|
||||
return Some(i + 1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub struct ScanResult {
|
||||
pub first_magic: Option<usize>,
|
||||
pub interrupted: bool,
|
||||
}
|
||||
|
||||
/// Scans `buf` for blocks. `file_offset` is the absolute position of `buf[0]` in the file.
|
||||
/// Calls `on_block` for each complete block found.
|
||||
pub fn scan_bytes(
|
||||
buf: &mut [u8],
|
||||
blk_index: u16,
|
||||
file_offset: usize,
|
||||
xor_bytes: XORBytes,
|
||||
mut on_block: impl FnMut(BlkMetadata, Vec<u8>, XORIndex) -> ControlFlow<()>,
|
||||
) -> ScanResult {
|
||||
let mut xor_i = XORIndex::default();
|
||||
xor_i.add_assign(file_offset);
|
||||
let mut first_magic = None;
|
||||
let mut i = 0;
|
||||
|
||||
while let Some(off) = find_magic(&buf[i..], &mut xor_i, xor_bytes) {
|
||||
let before = i;
|
||||
i += off;
|
||||
first_magic.get_or_insert(before + off.saturating_sub(4));
|
||||
if i + 4 > buf.len() {
|
||||
break;
|
||||
}
|
||||
let len = u32::from_le_bytes(
|
||||
xor_i
|
||||
.bytes(&mut buf[i..i + 4], xor_bytes)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
) as usize;
|
||||
i += 4;
|
||||
if i + len > buf.len() {
|
||||
break;
|
||||
}
|
||||
let position = BlkPosition::new(blk_index, (file_offset + i) as u32);
|
||||
let metadata = BlkMetadata::new(position, len as u32);
|
||||
if on_block(metadata, buf[i..i + len].to_vec(), xor_i).is_break() {
|
||||
return ScanResult {
|
||||
first_magic,
|
||||
interrupted: true,
|
||||
};
|
||||
}
|
||||
i += len;
|
||||
xor_i.add_assign(len);
|
||||
}
|
||||
|
||||
ScanResult {
|
||||
first_magic,
|
||||
interrupted: false,
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,12 @@ impl XORIndex {
|
||||
pub fn add_assign(&mut self, i: usize) {
|
||||
self.0 = (self.0 + i) % XOR_LEN;
|
||||
}
|
||||
|
||||
/// XOR-decode `buffer` starting at `offset`.
|
||||
#[inline]
|
||||
pub fn decode_at(buffer: &mut [u8], offset: usize, xor_bytes: XORBytes) {
|
||||
let mut xori = Self::default();
|
||||
xori.add_assign(offset);
|
||||
xori.bytes(buffer, xor_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,4 +240,8 @@ impl ClientInner {
|
||||
) -> Result<String> {
|
||||
Ok(self.call_with_retry(|c| c.get_raw_transaction_hex(txid, block_hash))?)
|
||||
}
|
||||
|
||||
pub fn send_raw_transaction(&self, hex: &str) -> Result<bitcoin::Txid> {
|
||||
Ok(self.call_once(|c| c.send_raw_transaction(hex))?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,6 +294,14 @@ impl ClientInner {
|
||||
})?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn send_raw_transaction(&self, hex: &str) -> Result<bitcoin::Txid> {
|
||||
let hex = hex.to_string();
|
||||
Ok(self.call_with_retry(|c| {
|
||||
let args = [serde_json::Value::String(hex.clone())];
|
||||
c.call("sendrawtransaction", &args)
|
||||
})?)
|
||||
}
|
||||
}
|
||||
|
||||
// Local deserialization structs for raw RPC responses
|
||||
|
||||
@@ -137,6 +137,12 @@ impl Client {
|
||||
None
|
||||
};
|
||||
|
||||
let witness = txin
|
||||
.witness
|
||||
.iter()
|
||||
.map(bitcoin::hex::DisplayHex::to_lower_hex_string)
|
||||
.collect();
|
||||
|
||||
Ok(TxIn {
|
||||
is_coinbase,
|
||||
prevout: txout,
|
||||
@@ -144,8 +150,10 @@ impl Client {
|
||||
vout: txin.previous_output.vout.into(),
|
||||
script_sig: txin.script_sig,
|
||||
script_sig_asm: (),
|
||||
witness,
|
||||
sequence: txin.sequence.into(),
|
||||
inner_redeem_script_asm: (),
|
||||
inner_witness_script_asm: (),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
@@ -232,6 +240,10 @@ impl Client {
|
||||
.get_raw_transaction_hex(txid.into(), block_hash.map(|h| h.into()))
|
||||
}
|
||||
|
||||
pub fn send_raw_transaction(&self, hex: &str) -> Result<Txid> {
|
||||
self.0.send_raw_transaction(hex).map(Txid::from)
|
||||
}
|
||||
|
||||
/// Checks if a block is in the main chain (has positive confirmations)
|
||||
pub fn is_in_main_chain(&self, hash: &BlockHash) -> Result<bool> {
|
||||
let block_info = self.get_block_info(hash)?;
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, Uri},
|
||||
};
|
||||
use brk_query::BLOCK_TXS_PAGE_SIZE;
|
||||
use brk_types::{
|
||||
BlockHashParam, BlockHashStartIndex, BlockHashTxIndex, BlockInfo, BlockStatus, BlockTimestamp,
|
||||
HeightParam, TimestampParam, Transaction, Txid,
|
||||
};
|
||||
|
||||
use crate::{CacheStrategy, extended::TransformResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub trait BlockRoutes {
|
||||
fn add_block_routes(self) -> Self;
|
||||
}
|
||||
|
||||
impl BlockRoutes for ApiRouter<AppState> {
|
||||
fn add_block_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/api/blocks",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(None))
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_blocks")
|
||||
.blocks_tag()
|
||||
.summary("Recent blocks")
|
||||
.description("Retrieve the last 10 blocks. Returns block metadata for each block.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*")
|
||||
.ok_response::<Vec<BlockInfo>>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/blocks/{height}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(Some(path.height))).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_blocks_from_height")
|
||||
.blocks_tag()
|
||||
.summary("Blocks from height")
|
||||
.description(
|
||||
"Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*",
|
||||
)
|
||||
.ok_response::<Vec<BlockInfo>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block-height/{height}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_height(path.height)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_by_height")
|
||||
.blocks_tag()
|
||||
.summary("Block by height")
|
||||
.description(
|
||||
"Retrieve block information by block height. Returns block metadata including hash, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-height)*",
|
||||
)
|
||||
.ok_response::<BlockInfo>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block(&path.hash)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block")
|
||||
.blocks_tag()
|
||||
.summary("Block information")
|
||||
.description(
|
||||
"Retrieve block information by block hash. Returns block metadata including height, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block)*",
|
||||
)
|
||||
.ok_response::<BlockInfo>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}/status",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_status(&path.hash)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_status")
|
||||
.blocks_tag()
|
||||
.summary("Block status")
|
||||
.description(
|
||||
"Retrieve the status of a block. Returns whether the block is in the best chain and, if so, its height and the hash of the next block.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-status)*",
|
||||
)
|
||||
.ok_response::<BlockStatus>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}/txids",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txids(&path.hash)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_txids")
|
||||
.blocks_tag()
|
||||
.summary("Block transaction IDs")
|
||||
.description(
|
||||
"Retrieve all transaction IDs in a block. Returns an array of txids in block order.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transaction-ids)*",
|
||||
)
|
||||
.ok_response::<Vec<Txid>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}/txs/{start_index}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashStartIndex>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txs(&path.hash, path.start_index)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_txs")
|
||||
.blocks_tag()
|
||||
.summary("Block transactions (paginated)")
|
||||
.description(&format!(
|
||||
"Retrieve transactions in a block by block hash, starting from the specified index. Returns up to {} transactions at a time.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transactions)*",
|
||||
BLOCK_TXS_PAGE_SIZE
|
||||
))
|
||||
.ok_response::<Vec<Transaction>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}/txid/{index}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashTxIndex>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_text(&headers, CacheStrategy::Static, &uri, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_txid")
|
||||
.blocks_tag()
|
||||
.summary("Transaction ID at index")
|
||||
.description(
|
||||
"Retrieve a single transaction ID at a specific index within a block. Returns plain text txid.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transaction-id)*",
|
||||
)
|
||||
.ok_response::<Txid>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}/raw",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<BlockHashParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_bytes(&headers, CacheStrategy::Static, &uri, move |q| q.block_raw(&path.hash)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_raw")
|
||||
.blocks_tag()
|
||||
.summary("Raw block")
|
||||
.description(
|
||||
"Returns the raw block data in binary format.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-raw)*",
|
||||
)
|
||||
.ok_response::<Vec<u8>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/timestamp/{timestamp}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<TimestampParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_timestamp(path.timestamp)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_by_timestamp")
|
||||
.blocks_tag()
|
||||
.summary("Block by timestamp")
|
||||
.description("Find the block closest to a given UNIX timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-timestamp)*")
|
||||
.ok_response::<BlockTimestamp>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{HeaderMap, Uri},
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use brk_types::{Dollars, MempoolBlock, MempoolInfo, RecommendedFees, Txid};
|
||||
|
||||
use crate::extended::TransformResponseExtended;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub trait MempoolRoutes {
|
||||
fn add_mempool_routes(self) -> Self;
|
||||
}
|
||||
|
||||
impl MempoolRoutes for ApiRouter<AppState> {
|
||||
fn add_mempool_routes(self) -> Self {
|
||||
self
|
||||
.route("/api/mempool", get(Redirect::temporary("/api#tag/mempool")))
|
||||
.api_route(
|
||||
"/api/mempool/info",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_info()).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_mempool")
|
||||
.mempool_tag()
|
||||
.summary("Mempool statistics")
|
||||
.description("Get current mempool statistics including transaction count, total vsize, and total fees.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool)*")
|
||||
.ok_response::<MempoolInfo>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/mempool/txids",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_txids()).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_mempool_txids")
|
||||
.mempool_tag()
|
||||
.summary("Mempool transaction IDs")
|
||||
.description("Get all transaction IDs currently in the mempool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-transaction-ids)*")
|
||||
.ok_response::<Vec<Txid>>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/mempool/price",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.live_price()).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_live_price")
|
||||
.mempool_tag()
|
||||
.summary("Live BTC/USD price")
|
||||
.description(
|
||||
"Returns the current BTC/USD price in dollars, derived from \
|
||||
on-chain round-dollar output patterns in the last 12 blocks \
|
||||
plus mempool.",
|
||||
)
|
||||
.ok_response::<Dollars>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/fees/recommended",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.recommended_fees()).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_recommended_fees")
|
||||
.mempool_tag()
|
||||
.summary("Recommended fees")
|
||||
.description("Get recommended fee rates for different confirmation targets based on current mempool state.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
|
||||
.ok_response::<RecommendedFees>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/fees/mempool-blocks",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.mempool_blocks()).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_mempool_blocks")
|
||||
.mempool_tag()
|
||||
.summary("Projected mempool blocks")
|
||||
.description("Get projected blocks from the mempool for fee estimation. Each block contains statistics about transactions that would be included if a block were mined now.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*")
|
||||
.ok_response::<Vec<MempoolBlock>>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+23
-18
@@ -5,15 +5,14 @@ use axum::{
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use brk_types::{
|
||||
AddrParam, AddrStats, AddrTxidsParam, AddrValidation, Transaction, Txid, Utxo,
|
||||
ValidateAddrParam,
|
||||
use brk_types::{AddrStats, AddrValidation, Transaction, Txid, Utxo, Version};
|
||||
|
||||
use crate::{
|
||||
AppState, CacheStrategy,
|
||||
extended::TransformResponseExtended,
|
||||
params::{AddrParam, AddrTxidsParam, ValidateAddrParam},
|
||||
};
|
||||
|
||||
use crate::{CacheStrategy, extended::TransformResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub trait AddrRoutes {
|
||||
fn add_addr_routes(self) -> Self;
|
||||
}
|
||||
@@ -31,13 +30,14 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
Path(path): Path<AddrParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.addr(path.addr)).await
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr, false);
|
||||
state.cached_json(&headers, strategy, &uri, move |q| q.addr(path.addr)).await
|
||||
}, |op| op
|
||||
.id("get_address")
|
||||
.addrs_tag()
|
||||
.summary("Address information")
|
||||
.description("Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address)*")
|
||||
.ok_response::<AddrStats>()
|
||||
.json_response::<AddrStats>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
@@ -53,13 +53,14 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
Query(params): Query<AddrTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 25)).await
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr, false);
|
||||
state.cached_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 50)).await
|
||||
}, |op| op
|
||||
.id("get_address_txs")
|
||||
.addrs_tag()
|
||||
.summary("Address transactions")
|
||||
.description("Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*")
|
||||
.ok_response::<Vec<Transaction>>()
|
||||
.json_response::<Vec<Transaction>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
@@ -75,13 +76,14 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
Query(params): Query<AddrTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 25)).await
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr, true);
|
||||
state.cached_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 25)).await
|
||||
}, |op| op
|
||||
.id("get_address_confirmed_txs")
|
||||
.addrs_tag()
|
||||
.summary("Address confirmed transactions")
|
||||
.description("Get confirmed transactions for an address, 25 per page. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*")
|
||||
.ok_response::<Vec<Transaction>>()
|
||||
.json_response::<Vec<Transaction>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
@@ -103,7 +105,8 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
.addrs_tag()
|
||||
.summary("Address mempool transactions")
|
||||
.description("Get unconfirmed transaction IDs for an address from the mempool (up to 50).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-mempool)*")
|
||||
.ok_response::<Vec<Txid>>()
|
||||
.json_response::<Vec<Txid>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
@@ -117,13 +120,14 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
Path(path): Path<AddrParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.addr_utxos(path.addr)).await
|
||||
let strategy = state.addr_cache(Version::ONE, &path.addr, false);
|
||||
state.cached_json(&headers, strategy, &uri, move |q| q.addr_utxos(path.addr)).await
|
||||
}, |op| op
|
||||
.id("get_address_utxos")
|
||||
.addrs_tag()
|
||||
.summary("Address UTXOs")
|
||||
.description("Get unspent transaction outputs (UTXOs) for an address. Returns txid, vout, value, and confirmation status for each UTXO.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-utxo)*")
|
||||
.ok_response::<Vec<Utxo>>()
|
||||
.json_response::<Vec<Utxo>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
@@ -143,9 +147,10 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
.id("validate_address")
|
||||
.addrs_tag()
|
||||
.summary("Validate address")
|
||||
.description("Validate a Bitcoin address and get information about its type and scriptPubKey.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)*")
|
||||
.ok_response::<AddrValidation>()
|
||||
.description("Validate a Bitcoin address and get information about its type and scriptPubKey. Returns `isvalid: false` with an error message for invalid addresses.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)*")
|
||||
.json_response::<AddrValidation>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
),
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user