Compare commits

...

46 Commits

Author SHA1 Message Date
nym21 dac66c988d release: v0.3.0-beta.1 2026-04-08 17:12:32 +02:00
nym21 303d168681 docs: update generated docs 2026-04-08 17:12:03 +02:00
nym21 1ddb3385e2 website: css 2026-04-08 17:06:54 +02:00
nym21 eb75274dbf website: fixes 2026-04-08 13:11:03 +02:00
nym21 3a7887348c global: snapshot 2026-04-08 12:09:35 +02:00
nym21 0a4cb0601f release: v0.3.0-beta.0 2026-04-08 01:46:43 +02:00
nym21 861e29277c docs: update generated docs 2026-04-08 01:46:13 +02:00
nym21 c76b149ef9 website: fixes 2026-04-08 01:43:58 +02:00
nym21 4c4c6fc840 global: snapshot 2026-04-08 01:38:03 +02:00
nym21 0c14dfe924 query: more optimizations 2026-04-07 17:57:57 +02:00
nym21 17e531b4ee query: more optimizations 2026-04-07 17:43:11 +02:00
nym21 f022f62cce global: snap 2026-04-07 13:49:02 +02:00
nym21 e91f1386b1 website: snap 2026-04-06 22:30:02 +02:00
nym21 02f543af38 release: v0.3.0-alpha.6 2026-04-05 22:48:37 +02:00
nym21 20c96fb551 docs: update generated docs 2026-04-05 22:48:10 +02:00
nym21 acd3d6f425 server: cache fixes 2026-04-05 22:43:30 +02:00
nym21 2b15a24b6d website: add pool logos 2026-04-05 19:46:41 +02:00
nym21 7fac0bc613 global: snap 2026-04-04 20:13:03 +02:00
nym21 62f51761ee global: snap 2026-04-04 18:19:11 +02:00
nym21 5340cc288e release: v0.3.0-alpha.5 2026-04-04 13:10:48 +02:00
nym21 befe3c8fb7 docs: update generated docs 2026-04-04 13:10:28 +02:00
nym21 41ec24c81e server: ms endpoint fixes 2026-04-04 13:05:39 +02:00
nym21 42b497ff65 server: ms endpoint fixes 2026-04-04 12:16:15 +02:00
nym21 01d908a560 release: v0.3.0-alpha.4 2026-04-04 11:59:17 +02:00
nym21 42debcce80 docs: update generated docs 2026-04-04 11:58:50 +02:00
nym21 8bc993eceb global: fixes 2026-04-04 11:53:27 +02:00
nym21 366ac33e23 release: v0.3.0-alpha.3 2026-04-04 01:05:04 +02:00
nym21 b5a7023bd3 docs: update generated docs 2026-04-04 01:04:38 +02:00
nym21 883b38c77c global: snap 2026-04-04 00:59:37 +02:00
nym21 59c767a9e2 release: v0.3.0-alpha.2 2026-04-03 17:56:37 +02:00
nym21 9b5bb848f7 docs: update generated docs 2026-04-03 17:56:12 +02:00
nym21 5bf06530ce store: replace fjal reset by dir nuking 2026-04-03 17:49:46 +02:00
nym21 768e6870cb global: snap 2026-04-03 15:51:27 +02:00
nym21 79829ddd53 changelog: updated 2026-04-03 01:19:47 +02:00
nym21 78082801c6 clients: bump versions 2026-04-03 00:52:09 +02:00
nym21 50771ddccc release: v0.3.0-alpha.1 2026-04-03 00:16:03 +02:00
nym21 3a8a9ddecc docs: update generated docs 2026-04-03 00:15:43 +02:00
nym21 6cd45c1f1f deps: bumped 2026-04-02 23:54:12 +02:00
nym21 1a2db43cf5 fmt: global 2026-04-02 23:50:01 +02:00
nym21 4840e564f4 server: moved params from brk_types 2026-04-02 23:49:01 +02:00
nym21 744dce932c types: docs 2026-04-02 23:08:25 +02:00
nym21 8dfc1bc932 server: ms endpoint fixes 2026-04-02 22:37:34 +02:00
nym21 d92cf43c57 server: reorg 2026-04-02 13:19:56 +02:00
nym21 099699872e global: fixes 2026-04-02 12:39:20 +02:00
nym21 5099903043 clients: bump versions 2026-04-01 23:00:31 +02:00
nym21 982fe47a33 pr: #32
v0.3.0-alpha.0
2026-04-01 22:41:39 +02:00
828 changed files with 17782 additions and 8489 deletions
Generated
+89 -81
View File
@@ -338,7 +338,7 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "brk"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_bencher",
"brk_bindgen",
@@ -399,7 +399,7 @@ dependencies = [
[[package]]
name = "brk_alloc"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"libmimalloc-sys",
"mimalloc",
@@ -407,7 +407,7 @@ dependencies = [
[[package]]
name = "brk_bencher"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_error",
"brk_logger",
@@ -417,14 +417,14 @@ dependencies = [
[[package]]
name = "brk_bencher_visualizer"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"plotters",
]
[[package]]
name = "brk_bindgen"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_cohort",
"brk_query",
@@ -437,7 +437,7 @@ dependencies = [
[[package]]
name = "brk_cli"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"anyhow",
"brk_alloc",
@@ -462,7 +462,7 @@ dependencies = [
[[package]]
name = "brk_client"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_cohort",
"brk_types",
@@ -473,7 +473,7 @@ dependencies = [
[[package]]
name = "brk_cohort"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_error",
"brk_traversable",
@@ -485,7 +485,7 @@ dependencies = [
[[package]]
name = "brk_computer"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"bitcoin",
"brk_alloc",
@@ -514,7 +514,7 @@ dependencies = [
[[package]]
name = "brk_error"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -531,7 +531,7 @@ dependencies = [
[[package]]
name = "brk_fetcher"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_error",
"brk_logger",
@@ -543,7 +543,7 @@ dependencies = [
[[package]]
name = "brk_indexer"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"bitcoin",
"brk_alloc",
@@ -558,8 +558,8 @@ dependencies = [
"brk_types",
"color-eyre",
"fjall",
"parking_lot",
"rayon",
"rlimit",
"rustc-hash",
"schemars",
"serde",
@@ -569,7 +569,7 @@ dependencies = [
[[package]]
name = "brk_iterator"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_error",
"brk_reader",
@@ -579,7 +579,7 @@ dependencies = [
[[package]]
name = "brk_logger"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"jiff",
"owo-colors",
@@ -590,7 +590,7 @@ dependencies = [
[[package]]
name = "brk_mempool"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_error",
"brk_logger",
@@ -605,7 +605,7 @@ dependencies = [
[[package]]
name = "brk_oracle"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_indexer",
"brk_types",
@@ -615,7 +615,7 @@ dependencies = [
[[package]]
name = "brk_query"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"bitcoin",
"brk_computer",
@@ -637,7 +637,7 @@ dependencies = [
[[package]]
name = "brk_reader"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"bitcoin",
"brk_error",
@@ -647,12 +647,13 @@ dependencies = [
"derive_more",
"parking_lot",
"rayon",
"rlimit",
"tracing",
]
[[package]]
name = "brk_rpc"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -669,7 +670,7 @@ dependencies = [
[[package]]
name = "brk_server"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"aide",
"axum",
@@ -704,7 +705,7 @@ dependencies = [
[[package]]
name = "brk_store"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_error",
"brk_types",
@@ -715,7 +716,7 @@ dependencies = [
[[package]]
name = "brk_traversable"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"brk_traversable_derive",
"brk_types",
@@ -728,7 +729,7 @@ dependencies = [
[[package]]
name = "brk_traversable_derive"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"proc-macro2",
"quote",
@@ -737,7 +738,7 @@ dependencies = [
[[package]]
name = "brk_types"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"bitcoin",
"brk_error",
@@ -760,7 +761,7 @@ dependencies = [
[[package]]
name = "brk_website"
version = "0.3.0-alpha.0"
version = "0.3.0-beta.1"
dependencies = [
"axum",
"brk_logger",
@@ -1304,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",
@@ -1314,7 +1315,7 @@ dependencies = [
"flume",
"log",
"lsm-tree",
"lz4_flex",
"lz4_flex 0.11.6",
"tempfile",
"xxhash-rust",
]
@@ -1727,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",
@@ -1740,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",
@@ -1753,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",
@@ -1767,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",
@@ -1787,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",
@@ -2055,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"
@@ -2082,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",
@@ -2092,7 +2094,7 @@ dependencies = [
"enum_dispatch",
"interval-heap",
"log",
"lz4_flex",
"lz4_flex 0.11.6",
"quick_cache",
"rustc-hash",
"self_cell",
@@ -2104,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"
@@ -2401,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",
]
@@ -2534,9 +2542,9 @@ dependencies = [
[[package]]
name = "rawdb"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fd9f9db42fd2d1adfbd7cf447f021776b3b8fd15e09788988fc18c61e1f6bc"
checksum = "f23b5d5fae99af33e8d0c82763b890c469dcf18b48600ed78b0d70fce4dbe189"
dependencies = [
"libc",
"log",
@@ -3123,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",
@@ -3171,9 +3179,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.1.1+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "994b95d9e7bae62b34bab0e2a4510b801fa466066a6a8b2b57361fa1eba068ee"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [
"indexmap",
"serde_core",
@@ -3195,9 +3203,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.1.1+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow",
]
@@ -3428,14 +3436,14 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]]
name = "vecdb"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5422c45d12de71456700c199f9553319cb99e76311e413316dca7e9efd5133b6"
checksum = "a5fe60956ddba8c141ca8020aaf5bea55683475b83d19006c5f44b85c71bf974"
dependencies = [
"itoa",
"libc",
"log",
"lz4_flex",
"lz4_flex 0.13.0",
"parking_lot",
"pco",
"rawdb",
@@ -3451,9 +3459,9 @@ dependencies = [
[[package]]
name = "vecdb_derive"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b075be4cec2d718d40dc422cef038c10d6fcce4aad594199cc0a301a4985146"
checksum = "789897c1999d5d74f977020ad3d449846df046194103a4afcbac6d49baeaaffc"
dependencies = [
"quote",
"syn",
@@ -3880,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"
@@ -3903,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",
@@ -3914,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",
@@ -3946,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",
@@ -3973,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",
@@ -3984,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",
@@ -3995,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",
+26 -26
View File
@@ -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.3.0-alpha.0"
package.version = "0.3.0-beta.1"
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.3.0-alpha.0", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.0-alpha.0", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.0-alpha.0", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.0-alpha.0", path = "crates/brk_cli" }
brk_client = { version = "0.3.0-alpha.0", path = "crates/brk_client" }
brk_cohort = { version = "0.3.0-alpha.0", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.0-alpha.0", path = "crates/brk_computer" }
brk_error = { version = "0.3.0-alpha.0", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.0-alpha.0", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.0-alpha.0", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.0-alpha.0", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.0-alpha.0", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.0-alpha.0", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.0-alpha.0", path = "crates/brk_oracle" }
brk_query = { version = "0.3.0-alpha.0", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.0-alpha.0", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.0-alpha.0", path = "crates/brk_rpc" }
brk_server = { version = "0.3.0-alpha.0", path = "crates/brk_server" }
brk_store = { version = "0.3.0-alpha.0", path = "crates/brk_store" }
brk_traversable = { version = "0.3.0-alpha.0", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.0-alpha.0", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.0-alpha.0", path = "crates/brk_types" }
brk_website = { version = "0.3.0-alpha.0", path = "crates/brk_website" }
brk_alloc = { version = "0.3.0-beta.1", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.0-beta.1", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.0-beta.1", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.0-beta.1", path = "crates/brk_cli" }
brk_client = { version = "0.3.0-beta.1", path = "crates/brk_client" }
brk_cohort = { version = "0.3.0-beta.1", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.0-beta.1", path = "crates/brk_computer" }
brk_error = { version = "0.3.0-beta.1", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.0-beta.1", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.0-beta.1", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.0-beta.1", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.0-beta.1", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.0-beta.1", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.0-beta.1", path = "crates/brk_oracle" }
brk_query = { version = "0.3.0-beta.1", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.0-beta.1", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.0-beta.1", path = "crates/brk_rpc" }
brk_server = { version = "0.3.0-beta.1", path = "crates/brk_server" }
brk_store = { version = "0.3.0-beta.1", path = "crates/brk_store" }
brk_traversable = { version = "0.3.0-beta.1", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.0-beta.1", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.0-beta.1", path = "crates/brk_types" }
brk_website = { version = "0.3.0-beta.1", 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.2", 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]
@@ -69,16 +69,32 @@ 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 {
@@ -109,11 +125,19 @@ 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();
}
}
@@ -404,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;
}}
@@ -417,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;
@@ -440,7 +443,7 @@ 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 = _addCamelGetters(await res.json());
// Skip update if ETag matches and cache already delivered
@@ -472,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();
}}
@@ -488,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);
}}
}}
@@ -167,11 +167,9 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
if endpoint.query_params.is_empty() {
if endpoint.path_params.is_empty() {
writeln!(output, " return self.{}('{}')", fetch_method, path)
.unwrap();
writeln!(output, " return self.{}('{}')", fetch_method, path).unwrap();
} else {
writeln!(output, " return self.{}(f'{}')", fetch_method, path)
.unwrap();
writeln!(output, " return self.{}(f'{}')", fetch_method, path).unwrap();
}
} else {
writeln!(output, " params = []").unwrap();
@@ -197,8 +197,7 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
.unwrap();
writeln!(output, " }}").unwrap();
} else {
writeln!(output, " self.base.{}(&path)", fetch_method)
.unwrap();
writeln!(output, " self.base.{}(&path)", fetch_method).unwrap();
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ owo-colors = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
toml = "1.1.1"
toml = "1.1.2"
vecdb = { workspace = true }
[[bin]]
+4826 -1102
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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..]))
}
}
+1 -1
View File
@@ -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]))
}
}
@@ -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, _)| {
@@ -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(
+3 -3
View File
@@ -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,
)?;
@@ -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);
+2 -3
View File
@@ -44,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();
@@ -123,8 +123,7 @@ impl Vecs {
self.pool.len()
);
}
self.pool
.validate_computed_version_or_reset(dep_version)?;
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();
+16 -1
View File
@@ -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,
@@ -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(
+1 -1
View File
@@ -19,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 }
+60 -14
View File
@@ -2,16 +2,18 @@
use std::{
fs,
path::Path,
path::{Path, PathBuf},
sync::Arc,
thread::{self, sleep},
time::{Duration, Instant},
};
use brk_error::Result;
use brk_reader::Reader;
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, WritableVec, unlikely,
@@ -33,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 {
@@ -42,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(),
}
}
}
@@ -54,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");
@@ -75,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() {
@@ -95,6 +106,18 @@ impl Indexer {
}
}
/// 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)
}
@@ -108,6 +131,21 @@ impl Indexer {
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,
reader: &Reader,
@@ -117,6 +155,8 @@ impl Indexer {
) -> 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();
@@ -141,8 +181,7 @@ impl Indexer {
}
None => {
info!("Data inconsistency detected, resetting indexer...");
self.vecs.reset()?;
self.stores.reset()?;
self.full_reset()?;
(Indexes::default(), None)
}
}
@@ -263,6 +302,8 @@ impl Indexer {
export(stores, vecs, height)?;
readers = Readers::new(vecs);
}
*self.tip_blockhash.write() = block.block_hash().into();
}
drop(readers);
@@ -277,6 +318,9 @@ 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 {
@@ -292,6 +336,8 @@ impl Indexer {
}
db.compact()?;
info!("Exported in {:?}", i.elapsed());
Ok(())
});
+6 -16
View File
@@ -42,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);
}
@@ -84,7 +85,7 @@ impl Stores {
)
};
Ok(Self {
let stores = Self {
db: database.clone(),
addr_type_to_addr_hash_to_addr_index: ByAddrType::new_with_index(
@@ -113,7 +114,9 @@ impl Stores {
Kind::Recent,
5,
)?,
})
};
Ok(stores)
}
pub fn starting_height(&self) -> Height {
@@ -398,17 +401,4 @@ impl Stores {
.remove(AddrIndexTxIndex::from((addr_index, tx_index)));
}
}
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 -1
View File
@@ -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"
)
});
+28
View File
@@ -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;
}
+87 -58
View File
@@ -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;
+163 -57
View File
@@ -1,9 +1,11 @@
use std::io::Read;
use bitcoin::consensus::Decodable;
use bitcoin::hex::DisplayHex;
use brk_error::{Error, Result};
use brk_types::{
BlockExtras, BlockHash, BlockHashPrefix, BlockHeader, BlockInfo, BlockInfoV1, BlockPool,
FeeRate, Height, Sats, Timestamp, TxIndex, VSize, pools,
FeeRate, Height, PoolSlug, Sats, Timestamp, TxIndex, VSize, pools,
};
use vecdb::{AnyVec, ReadableVec, VecIndex};
@@ -20,7 +22,7 @@ impl Query {
}
pub fn block_by_height(&self, height: Height) -> Result<BlockInfo> {
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()));
}
@@ -30,7 +32,7 @@ impl Query {
}
pub fn block_by_height_v1(&self, height: Height) -> Result<BlockInfoV1> {
let max_height = self.max_height();
let max_height = self.height();
if height > max_height {
return Err(Error::OutOfRange("Block height out of range".into()));
}
@@ -46,7 +48,7 @@ impl Query {
}
pub fn block_hash_by_height(&self, height: Height) -> Result<BlockHash> {
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()));
}
@@ -123,13 +125,17 @@ impl Query {
blocks.push(BlockInfo {
id: blockhashes[i].clone(),
height: Height::from(begin + i),
header,
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],
previous_block_hash: header.previous_block_hash,
median_time,
difficulty: *difficulties[i],
});
}
@@ -138,7 +144,7 @@ impl Query {
pub(crate) fn blocks_v1_range(&self, begin: usize, end: usize) -> Result<Vec<BlockInfoV1>> {
if begin >= end {
return Ok(Vec::new());
return Ok(vec![]);
}
let count = end - begin;
@@ -214,6 +220,7 @@ impl Query {
.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
@@ -245,15 +252,6 @@ impl Query {
let fa_pct90 = fad.pct90.height.collect_range_at(begin, end);
let fa_max = fad.max.height.collect_range_at(begin, end);
// Bulk read tx positions range covering all coinbase txs (first tx of each block)
let tx_pos_begin = first_tx_indexes[0].to_usize();
let tx_pos_end = first_tx_indexes[count - 1].to_usize() + 1;
let all_tx_positions = indexer
.vecs
.transactions
.position
.collect_range_at(tx_pos_begin, tx_pos_end);
// Bulk read median time window
let median_start = begin.saturating_sub(10);
let median_timestamps = indexer
@@ -265,15 +263,58 @@ impl Query {
let mut blocks = Vec::with_capacity(count);
for i in (0..count).rev() {
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()) 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];
@@ -287,16 +328,11 @@ impl Query {
let pool_slug = pool_slugs[i];
let pool = all_pools.get(pool_slug);
let (
coinbase_raw,
coinbase_address,
coinbase_addresses,
coinbase_signature,
coinbase_signature_ascii,
) = Self::parse_coinbase_tx(
reader,
all_tx_positions[first_tx_indexes[i].to_usize() - tx_pos_begin],
);
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);
@@ -304,13 +340,17 @@ impl Query {
let info = BlockInfo {
id: blockhashes[i].clone(),
height: Height::from(begin + i),
header,
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,
difficulty: *difficulties[i],
};
let total_input_amt = input_volumes[i];
@@ -330,9 +370,10 @@ impl Query {
],
reward: subsidy + total_fees,
pool: BlockPool {
id: pool.unique_id(),
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
@@ -371,6 +412,9 @@ impl Query {
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 });
@@ -405,10 +449,6 @@ impl Query {
.map_err(|_| Error::Internal("Failed to decode block header"))
}
fn max_height(&self) -> Height {
Height::from(self.indexer().vecs.blocks.blockhash.len().saturating_sub(1))
}
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);
@@ -440,33 +480,95 @@ impl Query {
Timestamp::from(sorted[sorted.len() / 2])
}
fn parse_coinbase_tx(
reader: &brk_reader::Reader,
position: brk_types::BlkPosition,
) -> (String, Option<String>, Vec<String>, String, String) {
let raw_bytes = match reader.read_raw_bytes(position, 1000) {
Ok(bytes) => bytes,
Err(_) => return (String::new(), None, vec![], String::new(), String::new()),
};
fn compact_size_len(tx_count: u32) -> u32 {
if tx_count <= 0xFC {
1
} else if tx_count <= 0xFFFF {
3
} else {
5
}
}
let tx = match bitcoin::Transaction::consensus_decode(&mut raw_bytes.as_slice()) {
Ok(tx) => tx,
Err(_) => return (String::new(), None, vec![], String::new(), String::new()),
};
/// 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;
}
let coinbase_raw = tx
// 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,
};
let scriptsig_bytes: Vec<u8> = tx
.input
.first()
.map(|input| input.script_sig.as_bytes().to_lower_hex_string())
.map(|input| input.script_sig.as_bytes().to_vec())
.unwrap_or_default();
let coinbase_signature_ascii = tx
.input
.first()
.map(|input| input.script_sig.as_bytes().iter().map(|&b| b as char).collect::<String>())
.unwrap_or_default();
let coinbase_raw = scriptsig_bytes.to_lower_hex_string();
let coinbase_addresses: Vec<String> = tx
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| {
@@ -475,11 +577,14 @@ impl Query {
.map(|a| a.to_string())
})
.collect();
coinbase_addresses.dedup();
let coinbase_address = coinbase_addresses.first().cloned();
let coinbase_signature = tx
.output
.first()
.iter()
.find(|output| !output.script_pubkey.is_op_return())
.or(tx.output.first())
.map(|output| output.script_pubkey.to_asm_string())
.unwrap_or_default();
@@ -489,6 +594,7 @@ impl Query {
coinbase_addresses,
coinbase_signature,
coinbase_signature_ascii,
scriptsig_bytes,
)
}
}
+5 -5
View File
@@ -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 {
+204 -90
View File
@@ -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> {
@@ -24,108 +39,207 @@ impl Query {
// === Helper methods ===
pub(crate) 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
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))
}
}
+24 -11
View File
@@ -1,7 +1,9 @@
use std::cmp::Ordering;
use brk_error::{Error, Result};
use brk_types::{
CpfpEntry, CpfpInfo, MempoolBlock, MempoolInfo, MempoolRecentTx, RecommendedFees, Txid,
TxidParam, TxidPrefix, Weight,
CpfpEntry, CpfpInfo, FeeRate, MempoolBlock, MempoolInfo, MempoolRecentTx, RecommendedFees,
Txid, TxidPrefix, Weight,
};
use crate::Query;
@@ -49,14 +51,14 @@ impl Query {
Ok(mempool.get_txs().recent().to_vec())
}
pub fn cpfp(&self, TxidParam { txid }: TxidParam) -> Result<CpfpInfo> {
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 prefix = TxidPrefix::from(txid);
let entry = entries
.get(&prefix)
.ok_or(Error::NotFound("Transaction not in mempool".into()))?;
let Some(entry) = entries.get(&prefix) else {
return Ok(CpfpInfo::default());
};
// Ancestors: walk up the depends chain
let mut ancestors = Vec::new();
@@ -72,10 +74,9 @@ impl Query {
}
}
// Descendants: find entries that depend on this tx's prefix
let mut descendants = Vec::new();
for e in entries.entries().iter().flatten() {
if e.depends.contains(&prefix) {
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),
@@ -86,10 +87,22 @@ impl Query {
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,
effective_fee_per_vsize: Some(effective_fee_per_vsize),
fee: Some(entry.fee),
adjusted_vsize: Some(entry.vsize),
})
}
@@ -1,59 +1,62 @@
// 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 &timestamps {
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)
}
}
+13 -40
View File
@@ -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())
}
}
+30 -38
View File
@@ -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(&timestamps) {
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
}
}
+15 -8
View File
@@ -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,
})
}
}
+9 -10
View File
@@ -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,
};
+7 -10
View File
@@ -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,17 +56,13 @@ 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,
avg_hashrate: *hr as u128,
@@ -79,9 +75,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();
+1 -1
View File
@@ -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;
+288 -82
View File
@@ -1,17 +1,30 @@
use brk_error::{Error, Result};
use brk_types::{
BlockInfoV1, Height, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo,
PoolHashrateEntry, 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,31 @@ 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)),
})
}
@@ -187,122 +292,223 @@ impl Query {
let max_height = self.height().to_usize();
let start = start_height.map(|h| h.to_usize()).unwrap_or(max_height);
// BytesVec reader gives O(1) mmap reads — efficient for backward scan
let reader = computer.pools.pool.reader();
let end = start.min(reader.len().saturating_sub(1));
let mut heights = Vec::with_capacity(10);
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() >= 10 {
if heights.len() >= POOL_BLOCKS_LIMIT {
break;
}
}
}
// Group consecutive descending heights into ranges for batch reads
let mut blocks = Vec::with_capacity(heights.len());
for h in heights {
if let Ok(mut v) = self.blocks_v1_range(h, h + 1) {
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 pools_list = pools();
let pool = pools_list.get(slug);
let entries = self.compute_pool_hashrate_entries(slug, 0)?;
Ok(entries
.into_iter()
.map(|(ts, hr, share)| PoolHashrateEntry {
timestamp: ts,
avg_hashrate: hr,
share,
pool_name: pool.name.to_string(),
})
.collect())
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 current_height = self.height().to_usize();
let start = match time_period {
Some(tp) => current_height.saturating_sub(tp.block_count()),
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() {
if let Ok(pool_entries) = self.compute_pool_hashrate_entries(pool.slug, start) {
for (ts, hr, share) in pool_entries {
if share > 0.0 {
entries.push(PoolHashrateEntry {
timestamp: ts,
avg_hashrate: hr,
share,
pool_name: pool.name.to_string(),
});
}
}
}
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)
}
/// Compute (timestamp, hashrate, share) tuples for a pool from `start_height`.
fn compute_pool_hashrate_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_height: usize,
) -> Result<Vec<(brk_types::Timestamp, u128, f64)>> {
start_day: usize,
end_day: usize,
) -> Result<Vec<Option<StoredU64>>> {
let computer = self.computer();
let indexer = self.indexer();
let end = self.height().to_usize() + 1;
let start = start_height;
let dominance_bps = computer
computer
.pools
.major
.get(&slug)
.map(|v| &v.base.dominance.bps.height)
.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.dominance.bps.height)
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()))?;
.ok_or_else(|| Error::NotFound("Pool not found".into()))
}
let total = end - start;
let step = (total / 200).max(1);
/// 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![];
}
// Batch read everything for the range
let timestamps = indexer.vecs.blocks.timestamp.collect_range_at(start, end);
let bps_values = dominance_bps.collect_range_at(start, end);
let day1_values = computer.indexes.height.day1.collect_range_at(start, end);
let hashrate_vec = &computer.mining.hashrate.rate.base.day1;
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);
// Pre-read all needed hashrates by collecting unique day1 values
let max_day = day1_values.iter().map(|d| d.to_usize()).max().unwrap_or(0);
let min_day = day1_values.iter().map(|d| d.to_usize()).min().unwrap_or(0);
let hashrates = hashrate_vec.collect_range_dyn(min_day, max_day + 1);
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;
}
Ok((0..total)
.step_by(step)
.filter_map(|i| {
let bps = *bps_values[i];
let share = bps as f64 / 10000.0;
let day_idx = day1_values[i].to_usize() - min_day;
let network_hr = f64::from(*hashrates.get(day_idx)?.as_ref()?);
Some((timestamps[i], (network_hr * share) as u128, share))
})
.collect())
entries
}
}
+33 -30
View File
@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_types::{Dollars, ExchangeRates, HistoricalPrice, HistoricalPriceEntry, Timestamp};
use vecdb::{ReadableVec, VecIndex};
use brk_types::{Dollars, ExchangeRates, HistoricalPrice, HistoricalPriceEntry, Hour4, Timestamp};
use vecdb::ReadableVec;
use crate::Query;
@@ -21,38 +21,41 @@ impl Query {
}
pub fn historical_price(&self, timestamp: Option<Timestamp>) -> Result<HistoricalPrice> {
let indexer = self.indexer();
let computer = self.computer();
let max_height = self.height().to_usize();
let end = max_height + 1;
let timestamps = indexer.vecs.blocks.timestamp.collect();
let all_prices = computer.prices.spot.cents.height.collect();
let prices = if let Some(target_ts) = timestamp {
let target = usize::from(target_ts);
let h = timestamps
.binary_search_by_key(&target, |t| usize::from(*t))
.unwrap_or_else(|i| i.min(max_height));
vec![HistoricalPriceEntry {
time: usize::from(timestamps[h]) as u64,
usd: Dollars::from(all_prices[h]),
}]
} else {
let step = (max_height / 200).max(1);
(0..end)
.step_by(step)
.map(|h| HistoricalPriceEntry {
time: usize::from(timestamps[h]) as u64,
usd: Dollars::from(all_prices[h]),
})
.collect()
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())
}
}
+184 -283
View File
@@ -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::{
Height, MerkleProof, 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
@@ -61,7 +59,12 @@ impl Query {
.height
.collect_one(tx_index)
.unwrap();
let block_hash = indexer.vecs.blocks.blockhash.read_once(height)?;
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,8 +75,15 @@ impl Query {
})
}
pub fn transaction_raw(&self, TxidParam { txid }: TxidParam) -> Result<Vec<u8>> {
let prefix = TxidPrefix::from(&txid);
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
@@ -86,16 +96,16 @@ impl Query {
self.transaction_raw_by_index(tx_index)
}
pub fn transaction_hex(&self, TxidParam { txid }: TxidParam) -> Result<String> {
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
@@ -109,317 +119,208 @@ 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 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
.first_txout_index
.read_once(tx_index)?;
let next = indexer
.vecs
.transactions
.first_txout_index
.read_once(tx_index.incremented())?;
Ok((tx_index, first, usize::from(next) - usize::from(first)))
}
// 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
/// Resolve spend status for a single output.
fn resolve_outspend(&self, txout_index: TxOutIndex) -> Result<TxOutspend> {
let indexer = self.indexer();
let txin_index = self
.computer()
.outputs
.spent
.txin_index
.reader()
.get(usize::from(txout_index));
if txin_index == TxInIndex::UNSPENT {
return Ok(TxOutspend::UNSPENT);
}
let spending_tx_index = indexer
.vecs
.inputs
.tx_index
.collect_one_at(usize::from(txin_index))
.unwrap();
let spending_first_txin = indexer
.vecs
.transactions
.first_txin_index
.collect_one(spending_tx_index)
.unwrap();
let spending_height = indexer
.vecs
.transactions
.height
.collect_one(tx_index)
.unwrap();
let version = indexer
.vecs
.transactions
.tx_version
.collect_one(tx_index)
.unwrap();
let lock_time = indexer
.vecs
.transactions
.raw_locktime
.collect_one(tx_index)
.collect_one(spending_tx_index)
.unwrap();
Ok(TxOutspend {
spent: true,
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(
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 first_txin_index = indexer
.vecs
.transactions
.first_txin_index
.collect_one(tx_index)
.unwrap();
let position = indexer
.vecs
.transactions
.position
.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)
}
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())
Ok(self
.transaction_raw_by_index(tx_index)?
.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)
.unwrap();
// Calculate vin
let spending_first_txin_index = 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),
status: Some(TxStatus {
confirmed: true,
block_height: Some(spending_height),
block_hash: Some(block_hash),
block_time: Some(block_time),
}),
})
}
fn resolve_tx(&self, txid: &Txid) -> Result<(TxIndex, Height)> {
pub fn resolve_tx(&self, txid: &Txid) -> Result<(TxIndex, Height)> {
let indexer = self.indexer();
let prefix = TxidPrefix::from(txid);
let tx_index: TxIndex = indexer
@@ -441,12 +342,12 @@ impl Query {
self.client().send_raw_transaction(hex)
}
pub fn merkleblock_proof(&self, txid_param: TxidParam) -> Result<String> {
let (_, height) = self.resolve_tx(&txid_param.txid)?;
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_param.txid).into();
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
@@ -454,8 +355,8 @@ impl Query {
Ok(bitcoin::consensus::encode::serialize_hex(&mb))
}
pub fn merkle_proof(&self, txid_param: TxidParam) -> Result<MerkleProof> {
let (tx_index, height) = self.resolve_tx(&txid_param.txid)?;
pub fn merkle_proof(&self, txid: &Txid) -> Result<MerkleProof> {
let (tx_index, height) = self.resolve_tx(txid)?;
let first_tx = self
.indexer()
.vecs
+11 -1
View File
@@ -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();
+1
View File
@@ -20,3 +20,4 @@ derive_more = { workspace = true }
tracing = { workspace = true }
parking_lot = { workspace = true }
rayon = { workspace = true }
rlimit = "0.11.0"
+63 -11
View File
@@ -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,
@@ -53,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,
@@ -60,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,
}
@@ -86,26 +96,49 @@ 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>> {
@@ -441,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)
}
}
+8
View File
@@ -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);
}
}
+8
View File
@@ -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<_>>>()?;
@@ -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,7 +30,8 @@ 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()
@@ -53,7 +53,8 @@ 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()
@@ -75,7 +76,8 @@ 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()
@@ -104,6 +106,7 @@ impl AddrRoutes for ApiRouter<AppState> {
.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)*")
.json_response::<Vec<Txid>>()
.not_modified()
.bad_request()
.not_found()
.server_error()
@@ -117,7 +120,8 @@ 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()
@@ -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)*")
.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()
),
)
}
@@ -5,13 +5,14 @@ use axum::{
};
use brk_query::BLOCK_TXS_PAGE_SIZE;
use brk_types::{
BlockHashParam, BlockHashStartIndex, BlockHashTxIndex, BlockInfo, BlockInfoV1, BlockStatus,
BlockTimestamp, HeightParam, TimestampParam, Transaction, TxIndex, Txid,
BlockInfo, BlockInfoV1, BlockStatus, BlockTimestamp, Transaction, TxIndex, Txid, Version,
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
use crate::{
AppState, CacheStrategy,
extended::TransformResponseExtended,
params::{BlockHashParam, BlockHashStartIndex, BlockHashTxIndex, HeightParam, TimestampParam},
};
pub trait BlockRoutes {
fn add_block_routes(self) -> Self;
@@ -26,7 +27,8 @@ impl BlockRoutes for ApiRouter<AppState> {
headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block(&path.hash)).await
let strategy = state.block_cache(Version::ONE, &path.hash);
state.cached_json(&headers, strategy, &uri, move |q| q.block(&path.hash)).await
},
|op| {
op.id("get_block")
@@ -47,7 +49,8 @@ impl BlockRoutes for ApiRouter<AppState> {
"/api/v1/block/{hash}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<BlockHashParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
let strategy = state.block_cache(Version::ONE, &path.hash);
state.cached_json(&headers, strategy, &uri, move |q| {
let height = q.height_by_hash(&path.hash)?;
q.block_by_height_v1(height)
}).await
@@ -59,6 +62,7 @@ impl BlockRoutes for ApiRouter<AppState> {
.description("Returns block details with extras by hash.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-v1)*")
.json_response::<BlockInfoV1>()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
@@ -68,15 +72,17 @@ impl BlockRoutes for ApiRouter<AppState> {
"/api/block/{hash}/header",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<BlockHashParam>, State(state): State<AppState>| {
state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.block_header_hex(&path.hash)).await
let strategy = state.block_cache(Version::ONE, &path.hash);
state.cached_text(&headers, strategy, &uri, move |q| q.block_header_hex(&path.hash)).await
},
|op| {
op.id("get_block_header")
.blocks_tag()
.summary("Block header")
.description("Returns the hex-encoded block header.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-header)*")
.description("Returns the hex-encoded 80-byte block header.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-header)*")
.text_response()
.not_modified()
.bad_request()
.not_found()
.server_error()
},
@@ -89,7 +95,7 @@ impl BlockRoutes for ApiRouter<AppState> {
headers: HeaderMap,
Path(path): Path<HeightParam>,
State(state): State<AppState>| {
state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.block_hash_by_height(path.height).map(|h| h.to_string())).await
state.cached_text(&headers, state.height_cache(Version::ONE, path.height), &uri, move |q| q.block_hash_by_height(path.height).map(|h| h.to_string())).await
},
|op| {
op.id("get_block_by_height")
@@ -113,7 +119,7 @@ impl BlockRoutes for ApiRouter<AppState> {
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
state.cached_json(&headers, state.timestamp_cache(Version::ONE, path.timestamp), &uri, move |q| q.block_by_timestamp(path.timestamp)).await
},
|op| {
op.id("get_block_by_timestamp")
@@ -135,7 +141,8 @@ impl BlockRoutes for ApiRouter<AppState> {
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
let strategy = state.block_cache(Version::ONE, &path.hash);
state.cached_bytes(&headers, strategy, &uri, move |q| q.block_raw(&path.hash)).await
},
|op| {
op.id("get_block_raw")
@@ -144,7 +151,7 @@ impl BlockRoutes for ApiRouter<AppState> {
.description(
"Returns the raw block data in binary format.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-raw)*",
)
.json_response::<Vec<u8>>()
.binary_response()
.not_modified()
.bad_request()
.not_found()
@@ -159,7 +166,7 @@ impl BlockRoutes for ApiRouter<AppState> {
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
state.cached_json(&headers, state.block_status_cache(Version::ONE, &path.hash), &uri, move |q| q.block_status(&path.hash)).await
},
|op| {
op.id("get_block_status")
@@ -180,7 +187,7 @@ impl BlockRoutes for ApiRouter<AppState> {
"/api/blocks/tip/height",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_text(&headers, CacheStrategy::Height, &uri, |q| Ok(q.height().to_string())).await
state.cached_text(&headers, CacheStrategy::Tip, &uri, |q| Ok(q.indexed_height().to_string())).await
},
|op| {
op.id("get_block_tip_height")
@@ -197,7 +204,7 @@ impl BlockRoutes for ApiRouter<AppState> {
"/api/blocks/tip/hash",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_text(&headers, CacheStrategy::Height, &uri, |q| q.block_hash_by_height(q.height()).map(|h| h.to_string())).await
state.cached_text(&headers, CacheStrategy::Tip, &uri, |q| Ok(q.tip_blockhash().to_string())).await
},
|op| {
op.id("get_block_tip_hash")
@@ -217,7 +224,8 @@ impl BlockRoutes for ApiRouter<AppState> {
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
let strategy = state.block_cache(Version::ONE, &path.hash);
state.cached_text(&headers, strategy, &uri, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
},
|op| {
op.id("get_block_txid")
@@ -241,7 +249,8 @@ impl BlockRoutes for ApiRouter<AppState> {
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
let strategy = state.block_cache(Version::ONE, &path.hash);
state.cached_json(&headers, strategy, &uri, move |q| q.block_txids(&path.hash)).await
},
|op| {
op.id("get_block_txids")
@@ -265,7 +274,8 @@ impl BlockRoutes for ApiRouter<AppState> {
headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txs(&path.hash, TxIndex::default())).await
let strategy = state.block_cache(Version::ONE, &path.hash);
state.cached_json(&headers, strategy, &uri, move |q| q.block_txs(&path.hash, TxIndex::default())).await
},
|op| {
op.id("get_block_txs")
@@ -290,7 +300,8 @@ impl BlockRoutes for ApiRouter<AppState> {
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
let strategy = state.block_cache(Version::ONE, &path.hash);
state.cached_json(&headers, strategy, &uri, move |q| q.block_txs(&path.hash, path.start_index)).await
},
|op| {
op.id("get_block_txs_from_index")
@@ -313,7 +324,7 @@ impl BlockRoutes for ApiRouter<AppState> {
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(None))
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.blocks(None))
.await
},
|op| {
@@ -334,7 +345,7 @@ impl BlockRoutes for ApiRouter<AppState> {
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
state.cached_json(&headers, state.height_cache(Version::ONE, path.height), &uri, move |q| q.blocks(Some(path.height))).await
},
|op| {
op.id("get_blocks_from_height")
@@ -355,7 +366,7 @@ impl BlockRoutes for ApiRouter<AppState> {
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks_v1(None))
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.blocks_v1(None))
.await
},
|op| {
@@ -376,7 +387,7 @@ impl BlockRoutes for ApiRouter<AppState> {
headers: HeaderMap,
Path(path): Path<HeightParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks_v1(Some(path.height))).await
state.cached_json(&headers, state.height_cache(Version::ONE, path.height), &uri, move |q| q.blocks_v1(Some(path.height))).await
},
|op| {
op.id("get_blocks_v1_from_height")
@@ -5,9 +5,7 @@ use axum::{
};
use brk_types::{MempoolBlock, RecommendedFees};
use crate::extended::TransformResponseExtended;
use super::AppState;
use crate::{AppState, extended::TransformResponseExtended};
pub trait FeesRoutes {
fn add_fees_routes(self) -> Self;
@@ -31,6 +29,7 @@ impl FeesRoutes for ApiRouter<AppState> {
.summary("Projected mempool blocks")
.description("Get projected blocks from the mempool for fee estimation.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*")
.json_response::<Vec<MempoolBlock>>()
.not_modified()
.server_error()
},
),
@@ -51,6 +50,7 @@ impl FeesRoutes for ApiRouter<AppState> {
.summary("Recommended fees")
.description("Get recommended fee rates for different confirmation targets.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
.json_response::<RecommendedFees>()
.not_modified()
.server_error()
},
),
@@ -71,6 +71,7 @@ impl FeesRoutes for ApiRouter<AppState> {
.summary("Precise recommended fees")
.description("Get recommended fee rates with up to 3 decimal places.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)*")
.json_response::<RecommendedFees>()
.not_modified()
.server_error()
},
),
@@ -3,14 +3,12 @@ use axum::{
extract::{Query, State},
http::{HeaderMap, Uri},
};
use brk_types::{
DifficultyAdjustment, HistoricalPrice, OptionalTimestampParam, Prices, Timestamp,
use brk_types::{DifficultyAdjustment, HistoricalPrice, Prices, Timestamp, Version};
use crate::{
AppState, CacheStrategy, extended::TransformResponseExtended, params::OptionalTimestampParam,
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
pub trait GeneralRoutes {
fn add_general_routes(self) -> Self;
}
@@ -22,7 +20,7 @@ impl GeneralRoutes for ApiRouter<AppState> {
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, |q| {
q.difficulty_adjustment()
})
.await
@@ -57,6 +55,7 @@ impl GeneralRoutes for ApiRouter<AppState> {
.summary("Current BTC price")
.description("Returns bitcoin latest price (on-chain derived, USD only).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-price)*")
.json_response::<Prices>()
.not_modified()
.server_error()
},
),
@@ -68,8 +67,12 @@ impl GeneralRoutes for ApiRouter<AppState> {
headers: HeaderMap,
Query(params): Query<OptionalTimestampParam>,
State(state): State<AppState>| {
let strategy = params
.timestamp
.map(|ts| state.timestamp_cache(Version::ONE, ts))
.unwrap_or(CacheStrategy::Tip);
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, strategy, &uri, move |q| {
q.historical_price(params.timestamp)
})
.await
@@ -5,9 +5,7 @@ use axum::{
};
use brk_types::{Dollars, MempoolInfo, MempoolRecentTx, Txid};
use crate::extended::TransformResponseExtended;
use super::AppState;
use crate::{AppState, extended::TransformResponseExtended};
pub trait MempoolRoutes {
fn add_mempool_routes(self) -> Self;
@@ -29,6 +27,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
.summary("Mempool statistics")
.description("Get current mempool statistics including transaction count, total vsize, total fees, and fee histogram.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool)*")
.json_response::<MempoolInfo>()
.not_modified()
.server_error()
},
),
@@ -47,6 +46,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
.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)*")
.json_response::<Vec<Txid>>()
.not_modified()
.server_error()
},
),
@@ -65,6 +65,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
.summary("Recent mempool transactions")
.description("Get the last 10 transactions to enter the mempool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-recent)*")
.json_response::<Vec<MempoolRecentTx>>()
.not_modified()
.server_error()
},
),
@@ -87,6 +88,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
plus mempool.",
)
.json_response::<Dollars>()
.not_modified()
.server_error()
},
),
@@ -2,19 +2,20 @@ use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::{Path, State},
http::{HeaderMap, Uri},
response::{IntoResponse, Redirect, Response},
response::Redirect,
routing::get,
};
use brk_types::{
BlockCountParam, BlockFeesEntry, BlockInfoV1, BlockRewardsEntry, BlockSizesWeights,
DifficultyAdjustmentEntry, HashrateSummary, PoolDetail,
PoolHashrateEntry, PoolInfo, PoolSlugAndHeightParam, PoolSlugParam, PoolsSummary,
RewardStats, TimePeriodParam,
BlockFeeRatesEntry, BlockFeesEntry, BlockInfoV1, BlockRewardsEntry, BlockSizesWeights,
DifficultyAdjustmentEntry, HashrateSummary, PoolDetail, PoolHashrateEntry, PoolInfo,
PoolsSummary, RewardStats, Version,
};
use crate::{CacheStrategy, Error, extended::TransformResponseExtended};
use super::AppState;
use crate::{
AppState, CacheStrategy,
extended::TransformResponseExtended,
params::{BlockCountParam, PoolSlugAndHeightParam, PoolSlugParam, TimePeriodParam},
};
pub trait MiningRoutes {
fn add_mining_routes(self) -> Self;
@@ -48,15 +49,16 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pools/{time_period}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.mining_pools(path.time_period)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.mining_pools(path.time_period)).await
},
|op| {
op.id("get_pool_stats")
.mining_tag()
.summary("Mining pool statistics")
.description("Get mining pool statistics for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)*")
.description("Get mining pool statistics for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)*")
.json_response::<PoolsSummary>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -65,7 +67,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pool/{slug}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<PoolSlugParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_detail(path.slug)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_detail(path.slug)).await
},
|op| {
op.id("get_pool")
@@ -83,7 +85,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/hashrate/pools",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.pools_hashrate(None)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, |q| q.pools_hashrate(None)).await
},
|op| {
op.id("get_pools_hashrate")
@@ -100,15 +102,16 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/hashrate/pools/{time_period}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pools_hashrate(Some(path.time_period))).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pools_hashrate(Some(path.time_period))).await
},
|op| {
op.id("get_pools_hashrate_by_period")
.mining_tag()
.summary("All pools hashrate")
.description("Get hashrate data for all mining pools for a time period. Valid periods: 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)*")
.description("Get hashrate data for all mining pools for a time period. Valid periods: `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)*")
.json_response::<Vec<PoolHashrateEntry>>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -117,7 +120,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pool/{slug}/hashrate",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<PoolSlugParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_hashrate(path.slug)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_hashrate(path.slug)).await
},
|op| {
op.id("get_pool_hashrate")
@@ -135,7 +138,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pool/{slug}/blocks",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<PoolSlugParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_blocks(path.slug, None)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_blocks(path.slug, None)).await
},
|op| {
op.id("get_pool_blocks")
@@ -153,7 +156,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pool/{slug}/blocks/{height}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(PoolSlugAndHeightParam {slug, height}): Path<PoolSlugAndHeightParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_blocks(slug, Some(height))).await
state.cached_json(&headers, state.height_cache(Version::ONE, height), &uri, move |q| q.pool_blocks(slug, Some(height))).await
},
|op| {
op.id("get_pool_blocks_from")
@@ -171,7 +174,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/hashrate",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.hashrate(None)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, |q| q.hashrate(None)).await
},
|op| {
op.id("get_hashrate")
@@ -188,15 +191,16 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/hashrate/{time_period}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.hashrate(Some(path.time_period))).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.hashrate(Some(path.time_period))).await
},
|op| {
op.id("get_hashrate_by_period")
.mining_tag()
.summary("Network hashrate")
.description("Get network hashrate and difficulty data for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)*")
.description("Get network hashrate and difficulty data for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)*")
.json_response::<HashrateSummary>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -205,7 +209,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/difficulty-adjustments",
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.difficulty_adjustments(None)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, |q| q.difficulty_adjustments(None)).await
},
|op| {
op.id("get_difficulty_adjustments")
@@ -222,15 +226,16 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/difficulty-adjustments/{time_period}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.difficulty_adjustments(Some(path.time_period))).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.difficulty_adjustments(Some(path.time_period))).await
},
|op| {
op.id("get_difficulty_adjustments_by_period")
.mining_tag()
.summary("Difficulty adjustments")
.description("Get historical difficulty adjustments for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)*")
.description("Get historical difficulty adjustments for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)*")
.json_response::<Vec<DifficultyAdjustmentEntry>>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -239,7 +244,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/reward-stats/{block_count}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<BlockCountParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.reward_stats(path.block_count)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.reward_stats(path.block_count)).await
},
|op| {
op.id("get_reward_stats")
@@ -248,6 +253,7 @@ impl MiningRoutes for ApiRouter<AppState> {
.description("Get mining reward statistics for the last N blocks including total rewards, fees, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-reward-stats)*")
.json_response::<RewardStats>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -256,15 +262,16 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/blocks/fees/{time_period}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_fees(path.time_period)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_fees(path.time_period)).await
},
|op| {
op.id("get_block_fees")
.mining_tag()
.summary("Block fees")
.description("Get average block fees for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)*")
.description("Get average total fees per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)*")
.json_response::<Vec<BlockFeesEntry>>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -273,15 +280,16 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/blocks/rewards/{time_period}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_rewards(path.time_period)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_rewards(path.time_period)).await
},
|op| {
op.id("get_block_rewards")
.mining_tag()
.summary("Block rewards")
.description("Get average block rewards (coinbase = subsidy + fees) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)*")
.description("Get average coinbase reward (subsidy + fees) per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)*")
.json_response::<Vec<BlockRewardsEntry>>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -289,14 +297,17 @@ impl MiningRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/mining/blocks/fee-rates/{time_period}",
get_with(
async |Path(_path): Path<TimePeriodParam>| -> Response {
Error::not_implemented("Fee rate percentiles are not yet available").into_response()
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_fee_rates(path.time_period)).await
},
|op| {
op.id("get_block_fee_rates")
.mining_tag()
.summary("Block fee rates (WIP)")
.description("**Work in progress.** Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*")
.summary("Block fee rates")
.description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*")
.json_response::<Vec<BlockFeeRatesEntry>>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -305,15 +316,16 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/blocks/sizes-weights/{time_period}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_sizes_weights(path.time_period)).await
state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_sizes_weights(path.time_period)).await
},
|op| {
op.id("get_block_sizes_weights")
.mining_tag()
.summary("Block sizes and weights")
.description("Get average block sizes and weights for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)*")
.description("Get average block sizes and weights for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)*")
.json_response::<BlockSizesWeights>()
.not_modified()
.bad_request()
.server_error()
},
),
@@ -0,0 +1,35 @@
use aide::axum::ApiRouter;
use crate::AppState;
mod addrs;
mod blocks;
mod fees;
mod general;
mod mempool;
mod mining;
mod transactions;
use addrs::AddrRoutes;
use blocks::BlockRoutes;
use fees::FeesRoutes;
use general::GeneralRoutes;
use mempool::MempoolRoutes;
use mining::MiningRoutes;
use transactions::TxRoutes;
pub trait MempoolSpaceRoutes {
fn add_mempool_space_routes(self) -> Self;
}
impl MempoolSpaceRoutes for ApiRouter<AppState> {
fn add_mempool_space_routes(self) -> Self {
self.add_general_routes()
.add_addr_routes()
.add_block_routes()
.add_mining_routes()
.add_fees_routes()
.add_mempool_routes()
.add_tx_routes()
}
}
@@ -6,15 +6,15 @@ use axum::{
extract::{Path, State},
http::{HeaderMap, Uri},
};
use brk_types::{
CpfpInfo, MerkleProof, Transaction, TxOutspend, TxStatus, Txid, TxidParam, TxidVout,
TxidsParam,
use brk_types::{CpfpInfo, MerkleProof, Transaction, TxOutspend, TxStatus, Txid, Version};
use crate::{
AppState, CacheStrategy,
cache::CacheParams,
extended::{ResponseExtended, TransformResponseExtended},
params::{TxidParam, TxidVout, TxidsParam},
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use super::AppState;
pub trait TxRoutes {
fn add_tx_routes(self) -> Self;
}
@@ -25,15 +25,17 @@ impl TxRoutes for ApiRouter<AppState> {
.api_route(
"/api/v1/cpfp/{txid}",
get_with(
async |uri: Uri, headers: HeaderMap, Path(txid): Path<TxidParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::MempoolHash(0), &uri, move |q| q.cpfp(txid)).await
async |uri: Uri, headers: HeaderMap, Path(param): Path<TxidParam>, State(state): State<AppState>| {
state.cached_json(&headers, state.tx_cache(Version::ONE, &param.txid), &uri, move |q| q.cpfp(&param.txid)).await
},
|op| op
.id("get_cpfp")
.transactions_tag()
.summary("CPFP info")
.description("Returns ancestors and descendants for a CPFP transaction.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)*")
.description("Returns ancestors and descendants for a CPFP (Child Pays For Parent) transaction, including the effective fee rate of the package.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)*")
.json_response::<CpfpInfo>()
.not_modified()
.bad_request()
.not_found()
.server_error(),
),
@@ -44,10 +46,10 @@ impl TxRoutes for ApiRouter<AppState> {
async |
uri: Uri,
headers: HeaderMap,
Path(txid): Path<TxidParam>,
Path(param): Path<TxidParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.transaction(txid)).await
state.cached_json(&headers, state.tx_cache(Version::ONE, &param.txid), &uri, move |q| q.transaction(&param.txid)).await
},
|op| op
.id("get_tx")
@@ -69,10 +71,10 @@ impl TxRoutes for ApiRouter<AppState> {
async |
uri: Uri,
headers: HeaderMap,
Path(txid): Path<TxidParam>,
Path(param): Path<TxidParam>,
State(state): State<AppState>
| {
state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_hex(txid)).await
state.cached_text(&headers, state.tx_cache(Version::ONE, &param.txid), &uri, move |q| q.transaction_hex(&param.txid)).await
},
|op| op
.id("get_tx_hex")
@@ -91,8 +93,8 @@ impl TxRoutes for ApiRouter<AppState> {
.api_route(
"/api/tx/{txid}/merkleblock-proof",
get_with(
async |uri: Uri, headers: HeaderMap, Path(txid): Path<TxidParam>, State(state): State<AppState>| {
state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.merkleblock_proof(txid)).await
async |uri: Uri, headers: HeaderMap, Path(param): Path<TxidParam>, State(state): State<AppState>| {
state.cached_text(&headers, state.tx_cache(Version::ONE, &param.txid), &uri, move |q| q.merkleblock_proof(&param.txid)).await
},
|op| op
.id("get_tx_merkleblock_proof")
@@ -109,8 +111,8 @@ impl TxRoutes for ApiRouter<AppState> {
.api_route(
"/api/tx/{txid}/merkle-proof",
get_with(
async |uri: Uri, headers: HeaderMap, Path(txid): Path<TxidParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.merkle_proof(txid)).await
async |uri: Uri, headers: HeaderMap, Path(param): Path<TxidParam>, State(state): State<AppState>| {
state.cached_json(&headers, state.tx_cache(Version::ONE, &param.txid), &uri, move |q| q.merkle_proof(&param.txid)).await
},
|op| op
.id("get_tx_merkle_proof")
@@ -133,8 +135,16 @@ impl TxRoutes for ApiRouter<AppState> {
Path(path): Path<TxidVout>,
State(state): State<AppState>
| {
let txid = TxidParam { txid: path.txid };
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.outspend(txid, path.vout)).await
let v = Version::ONE;
let immutable = CacheParams::immutable(v);
if immutable.matches_etag(&headers) {
return ResponseExtended::new_not_modified_with(&immutable);
}
let outspend = state.run(move |q| q.outspend(&path.txid, path.vout)).await;
let height = state.sync(|q| q.height());
let is_deep = outspend.as_ref().is_ok_and(|o| o.is_deeply_spent(height));
let strategy = if is_deep { CacheStrategy::Immutable(v) } else { CacheStrategy::Tip };
state.cached_json(&headers, strategy, &uri, move |_| outspend).await
},
|op| op
.id("get_tx_outspend")
@@ -156,10 +166,19 @@ impl TxRoutes for ApiRouter<AppState> {
async |
uri: Uri,
headers: HeaderMap,
Path(txid): Path<TxidParam>,
Path(param): Path<TxidParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.outspends(txid)).await
let v = Version::ONE;
let immutable = CacheParams::immutable(v);
if immutable.matches_etag(&headers) {
return ResponseExtended::new_not_modified_with(&immutable);
}
let outspends = state.run(move |q| q.outspends(&param.txid)).await;
let height = state.sync(|q| q.height());
let all_deep = outspends.as_ref().is_ok_and(|os| os.iter().all(|o| o.is_deeply_spent(height)));
let strategy = if all_deep { CacheStrategy::Immutable(v) } else { CacheStrategy::Tip };
state.cached_json(&headers, strategy, &uri, move |_| outspends).await
},
|op| op
.id("get_tx_outspends")
@@ -178,15 +197,15 @@ impl TxRoutes for ApiRouter<AppState> {
.api_route(
"/api/tx/{txid}/raw",
get_with(
async |uri: Uri, headers: HeaderMap, Path(txid): Path<TxidParam>, State(state): State<AppState>| {
state.cached_bytes(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_raw(txid)).await
async |uri: Uri, headers: HeaderMap, Path(param): Path<TxidParam>, State(state): State<AppState>| {
state.cached_bytes(&headers, state.tx_cache(Version::ONE, &param.txid), &uri, move |q| q.transaction_raw(&param.txid)).await
},
|op| op
.id("get_tx_raw")
.transactions_tag()
.summary("Transaction raw")
.description("Returns a transaction as binary data.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-raw)*")
.json_response::<Vec<u8>>()
.binary_response()
.not_modified()
.bad_request()
.not_found()
@@ -199,10 +218,10 @@ impl TxRoutes for ApiRouter<AppState> {
async |
uri: Uri,
headers: HeaderMap,
Path(txid): Path<TxidParam>,
Path(param): Path<TxidParam>,
State(state): State<AppState>
| {
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_status(txid)).await
state.cached_json(&headers, state.tx_cache(Version::ONE, &param.txid), &uri, move |q| q.transaction_status(&param.txid)).await
},
|op| op
.id("get_tx_status")
@@ -223,7 +242,7 @@ impl TxRoutes for ApiRouter<AppState> {
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
let params = TxidsParam::from_query(uri.query().unwrap_or(""));
state.cached_json(&headers, CacheStrategy::MempoolHash(0), &uri, move |q| q.transaction_times(&params.txids)).await
state.cached_json(&headers, state.mempool_cache(), &uri, move |q| q.transaction_times(&params.txids)).await
},
|op| op
.id("get_transaction_times")
@@ -231,6 +250,7 @@ impl TxRoutes for ApiRouter<AppState> {
.summary("Transaction first-seen times")
.description("Returns timestamps when transactions were first seen in the mempool. Returns 0 for mined or unknown transactions.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-times)*")
.json_response::<Vec<u64>>()
.not_modified()
.server_error(),
),
)
@@ -239,7 +259,8 @@ impl TxRoutes for ApiRouter<AppState> {
post_with(
async |State(state): State<AppState>, body: String| {
let hex = body.trim().to_string();
state.sync(|q| q.broadcast_transaction(&hex))
state.run(move |q| q.broadcast_transaction(&hex))
.await
.map(|txid| txid.to_string())
.map_err(crate::Error::from)
},
@@ -9,10 +9,12 @@ use axum::{
};
use brk_traversable::TreeNode;
use brk_types::{
CostBasisCohortParam, CostBasisFormatted, CostBasisParams, CostBasisQuery, DataRangeFormat,
Date, DetailedSeriesCount, Index, IndexInfo, PaginatedSeries, Pagination, SearchQuery,
SeriesData, SeriesInfo, SeriesList, SeriesName, SeriesSelection, SeriesSelectionLegacy,
CostBasisFormatted, DataRangeFormat, Date, DetailedSeriesCount, Index, IndexInfo,
PaginatedSeries, Pagination, SearchQuery, SeriesData, SeriesInfo, SeriesList, SeriesName,
SeriesSelection, SeriesSelectionLegacy,
};
use crate::params::{CostBasisCohortParam, CostBasisParams, CostBasisQuery};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -275,7 +277,7 @@ impl ApiMetricsLegacyRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Path(path): Path<LegacySeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
q.latest(&path.metric, path.index)
})
.await
@@ -301,7 +303,7 @@ impl ApiMetricsLegacyRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Path(path): Path<LegacySeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
q.len(&path.metric, path.index)
})
.await
@@ -327,7 +329,7 @@ impl ApiMetricsLegacyRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Path(path): Path<LegacySeriesWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
q.version(&path.metric, path.index)
})
.await
@@ -376,7 +378,7 @@ impl ApiMetricsLegacyRoutes for ApiRouter<AppState> {
Path(params): Path<CostBasisCohortParam>,
State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
q.cost_basis_dates(&params.cohort)
})
.await
+11 -25
View File
@@ -6,7 +6,7 @@ use aide::{
};
use axum::{
Extension,
http::{HeaderMap, header},
http::HeaderMap,
response::{Html, Redirect, Response},
routing::get,
};
@@ -14,26 +14,19 @@ use axum::{
use crate::{
Error,
api::{
addrs::AddrRoutes, blocks::BlockRoutes, fees::FeesRoutes, general::GeneralRoutes,
mempool::MempoolRoutes, metrics_legacy::ApiMetricsLegacyRoutes, mining::MiningRoutes,
series::ApiSeriesRoutes, server::ServerRoutes, transactions::TxRoutes,
mempool_space::MempoolSpaceRoutes, metrics::ApiMetricsLegacyRoutes,
series::ApiSeriesRoutes, server::ServerRoutes,
},
extended::{ResponseExtended, TransformResponseExtended},
};
use super::AppState;
mod addrs;
mod blocks;
mod fees;
mod general;
mod mempool;
mod metrics_legacy;
mod mining;
mod mempool_space;
mod metrics;
mod openapi;
mod series;
mod server;
mod transactions;
pub use openapi::*;
@@ -46,13 +39,7 @@ impl ApiRoutes for ApiRouter<AppState> {
self.add_server_routes()
.add_series_routes()
.add_metrics_legacy_routes()
.add_general_routes()
.add_addr_routes()
.add_block_routes()
.add_mining_routes()
.add_fees_routes()
.add_mempool_routes()
.add_tx_routes()
.add_mempool_space_routes()
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
.api_route(
"/openapi.json",
@@ -91,13 +78,12 @@ impl ApiRoutes for ApiRouter<AppState> {
)
.route("/api", get(Html::from(include_str!("./scalar.html"))))
// Pre-compressed with: brotli -c -q 11 scalar.js > scalar.js.br
.route("/scalar.js", get(|| async {
(
[
(header::CONTENT_TYPE, "application/javascript"),
(header::CONTENT_ENCODING, "br"),
],
.route("/scalar.js", get(|headers: HeaderMap| async move {
Response::static_bytes(
&headers,
include_bytes!("./scalar.js.br").as_slice(),
"application/javascript",
"br",
)
}))
.route(
+4 -4
View File
@@ -4,15 +4,15 @@ use axum::{
Extension,
body::{Body, Bytes},
extract::{Query, State},
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
http::{HeaderMap, Uri},
response::Response,
};
use brk_types::{Format, Output, SeriesSelection};
use crate::{
Result,
api::series::{CACHE_CONTROL, max_weight},
extended::{ContentEncoding, HeaderMapExtended},
extended::{ContentEncoding, HeaderMapExtended, ResponseExtended},
};
use super::AppState;
@@ -34,7 +34,7 @@ pub async fn handler(
let csv_filename = resolved.csv_filename();
if headers.has_etag(etag.as_str()) {
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
return Ok(Response::new_not_modified(&etag, CACHE_CONTROL));
}
// Phase 2: Format (expensive, server-side cached)
+4 -4
View File
@@ -4,8 +4,8 @@ use axum::{
Extension,
body::{Body, Bytes},
extract::{Query, State},
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
http::{HeaderMap, Uri},
response::Response,
};
use brk_error::Result as BrkResult;
use brk_query::{Query as BrkQuery, ResolvedQuery};
@@ -14,7 +14,7 @@ use brk_types::{Format, Output, SeriesOutput, SeriesSelection};
use crate::{
Result,
api::series::{CACHE_CONTROL, max_weight},
extended::{ContentEncoding, HeaderMapExtended},
extended::{ContentEncoding, HeaderMapExtended, ResponseExtended},
};
use super::AppState;
@@ -57,7 +57,7 @@ async fn format_and_respond(
let csv_filename = resolved.csv_filename();
if headers.has_etag(etag.as_str()) {
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
return Ok(Response::new_not_modified(&etag, CACHE_CONTROL));
}
// Phase 2: Format (expensive, server-side cached)
+4 -4
View File
@@ -4,15 +4,15 @@ use axum::{
Extension,
body::{Body, Bytes},
extract::{Query, State},
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
http::{HeaderMap, Uri},
response::Response,
};
use brk_types::{Format, OutputLegacy, SeriesSelection};
use crate::{
Result,
api::series::{CACHE_CONTROL, max_weight},
extended::{ContentEncoding, HeaderMapExtended},
extended::{ContentEncoding, HeaderMapExtended, ResponseExtended},
};
pub const SUNSET: &str = "2027-01-01T00:00:00Z";
@@ -36,7 +36,7 @@ pub async fn handler(
let csv_filename = resolved.csv_filename();
if headers.has_etag(etag.as_str()) {
return Ok((StatusCode::NOT_MODIFIED, "").into_response());
return Ok(Response::new_not_modified(&etag, CACHE_CONTROL));
}
// Phase 2: Format (expensive, server-side cached)
+18 -10
View File
@@ -9,12 +9,16 @@ use axum::{
};
use brk_traversable::TreeNode;
use brk_types::{
CostBasisCohortParam, CostBasisFormatted, CostBasisParams, CostBasisQuery, DataRangeFormat,
Date, IndexInfo, PaginatedSeries, Pagination, SearchQuery, SeriesCount, SeriesData, SeriesInfo,
SeriesNameWithIndex, SeriesParam, SeriesSelection,
CostBasisFormatted, DataRangeFormat, Date, IndexInfo, PaginatedSeries, Pagination, SearchQuery,
SeriesCount, SeriesData, SeriesInfo, SeriesNameWithIndex, SeriesSelection,
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
use crate::{
CacheStrategy,
cache::CACHE_CONTROL,
extended::TransformResponseExtended,
params::{CostBasisCohortParam, CostBasisParams, CostBasisQuery, SeriesParam},
};
use super::AppState;
@@ -26,8 +30,6 @@ pub mod legacy;
const MAX_WEIGHT: usize = 4 * 8 * 10_000;
/// Maximum allowed request weight for localhost (50MB)
const MAX_WEIGHT_LOCALHOST: usize = 50 * 1_000_000;
/// Cache control header for series data responses
const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate";
/// Returns the max weight for a request based on the client address.
/// Localhost requests get a generous limit, external requests get a stricter one.
@@ -246,7 +248,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Path(path): Path<SeriesNameWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
q.latest(&path.series, path.index)
})
.await
@@ -259,6 +261,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
"Returns the single most recent value for a series, unwrapped (not inside a SeriesData object)."
)
.json_response::<serde_json::Value>()
.not_modified()
.not_found(),
),
)
@@ -270,7 +273,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Path(path): Path<SeriesNameWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
q.len(&path.series, path.index)
})
.await
@@ -281,6 +284,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
.summary("Get series data length")
.description("Returns the total number of data points for a series at the given index.")
.json_response::<usize>()
.not_modified()
.not_found(),
),
)
@@ -292,7 +296,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
State(state): State<AppState>,
Path(path): Path<SeriesNameWithIndex>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
q.version(&path.series, path.index)
})
.await
@@ -303,6 +307,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
.summary("Get series version")
.description("Returns the current version of a series. Changes when the series data is updated.")
.json_response::<brk_types::Version>()
.not_modified()
.not_found(),
),
)
@@ -340,6 +345,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
.summary("Available cost basis cohorts")
.description("List available cohorts for cost basis distribution.")
.json_response::<Vec<String>>()
.not_modified()
.server_error()
},
),
@@ -352,7 +358,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
Path(params): Path<CostBasisCohortParam>,
State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
q.cost_basis_dates(&params.cohort)
})
.await
@@ -363,6 +369,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
.summary("Available cost basis dates")
.description("List available dates for a cohort's cost basis distribution.")
.json_response::<Vec<Date>>()
.not_modified()
.not_found()
.server_error()
},
@@ -398,6 +405,7 @@ impl ApiSeriesRoutes for ApiRouter<AppState> {
- `value`: supply (default, in BTC), realized (USD), unrealized (USD)",
)
.json_response::<CostBasisFormatted>()
.not_modified()
.not_found()
.server_error()
},
+2 -2
View File
@@ -77,7 +77,7 @@ impl ServerRoutes for ApiRouter<AppState> {
get_with(
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
let tip_height = q.client().get_last_height()?;
Ok(q.sync_status(tip_height))
})
@@ -102,7 +102,7 @@ impl ServerRoutes for ApiRouter<AppState> {
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
let brk_path = state.data_path.clone();
state
.cached_json(&headers, CacheStrategy::Height, &uri, move |q| {
.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| {
let brk_bytes = dir_size(&brk_path)?;
let bitcoin_bytes = dir_size(q.blocks_dir())?;
Ok(DiskUsage::new(brk_bytes, bitcoin_bytes))
+58 -25
View File
@@ -1,32 +1,73 @@
use axum::http::HeaderMap;
use brk_types::{BlockHashPrefix, Version};
use crate::{VERSION, extended::HeaderMapExtended};
/// Cache strategy for HTTP responses.
pub enum CacheStrategy {
/// Data that changes with each new block (addresses, mining stats, txs, outspends)
/// Etag = VERSION-{height}, Cache-Control: must-revalidate
Height,
/// Chain-dependent data (addresses, mining stats, txs, outspends).
/// Etag = {tip_hash_prefix:x}. Invalidates on any tip change including reorgs.
Tip,
/// Static/immutable data (blocks by hash, validate-address, series catalog)
/// Etag = VERSION only, Cache-Control: must-revalidate
/// Immutable data identified by hash in the URL (blocks by hash, confirmed tx data).
/// Etag = {version}. Permanent; only bumped when response format changes.
Immutable(Version),
/// Static non-chain data (validate-address, series catalog, pool list).
/// Etag = CARGO_PKG_VERSION. Invalidates on deploy.
Static,
/// Mempool data - etag from next projected block hash + short max-age
/// Etag = VERSION-m{hash:x}, Cache-Control: max-age=1, must-revalidate
/// Immutable data bound to a specific block (confirmed tx data, block status).
/// Etag = {version}-{block_hash_prefix:x}. Invalidates naturally on reorg.
BlockBound(Version, BlockHashPrefix),
/// Mempool data — etag from next projected block hash.
/// Etag = m{hash:x}. Invalidates on mempool change.
MempoolHash(u64),
}
pub(crate) const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate";
/// Resolved cache parameters
pub struct CacheParams {
pub etag: Option<String>,
pub cache_control: String,
pub cache_control: &'static str,
}
impl CacheParams {
/// Cache params using VERSION as etag
pub fn version() -> Self {
Self::resolve(&CacheStrategy::Static, || unreachable!())
pub fn tip(tip: BlockHashPrefix) -> Self {
Self {
etag: Some(format!("t{:x}", *tip)),
cache_control: CACHE_CONTROL,
}
}
pub fn immutable(version: Version) -> Self {
Self {
etag: Some(format!("i{version}")),
cache_control: CACHE_CONTROL,
}
}
pub fn block_bound(version: Version, prefix: BlockHashPrefix) -> Self {
Self {
etag: Some(format!("b{version}-{:x}", *prefix)),
cache_control: CACHE_CONTROL,
}
}
pub fn static_version() -> Self {
Self {
etag: Some(format!("s{VERSION}")),
cache_control: CACHE_CONTROL,
}
}
pub fn mempool_hash(hash: u64) -> Self {
Self {
etag: Some(format!("m{hash:x}")),
cache_control: CACHE_CONTROL,
}
}
pub fn etag_str(&self) -> &str {
@@ -39,21 +80,13 @@ impl CacheParams {
.is_some_and(|etag| headers.has_etag(etag))
}
pub fn resolve(strategy: &CacheStrategy, height: impl FnOnce() -> u32) -> Self {
use CacheStrategy::*;
pub fn resolve(strategy: &CacheStrategy, tip: impl FnOnce() -> BlockHashPrefix) -> Self {
match strategy {
Height => Self {
etag: Some(format!("{VERSION}-{}", height())),
cache_control: "public, max-age=1, must-revalidate".into(),
},
Static => Self {
etag: Some(VERSION.to_string()),
cache_control: "public, max-age=1, must-revalidate".into(),
},
MempoolHash(hash) => Self {
etag: Some(format!("{VERSION}-m{hash:x}")),
cache_control: "public, max-age=1, must-revalidate".into(),
},
CacheStrategy::Tip => Self::tip(tip()),
CacheStrategy::Immutable(v) => Self::immutable(*v),
CacheStrategy::BlockBound(v, prefix) => Self::block_bound(*v, *prefix),
CacheStrategy::Static => Self::static_version(),
CacheStrategy::MempoolHash(hash) => Self::mempool_hash(*hash),
}
}
}
@@ -19,6 +19,8 @@ pub trait HeaderMapExtended {
fn insert_content_type_application_json(&mut self);
fn insert_content_type_text_csv(&mut self);
fn insert_vary_accept_encoding(&mut self);
fn insert_deprecation(&mut self, sunset: &'static str);
}
@@ -59,6 +61,7 @@ impl HeaderMapExtended for HeaderMap {
fn insert_content_encoding(&mut self, encoding: ContentEncoding) {
if let Some(value) = encoding.header_value() {
self.insert(header::CONTENT_ENCODING, value);
self.insert_vary_accept_encoding();
}
}
@@ -70,6 +73,10 @@ impl HeaderMapExtended for HeaderMap {
self.insert(header::CONTENT_TYPE, "text/csv".parse().unwrap());
}
fn insert_vary_accept_encoding(&mut self) {
self.insert(header::VARY, "Accept-Encoding".parse().unwrap());
}
fn insert_deprecation(&mut self, sunset: &'static str) {
self.insert("Deprecation", "true".parse().unwrap());
self.insert("Sunset", sunset.parse().unwrap());
+43 -7
View File
@@ -1,8 +1,9 @@
use axum::{
body::Body,
http::{HeaderMap, Response, StatusCode},
http::{HeaderMap, Response, StatusCode, header},
response::IntoResponse,
};
use brk_types::Etag;
use serde::Serialize;
use super::header_map::HeaderMapExtended;
@@ -12,22 +13,36 @@ pub trait ResponseExtended
where
Self: Sized,
{
fn new_not_modified() -> Self;
fn new_not_modified(etag: &Etag, cache_control: &str) -> Self;
fn new_not_modified_with(params: &CacheParams) -> Self;
fn new_json_cached<T>(value: T, params: &CacheParams) -> Self
where
T: Serialize;
fn static_json<T>(headers: &HeaderMap, value: T) -> Self
where
T: Serialize;
fn static_bytes(
headers: &HeaderMap,
bytes: &'static [u8],
content_type: &'static str,
content_encoding: &'static str,
) -> Self;
}
impl ResponseExtended for Response<Body> {
fn new_not_modified() -> Response<Body> {
fn new_not_modified(etag: &Etag, cache_control: &str) -> Response<Body> {
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
let _headers = response.headers_mut();
let headers = response.headers_mut();
headers.insert_etag(etag.as_str());
headers.insert_cache_control(cache_control);
response
}
fn new_not_modified_with(params: &CacheParams) -> Response<Body> {
let etag = Etag::from(params.etag_str());
Self::new_not_modified(&etag, params.cache_control)
}
fn new_json_cached<T>(value: T, params: &CacheParams) -> Self
where
T: Serialize,
@@ -36,7 +51,7 @@ impl ResponseExtended for Response<Body> {
let mut response = Response::builder().body(bytes.into()).unwrap();
let headers = response.headers_mut();
headers.insert_content_type_application_json();
headers.insert_cache_control(&params.cache_control);
headers.insert_cache_control(params.cache_control);
if let Some(etag) = &params.etag {
headers.insert_etag(etag);
}
@@ -47,10 +62,31 @@ impl ResponseExtended for Response<Body> {
where
T: Serialize,
{
let params = CacheParams::version();
let params = CacheParams::static_version();
if params.matches_etag(headers) {
return Self::new_not_modified();
return Self::new_not_modified_with(&params);
}
Self::new_json_cached(value, &params)
}
fn static_bytes(
headers: &HeaderMap,
bytes: &'static [u8],
content_type: &'static str,
content_encoding: &'static str,
) -> Self {
let params = CacheParams::static_version();
if params.matches_etag(headers) {
return Self::new_not_modified_with(&params);
}
let mut response = Response::new(Body::from(bytes));
let h = response.headers_mut();
h.insert(header::CONTENT_TYPE, content_type.parse().unwrap());
h.insert(header::CONTENT_ENCODING, content_encoding.parse().unwrap());
h.insert_cache_control(params.cache_control);
if let Some(etag) = &params.etag {
h.insert_etag(etag);
}
response
}
}
@@ -31,6 +31,8 @@ pub trait TransformResponseExtended<'t> {
F: FnOnce(TransformResponse<'_, R>) -> TransformResponse<'_, R>;
/// 200 with text/plain content type
fn text_response(self) -> Self;
/// 200 with application/octet-stream content type
fn binary_response(self) -> Self;
/// 200 with text/csv content type (adds CSV as alternative response format)
fn csv_response(self) -> Self;
/// 400
@@ -108,6 +110,10 @@ impl<'t> TransformResponseExtended<'t> for TransformOperation<'t> {
self.response_with::<200, String, _>(|res| res.description("Successful response"))
}
fn binary_response(self) -> Self {
self.response_with::<200, Vec<u8>, _>(|res| res.description("Raw binary data"))
}
fn csv_response(mut self) -> Self {
// Add text/csv content type to existing 200 response
if let Some(responses) = &mut self.inner_mut().responses
+3 -1
View File
@@ -4,7 +4,7 @@ use std::{
any::Any,
net::SocketAddr,
path::PathBuf,
sync::Arc,
sync::{Arc, atomic::AtomicU64},
time::{Duration, Instant},
};
@@ -36,6 +36,7 @@ mod api;
pub mod cache;
mod error;
mod extended;
pub mod params;
mod state;
pub use api::ApiRoutes;
@@ -58,6 +59,7 @@ impl Server {
data_path,
website,
cache: Arc::new(Cache::new(1_000)),
last_tip: Arc::new(AtomicU64::new(0)),
started_at: jiff::Timestamp::now(),
started_instant: Instant::now(),
})
@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Addr;
use brk_types::Addr;
/// Bitcoin address path parameter
#[derive(Deserialize, JsonSchema)]
pub struct AddrParam {
#[serde(rename = "address")]
@@ -1,7 +1,7 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Txid;
use brk_types::Txid;
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct AddrTxidsParam {
@@ -1,6 +1,7 @@
use schemars::JsonSchema;
use serde::Deserialize;
/// Block count path parameter
#[derive(Deserialize, JsonSchema)]
pub struct BlockCountParam {
/// Number of recent blocks to include
@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::BlockHash;
use brk_types::BlockHash;
/// Block hash path parameter
#[derive(Deserialize, JsonSchema)]
pub struct BlockHashParam {
pub hash: BlockHash,
@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{BlockHash, TxIndex};
use brk_types::{BlockHash, TxIndex};
/// Block hash + starting transaction index path parameters
#[derive(Deserialize, JsonSchema)]
pub struct BlockHashStartIndex {
/// Bitcoin block hash
@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{BlockHash, TxIndex};
use brk_types::{BlockHash, TxIndex};
/// Block hash + transaction index path parameters
#[derive(Deserialize, JsonSchema)]
pub struct BlockHashTxIndex {
/// Bitcoin block hash
@@ -1,33 +1,7 @@
use std::{fmt, ops::Deref};
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{CostBasisBucket, CostBasisValue, Date};
/// Cohort identifier for cost basis distribution.
#[derive(Deserialize, JsonSchema)]
#[schemars(example = &"all", example = &"sth", example = &"lth")]
pub struct Cohort(String);
impl fmt::Display for Cohort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl<T: Into<String>> From<T> for Cohort {
fn from(s: T) -> Self {
Self(s.into())
}
}
impl Deref for Cohort {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
use brk_types::{Cohort, CostBasisBucket, CostBasisValue, Date};
/// Path parameters for cost basis distribution endpoint.
#[derive(Deserialize, JsonSchema)]
@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Height;
use brk_types::Height;
/// Block height path parameter
#[derive(Deserialize, JsonSchema)]
pub struct HeightParam {
pub height: Height,
@@ -1,7 +1,7 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Limit;
use brk_types::Limit;
#[derive(Deserialize, JsonSchema)]
pub struct LimitParam {
+35
View File
@@ -0,0 +1,35 @@
mod addr_param;
mod addr_txids_param;
mod block_count_param;
mod blockhash_param;
mod blockhash_start_index;
mod blockhash_tx_index;
mod cost_basis_params;
mod height_param;
mod limit_param;
mod pool_slug_param;
mod series_param;
mod time_period_param;
mod timestamp_param;
mod txid_param;
mod txid_vout;
mod txids_param;
mod validate_addr_param;
pub use addr_param::*;
pub use addr_txids_param::*;
pub use block_count_param::*;
pub use blockhash_param::*;
pub use blockhash_start_index::*;
pub use blockhash_tx_index::*;
pub use cost_basis_params::*;
pub use height_param::*;
pub use limit_param::*;
pub use pool_slug_param::*;
pub use series_param::*;
pub use time_period_param::*;
pub use timestamp_param::*;
pub use txid_param::*;
pub use txid_vout::*;
pub use txids_param::*;
pub use validate_addr_param::*;
@@ -1,13 +1,15 @@
use schemars::JsonSchema;
use serde::Deserialize;
use super::{Height, PoolSlug};
use brk_types::{Height, PoolSlug};
/// Mining pool slug path parameter
#[derive(Deserialize, JsonSchema)]
pub struct PoolSlugParam {
pub slug: PoolSlug,
}
/// Mining pool slug + block height path parameters
#[derive(Deserialize, JsonSchema)]
pub struct PoolSlugAndHeightParam {
pub slug: PoolSlug,
@@ -1,7 +1,7 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::SeriesName;
use brk_types::SeriesName;
#[derive(Deserialize, JsonSchema)]
pub struct SeriesParam {
@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use super::TimePeriod;
use brk_types::TimePeriod;
/// Time period path parameter (24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y)
#[derive(Deserialize, JsonSchema)]
pub struct TimePeriodParam {
#[schemars(example = &"24h")]
@@ -1,13 +1,15 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Timestamp;
use brk_types::Timestamp;
/// UNIX timestamp path parameter
#[derive(Deserialize, JsonSchema)]
pub struct TimestampParam {
pub timestamp: Timestamp,
}
/// Optional UNIX timestamp query parameter
#[derive(Deserialize, JsonSchema)]
pub struct OptionalTimestampParam {
pub timestamp: Option<Timestamp>,
@@ -1,8 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::Txid;
use brk_types::Txid;
/// Transaction ID path parameter
#[derive(Deserialize, JsonSchema)]
pub struct TxidParam {
pub txid: Txid,
@@ -1,7 +1,7 @@
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{Txid, Vout};
use brk_types::{Txid, Vout};
/// Transaction output reference (txid + output index)
#[derive(Deserialize, JsonSchema)]
@@ -2,7 +2,7 @@ use std::str::FromStr;
use schemars::JsonSchema;
use crate::Txid;
use brk_types::Txid;
/// Query parameter for transaction-times endpoint.
#[derive(JsonSchema)]

Some files were not shown because too many files have changed in this diff Show More