global: snapshot

This commit is contained in:
nym21
2026-01-15 23:34:43 +01:00
parent b0d933a7ab
commit 967d2c7f35
67 changed files with 6854 additions and 5210 deletions
Generated
+7 -190
View File
@@ -247,12 +247,6 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bech32"
version = "0.11.1"
@@ -396,7 +390,6 @@ dependencies = [
"brk_indexer",
"brk_iterator",
"brk_logger",
"brk_mcp",
"brk_mempool",
"brk_query",
"brk_reader",
@@ -442,6 +435,7 @@ dependencies = [
"oas3",
"serde",
"serde_json",
"tracing",
]
[[package]]
@@ -597,19 +591,6 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "brk_mcp"
version = "0.1.0-alpha.3"
dependencies = [
"axum",
"brk_rmcp",
"minreq",
"schemars",
"serde",
"serde_json",
"tracing",
]
[[package]]
name = "brk_mempool"
version = "0.1.0-alpha.3"
@@ -660,49 +641,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "brk_rmcp"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0eee22d5688b9b4bbd0071a8d8b540243a8df014eeff4bfe6f90e774e83755"
dependencies = [
"base64 0.22.1",
"brk_rmcp-macros",
"bytes",
"chrono",
"futures",
"http",
"http-body",
"http-body-util",
"paste",
"pin-project-lite",
"rand 0.9.2",
"schemars",
"serde",
"serde_json",
"sse-stream",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tokio-util",
"tower-service",
"tracing",
"uuid",
]
[[package]]
name = "brk_rmcp-macros"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac2141d6358651fcdcabd43fa4dae4525cbba376bc9303a821cb170c2909f4b"
dependencies = [
"darling",
"proc-macro2",
"quote",
"serde_json",
"syn",
]
[[package]]
name = "brk_rpc"
version = "0.1.0-alpha.3"
@@ -728,7 +666,6 @@ dependencies = [
"brk_fetcher",
"brk_indexer",
"brk_logger",
"brk_mcp",
"brk_mempool",
"brk_query",
"brk_reader",
@@ -901,7 +838,6 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
@@ -1181,41 +1117,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
[[package]]
name = "darling"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "dashmap"
version = "6.1.0"
@@ -1446,12 +1347,6 @@ dependencies = [
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "font-kit"
version = "0.14.3"
@@ -1889,12 +1784,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.1.0"
@@ -2082,7 +1971,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf"
dependencies = [
"base64 0.13.1",
"base64",
"minreq",
"serde",
"serde_json",
@@ -2450,12 +2339,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathfinder_geometry"
version = "0.5.1"
@@ -2670,18 +2553,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
"rand_chacha",
"rand_core",
]
[[package]]
@@ -2691,17 +2564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
"rand_core",
]
[[package]]
@@ -2713,22 +2576,13 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_xoshiro"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
dependencies = [
"rand_core 0.6.4",
"rand_core",
]
[[package]]
@@ -2950,7 +2804,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
dependencies = [
"chrono",
"dyn-clone",
"indexmap",
"ref-cast",
@@ -2994,7 +2847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
"bitcoin_hashes",
"rand 0.8.5",
"rand",
"secp256k1-sys",
"serde",
]
@@ -3193,19 +3046,6 @@ dependencies = [
"lock_api",
]
[[package]]
name = "sse-stream"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a"
dependencies = [
"bytes",
"futures-util",
"http-body",
"http-body-util",
"pin-project-lite",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -3376,7 +3216,6 @@ version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio",
"pin-project-lite",
@@ -3396,17 +3235,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-stream"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -3661,17 +3489,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [
"getrandom 0.3.4",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"
-1
View File
@@ -52,7 +52,6 @@ brk_fetcher = { version = "0.1.0-alpha.3", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.1.0-alpha.3", path = "crates/brk_indexer" }
brk_iterator = { version = "0.1.0-alpha.3", path = "crates/brk_iterator" }
brk_logger = { version = "0.1.0-alpha.3", path = "crates/brk_logger" }
brk_mcp = { version = "0.1.0-alpha.3", path = "crates/brk_mcp" }
brk_mempool = { version = "0.1.0-alpha.3", path = "crates/brk_mempool" }
brk_query = { version = "0.1.0-alpha.3", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.1.0-alpha.3", path = "crates/brk_reader" }
-3
View File
@@ -19,7 +19,6 @@ full = [
"indexer",
"iterator",
"logger",
"mcp",
"mempool",
"query",
"reader",
@@ -39,7 +38,6 @@ cohort = ["brk_cohort"]
indexer = ["brk_indexer"]
iterator = ["brk_iterator"]
logger = ["brk_logger"]
mcp = ["brk_mcp"]
mempool = ["brk_mempool"]
query = ["brk_query"]
reader = ["brk_reader"]
@@ -60,7 +58,6 @@ brk_cohort = { workspace = true, optional = true }
brk_indexer = { workspace = true, optional = true }
brk_iterator = { workspace = true, optional = true }
brk_logger = { workspace = true, optional = true }
brk_mcp = { workspace = true, optional = true }
brk_mempool = { workspace = true, optional = true }
brk_query = { workspace = true, optional = true }
brk_reader = { workspace = true, optional = true }
-1
View File
@@ -51,7 +51,6 @@ Feature flags match crate names without the `brk_` prefix. Use `full` to enable
|-------|-------------|
| [brk_client](https://docs.rs/brk_client) | Generated Rust API client |
| [brk_bindgen](https://docs.rs/brk_bindgen) | Generate typed clients (Rust, JavaScript, Python) |
| [brk_mcp](https://docs.rs/brk_mcp) | Model Context Protocol server for LLM integration |
**Internal**
-4
View File
@@ -40,10 +40,6 @@ pub use brk_iterator as iterator;
#[doc(inline)]
pub use brk_logger as logger;
#[cfg(feature = "mcp")]
#[doc(inline)]
pub use brk_mcp as mcp;
#[cfg(feature = "mempool")]
#[doc(inline)]
pub use brk_mempool as mempool;
+1
View File
@@ -14,3 +14,4 @@ brk_types = { workspace = true }
oas3 = "0.20"
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
+1 -1
View File
@@ -4,7 +4,7 @@ Code generation for BRK client libraries.
## What It Enables
Generate typed client libraries for Rust, JavaScript/TypeScript, and Python from the OpenAPI specification. Keeps frontend code in sync with available metrics and API endpoints without manual maintenance.
Generate typed client libraries for Rust, JavaScript, and Python from the OpenAPI specification. Keeps frontend code in sync with available metrics and API endpoints without manual maintenance.
## Key Features
+1 -1
View File
@@ -75,7 +75,7 @@ brk --help
1. **Indexer**: Processes blocks into queryable indexes
2. **Computer**: Derives 1000+ on-chain metrics
3. **Mempool**: Real-time fee estimation
4. **Server**: REST API + MCP endpoint
4. **Server**: REST API with OpenAPI docs
5. **Bundler**: JS bundling for web interface (if enabled)
## Performance
+1 -14
View File
@@ -54,19 +54,6 @@ pub fn run() -> color_eyre::Result<()> {
let mut indexer = Indexer::forced_import(&config.brkdir())?;
#[cfg(not(debug_assertions))]
{
// Pre-run indexer if too far behind, then drop and reimport to reduce memory
let chain_height = client.get_last_height()?;
let indexed_height = indexer.vecs.starting_height();
if chain_height.saturating_sub(*indexed_height) > 1000 {
indexer.index(&blocks, &client, &exit)?;
drop(indexer);
Mimalloc::collect();
indexer = Indexer::forced_import(&config.brkdir())?;
}
}
let mut computer = Computer::forced_import(&config.brkdir(), &indexer, config.fetcher())?;
let mempool = Mempool::new(&client);
@@ -96,7 +83,7 @@ pub fn run() -> color_eyre::Result<()> {
let server = Server::new(&query, data_path, website_source);
tokio::spawn(async move {
server.serve(true).await.unwrap();
server.serve().await.unwrap();
});
Ok(()) as Result<()>
+280 -203
View File
@@ -370,6 +370,7 @@ const _I29: &[Index] = &[Index::WeekIndex];
const _I30: &[Index] = &[Index::YearIndex];
const _I31: &[Index] = &[Index::LoadedAddressIndex];
const _I32: &[Index] = &[Index::EmptyAddressIndex];
const _I33: &[Index] = &[Index::PairOutputIndex];
#[inline]
fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> MetricEndpointBuilder<T> {
@@ -855,6 +856,20 @@ impl<T: DeserializeOwned> MetricPattern32<T> {
impl<T> AnyMetricPattern for MetricPattern32<T> { fn name(&self) -> &str { &self.name } fn indexes(&self) -> &'static [Index] { _I32 } }
impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern32<T> { fn get(&self, index: Index) -> Option<MetricEndpointBuilder<T>> { _I32.contains(&index).then(|| _ep(&self.by.client, &self.by.name, index)) } }
pub struct MetricPattern33By<T> { client: Arc<BrkClientBase>, name: Arc<str>, _marker: std::marker::PhantomData<T> }
impl<T: DeserializeOwned> MetricPattern33By<T> {
pub fn pairoutputindex(&self) -> MetricEndpointBuilder<T> { _ep(&self.client, &self.name, Index::PairOutputIndex) }
}
pub struct MetricPattern33<T> { name: Arc<str>, pub by: MetricPattern33By<T> }
impl<T: DeserializeOwned> MetricPattern33<T> {
pub fn new(client: Arc<BrkClientBase>, name: String) -> Self { let name: Arc<str> = name.into(); Self { name: name.clone(), by: MetricPattern33By { client, name, _marker: std::marker::PhantomData } } }
pub fn name(&self) -> &str { &self.name }
}
impl<T> AnyMetricPattern for MetricPattern33<T> { fn name(&self) -> &str { &self.name } fn indexes(&self) -> &'static [Index] { _I33 } }
impl<T: DeserializeOwned> MetricPattern<T> for MetricPattern33<T> { fn get(&self, index: Index) -> Option<MetricEndpointBuilder<T>> { _I33.contains(&index).then(|| _ep(&self.by.client, &self.by.name, index)) } }
// Reusable pattern structs
/// Pattern struct for repeated tree structure.
@@ -1253,56 +1268,6 @@ impl Price111dSmaPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivePriceRatioPattern {
pub ratio: MetricPattern4<StoredF32>,
pub ratio_1m_sma: MetricPattern4<StoredF32>,
pub ratio_1w_sma: MetricPattern4<StoredF32>,
pub ratio_1y_sd: Ratio1ySdPattern,
pub ratio_2y_sd: Ratio1ySdPattern,
pub ratio_4y_sd: Ratio1ySdPattern,
pub ratio_pct1: MetricPattern4<StoredF32>,
pub ratio_pct1_usd: MetricPattern4<Dollars>,
pub ratio_pct2: MetricPattern4<StoredF32>,
pub ratio_pct2_usd: MetricPattern4<Dollars>,
pub ratio_pct5: MetricPattern4<StoredF32>,
pub ratio_pct5_usd: MetricPattern4<Dollars>,
pub ratio_pct95: MetricPattern4<StoredF32>,
pub ratio_pct95_usd: MetricPattern4<Dollars>,
pub ratio_pct98: MetricPattern4<StoredF32>,
pub ratio_pct98_usd: MetricPattern4<Dollars>,
pub ratio_pct99: MetricPattern4<StoredF32>,
pub ratio_pct99_usd: MetricPattern4<Dollars>,
pub ratio_sd: Ratio1ySdPattern,
}
impl ActivePriceRatioPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ratio: MetricPattern4::new(client.clone(), acc.clone()),
ratio_1m_sma: MetricPattern4::new(client.clone(), _m(&acc, "1m_sma")),
ratio_1w_sma: MetricPattern4::new(client.clone(), _m(&acc, "1w_sma")),
ratio_1y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "1y")),
ratio_2y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "2y")),
ratio_4y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "4y")),
ratio_pct1: MetricPattern4::new(client.clone(), _m(&acc, "pct1")),
ratio_pct1_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct1_usd")),
ratio_pct2: MetricPattern4::new(client.clone(), _m(&acc, "pct2")),
ratio_pct2_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct2_usd")),
ratio_pct5: MetricPattern4::new(client.clone(), _m(&acc, "pct5")),
ratio_pct5_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct5_usd")),
ratio_pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")),
ratio_pct95_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct95_usd")),
ratio_pct98: MetricPattern4::new(client.clone(), _m(&acc, "pct98")),
ratio_pct98_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct98_usd")),
ratio_pct99: MetricPattern4::new(client.clone(), _m(&acc, "pct99")),
ratio_pct99_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct99_usd")),
ratio_sd: Ratio1ySdPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct PercentilesPattern {
pub pct05: MetricPattern4<Dollars>,
@@ -1353,6 +1318,56 @@ impl PercentilesPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct ActivePriceRatioPattern {
pub ratio: MetricPattern4<StoredF32>,
pub ratio_1m_sma: MetricPattern4<StoredF32>,
pub ratio_1w_sma: MetricPattern4<StoredF32>,
pub ratio_1y_sd: Ratio1ySdPattern,
pub ratio_2y_sd: Ratio1ySdPattern,
pub ratio_4y_sd: Ratio1ySdPattern,
pub ratio_pct1: MetricPattern4<StoredF32>,
pub ratio_pct1_usd: MetricPattern4<Dollars>,
pub ratio_pct2: MetricPattern4<StoredF32>,
pub ratio_pct2_usd: MetricPattern4<Dollars>,
pub ratio_pct5: MetricPattern4<StoredF32>,
pub ratio_pct5_usd: MetricPattern4<Dollars>,
pub ratio_pct95: MetricPattern4<StoredF32>,
pub ratio_pct95_usd: MetricPattern4<Dollars>,
pub ratio_pct98: MetricPattern4<StoredF32>,
pub ratio_pct98_usd: MetricPattern4<Dollars>,
pub ratio_pct99: MetricPattern4<StoredF32>,
pub ratio_pct99_usd: MetricPattern4<Dollars>,
pub ratio_sd: Ratio1ySdPattern,
}
impl ActivePriceRatioPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ratio: MetricPattern4::new(client.clone(), acc.clone()),
ratio_1m_sma: MetricPattern4::new(client.clone(), _m(&acc, "1m_sma")),
ratio_1w_sma: MetricPattern4::new(client.clone(), _m(&acc, "1w_sma")),
ratio_1y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "1y")),
ratio_2y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "2y")),
ratio_4y_sd: Ratio1ySdPattern::new(client.clone(), _m(&acc, "4y")),
ratio_pct1: MetricPattern4::new(client.clone(), _m(&acc, "pct1")),
ratio_pct1_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct1_usd")),
ratio_pct2: MetricPattern4::new(client.clone(), _m(&acc, "pct2")),
ratio_pct2_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct2_usd")),
ratio_pct5: MetricPattern4::new(client.clone(), _m(&acc, "pct5")),
ratio_pct5_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct5_usd")),
ratio_pct95: MetricPattern4::new(client.clone(), _m(&acc, "pct95")),
ratio_pct95_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct95_usd")),
ratio_pct98: MetricPattern4::new(client.clone(), _m(&acc, "pct98")),
ratio_pct98_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct98_usd")),
ratio_pct99: MetricPattern4::new(client.clone(), _m(&acc, "pct99")),
ratio_pct99_usd: MetricPattern4::new(client.clone(), _m(&acc, "pct99_usd")),
ratio_sd: Ratio1ySdPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct RelativePattern5 {
pub neg_unrealized_loss_rel_to_market_cap: MetricPattern1<StoredF32>,
@@ -1779,36 +1794,6 @@ impl AddrCountPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct FullnessPattern<T> {
pub average: MetricPattern2<T>,
pub base: MetricPattern11<T>,
pub max: MetricPattern2<T>,
pub median: MetricPattern6<T>,
pub min: MetricPattern2<T>,
pub pct10: MetricPattern6<T>,
pub pct25: MetricPattern6<T>,
pub pct75: MetricPattern6<T>,
pub pct90: MetricPattern6<T>,
}
impl<T: DeserializeOwned> FullnessPattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
average: MetricPattern2::new(client.clone(), _m(&acc, "average")),
base: MetricPattern11::new(client.clone(), acc.clone()),
max: MetricPattern2::new(client.clone(), _m(&acc, "max")),
median: MetricPattern6::new(client.clone(), _m(&acc, "median")),
min: MetricPattern2::new(client.clone(), _m(&acc, "min")),
pct10: MetricPattern6::new(client.clone(), _m(&acc, "pct10")),
pct25: MetricPattern6::new(client.clone(), _m(&acc, "pct25")),
pct75: MetricPattern6::new(client.clone(), _m(&acc, "pct75")),
pct90: MetricPattern6::new(client.clone(), _m(&acc, "pct90")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct FeeRatePattern<T> {
pub average: MetricPattern1<T>,
@@ -1839,6 +1824,36 @@ impl<T: DeserializeOwned> FeeRatePattern<T> {
}
}
/// Pattern struct for repeated tree structure.
pub struct FullnessPattern<T> {
pub average: MetricPattern2<T>,
pub base: MetricPattern11<T>,
pub max: MetricPattern2<T>,
pub median: MetricPattern6<T>,
pub min: MetricPattern2<T>,
pub pct10: MetricPattern6<T>,
pub pct25: MetricPattern6<T>,
pub pct75: MetricPattern6<T>,
pub pct90: MetricPattern6<T>,
}
impl<T: DeserializeOwned> FullnessPattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
average: MetricPattern2::new(client.clone(), _m(&acc, "average")),
base: MetricPattern11::new(client.clone(), acc.clone()),
max: MetricPattern2::new(client.clone(), _m(&acc, "max")),
median: MetricPattern6::new(client.clone(), _m(&acc, "median")),
min: MetricPattern2::new(client.clone(), _m(&acc, "min")),
pct10: MetricPattern6::new(client.clone(), _m(&acc, "pct10")),
pct25: MetricPattern6::new(client.clone(), _m(&acc, "pct25")),
pct75: MetricPattern6::new(client.clone(), _m(&acc, "pct75")),
pct90: MetricPattern6::new(client.clone(), _m(&acc, "pct90")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _0satsPattern {
pub activity: ActivityPattern2,
@@ -1868,27 +1883,29 @@ impl _0satsPattern {
}
/// Pattern struct for repeated tree structure.
pub struct _100btcPattern {
pub activity: ActivityPattern2,
pub cost_basis: CostBasisPattern,
pub outputs: OutputsPattern,
pub realized: RealizedPattern,
pub relative: RelativePattern,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
pub struct PhaseDailyCentsPattern<T> {
pub average: MetricPattern6<T>,
pub max: MetricPattern6<T>,
pub median: MetricPattern6<T>,
pub min: MetricPattern6<T>,
pub pct10: MetricPattern6<T>,
pub pct25: MetricPattern6<T>,
pub pct75: MetricPattern6<T>,
pub pct90: MetricPattern6<T>,
}
impl _100btcPattern {
impl<T: DeserializeOwned> PhaseDailyCentsPattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: ActivityPattern2::new(client.clone(), acc.clone()),
cost_basis: CostBasisPattern::new(client.clone(), acc.clone()),
outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: RealizedPattern::new(client.clone(), acc.clone()),
relative: RelativePattern::new(client.clone(), acc.clone()),
supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")),
unrealized: UnrealizedPattern::new(client.clone(), acc.clone()),
average: MetricPattern6::new(client.clone(), _m(&acc, "average")),
max: MetricPattern6::new(client.clone(), _m(&acc, "max")),
median: MetricPattern6::new(client.clone(), _m(&acc, "median")),
min: MetricPattern6::new(client.clone(), _m(&acc, "min")),
pct10: MetricPattern6::new(client.clone(), _m(&acc, "pct10")),
pct25: MetricPattern6::new(client.clone(), _m(&acc, "pct25")),
pct75: MetricPattern6::new(client.clone(), _m(&acc, "pct75")),
pct90: MetricPattern6::new(client.clone(), _m(&acc, "pct90")),
}
}
}
@@ -1919,32 +1936,6 @@ impl PeriodCagrPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct UnrealizedPattern {
pub neg_unrealized_loss: MetricPattern1<Dollars>,
pub net_unrealized_pnl: MetricPattern1<Dollars>,
pub supply_in_loss: ActiveSupplyPattern,
pub supply_in_profit: ActiveSupplyPattern,
pub total_unrealized_pnl: MetricPattern1<Dollars>,
pub unrealized_loss: MetricPattern1<Dollars>,
pub unrealized_profit: MetricPattern1<Dollars>,
}
impl UnrealizedPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
neg_unrealized_loss: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss")),
net_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl")),
supply_in_loss: ActiveSupplyPattern::new(client.clone(), _m(&acc, "supply_in_loss")),
supply_in_profit: ActiveSupplyPattern::new(client.clone(), _m(&acc, "supply_in_profit")),
total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "total_unrealized_pnl")),
unrealized_loss: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss")),
unrealized_profit: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _10yTo12yPattern {
pub activity: ActivityPattern2,
@@ -1971,6 +1962,32 @@ impl _10yTo12yPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct UnrealizedPattern {
pub neg_unrealized_loss: MetricPattern1<Dollars>,
pub net_unrealized_pnl: MetricPattern1<Dollars>,
pub supply_in_loss: ActiveSupplyPattern,
pub supply_in_profit: ActiveSupplyPattern,
pub total_unrealized_pnl: MetricPattern1<Dollars>,
pub unrealized_loss: MetricPattern1<Dollars>,
pub unrealized_profit: MetricPattern1<Dollars>,
}
impl UnrealizedPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
neg_unrealized_loss: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss")),
net_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl")),
supply_in_loss: ActiveSupplyPattern::new(client.clone(), _m(&acc, "supply_in_loss")),
supply_in_profit: ActiveSupplyPattern::new(client.clone(), _m(&acc, "supply_in_profit")),
total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "total_unrealized_pnl")),
unrealized_loss: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss")),
unrealized_profit: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _0satsPattern2 {
pub activity: ActivityPattern2,
@@ -1997,6 +2014,32 @@ impl _0satsPattern2 {
}
}
/// Pattern struct for repeated tree structure.
pub struct _100btcPattern {
pub activity: ActivityPattern2,
pub cost_basis: CostBasisPattern,
pub outputs: OutputsPattern,
pub realized: RealizedPattern,
pub relative: RelativePattern,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _100btcPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
activity: ActivityPattern2::new(client.clone(), acc.clone()),
cost_basis: CostBasisPattern::new(client.clone(), acc.clone()),
outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: RealizedPattern::new(client.clone(), acc.clone()),
relative: RelativePattern::new(client.clone(), acc.clone()),
supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")),
unrealized: UnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _10yPattern {
pub activity: ActivityPattern2,
@@ -2066,19 +2109,19 @@ impl<T: DeserializeOwned> SplitPattern2<T> {
}
/// Pattern struct for repeated tree structure.
pub struct _2015Pattern {
pub bitcoin: MetricPattern4<Bitcoin>,
pub dollars: MetricPattern4<Dollars>,
pub sats: MetricPattern4<Sats>,
pub struct CoinbasePattern2 {
pub bitcoin: BlockCountPattern<Bitcoin>,
pub dollars: BlockCountPattern<Dollars>,
pub sats: BlockCountPattern<Sats>,
}
impl _2015Pattern {
impl CoinbasePattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
bitcoin: MetricPattern4::new(client.clone(), _m(&acc, "btc")),
dollars: MetricPattern4::new(client.clone(), _m(&acc, "usd")),
sats: MetricPattern4::new(client.clone(), acc.clone()),
bitcoin: BlockCountPattern::new(client.clone(), _m(&acc, "btc")),
dollars: BlockCountPattern::new(client.clone(), _m(&acc, "usd")),
sats: BlockCountPattern::new(client.clone(), acc.clone()),
}
}
}
@@ -2101,24 +2144,6 @@ impl CoinbasePattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct CoinbasePattern2 {
pub bitcoin: BlockCountPattern<Bitcoin>,
pub dollars: BlockCountPattern<Dollars>,
pub sats: BlockCountPattern<Sats>,
}
impl CoinbasePattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
bitcoin: BlockCountPattern::new(client.clone(), _m(&acc, "btc")),
dollars: BlockCountPattern::new(client.clone(), _m(&acc, "usd")),
sats: BlockCountPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct SegwitAdoptionPattern {
pub base: MetricPattern11<StoredF32>,
@@ -2138,19 +2163,19 @@ impl SegwitAdoptionPattern {
}
/// Pattern struct for repeated tree structure.
pub struct CostBasisPattern2 {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
pub percentiles: PercentilesPattern,
pub struct _2015Pattern {
pub bitcoin: MetricPattern4<Bitcoin>,
pub dollars: MetricPattern4<Dollars>,
pub sats: MetricPattern4<Sats>,
}
impl CostBasisPattern2 {
impl _2015Pattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")),
min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")),
percentiles: PercentilesPattern::new(client.clone(), _m(&acc, "cost_basis")),
bitcoin: MetricPattern4::new(client.clone(), _m(&acc, "btc")),
dollars: MetricPattern4::new(client.clone(), _m(&acc, "usd")),
sats: MetricPattern4::new(client.clone(), acc.clone()),
}
}
}
@@ -2173,6 +2198,24 @@ impl ActiveSupplyPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct CostBasisPattern2 {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
pub percentiles: PercentilesPattern,
}
impl CostBasisPattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
max: MetricPattern1::new(client.clone(), _m(&acc, "max_cost_basis")),
min: MetricPattern1::new(client.clone(), _m(&acc, "min_cost_basis")),
percentiles: PercentilesPattern::new(client.clone(), _m(&acc, "cost_basis")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct UnclaimedRewardsPattern {
pub bitcoin: BitcoinPattern2<Bitcoin>,
@@ -2191,6 +2234,22 @@ impl UnclaimedRewardsPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct RelativePattern4 {
pub supply_in_loss_rel_to_own_supply: MetricPattern1<StoredF64>,
pub supply_in_profit_rel_to_own_supply: MetricPattern1<StoredF64>,
}
impl RelativePattern4 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "loss_rel_to_own_supply")),
supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "profit_rel_to_own_supply")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CostBasisPattern {
pub max: MetricPattern1<Dollars>,
@@ -2223,22 +2282,6 @@ impl SupplyPattern2 {
}
}
/// Pattern struct for repeated tree structure.
pub struct RelativePattern4 {
pub supply_in_loss_rel_to_own_supply: MetricPattern1<StoredF64>,
pub supply_in_profit_rel_to_own_supply: MetricPattern1<StoredF64>,
}
impl RelativePattern4 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "loss_rel_to_own_supply")),
supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "profit_rel_to_own_supply")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _1dReturns1mSdPattern {
pub sd: MetricPattern4<StoredF32>,
@@ -2255,6 +2298,22 @@ impl _1dReturns1mSdPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct SatsPattern<T> {
pub ohlc: MetricPattern1<T>,
pub split: SplitPattern2<T>,
}
impl<T: DeserializeOwned> SatsPattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ohlc: MetricPattern1::new(client.clone(), _m(&acc, "ohlc")),
split: SplitPattern2::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct BlockCountPattern<T> {
pub cumulative: MetricPattern1<T>,
@@ -2288,17 +2347,15 @@ impl<T: DeserializeOwned> BitcoinPattern2<T> {
}
/// Pattern struct for repeated tree structure.
pub struct SatsPattern<T> {
pub ohlc: MetricPattern1<T>,
pub split: SplitPattern2<T>,
pub struct OutputsPattern {
pub utxo_count: MetricPattern1<StoredU64>,
}
impl<T: DeserializeOwned> SatsPattern<T> {
impl OutputsPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
ohlc: MetricPattern1::new(client.clone(), _m(&acc, "ohlc_sats")),
split: SplitPattern2::new(client.clone(), _m(&acc, "sats")),
utxo_count: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
@@ -2317,20 +2374,6 @@ impl RealizedPriceExtraPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct OutputsPattern {
pub utxo_count: MetricPattern1<StoredU64>,
}
impl OutputsPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
utxo_count: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
// Metrics tree
/// Metrics tree node.
@@ -4899,8 +4942,8 @@ impl MetricsTree_Positions {
pub struct MetricsTree_Price {
pub cents: MetricsTree_Price_Cents,
pub oracle: MetricsTree_Price_Oracle,
pub sats: SatsPattern<OHLCSats>,
pub usd: MetricsTree_Price_Usd,
pub sats: MetricsTree_Price_Sats,
pub usd: SatsPattern<OHLCDollars>,
}
impl MetricsTree_Price {
@@ -4908,8 +4951,8 @@ impl MetricsTree_Price {
Self {
cents: MetricsTree_Price_Cents::new(client.clone(), format!("{base_path}_cents")),
oracle: MetricsTree_Price_Oracle::new(client.clone(), format!("{base_path}_oracle")),
sats: SatsPattern::new(client.clone(), "price".to_string()),
usd: MetricsTree_Price_Usd::new(client.clone(), format!("{base_path}_usd")),
sats: MetricsTree_Price_Sats::new(client.clone(), format!("{base_path}_sats")),
usd: SatsPattern::new(client.clone(), "price".to_string()),
}
}
}
@@ -4950,8 +4993,16 @@ impl MetricsTree_Price_Cents_Split {
/// Metrics tree node.
pub struct MetricsTree_Price_Oracle {
pub height_to_first_pairoutputindex: MetricPattern11<PairOutputIndex>,
pub ohlc_cents: MetricPattern6<OHLCCents>,
pub ohlc_dollars: MetricPattern6<OHLCDollars>,
pub output0_value: MetricPattern33<Sats>,
pub output1_value: MetricPattern33<Sats>,
pub pairoutputindex_to_txindex: MetricPattern33<TxIndex>,
pub phase_daily_cents: PhaseDailyCentsPattern<Cents>,
pub phase_daily_dollars: PhaseDailyCentsPattern<Dollars>,
pub phase_histogram: MetricPattern11<OracleBins>,
pub phase_price_cents: MetricPattern11<Cents>,
pub price_cents: MetricPattern11<Cents>,
pub tx_count: MetricPattern6<StoredU32>,
}
@@ -4959,8 +5010,16 @@ pub struct MetricsTree_Price_Oracle {
impl MetricsTree_Price_Oracle {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
height_to_first_pairoutputindex: MetricPattern11::new(client.clone(), "height_to_first_pairoutputindex".to_string()),
ohlc_cents: MetricPattern6::new(client.clone(), "oracle_ohlc_cents".to_string()),
ohlc_dollars: MetricPattern6::new(client.clone(), "oracle_ohlc".to_string()),
output0_value: MetricPattern33::new(client.clone(), "pair_output0_value".to_string()),
output1_value: MetricPattern33::new(client.clone(), "pair_output1_value".to_string()),
pairoutputindex_to_txindex: MetricPattern33::new(client.clone(), "pairoutputindex_to_txindex".to_string()),
phase_daily_cents: PhaseDailyCentsPattern::new(client.clone(), "phase_daily".to_string()),
phase_daily_dollars: PhaseDailyCentsPattern::new(client.clone(), "phase_daily_dollars".to_string()),
phase_histogram: MetricPattern11::new(client.clone(), "phase_histogram".to_string()),
phase_price_cents: MetricPattern11::new(client.clone(), "phase_price_cents".to_string()),
price_cents: MetricPattern11::new(client.clone(), "oracle_price_cents".to_string()),
tx_count: MetricPattern6::new(client.clone(), "oracle_tx_count".to_string()),
}
@@ -4968,16 +5027,16 @@ impl MetricsTree_Price_Oracle {
}
/// Metrics tree node.
pub struct MetricsTree_Price_Usd {
pub ohlc: MetricPattern1<OHLCDollars>,
pub split: SplitPattern2<Dollars>,
pub struct MetricsTree_Price_Sats {
pub ohlc: MetricPattern1<OHLCSats>,
pub split: SplitPattern2<Sats>,
}
impl MetricsTree_Price_Usd {
impl MetricsTree_Price_Sats {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
ohlc: MetricPattern1::new(client.clone(), "price_ohlc".to_string()),
split: SplitPattern2::new(client.clone(), "price".to_string()),
ohlc: MetricPattern1::new(client.clone(), "price_ohlc_sats".to_string()),
split: SplitPattern2::new(client.clone(), "price_sats".to_string()),
}
}
}
@@ -5323,7 +5382,7 @@ pub struct BrkClient {
impl BrkClient {
/// Client version.
pub const VERSION: &'static str = "v0.1.0-alpha.2";
pub const VERSION: &'static str = "v0.1.0-alpha.3";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {
@@ -5363,6 +5422,24 @@ impl BrkClient {
)
}
/// OpenAPI specification
///
/// Full OpenAPI 3.1 specification for this API.
///
/// Endpoint: `GET /api.json`
pub fn get_openapi(&self) -> Result<serde_json::Value> {
self.base.get_json(&format!("/api.json"))
}
/// Trimmed OpenAPI specification
///
/// Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information.
///
/// Endpoint: `GET /api.trimmed.json`
pub fn get_openapi_trimmed(&self) -> Result<serde_json::Value> {
self.base.get_json(&format!("/api.trimmed.json"))
}
/// Address information
///
/// Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR).
@@ -7,7 +7,7 @@ use brk_types::Version;
use schemars::JsonSchema;
use vecdb::{LazyVecFrom1, UnaryTransform, VecIndex};
use crate::internal::{ComputedVecValue, Full};
use crate::internal::{ComputedVecValue, Distribution, Full};
use super::LazyPercentiles;
@@ -61,4 +61,33 @@ where
),
}
}
pub fn from_distribution<F: UnaryTransform<S1T, T>>(
name: &str,
version: Version,
source: &Distribution<I, S1T>,
) -> Self {
Self {
average: LazyVecFrom1::transformed::<F>(
&format!("{name}_average"),
version,
source.boxed_average(),
),
min: LazyVecFrom1::transformed::<F>(
&format!("{name}_min"),
version,
source.boxed_min(),
),
max: LazyVecFrom1::transformed::<F>(
&format!("{name}_max"),
version,
source.boxed_max(),
),
percentiles: LazyPercentiles::from_percentiles::<F>(
name,
version,
&source.percentiles,
),
}
}
}
@@ -0,0 +1,11 @@
use brk_types::{Cents, Dollars};
use vecdb::UnaryTransform;
pub struct CentsToDollars;
impl UnaryTransform<Cents, Dollars> for CentsToDollars {
#[inline(always)]
fn apply(cents: Cents) -> Dollars {
Dollars::from(cents)
}
}
@@ -1,3 +1,4 @@
mod cents_to_dollars;
mod close_price_times_ratio;
mod close_price_times_sats;
mod difference_f32;
@@ -37,6 +38,7 @@ mod volatility_sqrt365;
mod volatility_sqrt7;
mod weight_to_fullness;
pub use cents_to_dollars::*;
pub use close_price_times_ratio::*;
pub use close_price_times_sats::*;
pub use difference_f32::*;
+694 -8
View File
@@ -1,10 +1,50 @@
//! # Phase Oracle - On-chain Price Discovery
//!
//! Uses `frac(log10(sats))` to bin outputs into 100 bins per block.
//! The peak bin indicates the price decade (cyclical: $6.3, $63, $630, $6300 all map to same bin).
//! Monthly/yearly calibration anchors resolve the decade ambiguity.
//!
//! ## What Worked
//!
//! **Transaction filters (in `compute_pair_index`):**
//! - `output_count == 2` - payment + change pattern
//! - `input_count <= 5` - matches Python UTXOracle
//! - `witness_size <= 2500` bytes total
//! - No OP_RETURN outputs
//! - No P2TR (taproot) outputs - significantly cleaned up 2021+ data
//! - No P2MS, Empty, Unknown outputs - allowlist approach
//! - No same-day spends - inputs must spend outputs confirmed on earlier days
//! - No both-outputs-round - skip tx if both outputs are round BTC amounts (±0.1%)
//!
//! **Output filters (in `OracleBins::sats_to_bin`):**
//! - Per-output min/max: 1k sats to 100k BTC (matches Python's 1e-5 to 1e5 BTC)
//!
//! **Peak finding:**
//! - Skip bin 0 when finding peak - round BTC amounts (0.001, 0.01, 0.1, 1.0 BTC) cluster there
//!
//! **Anchors:**
//! - Monthly anchors 2010-2020 for better decade selection in volatile early years
//! - Yearly anchors 2021+ when prices are more stable
//!
//! ## What Didn't Work
//!
//! - **Skip all round bins (0, 10, 20, ..., 90) before 2020** - made results worse, not better
//! - **Top-N tie-breaking with prev_price** - caused drift
//! - **50% margin threshold for round bin avoidance** - still had issues
//! - **Transaction-level min sats filter** - Python filters per-output, not per-tx
//!
//! ## Known Limitations
//!
//! - Pre-2017 data is noisy due to low transaction volume (weak signal)
//! - 2017 SegWit activation era has some spikes
use std::collections::VecDeque;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{
Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OutputType, Sats, StoredU32,
StoredU64, TxIndex,
Cents, Close, Date, DateIndex, Height, High, Low, OHLCCents, Open, OracleBins, OutputType,
PHASE_BINS, PairOutputIndex, Sats, StoredU32, StoredU64, TxIndex,
};
use tracing::info;
use vecdb::{
@@ -31,6 +71,653 @@ impl Vecs {
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Step 1: Compute pair output index (all 2-output transactions)
self.compute_pair_index(indexer, indexes, starting_indexes, exit)?;
// Step 2: Compute phase histograms (Layer 4)
self.compute_phase_histograms(starting_indexes, exit)?;
// Step 3: Compute phase oracle prices (Layer 5)
self.compute_phase_prices(starting_indexes, exit)?;
// Step 4: Compute phase daily average
self.compute_phase_daily_average(indexes, starting_indexes, exit)?;
// Step 6: Compute UTXOracle prices (Python port)
self.compute_prices(indexer, indexes, starting_indexes, exit)?;
// Step 7: Aggregate to daily OHLC
self.compute_daily_ohlc(indexes, starting_indexes, exit)?;
Ok(())
}
/// Compute the pair output index: all transactions with exactly 2 outputs
///
/// This is Layer 1 of the oracle computation - identifies all candidate
/// transactions for the payment+change pattern.
fn compute_pair_index(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Validate version - combine all source vec versions
let source_version = indexes.txindex.output_count.version()
+ indexes.txindex.input_count.version()
+ indexer.vecs.transactions.base_size.version()
+ indexer.vecs.transactions.total_size.version()
+ indexer.vecs.outputs.outputtype.version()
+ indexer.vecs.outputs.value.version()
+ indexer.vecs.inputs.outpoint.version()
+ indexes.height.dateindex.version();
self.pairoutputindex_to_txindex
.validate_computed_version_or_reset(source_version)?;
self.height_to_first_pairoutputindex
.validate_computed_version_or_reset(source_version)?;
self.output0_value
.validate_computed_version_or_reset(source_version)?;
self.output1_value
.validate_computed_version_or_reset(source_version)?;
let total_heights = indexer.vecs.blocks.timestamp.len();
let total_txs = indexer.vecs.transactions.height.len();
// Determine starting height (handle rollback + sync)
let start_height = self
.height_to_first_pairoutputindex
.len()
.min(starting_indexes.height.to_usize());
// Truncation point for pair vecs: first_pairoutputindex of start_height block
// (i.e., keep all pairs from blocks before start_height)
let pair_truncate_len =
if start_height > 0 && start_height <= self.height_to_first_pairoutputindex.len() {
self.height_to_first_pairoutputindex
.iter()?
.get(Height::from(start_height))
.map(|idx| idx.to_usize())
.unwrap_or(self.pairoutputindex_to_txindex.len())
} else if start_height == 0 {
0
} else {
self.pairoutputindex_to_txindex.len()
}
.min(self.pairoutputindex_to_txindex.len())
.min(self.output0_value.len())
.min(self.output1_value.len());
// Truncate all vecs together
self.height_to_first_pairoutputindex
.truncate_if_needed_at(start_height)?;
self.pairoutputindex_to_txindex
.truncate_if_needed_at(pair_truncate_len)?;
self.output0_value
.truncate_if_needed_at(pair_truncate_len)?;
self.output1_value
.truncate_if_needed_at(pair_truncate_len)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing pair index from height {} to {}",
start_height, total_heights
);
let mut height_to_first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
let mut txindex_to_output_count_iter = indexes.txindex.output_count.iter();
let mut txindex_to_input_count_iter = indexes.txindex.input_count.iter();
let mut txindex_to_base_size_iter = indexer.vecs.transactions.base_size.into_iter();
let mut txindex_to_total_size_iter = indexer.vecs.transactions.total_size.into_iter();
let mut txindex_to_first_txoutindex_iter =
indexer.vecs.transactions.first_txoutindex.into_iter();
let mut txindex_to_first_txinindex_iter =
indexer.vecs.transactions.first_txinindex.into_iter();
let mut txoutindex_to_outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
let mut txoutindex_to_value_iter = indexer.vecs.outputs.value.into_iter();
let mut txinindex_to_outpoint_iter = indexer.vecs.inputs.outpoint.into_iter();
let mut height_to_dateindex_iter = indexes.height.dateindex.iter();
let mut dateindex_to_first_height_iter = indexes.dateindex.first_height.iter();
// Track current date for same-day spend check
let mut current_dateindex = DateIndex::from(0usize);
let mut current_date_first_txindex = TxIndex::from(0usize);
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
for height in start_height..total_heights {
// Record first pairoutputindex for this block
let first_pairoutputindex =
PairOutputIndex::from(self.pairoutputindex_to_txindex.len());
self.height_to_first_pairoutputindex
.push(first_pairoutputindex);
// Get transaction range for this block
let first_txindex = height_to_first_txindex_iter.get_at_unwrap(height);
let next_first_txindex = height_to_first_txindex_iter
.get_at(height + 1)
.unwrap_or(TxIndex::from(total_txs));
// Update current date tracking for same-day spend check
let block_dateindex = height_to_dateindex_iter.get_unwrap(Height::from(height));
if block_dateindex != current_dateindex {
current_dateindex = block_dateindex;
if let Some(first_height) = dateindex_to_first_height_iter.get(block_dateindex) {
current_date_first_txindex = height_to_first_txindex_iter
.get_at(first_height.to_usize())
.unwrap_or(first_txindex);
}
}
// Skip coinbase (first tx in block)
let tx_start = first_txindex.to_usize() + 1;
let tx_end = next_first_txindex.to_usize();
for txindex in tx_start..tx_end {
// Check output count first (most common filter)
let output_count: StoredU64 =
txindex_to_output_count_iter.get_unwrap(TxIndex::from(txindex));
if *output_count != 2 {
continue;
}
// Filter: 1-5 inputs (same as UTXOracle)
let input_count: StoredU64 =
txindex_to_input_count_iter.get_unwrap(TxIndex::from(txindex));
if *input_count == 0 || *input_count > 5 {
continue;
}
// Filter: max 2500 bytes total witness size
let base_size: StoredU32 = txindex_to_base_size_iter.get_at_unwrap(txindex);
let total_size: StoredU32 = txindex_to_total_size_iter.get_at_unwrap(txindex);
let witness_size = *total_size - *base_size;
if witness_size > 2500 {
continue;
}
// Filter: only standard payment types (no OP_RETURN, P2TR, P2MS, Empty, Unknown)
let first_txoutindex = txindex_to_first_txoutindex_iter.get_at_unwrap(txindex);
let out0_type =
txoutindex_to_outputtype_iter.get_at_unwrap(first_txoutindex.to_usize());
let out1_type =
txoutindex_to_outputtype_iter.get_at_unwrap(first_txoutindex.to_usize() + 1);
if !matches!(
out0_type,
OutputType::P2PK65
| OutputType::P2PK33
| OutputType::P2PKH
| OutputType::P2SH
| OutputType::P2WPKH
| OutputType::P2WSH
| OutputType::P2A
) || !matches!(
out1_type,
OutputType::P2PK65
| OutputType::P2PK33
| OutputType::P2PKH
| OutputType::P2SH
| OutputType::P2WPKH
| OutputType::P2WSH
| OutputType::P2A
) {
continue;
}
// Filter: no same-day spends (input spending output confirmed today)
let first_txinindex = txindex_to_first_txinindex_iter.get_at_unwrap(txindex);
let mut has_same_day_spend = false;
for i in 0..*input_count as usize {
let txinindex = first_txinindex.to_usize() + i;
let outpoint = txinindex_to_outpoint_iter.get_at_unwrap(txinindex);
if !outpoint.is_coinbase() && outpoint.txindex() >= current_date_first_txindex {
has_same_day_spend = true;
break;
}
}
if has_same_day_spend {
continue;
}
// Get output values (Layer 3)
let value0: Sats =
txoutindex_to_value_iter.get_at_unwrap(first_txoutindex.to_usize());
let value1: Sats =
txoutindex_to_value_iter.get_at_unwrap(first_txoutindex.to_usize() + 1);
// Filter: skip if BOTH outputs are round BTC amounts (not price-related)
if value0.is_round_btc() && value1.is_round_btc() {
continue;
}
// Store Layer 1 & 3 data
// Note: min/max sats filtering done per-output in OracleBins::sats_to_bin
self.pairoutputindex_to_txindex.push(TxIndex::from(txindex));
self.output0_value.push(value0);
self.output1_value.push(value1);
}
// Log and flush every 1%
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Pair index computation: {}%", progress);
let _lock = exit.lock();
self.pairoutputindex_to_txindex.write()?;
self.height_to_first_pairoutputindex.write()?;
self.output0_value.write()?;
self.output1_value.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.pairoutputindex_to_txindex.write()?;
self.height_to_first_pairoutputindex.write()?;
self.output0_value.write()?;
self.output1_value.write()?;
}
info!(
"Pair index complete: {} pairs across {} blocks",
self.pairoutputindex_to_txindex.len(),
self.height_to_first_pairoutputindex.len()
);
Ok(())
}
/// Compute phase histograms per block (Layer 4)
///
/// Bins output values by frac(log10(sats)) into 100 bins per block.
fn compute_phase_histograms(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
let source_version = self.pairoutputindex_to_txindex.version();
self.phase_histogram
.validate_computed_version_or_reset(source_version)?;
let total_heights = self.height_to_first_pairoutputindex.len();
let total_pairs = self.pairoutputindex_to_txindex.len();
let start_height = self
.phase_histogram
.len()
.min(starting_indexes.height.to_usize());
self.phase_histogram.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase histograms from height {} to {}",
start_height, total_heights
);
let mut output0_iter = self.output0_value.iter()?;
let mut output1_iter = self.output1_value.iter()?;
let mut height_to_first_pair_iter = self.height_to_first_pairoutputindex.iter()?;
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
for height in start_height..total_heights {
// Get pair range for this block
let first_pair = height_to_first_pair_iter
.get_unwrap(Height::from(height))
.to_usize();
let next_first_pair = height_to_first_pair_iter
.get(Height::from(height + 1))
.map(|p| p.to_usize())
.unwrap_or(total_pairs);
// Build phase histogram
let mut histogram = OracleBins::ZERO;
for pair_idx in first_pair..next_first_pair {
let pair_idx = PairOutputIndex::from(pair_idx);
let sats0: Sats = output0_iter.get_unwrap(pair_idx);
let sats1: Sats = output1_iter.get_unwrap(pair_idx);
histogram.add(sats0);
histogram.add(sats1);
}
self.phase_histogram.push(histogram);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase histogram computation: {}%", progress);
let _lock = exit.lock();
self.phase_histogram.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_histogram.write()?;
}
info!(
"Phase histograms complete: {} blocks",
self.phase_histogram.len()
);
Ok(())
}
/// Compute phase oracle prices (Layer 5)
///
/// Derives prices from phase histograms using peak finding.
/// Uses monthly calibration anchors (2010-2020) then yearly (2021+).
fn compute_phase_prices(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
/// Monthly calibration anchors 2010-2020, then yearly 2021+
/// Format: (first_height_of_period, open_price)
const ANCHORS: [(u32, f64); 129] = [
// 2010 (monthly from Oct)
(82_998, 0.06), // 2010-10-01
(88_893, 0.19), // 2010-11-01
(94_802, 0.20), // 2010-12-01
// 2011
(100_410, 0.30), // 2011-01-01
(105_571, 0.55), // 2011-02-01
(111_137, 0.86), // 2011-03-01
(116_039, 0.78), // 2011-04-01
(121_127, 3.50), // 2011-05-01
(127_866, 8.74), // 2011-06-01
(134_122, 16.10), // 2011-07-01
(139_036, 13.35), // 2011-08-01
(143_409, 8.19), // 2011-09-01
(147_566, 5.14), // 2011-10-01
(151_315, 3.24), // 2011-11-01
(155_452, 2.97), // 2011-12-01
// 2012
(160_037, 4.72), // 2012-01-01
(164_781, 5.48), // 2012-02-01
(169_136, 4.86), // 2012-03-01
(173_805, 4.90), // 2012-04-01
(178_015, 4.94), // 2012-05-01
(182_429, 5.18), // 2012-06-01
(186_964, 6.68), // 2012-07-01
(191_737, 9.35), // 2012-08-01
(196_616, 10.16), // 2012-09-01
(201_311, 12.40), // 2012-10-01
(205_919, 11.20), // 2012-11-01
(210_350, 12.56), // 2012-12-01
// 2013
(214_563, 13.51), // 2013-01-01
(219_007, 20.41), // 2013-02-01
(223_665, 33.38), // 2013-03-01
(229_008, 93.03), // 2013-04-01
(233_975, 139.22), // 2013-05-01
(238_952, 128.81), // 2013-06-01
(244_160, 97.51), // 2013-07-01
(249_525, 106.21), // 2013-08-01
(255_362, 141.00), // 2013-09-01
(260_989, 141.89), // 2013-10-01
(267_188, 211.17), // 2013-11-01
(272_375, 1205.80), // 2013-12-01
// 2014
(277_996, 739.28), // 2014-01-01
(283_468, 805.22), // 2014-02-01
(288_370, 549.99), // 2014-03-01
(293_483, 456.98), // 2014-04-01
(298_513, 449.02), // 2014-05-01
(303_552, 626.21), // 2014-06-01
(308_672, 640.79), // 2014-07-01
(313_404, 580.00), // 2014-08-01
(318_531, 477.81), // 2014-09-01
(323_269, 387.00), // 2014-10-01
(327_939, 336.82), // 2014-11-01
(332_363, 379.89), // 2014-12-01
// 2015
(336_861, 322.30), // 2015-01-01
(341_392, 215.80), // 2015-02-01
(345_611, 255.70), // 2015-03-01
(350_162, 244.51), // 2015-04-01
(354_416, 236.11), // 2015-05-01
(358_881, 228.70), // 2015-06-01
(363_263, 262.89), // 2015-07-01
(367_846, 284.45), // 2015-08-01
(372_441, 231.35), // 2015-09-01
(376_910, 236.49), // 2015-10-01
(381_470, 316.00), // 2015-11-01
(386_119, 376.88), // 2015-12-01
// 2016
(391_182, 429.02), // 2016-01-01
(396_049, 365.52), // 2016-02-01
(400_601, 438.99), // 2016-03-01
(405_179, 416.02), // 2016-04-01
(409_638, 446.60), // 2016-05-01
(414_258, 530.69), // 2016-06-01
(418_723, 671.91), // 2016-07-01
(423_088, 624.22), // 2016-08-01
(427_737, 573.80), // 2016-09-01
(432_284, 609.67), // 2016-10-01
(436_828, 697.69), // 2016-11-01
(441_341, 742.33), // 2016-12-01
// 2017
(446_033, 970.41), // 2017-01-01
(450_945, 968.74), // 2017-02-01
(455_200, 1190.37), // 2017-03-01
(459_832, 1080.82), // 2017-04-01
(464_270, 1362.02), // 2017-05-01
(469_122, 2299.05), // 2017-06-01
(473_593, 2455.42), // 2017-07-01
(478_479, 2865.02), // 2017-08-01
(482_885, 4737.93), // 2017-09-01
(487_740, 4334.18), // 2017-10-01
(492_558, 6439.52), // 2017-11-01
(496_932, 9968.39), // 2017-12-01
// 2018
(501_961, 13888.32), // 2018-01-01
(507_016, 10115.79), // 2018-02-01
(511_385, 10306.80), // 2018-03-01
(516_040, 6922.18), // 2018-04-01
(520_650, 9243.39), // 2018-05-01
(525_367, 7486.93), // 2018-06-01
(529_967, 6386.45), // 2018-07-01
(534_613, 7725.93), // 2018-08-01
(539_416, 7016.31), // 2018-09-01
(543_835, 6565.64), // 2018-10-01
(548_214, 6305.13), // 2018-11-01
(552_084, 3971.61), // 2018-12-01
// 2019
(556_459, 3692.35), // 2019-01-01
(560_984, 3411.57), // 2019-02-01
(565_109, 3792.17), // 2019-03-01
(569_659, 4095.32), // 2019-04-01
(573_997, 5269.55), // 2019-05-01
(578_718, 8542.59), // 2019-06-01
(583_237, 10754.91), // 2019-07-01
(588_007, 10085.57), // 2019-08-01
(592_683, 9600.93), // 2019-09-01
(597_318, 8303.79), // 2019-10-01
(601_842, 9152.56), // 2019-11-01
(606_088, 7554.92), // 2019-12-01
// 2020
(610_691, 7167.07), // 2020-01-01
(615_428, 9333.17), // 2020-02-01
(619_582, 8526.76), // 2020-03-01
(623_837, 6424.03), // 2020-04-01
(628_350, 8627.93), // 2020-05-01
(632_542, 9448.95), // 2020-06-01
(637_091, 9134.01), // 2020-07-01
(641_680, 11354.08), // 2020-08-01
(646_201, 11657.26), // 2020-09-01
(650_732, 10779.19), // 2020-10-01
(654_933, 13809.85), // 2020-11-01
(658_977, 19698.14), // 2020-12-01
// 2021+ (yearly)
(663_913, 28_980.45), // 2021-01-01
(716_599, 46_195.56), // 2022-01-01
(769_787, 16_528.89), // 2023-01-01
(823_786, 42_241.10), // 2024-01-01
(877_259, 93_576.00), // 2025-01-01
(930_341, 87_648.22), // 2026-01-01
];
/// Find the calibration price for a given height
fn anchor_price_for_height(height: usize) -> Option<f64> {
let mut result = None;
for &(anchor_height, price) in &ANCHORS {
if height >= anchor_height as usize {
result = Some(price);
} else {
break;
}
}
result
}
let source_version = self.phase_histogram.version();
self.phase_price_cents
.validate_computed_version_or_reset(source_version)?;
let total_heights = self.phase_histogram.len();
let start_height = self
.phase_price_cents
.len()
.min(starting_indexes.height.to_usize());
self.phase_price_cents.truncate_if_needed_at(start_height)?;
if start_height >= total_heights {
return Ok(());
}
info!(
"Computing phase prices from height {} to {}",
start_height, total_heights
);
let mut histogram_iter = self.phase_histogram.iter()?;
let mut last_progress = (start_height * 100 / total_heights.max(1)) as u8;
// Fixed exponent calibrated for ~$63,000 (ceil(log10(63000)) = 5)
const EXPONENT: f64 = 5.0;
/// Convert a bin to price using anchor for decade selection
fn bin_to_price(bin: usize, anchor_price: f64) -> f64 {
let peak = (bin as f64 + 0.5) / PHASE_BINS as f64;
let raw_price = 10.0_f64.powf(EXPONENT - peak);
let decade_ratio = (anchor_price / raw_price).log10().round();
raw_price * 10.0_f64.powf(decade_ratio)
}
for height in start_height..total_heights {
// Before first anchor (pre-Oct 2010), output 0
let anchor_price = match anchor_price_for_height(height) {
Some(price) => price,
None => {
self.phase_price_cents.push(Cents::ZERO);
continue;
}
};
let histogram = histogram_iter.get_unwrap(Height::from(height));
// Skip empty histograms, use anchor price
if histogram.total_count() == 0 {
let price_cents = Cents::from((anchor_price * 100.0) as i64);
self.phase_price_cents.push(price_cents);
continue;
}
// Find peak bin, skipping bin 0 (round BTC amounts cluster there)
let peak_bin = histogram
.bins
.iter()
.enumerate()
.filter(|(bin, _)| *bin != 0)
.max_by_key(|(_, count)| *count)
.map(|(bin, _)| bin)
.unwrap_or(0);
let price = bin_to_price(peak_bin, anchor_price);
// Clamp to reasonable range ($0.001 to $10M)
let price = price.clamp(0.001, 10_000_000.0);
let price_cents = Cents::from((price * 100.0) as i64);
self.phase_price_cents.push(price_cents);
// Progress logging
let progress = (height * 100 / total_heights.max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Phase price computation: {}%", progress);
let _lock = exit.lock();
self.phase_price_cents.write()?;
}
}
// Final write
{
let _lock = exit.lock();
self.phase_price_cents.write()?;
}
info!(
"Phase prices complete: {} blocks",
self.phase_price_cents.len()
);
Ok(())
}
/// Compute daily distribution (min, max, average, percentiles) from phase oracle prices
fn compute_phase_daily_average(
&mut self,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
info!("Computing phase daily distribution");
self.phase_daily_cents.compute(
starting_indexes.dateindex,
&self.phase_price_cents,
&indexes.dateindex.first_height,
&indexes.dateindex.height_count,
exit,
)?;
info!(
"Phase daily distribution complete: {} days",
self.phase_daily_cents.len()
);
Ok(())
}
/// Compute oracle prices from on-chain data (UTXOracle port)
fn compute_prices(
&mut self,
indexer: &Indexer,
indexes: &indexes::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Validate versions
self.price_cents
@@ -92,8 +779,8 @@ impl Vecs {
};
// Progress tracking
let total_blocks = last_height.to_usize() - start_height.to_usize();
let mut last_progress = 0u8;
let mut last_progress =
(start_height.to_usize() * 100 / last_height.to_usize().max(1)) as u8;
let total_txs = indexer.vecs.transactions.height.len();
// Sparse entries for current block (reused buffer)
@@ -109,8 +796,7 @@ impl Vecs {
let height = Height::from(height);
// Log progress every 1%
let progress =
((height.to_usize() - start_height.to_usize()) * 100 / total_blocks.max(1)) as u8;
let progress = (height.to_usize() * 100 / last_height.to_usize().max(1)) as u8;
if progress > last_progress {
last_progress = progress;
info!("Oracle price computation: {}%", progress);
@@ -174,14 +860,14 @@ impl Vecs {
// Check outputs: no OP_RETURN, collect values
let mut has_opreturn = false;
let mut values: [Sats; 2] = [Sats::ZERO; 2];
for i in 0..2usize {
for (i, value) in values.iter_mut().enumerate() {
let txoutindex = first_txoutindex.to_usize() + i;
let outputtype = txoutindex_to_outputtype_iter.get_at_unwrap(txoutindex);
if outputtype == OutputType::OpReturn {
has_opreturn = true;
break;
}
values[i] = txoutindex_to_value_iter.get_at_unwrap(txoutindex);
*value = txoutindex_to_value_iter.get_at_unwrap(txoutindex);
}
if has_opreturn {
continue;
+36 -2
View File
@@ -3,12 +3,38 @@ use brk_types::{DateIndex, OHLCCents, OHLCDollars, Version};
use vecdb::{BytesVec, Database, ImportableVec, IterableCloneableVec, LazyVecFrom1, PcoVec};
use super::Vecs;
use crate::internal::{CentsToDollars, Distribution, LazyTransformDistribution};
impl Vecs {
pub fn forced_import(db: &Database, parent_version: Version) -> Result<Self> {
// v2: Fixed spike stencil positions and Gaussian center to match Python's empirical data
let version = parent_version + Version::TWO;
// v12: Add both-outputs-round filter
let version = parent_version + Version::new(12);
// Layer 1: Pair output index
let pairoutputindex_to_txindex =
PcoVec::forced_import(db, "pairoutputindex_to_txindex", version)?;
let height_to_first_pairoutputindex =
PcoVec::forced_import(db, "height_to_first_pairoutputindex", version)?;
// Layer 3: Output values
let output0_value = PcoVec::forced_import(db, "pair_output0_value", version)?;
let output1_value = PcoVec::forced_import(db, "pair_output1_value", version)?;
// Layer 4: Phase histograms (depends on Layer 1)
let phase_histogram = BytesVec::forced_import(db, "phase_histogram", version)?;
// Layer 5: Phase Oracle prices
// v20: Skip only bin 0 (reverted round bin skip)
let phase_version = version + Version::new(13);
let phase_price_cents = PcoVec::forced_import(db, "phase_price_cents", phase_version)?;
let phase_daily_cents = Distribution::forced_import(db, "phase_daily", phase_version)?;
let phase_daily_dollars = LazyTransformDistribution::from_distribution::<CentsToDollars>(
"phase_daily_dollars",
phase_version,
&phase_daily_cents,
);
// UTXOracle (Python port)
let price_cents = PcoVec::forced_import(db, "oracle_price_cents", version)?;
let ohlc_cents = BytesVec::forced_import(db, "oracle_ohlc_cents", version)?;
let tx_count = PcoVec::forced_import(db, "oracle_tx_count", version)?;
@@ -21,6 +47,14 @@ impl Vecs {
);
Ok(Self {
pairoutputindex_to_txindex,
height_to_first_pairoutputindex,
output0_value,
output1_value,
phase_histogram,
phase_price_cents,
phase_daily_cents,
phase_daily_dollars,
price_cents,
ohlc_cents,
ohlc_dollars,
+43 -6
View File
@@ -1,16 +1,53 @@
use brk_traversable::Traversable;
use brk_types::{Cents, DateIndex, Height, OHLCCents, OHLCDollars, StoredU32};
use brk_types::{
Cents, DateIndex, Dollars, Height, OHLCCents, OHLCDollars, OracleBins, PairOutputIndex, Sats,
StoredU32, TxIndex,
};
use vecdb::{BytesVec, LazyVecFrom1, PcoVec};
/// Vectors storing UTXOracle-derived price data
use crate::internal::{Distribution, LazyTransformDistribution};
/// Vectors storing oracle-derived price data
#[derive(Clone, Traversable)]
pub struct Vecs {
/// Per-block price estimate in cents
/// This enables OHLC derivation for any time period
// ========== Layer 1: Pair identification (requires chain scan) ==========
/// Maps PairOutputIndex to TxIndex for all 2-output transactions
/// This is the base index for oracle candidates (~400M entries)
pub pairoutputindex_to_txindex: PcoVec<PairOutputIndex, TxIndex>,
/// Maps Height to first PairOutputIndex in that block
/// Enables efficient per-block iteration over pairs
pub height_to_first_pairoutputindex: PcoVec<Height, PairOutputIndex>,
// ========== Layer 3: Output values (enables any price algorithm) ==========
/// First output value for each pair (index 0)
pub output0_value: PcoVec<PairOutputIndex, Sats>,
/// Second output value for each pair (index 1)
pub output1_value: PcoVec<PairOutputIndex, Sats>,
// ========== Layer 4: Phase histograms (per block) ==========
/// Phase histogram per block: frac(log10(sats)) binned into 100 bins
/// ~200 bytes per block, ~175 MB total
pub phase_histogram: BytesVec<Height, OracleBins>,
// ========== Layer 5: Phase Oracle prices (derived from histograms) ==========
/// Per-block price in cents from phase histogram analysis
/// Calibrated at block 840,000 (~$63,000)
/// TODO: Add interpolation for sub-bin precision
pub phase_price_cents: PcoVec<Height, Cents>,
/// Daily distribution (min, max, average, percentiles) from phase oracle in cents
pub phase_daily_cents: Distribution<DateIndex, Cents>,
/// Daily distribution in dollars (lazy conversion from cents)
pub phase_daily_dollars: LazyTransformDistribution<DateIndex, Dollars, Cents>,
// ========== UTXOracle (Python port) ==========
/// Per-block price estimate in cents (sliding window + stencil matching)
pub price_cents: PcoVec<Height, Cents>,
/// Daily OHLC derived from height_to_price
/// Uses BytesVec because OHLCCents is a complex type
/// Daily OHLC derived from price_cents
pub ohlc_cents: BytesVec<DateIndex, OHLCCents>,
/// Daily OHLC in dollars (lazy conversion from cents)
+1 -1
View File
@@ -4,7 +4,7 @@ Colorized, timestamped logging with optional file output and hooks.
## What It Enables
Drop-in logging initialization that silences noisy dependencies (bitcoin, fjall, rolldown, rmcp) while keeping your logs readable with color-coded levels and local timestamps.
Drop-in logging initialization that silences noisy dependencies (bitcoin, fjall, rolldown, ...) while keeping your logs readable with color-coded levels and local timestamps.
## Key Features
+8 -4
View File
@@ -20,7 +20,7 @@ type LogHook = Box<dyn Fn(&str) + Send + Sync>;
static GUARD: OnceLock<WorkerGuard> = OnceLock::new();
static LOG_HOOK: OnceLock<LogHook> = OnceLock::new();
const MAX_LOG_FILES: u64 = 2;
const MAX_LOG_FILES: u64 = 5;
const MAX_FILE_SIZE_MB: u64 = 42;
// Don't remove, used to know the target of unwanted logs
@@ -117,9 +117,13 @@ impl<const ANSI: bool> tracing::field::Visit for FieldVisitor<ANSI> {
match name {
"uri" => self.uri = Some(format!("{value:?}")),
"latency" => self.latency = Some(format!("{value:?}")),
"message" => { let _ = write!(self.result, "{value:?}"); }
"message" => {
let _ = write!(self.result, "{value:?}");
}
_ if name.starts_with("log.") => {}
_ => { let _ = write!(self.result, "{}={:?} ", name, value); }
_ => {
let _ = write!(self.result, "{}={:?} ", name, value);
}
}
}
}
@@ -214,7 +218,7 @@ pub fn init(path: Option<&Path>) -> io::Result<()> {
const DEFAULT_LEVEL: &str = "info";
let default_filter = format!(
"{DEFAULT_LEVEL},bitcoin=off,bitcoincore-rpc=off,fjall=off,brk_fjall=off,lsm_tree=off,brk_rolldown=off,rolldown=off,rmcp=off,brk_rmcp=off,tracing=off,aide=off,rustls=off,notify=off,oxc_resolver=off,tower_http=off"
"{DEFAULT_LEVEL},bitcoin=off,bitcoincore-rpc=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"
);
let filter =
-23
View File
@@ -1,23 +0,0 @@
[package]
name = "brk_mcp"
description = "A bridge for LLMs to access BRK"
version.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
axum = { workspace = true }
brk_rmcp = { version = "0.8.0", features = [
"transport-worker",
"transport-streamable-http-server",
] }
tracing = { workspace = true }
minreq = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
[package.metadata.cargo-machete]
ignored = ["serde_json"]
-34
View File
@@ -1,34 +0,0 @@
# brk_mcp
Model Context Protocol (MCP) server for Bitcoin on-chain data.
## What It Enables
Expose BRK's REST API to AI assistants via MCP. The LLM reads the OpenAPI spec and calls any endpoint through a generic fetch tool.
## Available Tools
| Tool | Description |
|------|-------------|
| `get_openapi` | Get the OpenAPI specification for all REST endpoints |
| `fetch` | Call any REST API endpoint by path and query |
## Workflow
1. LLM calls `get_openapi` to discover available endpoints
2. LLM calls `fetch` with the desired path and query parameters
## Usage
```rust,ignore
let mcp = MCP::new("http://127.0.0.1:3110", openapi_json);
```
## Integration
The MCP server is integrated into `brk_server` and exposed at `/mcp` endpoint.
## Built On
- `brk_rmcp` for MCP protocol implementation
- `minreq` for HTTP requests
-110
View File
@@ -1,110 +0,0 @@
#![doc = include_str!("../README.md")]
use std::sync::Arc;
use brk_rmcp::{
ErrorData as McpError, RoleServer, ServerHandler,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::*,
service::RequestContext,
tool, tool_handler, tool_router,
};
use schemars::JsonSchema;
use serde::Deserialize;
use tracing::info;
pub mod route;
#[derive(Clone)]
pub struct MCP {
base_url: Arc<String>,
openapi_json: Arc<String>,
tool_router: ToolRouter<MCP>,
}
/// Parameters for fetching from the REST API.
#[derive(Deserialize, JsonSchema)]
pub struct FetchParams {
/// API path (e.g., "/api/blocks" or "/api/metrics/list")
pub path: String,
/// Optional query string (e.g., "page=0" or "from=-1&to=-10")
pub query: Option<String>,
}
#[tool_router]
impl MCP {
pub fn new(base_url: impl Into<String>, openapi_json: impl Into<String>) -> Self {
Self {
base_url: Arc::new(base_url.into()),
openapi_json: Arc::new(openapi_json.into()),
tool_router: Self::tool_router(),
}
}
#[tool(
description = "Get the OpenAPI specification describing all available REST API endpoints."
)]
async fn get_openapi(&self) -> Result<CallToolResult, McpError> {
info!("mcp: get_openapi");
Ok(CallToolResult::success(vec![Content::text(
self.openapi_json.as_str(),
)]))
}
#[tool(
description = "Call a REST API endpoint. Use get_openapi first to discover available endpoints."
)]
async fn fetch(
&self,
Parameters(params): Parameters<FetchParams>,
) -> Result<CallToolResult, McpError> {
info!("mcp: fetch {}", params.path);
let url = match &params.query {
Some(q) if !q.is_empty() => format!("{}{}?{}", self.base_url, params.path, q),
_ => format!("{}{}", self.base_url, params.path),
};
match minreq::get(&url).with_timeout(30).send() {
Ok(response) => {
let body = response.as_str().unwrap_or("").to_string();
Ok(CallToolResult::success(vec![Content::text(body)]))
}
Err(e) => Err(McpError::internal_error(
format!("HTTP request failed: {e}"),
None,
)),
}
}
}
#[tool_handler]
impl ServerHandler for MCP {
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: ProtocolVersion::LATEST,
capabilities: ServerCapabilities::builder().enable_tools().build(),
server_info: Implementation::from_build_env(),
instructions: Some(
"
Bitcoin Research Kit (BRK) - Bitcoin on-chain metrics and market data.
Workflow:
1. Call get_openapi to get the full API specification
2. Use fetch to call any endpoint described in the spec
Example: fetch with path=\"/api/metrics/list\" to list metrics.
"
.to_string(),
),
}
}
async fn initialize(
&self,
_request: InitializeRequestParam,
_context: RequestContext<RoleServer>,
) -> Result<InitializeResult, McpError> {
Ok(self.get_info())
}
}
-26
View File
@@ -1,26 +0,0 @@
use std::sync::Arc;
use axum::Router;
use brk_rmcp::transport::{
StreamableHttpServerConfig,
streamable_http_server::{StreamableHttpService, session::local::LocalSessionManager},
};
use tracing::info;
use crate::MCP;
/// Create an MCP service router.
pub fn mcp_router(base_url: String, openapi_json: Arc<String>) -> Router {
info!("Setting up MCP...");
let service = StreamableHttpService::new(
move || Ok(MCP::new(base_url.clone(), openapi_json.as_str())),
LocalSessionManager::default().into(),
StreamableHttpServerConfig {
stateful_mode: false,
..Default::default()
},
);
Router::new().nest_service("/mcp", service)
}
+5 -4
View File
@@ -23,12 +23,13 @@ let query = Query::build(&reader, &indexer, &computer, Some(mempool));
// Current height
let height = query.height();
// Metric queries
let data = query.search_and_format(MetricSelection {
// Metric queries (two-phase: resolve then format)
let resolved = query.resolve(MetricSelection {
metrics: vec!["supply".into()],
index: Index::Height,
range: DataRangeFormat::default(),
})?;
}, usize::MAX)?;
let data = query.format(resolved)?;
// Block queries
let info = query.block_by_height(Height::new(840_000))?;
@@ -44,7 +45,7 @@ let stats = query.address(address)?;
| Domain | Methods |
|--------|---------|
| Metrics | `metrics`, `search_and_format`, `metric_to_indexes` |
| Metrics | `metrics`, `resolve`, `format`, `metric_to_indexes` |
| Blocks | `block`, `block_by_height`, `blocks`, `block_txs`, `block_status`, `block_by_timestamp` |
| Transactions | `transaction`, `transaction_status`, `transaction_hex`, `outspend`, `outspends` |
| Addresses | `address`, `address_txids`, `address_utxos` |
+84 -114
View File
@@ -3,12 +3,15 @@ use std::collections::BTreeMap;
use brk_error::{Error, Result};
use brk_traversable::TreeNode;
use brk_types::{
DetailedMetricCount, Format, Index, IndexInfo, Limit, Metric, MetricData, PaginatedMetrics,
Pagination, PaginationIndex,
DetailedMetricCount, Format, Index, IndexInfo, Limit, Metric, MetricData, MetricOutput,
MetricSelection, Output, PaginatedMetrics, Pagination, PaginationIndex,
};
use vecdb::AnyExportableVec;
use crate::{vecs::{IndexToVec, MetricToVec}, DataRangeFormat, MetricSelection, Output, Query};
use crate::{
Query, ResolvedQuery,
vecs::{IndexToVec, MetricToVec},
};
/// Estimated bytes per column header
const CSV_HEADER_BYTES_PER_COL: usize = 10;
@@ -33,19 +36,25 @@ impl Query {
// Metric doesn't exist, suggest alternatives
Error::MetricNotFound {
metric: metric.to_string(),
suggestion: self.match_metric(metric, Limit::MIN).first().map(|s| s.to_string()),
suggestion: self
.match_metric(metric, Limit::MIN)
.first()
.map(|s| s.to_string()),
}
}
pub(crate) fn columns_to_csv(
columns: &[&dyn AnyExportableVec],
from: Option<i64>,
to: Option<i64>,
start: usize,
end: usize,
) -> Result<String> {
if columns.is_empty() {
return Ok(String::new());
}
let from = Some(start as i64);
let to = Some(end as i64);
let num_rows = columns[0].range_count(from, to);
let num_cols = columns.len();
@@ -79,82 +88,6 @@ impl Query {
Ok(csv)
}
/// Format single metric - returns `MetricData`
pub fn format(
&self,
metric: &dyn AnyExportableVec,
params: &DataRangeFormat,
) -> Result<Output> {
let len = metric.len();
let from = params.start().map(|start| metric.i64_to_usize(start));
let to = params.end_for_len(len).map(|end| metric.i64_to_usize(end));
Ok(match params.format() {
Format::CSV => Output::CSV(Self::columns_to_csv(
&[metric],
from.map(|v| v as i64),
to.map(|v| v as i64),
)?),
Format::JSON => {
let mut buf = Vec::new();
MetricData::serialize(metric, from, to, &mut buf)?;
Output::Json(buf)
}
})
}
/// Format multiple metrics - returns `Vec<MetricData>`
pub fn format_bulk(
&self,
metrics: &[&dyn AnyExportableVec],
params: &DataRangeFormat,
) -> Result<Output> {
// Use min length across metrics for consistent count resolution
let min_len = metrics.iter().map(|v| v.len()).min().unwrap_or(0);
let from = params.start().map(|start| {
metrics
.iter()
.map(|v| v.i64_to_usize(start))
.min()
.unwrap_or_default()
});
let to = params.end_for_len(min_len).map(|end| {
metrics
.iter()
.map(|v| v.i64_to_usize(end))
.min()
.unwrap_or_default()
});
let format = params.format();
Ok(match format {
Format::CSV => Output::CSV(Self::columns_to_csv(
metrics,
from.map(|v| v as i64),
to.map(|v| v as i64),
)?),
Format::JSON => {
if metrics.is_empty() {
return Ok(Output::default(format));
}
let mut buf = Vec::new();
buf.push(b'[');
for (i, vec) in metrics.iter().enumerate() {
if i > 0 {
buf.push(b',');
}
MetricData::serialize(*vec, from, to, &mut buf)?;
}
buf.push(b']');
Output::Json(buf)
}
})
}
/// Search for vecs matching the given metrics and index.
/// Returns error if no metrics requested or any requested metric is not found.
pub fn search(&self, params: &MetricSelection) -> Result<Vec<&'static dyn AnyExportableVec>> {
@@ -185,22 +118,34 @@ impl Query {
.sum()
}
/// Search and format single metric
pub fn search_and_format(&self, params: MetricSelection) -> Result<Output> {
self.search_and_format_checked(params, usize::MAX)
}
/// Search and format single metric with weight limit
pub fn search_and_format_checked(
/// Resolve query metadata without formatting (cheap).
/// Use with `format` for lazy formatting after ETag check.
pub fn resolve(
&self,
params: MetricSelection,
max_weight: usize,
) -> Result<Output> {
) -> Result<ResolvedQuery> {
let vecs = self.search(&params)?;
let metric = vecs.first().expect("search guarantees non-empty on success");
let total = vecs.iter().map(|v| v.len()).min().unwrap_or(0);
let version: u64 = vecs.iter().map(|v| u64::from(v.version())).sum();
let weight = Self::weight(&vecs, params.start(), params.end_for_len(metric.len()));
let start = params
.start()
.map(|s| vecs.iter().map(|v| v.i64_to_usize(s)).min().unwrap_or(0))
.unwrap_or(0);
let end = params
.end_for_len(total)
.map(|e| {
vecs.iter()
.map(|v| v.i64_to_usize(e))
.min()
.unwrap_or(total)
})
.unwrap_or(total);
let weight = Self::weight(&vecs, Some(start as i64), Some(end as i64));
if weight > max_weight {
return Err(Error::WeightExceeded {
requested: weight,
@@ -208,32 +153,57 @@ impl Query {
});
}
self.format(*metric, &params.range)
Ok(ResolvedQuery {
vecs,
format: params.format(),
version,
total,
start,
end,
})
}
/// Search and format bulk metrics
pub fn search_and_format_bulk(&self, params: MetricSelection) -> Result<Output> {
self.search_and_format_bulk_checked(params, usize::MAX)
}
/// Format a resolved query (expensive).
/// Call after ETag/cache checks to avoid unnecessary work.
pub fn format(&self, resolved: ResolvedQuery) -> Result<MetricOutput> {
let ResolvedQuery {
vecs,
format,
version,
total,
start,
end,
} = resolved;
/// Search and format bulk metrics with weight limit (for DDoS prevention)
pub fn search_and_format_bulk_checked(
&self,
params: MetricSelection,
max_weight: usize,
) -> Result<Output> {
let vecs = self.search(&params)?;
let output = match format {
Format::CSV => Output::CSV(Self::columns_to_csv(&vecs, start, end)?),
Format::JSON => {
if vecs.len() == 1 {
let mut buf = Vec::new();
MetricData::serialize(vecs[0], start, end, &mut buf)?;
Output::Json(buf)
} else {
let mut buf = Vec::new();
buf.push(b'[');
for (i, vec) in vecs.iter().enumerate() {
if i > 0 {
buf.push(b',');
}
MetricData::serialize(*vec, start, end, &mut buf)?;
}
buf.push(b']');
Output::Json(buf)
}
}
};
let min_len = vecs.iter().map(|v| v.len()).min().expect("search guarantees non-empty");
let weight = Self::weight(&vecs, params.start(), params.end_for_len(min_len));
if weight > max_weight {
return Err(Error::WeightExceeded {
requested: weight,
max: max_weight,
});
}
self.format_bulk(&vecs, &params.range)
Ok(MetricOutput {
output,
version,
total,
start,
end,
})
}
pub fn metric_to_index_to_vec(&self) -> &BTreeMap<&str, IndexToVec<'_>> {
+42 -50
View File
@@ -1,73 +1,65 @@
use brk_error::{Error, Result};
use brk_types::Format;
use vecdb::AnyExportableVec;
use brk_error::Result;
use brk_types::{Format, LegacyValue, MetricOutputLegacy, OutputLegacy};
use crate::{DataRangeFormat, LegacyValue, MetricSelection, OutputLegacy, Query};
use crate::{Query, ResolvedQuery};
impl Query {
/// Deprecated - raw data without MetricData wrapper
pub fn format_legacy(&self, metrics: &[&dyn AnyExportableVec], params: &DataRangeFormat) -> Result<OutputLegacy> {
let min_len = metrics.iter().map(|v| v.len()).min().unwrap_or(0);
/// Deprecated - format a resolved query as legacy output (expensive).
pub fn format_legacy(&self, resolved: ResolvedQuery) -> Result<MetricOutputLegacy> {
let ResolvedQuery {
vecs,
format,
version,
total,
start,
end,
} = resolved;
let from = params
.start()
.map(|start| metrics.iter().map(|v| v.i64_to_usize(start)).min().unwrap_or_default());
if vecs.is_empty() {
return Ok(MetricOutputLegacy {
output: OutputLegacy::default(format),
version: 0,
total: 0,
start: 0,
end: 0,
});
}
let to = params
.end_for_len(min_len)
.map(|end| metrics.iter().map(|v| v.i64_to_usize(end)).min().unwrap_or_default());
let from = Some(start as i64);
let to = Some(end as i64);
let format = params.format();
Ok(match format {
Format::CSV => OutputLegacy::CSV(Self::columns_to_csv(metrics, from.map(|v| v as i64), to.map(|v| v as i64))?),
let output = match format {
Format::CSV => OutputLegacy::CSV(Self::columns_to_csv(&vecs, start, end)?),
Format::JSON => {
if metrics.is_empty() {
return Ok(OutputLegacy::default(format));
}
if metrics.len() == 1 {
let metric = metrics[0];
let count = metric.range_count(from.map(|v| v as i64), to.map(|v| v as i64));
if vecs.len() == 1 {
let metric = vecs[0];
let count = metric.range_count(from, to);
let mut buf = Vec::new();
if count == 1 {
metric.write_json_value(from, &mut buf)?;
metric.write_json_value(Some(start), &mut buf)?;
OutputLegacy::Json(LegacyValue::Value(buf))
} else {
metric.write_json(from, to, &mut buf)?;
metric.write_json(Some(start), Some(end), &mut buf)?;
OutputLegacy::Json(LegacyValue::List(buf))
}
} else {
let mut values = Vec::with_capacity(metrics.len());
for vec in metrics {
let mut values = Vec::with_capacity(vecs.len());
for vec in &vecs {
let mut buf = Vec::new();
vec.write_json(from, to, &mut buf)?;
vec.write_json(Some(start), Some(end), &mut buf)?;
values.push(buf);
}
OutputLegacy::Json(LegacyValue::Matrix(values))
}
}
};
Ok(MetricOutputLegacy {
output,
version,
total,
start,
end,
})
}
/// Deprecated - use search_and_format instead
pub fn search_and_format_legacy(&self, params: MetricSelection) -> Result<OutputLegacy> {
self.search_and_format_legacy_checked(params, usize::MAX)
}
/// Deprecated - use search_and_format_checked instead
pub fn search_and_format_legacy_checked(&self, params: MetricSelection, max_weight: usize) -> Result<OutputLegacy> {
let vecs = self.search(&params)?;
let min_len = vecs.iter().map(|v| v.len()).min().expect("search guarantees non-empty");
let weight = Self::weight(&vecs, params.start(), params.end_for_len(min_len));
if weight > max_weight {
return Err(Error::WeightExceeded {
requested: weight,
max: max_weight,
});
}
self.format_legacy(&vecs, &params.range)
}
}
+2 -7
View File
@@ -13,20 +13,15 @@ use vecdb::AnyStoredVec;
#[cfg(feature = "tokio")]
mod r#async;
mod output;
mod resolved;
mod vecs;
mod r#impl;
#[cfg(feature = "tokio")]
pub use r#async::*;
pub use brk_types::{
DataRange, DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics,
Pagination, PaginationIndex,
};
pub use r#impl::BLOCK_TXS_PAGE_SIZE;
pub use output::{LegacyValue, Output, OutputLegacy};
use resolved::ResolvedQuery;
pub use vecs::Vecs;
#[derive(Clone)]
+23
View File
@@ -0,0 +1,23 @@
use brk_types::{Etag, Format};
use vecdb::AnyExportableVec;
/// A resolved metric query ready for formatting.
/// Contains the vecs and metadata needed to build an ETag or format the output.
pub struct ResolvedQuery {
pub(crate) vecs: Vec<&'static dyn AnyExportableVec>,
pub(crate) format: Format,
pub(crate) version: u64,
pub(crate) total: usize,
pub(crate) start: usize,
pub(crate) end: usize,
}
impl ResolvedQuery {
pub fn etag(&self) -> Etag {
Etag::from_metric(self.version, self.total, self.start, self.end)
}
pub fn format(&self) -> Format {
self.format
}
}
-1
View File
@@ -18,7 +18,6 @@ brk_error = { workspace = true }
brk_fetcher = { workspace = true }
brk_indexer = { workspace = true }
brk_logger = { workspace = true }
brk_mcp = { workspace = true }
brk_query = { workspace = true }
brk_reader = { workspace = true }
brk_rpc = { workspace = true }
+24 -31
View File
@@ -2,57 +2,50 @@
HTTP API server for Bitcoin on-chain analytics.
## What It Enables
## Features
Serve BRK data via REST API with OpenAPI documentation, response caching, MCP endpoint, and optional static file hosting for web interfaces.
## Key Features
- **OpenAPI spec**: Auto-generated documentation at `/api.json` (interactive docs at `/api`)
- **Response caching**: LRU cache with 5000 entries for repeated queries
- **Compression**: Brotli, gzip, deflate, zstd support
- **OpenAPI spec**: Auto-generated docs at `/api` with full spec at `/api.json`
- **LLM-optimized**: Compact spec at `/api.trimmed.json` for AI tools
- **Response caching**: ETag-based with LRU cache (5000 entries)
- **Compression**: Brotli, gzip, deflate, zstd
- **Static files**: Optional web interface hosting
- **Request logging**: Colorized status/latency logging
## Core API
## Usage
```rust,ignore
let server = Server::new(&async_query, data_path, WebsiteSource::Filesystem(files_path));
// Or WebsiteSource::Embedded, or WebsiteSource::Disabled
server.serve(true).await?; // true enables MCP endpoint
server.serve().await?;
```
## API Endpoints
## Endpoints
| Path | Description |
|------|-------------|
| `/api/block-height/{height}` | Block by height |
| `/api` | Interactive API documentation |
| `/api.json` | Full OpenAPI specification |
| `/api.trimmed.json` | Compact OpenAPI for LLMs |
| `/api/address/{address}` | Address stats, transactions, UTXOs |
| `/api/block/{hash}` | Block info, transactions, status |
| `/api/block-height/{height}` | Block by height |
| `/api/tx/{txid}` | Transaction details, status, hex |
| `/api/address/{addr}` | Address stats, transactions, UTXOs |
| `/api/mempool` | Fee estimates, mempool stats |
| `/api/metrics` | Metric catalog and data queries |
| `/api/v1/mining/*` | Hashrate, difficulty, pools, rewards |
| `/api/mempool/*` | Fee estimates, projected blocks |
| `/mcp` | MCP endpoint (if enabled) |
| `/api/v1/mining/...` | Hashrate, difficulty, pools |
## Caching
Uses ETag-based caching with must-revalidate semantics:
- Height-indexed data: Cache until height changes
- Date-indexed data: Cache with longer TTL
- Mempool data: Short TTL, frequent updates
Uses ETag-based caching with `must-revalidate`:
- **Height-indexed**: Invalidates when new block arrives
- **Immutable**: 1-year cache for deeply-confirmed blocks/txs (6+ confirmations)
- **Mempool**: Short max-age, no ETag
## Configuration
Server binds to port 3110 by default, auto-incrementing if busy (up to 3210).
Binds to port 3110, auto-incrementing up to 3210 if busy.
## Recommended: mimalloc v3
## Dependencies
Use [mimalloc v3](https://crates.io/crates/mimalloc) as the global allocator to reduce memory usage.
## Built On
- `brk_query` for data access
- `brk_mcp` for MCP protocol
- `aide` + `axum` for HTTP routing and OpenAPI
- `tower-http` for compression and tracing
- `brk_query` - data access
- `aide` + `axum` - HTTP routing and OpenAPI
- `tower-http` - compression and tracing
+1 -1
View File
@@ -56,7 +56,7 @@ fn run() -> Result<()> {
runtime.block_on(async move {
let server = Server::new(&query, outputs_dir, WebsiteSource::Disabled);
let handle = tokio::spawn(async move { server.serve(true).await });
let handle = tokio::spawn(async move { server.serve().await });
// Await the handle to catch both panics and errors
match handle.await {
-59
View File
@@ -1,59 +0,0 @@
// use lru::LruCache;
// use std::num::NonZeroUsize;
// struct SmartBlkReader {
// // LRU cache of recently accessed files (memory mapped)
// mmap_cache: LruCache<String, memmap2::Mmap>,
// // Fallback to direct file I/O for cache misses
// max_cached_files: usize,
// }
// impl SmartBlkReader {
// fn new(max_cached: usize) -> Self {
// Self {
// mmap_cache: LruCache::new(NonZeroUsize::new(max_cached).unwrap()),
// max_cached_files: max_cached,
// }
// }
// fn get_transaction(
// &mut self,
// file_path: &str,
// offset: u64,
// length: usize,
// ) -> Result<Transaction, Box<dyn std::error::Error>> {
// // Try cache first
// if let Some(mmap) = self.mmap_cache.get(file_path) {
// let tx_data = &mmap[offset as usize..(offset as usize + length)];
// let mut cursor = std::io::Cursor::new(tx_data);
// return Ok(bitcoin::consensus::Decodable::consensus_decode(
// &mut cursor,
// )?);
// }
// // Cache miss - use direct I/O and potentially cache the file
// let mut file = File::open(file_path)?;
// file.seek(SeekFrom::Start(offset))?;
// let mut buffer = vec![0u8; length];
// file.read_exact(&mut buffer)?;
// // Optionally add to cache (based on access patterns)
// if self.should_cache_file(file_path) {
// let file_for_mmap = File::open(file_path)?;
// if let Ok(mmap) = unsafe { memmap2::MmapOptions::new().map(&file_for_mmap) } {
// self.mmap_cache.put(file_path.to_string(), mmap);
// }
// }
// let mut cursor = std::io::Cursor::new(&buffer);
// Ok(bitcoin::consensus::Decodable::consensus_decode(
// &mut cursor,
// )?)
// }
// fn should_cache_file(&self, _file_path: &str) -> bool {
// // Implement logic: recent files, frequently accessed files, etc.
// true
// }
// }
+5 -5
View File
@@ -44,7 +44,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block(&path.hash)).await
state.cached_json(&headers, CacheStrategy::Static, move |q| q.block(&path.hash)).await
},
|op| {
op.id("get_block")
@@ -135,7 +135,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txids(&path.hash)).await
state.cached_json(&headers, CacheStrategy::Static, move |q| q.block_txids(&path.hash)).await
},
|op| {
op.id("get_block_txids")
@@ -158,7 +158,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashStartIndex>,
State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txs(&path.hash, path.start_index)).await
state.cached_json(&headers, CacheStrategy::Static, move |q| q.block_txs(&path.hash, path.start_index)).await
},
|op| {
op.id("get_block_txs")
@@ -182,7 +182,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashTxIndex>,
State(state): State<AppState>| {
state.cached_text(&headers, CacheStrategy::Height, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
state.cached_text(&headers, CacheStrategy::Static, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
},
|op| {
op.id("get_block_txid")
@@ -226,7 +226,7 @@ impl BlockRoutes for ApiRouter<AppState> {
async |headers: HeaderMap,
Path(path): Path<BlockHashParam>,
State(state): State<AppState>| {
state.cached_bytes(&headers, CacheStrategy::Height, move |q| q.block_raw(&path.hash)).await
state.cached_bytes(&headers, CacheStrategy::Static, move |q| q.block_raw(&path.hash)).await
},
|op| {
op.id("get_block_raw")
+21 -27
View File
@@ -6,12 +6,12 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_query::{MetricSelection, Output};
use brk_types::Format;
use brk_types::{Format, MetricSelection, Output};
use quick_cache::sync::GuardResult;
use crate::{
CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
use super::AppState;
@@ -39,35 +39,32 @@ async fn req_to_response_res(
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
) -> brk_error::Result<Response> {
let format = params.format();
let height = query.sync(|q| q.height());
// Phase 1: Search and resolve metadata (cheap)
let resolved = query
.run(move |q| q.resolve(params, MAX_WEIGHT))
.await?;
let cache_params =
CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || {
height.into()
});
let format = resolved.format();
let etag = resolved.etag();
if cache_params.matches_etag(&headers) {
// Check if client has fresh cache
if headers.has_etag(etag.as_str()) {
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
response.headers_mut().insert_cors();
return Ok(response);
}
let cache_key = format!(
"{}{}{}",
uri.path(),
uri.query().unwrap_or(""),
cache_params.etag_str()
);
// Check server-side cache
let cache_key = format!("bulk-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag);
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
let mut response = if let GuardResult::Value(v) = guard_res {
Response::new(Body::from(v))
} else {
match query
.run(move |q| q.search_and_format_bulk_checked(params, MAX_WEIGHT))
.await?
{
// Phase 2: Format (expensive, only on cache miss)
let metric_output = query.run(move |q| q.format(resolved)).await?;
match metric_output.output {
Output::CSV(s) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(s.clone().into());
@@ -75,21 +72,18 @@ async fn req_to_response_res(
s.into_response()
}
Output::Json(v) => {
let json = v.to_vec();
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(json.clone().into());
let _ = g.insert(v.clone().into());
}
json.into_response()
Response::new(Body::from(v))
}
}
};
let headers = response.headers_mut();
headers.insert_cors();
if let Some(etag) = &cache_params.etag {
headers.insert_etag(etag);
}
headers.insert_cache_control(&cache_params.cache_control);
headers.insert_etag(etag.as_str());
headers.insert_cache_control(CACHE_CONTROL);
match format {
Format::CSV => {
+21 -27
View File
@@ -6,12 +6,12 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_query::{MetricSelection, Output};
use brk_types::Format;
use brk_types::{Format, MetricSelection, Output};
use quick_cache::sync::GuardResult;
use crate::{
CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
use super::AppState;
@@ -39,35 +39,32 @@ async fn req_to_response_res(
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
) -> brk_error::Result<Response> {
let format = params.format();
let height = query.sync(|q| q.height());
// Phase 1: Search and resolve metadata (cheap)
let resolved = query
.run(move |q| q.resolve(params, MAX_WEIGHT))
.await?;
let cache_params =
CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || {
height.into()
});
let format = resolved.format();
let etag = resolved.etag();
if cache_params.matches_etag(&headers) {
// Check if client has fresh cache
if headers.has_etag(etag.as_str()) {
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
response.headers_mut().insert_cors();
return Ok(response);
}
let cache_key = format!(
"single-{}{}{}",
uri.path(),
uri.query().unwrap_or(""),
cache_params.etag_str()
);
// Check server-side cache
let cache_key = format!("single-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag);
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
let mut response = if let GuardResult::Value(v) = guard_res {
Response::new(Body::from(v))
} else {
match query
.run(move |q| q.search_and_format_checked(params, MAX_WEIGHT))
.await?
{
// Phase 2: Format (expensive, only on cache miss)
let metric_output = query.run(move |q| q.format(resolved)).await?;
match metric_output.output {
Output::CSV(s) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(s.clone().into());
@@ -75,21 +72,18 @@ async fn req_to_response_res(
s.into_response()
}
Output::Json(v) => {
let json = v.to_vec();
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(json.clone().into());
let _ = g.insert(v.clone().into());
}
json.into_response()
Response::new(Body::from(v))
}
}
};
let headers = response.headers_mut();
headers.insert_cors();
if let Some(etag) = &cache_params.etag {
headers.insert_etag(etag);
}
headers.insert_cache_control(&cache_params.cache_control);
headers.insert_etag(etag.as_str());
headers.insert_cache_control(CACHE_CONTROL);
match format {
Format::CSV => {
+21 -24
View File
@@ -6,12 +6,12 @@ use axum::{
http::{HeaderMap, StatusCode, Uri},
response::{IntoResponse, Response},
};
use brk_query::{MetricSelection, OutputLegacy};
use brk_types::Format;
use brk_types::{Format, MetricSelection, OutputLegacy};
use quick_cache::sync::GuardResult;
use crate::{
CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended,
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
extended::HeaderMapExtended,
};
use super::AppState;
@@ -39,35 +39,34 @@ async fn req_to_response_res(
Query(params): Query<MetricSelection>,
AppState { query, cache, .. }: AppState,
) -> brk_error::Result<Response> {
let format = params.format();
let height = query.sync(|q| q.height());
// Phase 1: Search and resolve metadata (cheap)
let resolved = query
.run(move |q| q.resolve(params, MAX_WEIGHT))
.await?;
let cache_params =
CacheParams::resolve(&CacheStrategy::height_with(params.etag_suffix()), || {
height.into()
});
let format = resolved.format();
let etag = resolved.etag();
if cache_params.matches_etag(&headers) {
// Check if client has fresh cache
if headers.has_etag(etag.as_str()) {
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
response.headers_mut().insert_cors();
return Ok(response);
}
let cache_key = format!(
"legacy-{}{}{}",
uri.path(),
uri.query().unwrap_or(""),
cache_params.etag_str()
);
// Check server-side cache
let cache_key = format!("legacy-{}{}{}", uri.path(), uri.query().unwrap_or(""), etag);
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
let mut response = if let GuardResult::Value(v) = guard_res {
Response::new(Body::from(v))
} else {
match query
.run(move |q| q.search_and_format_legacy_checked(params, MAX_WEIGHT))
.await?
{
// Phase 2: Format (expensive, only on cache miss)
let metric_output = query
.run(move |q| q.format_legacy(resolved))
.await?;
match metric_output.output {
OutputLegacy::CSV(s) => {
if let GuardResult::Guard(g) = guard_res {
let _ = g.insert(s.clone().into());
@@ -86,10 +85,8 @@ async fn req_to_response_res(
let headers = response.headers_mut();
headers.insert_cors();
if let Some(etag) = &cache_params.etag {
headers.insert_etag(etag);
}
headers.insert_cache_control(&cache_params.cache_control);
headers.insert_etag(etag.as_str());
headers.insert_cache_control(CACHE_CONTROL);
match format {
Format::CSV => {
+6 -6
View File
@@ -4,13 +4,10 @@ use axum::{
http::{HeaderMap, Uri},
response::{IntoResponse, Response},
};
use brk_query::{
DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics, Pagination,
};
use brk_traversable::TreeNode;
use brk_types::{
Index, IndexInfo, LimitParam, Metric, MetricCount, MetricData, MetricParam, MetricWithIndex,
Metrics,
DataRangeFormat, Index, IndexInfo, LimitParam, Metric, MetricCount, MetricData, MetricParam,
MetricSelection, MetricSelectionLegacy, MetricWithIndex, Metrics, PaginatedMetrics, Pagination,
};
use crate::{CacheStrategy, extended::TransformResponseExtended};
@@ -23,6 +20,8 @@ mod legacy;
/// Maximum allowed request weight in bytes (650KB)
const MAX_WEIGHT: usize = 65 * 10_000;
/// Cache control header for metric data responses
const CACHE_CONTROL: &str = "public, max-age=1, must-revalidate";
pub trait ApiMetricsRoutes {
fn add_metrics_routes(self) -> Self;
@@ -250,7 +249,8 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
Query(params): Query<MetricSelectionLegacy>,
state: State<AppState>|
-> Response {
legacy::handler(uri, headers, Query(params.into()), state).await
let params: MetricSelection = params.into();
legacy::handler(uri, headers, Query(params), state).await
},
|op| op
.metrics_tag()
+10 -10
View File
@@ -64,7 +64,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pools/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with(format!("{:?}", path.time_period)), move |q| q.mining_pools(path.time_period)).await
state.cached_json(&headers, CacheStrategy::Height, move |q| q.mining_pools(path.time_period)).await
},
|op| {
op.id("get_pool_stats")
@@ -81,7 +81,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/pool/{slug}",
get_with(
async |headers: HeaderMap, Path(path): Path<PoolSlugParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with(path.slug), move |q| q.pool_detail(path.slug)).await
state.cached_json(&headers, CacheStrategy::Height, move |q| q.pool_detail(path.slug)).await
},
|op| {
op.id("get_pool")
@@ -99,7 +99,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/hashrate",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with("hashrate"), |q| q.hashrate(None)).await
state.cached_json(&headers, CacheStrategy::Height, |q| q.hashrate(None)).await
},
|op| {
op.id("get_hashrate")
@@ -116,7 +116,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/hashrate/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with(format!("hashrate-{:?}", path.time_period)), move |q| q.hashrate(Some(path.time_period))).await
state.cached_json(&headers, CacheStrategy::Height, move |q| q.hashrate(Some(path.time_period))).await
},
|op| {
op.id("get_hashrate_by_period")
@@ -133,7 +133,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/difficulty-adjustments",
get_with(
async |headers: HeaderMap, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with("diff-adj"), |q| q.difficulty_adjustments(None)).await
state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustments(None)).await
},
|op| {
op.id("get_difficulty_adjustments")
@@ -150,7 +150,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/difficulty-adjustments/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with(format!("diff-adj-{:?}", path.time_period)), move |q| q.difficulty_adjustments(Some(path.time_period))).await
state.cached_json(&headers, CacheStrategy::Height, move |q| q.difficulty_adjustments(Some(path.time_period))).await
},
|op| {
op.id("get_difficulty_adjustments_by_period")
@@ -167,7 +167,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/blocks/fees/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with(format!("fees-{:?}", path.time_period)), move |q| q.block_fees(path.time_period)).await
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_fees(path.time_period)).await
},
|op| {
op.id("get_block_fees")
@@ -184,7 +184,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/blocks/rewards/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with(format!("rewards-{:?}", path.time_period)), move |q| q.block_rewards(path.time_period)).await
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_rewards(path.time_period)).await
},
|op| {
op.id("get_block_rewards")
@@ -219,7 +219,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/blocks/sizes-weights/{time_period}",
get_with(
async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with(format!("sizes-{:?}", path.time_period)), move |q| q.block_sizes_weights(path.time_period)).await
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_sizes_weights(path.time_period)).await
},
|op| {
op.id("get_block_sizes_weights")
@@ -236,7 +236,7 @@ impl MiningRoutes for ApiRouter<AppState> {
"/api/v1/mining/reward-stats/{block_count}",
get_with(
async |headers: HeaderMap, Path(path): Path<BlockCountParam>, State(state): State<AppState>| {
state.cached_json(&headers, CacheStrategy::height_with(format!("reward-stats-{}", path.block_count)), move |q| q.reward_stats(path.block_count)).await
state.cached_json(&headers, CacheStrategy::Height, move |q| q.reward_stats(path.block_count)).await
},
|op| {
op.id("get_reward_stats")
+31 -12
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use aide::{
axum::ApiRouter,
axum::{ApiRouter, routing::get_with},
openapi::OpenApi,
};
use axum::{
@@ -12,13 +12,12 @@ use axum::{
};
use crate::{
VERSION,
api::{
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
metrics::ApiMetricsRoutes, mining::MiningRoutes, server::ServerRoutes,
transactions::TxRoutes,
},
extended::{HeaderMapExtended, ResponseExtended},
extended::{ResponseExtended, TransformResponseExtended},
};
use super::AppState;
@@ -48,19 +47,39 @@ impl ApiRoutes for ApiRouter<AppState> {
.add_metrics_routes()
.add_server_routes()
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
.route(
.api_route(
"/api.json",
get(
get_with(
async |headers: HeaderMap,
Extension(api): Extension<Arc<OpenApi>>|
-> Response { Response::static_json(&headers, &*api) },
|op| {
op.id("get_openapi")
.server_tag()
.summary("OpenAPI specification")
.description("Full OpenAPI 3.1 specification for this API.")
},
),
)
.api_route(
"/api.trimmed.json",
get_with(
async |headers: HeaderMap,
Extension(api_trimmed): Extension<Arc<String>>|
-> Response {
let etag = VERSION;
if headers.has_etag(etag) {
return Response::new_not_modified();
}
Response::new_json(&api, etag)
let value: serde_json::Value =
serde_json::from_str(&api_trimmed).unwrap();
Response::static_json(&headers, &value)
},
|op| {
op.id("get_openapi_trimmed")
.server_tag()
.summary("Trimmed OpenAPI specification")
.description(
"Compact OpenAPI specification optimized for LLM consumption. \
Removes redundant fields while preserving essential API information.",
)
.ok_response::<serde_json::Value>()
},
),
)
@@ -1,5 +1,3 @@
use aide::openapi::{Contact, Info, License, OpenApi, Tag};
//
// https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html
//
@@ -12,6 +10,12 @@ use aide::openapi::{Contact, Info, License, OpenApi, Tag};
// - https://api.supabase.com/api/v1
//
mod trim;
pub use trim::trim_openapi_json;
use aide::openapi::{Contact, Info, License, OpenApi, Tag};
use crate::VERSION;
pub fn create_openapi() -> OpenApi {
@@ -25,10 +29,11 @@ pub fn create_openapi() -> OpenApi {
- **Metrics**: Thousands of time-series metrics across multiple indexes (date, block height, etc.)
- **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-metrics endpoints follow the mempool.space API format
- **Multiple formats**: JSON and CSV output
- **LLM-optimized**: Compact OpenAPI spec at [`/api.trimmed.json`](/api.trimmed.json) for AI tools
### Client Libraries
- [JavaScript/TypeScript](https://www.npmjs.com/package/brk-client)
- [JavaScript](https://www.npmjs.com/package/brk-client)
- [Python](https://pypi.org/project/brk-client/)
- [Rust](https://crates.io/crates/brk_client)
@@ -56,6 +61,13 @@ pub fn create_openapi() -> OpenApi {
};
let tags = vec![
Tag {
name: "Server".to_string(),
description: Some(
"API metadata, health monitoring, and OpenAPI specifications.".to_string(),
),
..Default::default()
},
Tag {
name: "Metrics".to_string(),
description: Some(
@@ -115,14 +127,6 @@ pub fn create_openapi() -> OpenApi {
),
..Default::default()
},
Tag {
name: "Server".to_string(),
description: Some(
"API metadata and health monitoring. Version information and service status."
.to_string(),
),
..Default::default()
},
];
OpenApi {
+447
View File
@@ -0,0 +1,447 @@
use serde_json::{Map, Value};
/// Trims an OpenAPI spec JSON to reduce size for LLM consumption.
/// Removes redundant fields while preserving essential API information.
///
/// Transformations applied (in order):
/// 1. Remove error responses (304, 400, 404, 500)
/// 2. Compact responses to "returns": "Type"
/// 3. Remove per-endpoint tags and style
/// 4. Simplify parameter schema to type, remove param descriptions
/// 5. Remove summary
/// 6. Remove examples, replace $ref with type
/// 7. Flatten single-item allOf
/// 8. Flatten anyOf to type array
/// 9. Remove format
/// 10. Remove property descriptions
/// 11. Simplify properties to direct types
pub fn trim_openapi_json(json: &str) -> String {
let mut spec: Value = serde_json::from_str(json).expect("Invalid OpenAPI JSON");
trim_value(&mut spec);
serde_json::to_string(&spec).unwrap()
}
fn trim_value(value: &mut Value) {
match value {
Value::Object(obj) => {
// Step 1: Remove error responses
if let Some(Value::Object(responses)) = obj.get_mut("responses") {
for code in &["304", "400", "404", "500"] {
responses.remove(*code);
}
}
// Step 2: Compact responses to "returns": "Type"
if let Some(Value::Object(responses)) = obj.remove("responses")
&& let Some(returns) = extract_return_type(&responses)
{
obj.insert("returns".to_string(), Value::String(returns));
}
// Step 3: Remove per-endpoint tags and style
// (only remove "tags" if it's an array, not if it's the top-level tags definition)
if let Some(Value::Array(_)) = obj.get("tags") {
// This is a per-endpoint tags array like ["Addresses"], remove it
obj.remove("tags");
}
obj.remove("style");
// Step 4: Simplify parameters (schema to type, remove descriptions)
if let Some(Value::Array(params)) = obj.get_mut("parameters") {
for param in params {
simplify_parameter(param);
}
}
// Step 5: Remove summary
obj.remove("summary");
// Step 6: Remove examples, replace $ref with type
obj.remove("example");
obj.remove("examples");
if let Some(Value::String(ref_path)) = obj.remove("$ref") {
let type_name = ref_path.split('/').next_back().unwrap_or("any");
obj.insert("type".to_string(), Value::String(type_name.to_string()));
}
// Step 7: Flatten single-item allOf
if let Some(Value::Array(all_of)) = obj.remove("allOf")
&& all_of.len() == 1
&& let Some(Value::Object(inner)) = all_of.into_iter().next()
{
for (k, v) in inner {
obj.insert(k, v);
}
}
// Step 8: Flatten anyOf to type array
if let Some(Value::Array(any_of)) = obj.remove("anyOf") {
let types: Vec<Value> = any_of
.into_iter()
.filter_map(|item| {
if let Value::Object(o) = item {
if let Some(Value::String(ref_path)) = o.get("$ref") {
return Some(Value::String(
ref_path.split('/').next_back().unwrap_or("any").to_string(),
));
}
o.get("type").cloned()
} else {
None
}
})
.collect();
if !types.is_empty() {
obj.insert("type".to_string(), Value::Array(types));
}
}
// Step 9: Remove format
obj.remove("format");
// Step 10 & 11: Simplify properties (remove descriptions, simplify to direct types)
if let Some(Value::Object(props)) = obj.get_mut("properties") {
simplify_properties(props);
}
// Recurse into remaining values
for (_, v) in obj.iter_mut() {
trim_value(v);
}
}
Value::Array(arr) => {
for item in arr {
trim_value(item);
}
}
_ => {}
}
}
fn extract_return_type(responses: &Map<String, Value>) -> Option<String> {
let resp_200 = responses.get("200")?;
let content = resp_200.get("content")?;
let json_content = content.get("application/json")?;
let schema = json_content.get("schema")?;
Some(schema_to_type_string(schema))
}
fn schema_to_type_string(schema: &Value) -> String {
if let Some(Value::String(ref_path)) = schema.get("$ref") {
return ref_path.split('/').next_back().unwrap_or("any").to_string();
}
if let Some(Value::String(t)) = schema.get("type") {
if t == "array"
&& let Some(items) = schema.get("items")
{
return format!("array[{}]", schema_to_type_string(items));
}
return t.clone();
}
"any".to_string()
}
fn simplify_parameter(param: &mut Value) {
if let Value::Object(obj) = param {
// Remove description
obj.remove("description");
// Extract type from schema
if let Some(schema) = obj.remove("schema") {
let type_val = extract_type_from_schema(&schema);
obj.insert("type".to_string(), type_val);
}
}
}
fn extract_type_from_schema(schema: &Value) -> Value {
if let Value::Object(obj) = schema {
// Handle anyOf (optional fields)
if let Some(Value::Array(any_of)) = obj.get("anyOf") {
let types: Vec<Value> = any_of
.iter()
.filter_map(|item| {
if let Value::Object(o) = item {
if let Some(Value::String(ref_path)) = o.get("$ref") {
return Some(Value::String(
ref_path.split('/').next_back().unwrap_or("any").to_string(),
));
}
o.get("type").cloned()
} else {
None
}
})
.collect();
if types.len() == 1 {
return types.into_iter().next().unwrap();
}
return Value::Array(types);
}
// Handle $ref
if let Some(Value::String(ref_path)) = obj.get("$ref") {
return Value::String(ref_path.split('/').next_back().unwrap_or("any").to_string());
}
// Handle type
if let Some(t) = obj.get("type") {
return t.clone();
}
}
Value::String("any".to_string())
}
fn simplify_properties(props: &mut Map<String, Value>) {
let keys: Vec<String> = props.keys().cloned().collect();
for key in keys {
if let Some(prop_value) = props.get_mut(&key)
&& let Value::Object(prop_obj) = prop_value
{
// Remove description
prop_obj.remove("description");
// Check if we can simplify to just the type
let simplified = simplify_property_value(prop_obj);
*prop_value = simplified;
}
}
}
fn simplify_property_value(obj: &mut Map<String, Value>) -> Value {
// Remove validation constraints
for key in &["default", "minItems", "maxItems", "uniqueItems"] {
obj.remove(*key);
}
// Handle $ref - convert to type (runs before recursion would)
if let Some(Value::String(ref_path)) = obj.remove("$ref") {
let type_name = ref_path.split('/').next_back().unwrap_or("any");
return Value::String(type_name.to_string());
}
// Handle single-item allOf - flatten and extract type
if let Some(Value::Array(all_of)) = obj.remove("allOf")
&& all_of.len() == 1
&& let Some(Value::Object(inner)) = all_of.into_iter().next()
{
if let Some(Value::String(ref_path)) = inner.get("$ref") {
let type_name = ref_path.split('/').next_back().unwrap_or("any");
return Value::String(type_name.to_string());
}
if let Some(t) = inner.get("type") {
return t.clone();
}
}
// Handle anyOf - flatten to type array (runs before recursion would)
if let Some(Value::Array(any_of)) = obj.remove("anyOf") {
let types: Vec<Value> = any_of
.into_iter()
.filter_map(|item| {
if let Value::Object(o) = item {
if let Some(Value::String(ref_path)) = o.get("$ref") {
return Some(Value::String(
ref_path.split('/').next_back().unwrap_or("any").to_string(),
));
}
o.get("type").cloned()
} else {
None
}
})
.collect();
return Value::Array(types);
}
// If only "type" remains, return just the type value
if obj.len() == 1
&& let Some(t) = obj.get("type")
{
return t.clone();
}
// Handle array with items
if obj.get("type") == Some(&Value::String("array".to_string()))
&& let Some(items) = obj.get("items")
&& let Value::Object(items_obj) = items
&& items_obj.len() == 1
{
// Items can have either "type" or "$ref"
if let Some(Value::String(item_type)) = items_obj.get("type") {
return Value::String(format!("array[{}]", item_type));
}
if let Some(Value::String(ref_path)) = items_obj.get("$ref") {
let type_name = ref_path.split('/').next_back().unwrap_or("any");
return Value::String(format!("array[{}]", type_name));
}
}
Value::Object(obj.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trim_property_anyof() {
let input = r##"{
"type": "object",
"properties": {
"index": {
"anyOf": [
{"type": "TxIndex"},
{"type": "null"}
]
}
}
}"##;
let result = trim_openapi_json(input);
let parsed: Value = serde_json::from_str(&result).unwrap();
// Property should be simplified to array, not {"type": [...]}
let index = &parsed["properties"]["index"];
assert!(index.is_array(), "Expected array, got: {}", index);
assert_eq!(index[0], "TxIndex");
assert_eq!(index[1], "null");
}
#[test]
fn test_trim_parameter_anyof() {
let input = r##"{
"parameters": [{
"in": "query",
"name": "after_txid",
"schema": {
"anyOf": [
{"$ref": "#/components/schemas/Txid"},
{"type": "null"}
]
}
}]
}"##;
let result = trim_openapi_json(input);
let parsed: Value = serde_json::from_str(&result).unwrap();
// Parameter should have type array including null
let param = &parsed["parameters"][0];
assert_eq!(param["name"], "after_txid");
assert_eq!(param["type"][0], "Txid");
assert_eq!(param["type"][1], "null");
}
#[test]
fn test_trim_property_ref() {
let input = r##"{
"type": "object",
"properties": {
"txid": {
"$ref": "#/components/schemas/Txid"
}
}
}"##;
let result = trim_openapi_json(input);
let parsed: Value = serde_json::from_str(&result).unwrap();
// Property with $ref should be simplified to just the type name
assert_eq!(parsed["properties"]["txid"], "Txid");
}
#[test]
fn test_trim_nested_component_schema() {
// This matches the actual API structure: components > schemas > Type > properties
let input = r##"{
"components": {
"schemas": {
"AddressStats": {
"type": "object",
"properties": {
"address": {
"$ref": "#/components/schemas/Address"
},
"chain_stats": {
"$ref": "#/components/schemas/AddressChainStats"
}
}
}
}
}
}"##;
let result = trim_openapi_json(input);
let parsed: Value = serde_json::from_str(&result).unwrap();
let props = &parsed["components"]["schemas"]["AddressStats"]["properties"];
assert_eq!(props["address"], "Address", "address should be simplified");
assert_eq!(props["chain_stats"], "AddressChainStats", "chain_stats should be simplified");
}
#[test]
fn test_trim_property_allof_with_ref() {
// Real API uses allOf wrapper around $ref
let input = r##"{
"type": "object",
"properties": {
"address": {
"description": "Bitcoin address string",
"allOf": [
{"$ref": "#/components/schemas/Address"}
]
}
}
}"##;
let result = trim_openapi_json(input);
let parsed: Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["properties"]["address"], "Address");
}
#[test]
fn test_trim_property_array_with_ref() {
let input = r##"{
"type": "object",
"properties": {
"vin": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TxIn"
}
}
}
}"##;
let result = trim_openapi_json(input);
let parsed: Value = serde_json::from_str(&result).unwrap();
// Array with $ref items should be simplified to "array[Type]"
assert_eq!(parsed["properties"]["vin"], "array[TxIn]");
}
#[test]
fn test_trim_responses_to_returns() {
let input = r##"{
"responses": {
"200": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Block"}
}
}
},
"400": {"description": "Bad request"},
"500": {"description": "Error"}
}
}"##;
let result = trim_openapi_json(input);
let parsed: Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["returns"], "Block");
assert!(parsed.get("responses").is_none());
}
}
+12 -61
View File
@@ -2,44 +2,14 @@ use axum::http::HeaderMap;
use crate::{VERSION, extended::HeaderMapExtended};
/// Minimum confirmations before data is considered immutable
pub const MIN_CONFIRMATIONS: u32 = 6;
/// Cache strategy for HTTP responses.
///
/// # Future optimization: Immutable caching for blocks/txs
///
/// The `Immutable` variant supports caching deeply-confirmed blocks/txs forever
/// (1 year, `immutable` directive). To use it, you need the confirmation count:
///
/// ```ignore
/// // Example: cache block by hash as immutable if deeply confirmed
/// let confirmations = current_height - block_height + 1;
/// let prefix = *BlockHashPrefix::from(&hash);
/// state.cached_json(&headers, CacheStrategy::immutable(prefix, confirmations), |q| q.block(&hash)).await
/// ```
///
/// Currently all block/tx handlers use `Height` for simplicity since determining
/// confirmations requires knowing the block height upfront (an extra lookup).
/// This could be optimized by either:
/// 1. Including confirmation count in the response type
/// 2. Doing a lightweight height lookup before the main query
pub enum CacheStrategy {
/// Immutable data (blocks by hash with 6+ confirmations)
/// Etag = VERSION-{prefix:x}, Cache-Control: immutable, 1yr
/// Falls back to Height if < 6 confirmations
Immutable { prefix: u64, confirmations: u32 },
/// Data that changes with each new block (addresses, block-by-height)
/// Data that changes with each new block (addresses, mining stats, txs, outspends)
/// Etag = VERSION-{height}, Cache-Control: must-revalidate
Height,
/// Data that changes with height + depends on parameter
/// Etag = VERSION-{height}-{suffix}, Cache-Control: must-revalidate
HeightWith(String),
/// Static data (validate-address, metrics catalog)
/// Etag = VERSION only, Cache-Control: 1hr
/// Static/immutable data (blocks by hash, validate-address, metrics catalog)
/// Etag = VERSION only, Cache-Control: must-revalidate
Static,
/// Volatile data (mempool) - no etag, just max-age
@@ -47,21 +17,6 @@ pub enum CacheStrategy {
MaxAge(u64),
}
impl CacheStrategy {
/// Create Immutable strategy - pass *prefix (deref BlockHashPrefix/TxidPrefix to u64)
pub fn immutable(prefix: u64, confirmations: u32) -> Self {
Self::Immutable {
prefix,
confirmations,
}
}
/// Create HeightWith from any Display type
pub fn height_with(suffix: impl std::fmt::Display) -> Self {
Self::HeightWith(suffix.to_string())
}
}
/// Resolved cache parameters
pub struct CacheParams {
pub etag: Option<String>,
@@ -69,32 +24,28 @@ pub struct CacheParams {
}
impl CacheParams {
/// Cache params using VERSION as etag
pub fn version() -> Self {
Self::resolve(&CacheStrategy::Static, || unreachable!())
}
pub fn etag_str(&self) -> &str {
self.etag.as_deref().unwrap_or("")
}
pub fn matches_etag(&self, headers: &HeaderMap) -> bool {
self.etag.as_ref().is_some_and(|etag| headers.has_etag(etag))
self.etag
.as_ref()
.is_some_and(|etag| headers.has_etag(etag))
}
pub fn resolve(strategy: &CacheStrategy, height: impl FnOnce() -> u32) -> Self {
use CacheStrategy::*;
match strategy {
Immutable {
prefix,
confirmations,
} if *confirmations >= MIN_CONFIRMATIONS => Self {
etag: Some(format!("{VERSION}-{prefix:x}")),
cache_control: "public, max-age=31536000, immutable".into(),
},
Immutable { .. } | Height => Self {
Height => Self {
etag: Some(format!("{VERSION}-{}", height())),
cache_control: "public, max-age=1, must-revalidate".into(),
},
HeightWith(suffix) => Self {
etag: Some(format!("{VERSION}-{}-{suffix}", 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(),
+15 -1
View File
@@ -1,6 +1,6 @@
use axum::{
body::Body,
http::{Response, StatusCode},
http::{HeaderMap, Response, StatusCode},
response::IntoResponse,
};
use serde::Serialize;
@@ -20,6 +20,9 @@ where
where
T: Serialize;
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 new_text(value: &str, etag: &str) -> Self;
@@ -108,6 +111,17 @@ impl ResponseExtended for Response<Body> {
response
}
fn static_json<T>(headers: &HeaderMap, value: T) -> Self
where
T: Serialize,
{
let params = CacheParams::version();
if params.matches_etag(headers) {
return Self::new_not_modified();
}
Self::new_json_cached(value, &params)
}
fn new_text_cached(value: &str, params: &CacheParams) -> Self {
let mut response = Response::builder()
.body(value.to_string().into())
+4 -9
View File
@@ -13,7 +13,6 @@ use axum::{
serve,
};
use brk_error::Result;
use brk_mcp::route::mcp_router;
use brk_query::AsyncQuery;
use include_dir::{include_dir, Dir};
use quick_cache::sync::Cache;
@@ -67,7 +66,7 @@ impl Server {
})
}
pub async fn serve(self, mcp: bool) -> Result<()> {
pub async fn serve(self) -> Result<()> {
let state = self.0;
let compression_layer = CompressionLayer::new()
@@ -159,6 +158,8 @@ impl Server {
.python(workspace_root.join("packages/brk_client/brk_client/__init__.py"));
let openapi_json = Arc::new(serde_json::to_string(&openapi).unwrap());
let openapi_trimmed = Arc::new(trim_openapi_json(&openapi_json));
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
brk_bindgen::generate_clients(vecs, &openapi_json, &output_paths)
}));
@@ -169,17 +170,11 @@ impl Server {
Err(_) => error!("Client generation panicked"),
}
let router = if mcp {
let base_url = format!("http://127.0.0.1:{port}");
router.merge(mcp_router(base_url, openapi_json))
} else {
router
};
serve(
listener,
router
.layer(Extension(Arc::new(openapi)))
.layer(Extension(openapi_trimmed))
.into_make_service(),
)
.await?;
+7 -1
View File
@@ -1,4 +1,4 @@
use std::ops::{Add, Div, Mul, Sub};
use std::ops::{Add, AddAssign, Div, Mul, Sub};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -115,6 +115,12 @@ impl Add for Cents {
}
}
impl AddAssign for Cents {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl Sub for Cents {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
-5
View File
@@ -55,11 +55,6 @@ impl DataRange {
})
}
/// Returns a string for etag/cache key generation that captures all range parameters
pub fn etag_suffix(&self) -> String {
format!("{:?}{:?}{:?}", self.start, self.end, self.limit)
}
fn resolve_index(&self, idx: Option<i64>, len: usize, default: usize) -> usize {
match idx {
None => default,
+59
View File
@@ -0,0 +1,59 @@
use std::fmt;
/// HTTP ETag value.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Etag(String);
impl Etag {
/// Create from raw string
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
/// Get inner string reference
pub fn as_str(&self) -> &str {
&self.0
}
/// Consume and return inner string
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for Etag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for Etag {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Etag {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl Etag {
/// Create ETag from metric data response info.
///
/// Format varies based on whether the slice touches the end:
/// - Slice ends before total: `{version:x}-{start}-{end}` (len irrelevant, data won't change if metric grows)
/// - Slice reaches the end: `{version:x}-{start}-{total}` (len matters, new data would change results)
///
/// `version` is the metric version for single queries, or the sum of versions for bulk queries.
pub fn from_metric(version: u64, total: usize, start: usize, end: usize) -> Self {
if end < total {
// Fixed window not at the end - len doesn't matter
Self(format!("{version:x}-{start}-{end}"))
} else {
// Fetching up to current end - len matters
Self(format!("{version:x}-{start}-{total}"))
}
}
}
+11 -5
View File
@@ -5,12 +5,14 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::PrintableIndex;
use crate::PairOutputIndex;
use super::{
DateIndex, DecadeIndex, DifficultyEpoch, EmptyAddressIndex, EmptyOutputIndex, HalvingEpoch,
Height, TxInIndex, LoadedAddressIndex, MonthIndex, OpReturnIndex, TxOutIndex,
P2AAddressIndex, P2MSOutputIndex, P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex,
P2SHAddressIndex, P2TRAddressIndex, P2WPKHAddressIndex, P2WSHAddressIndex, QuarterIndex,
SemesterIndex, TxIndex, UnknownOutputIndex, WeekIndex, YearIndex,
Height, LoadedAddressIndex, MonthIndex, OpReturnIndex, P2AAddressIndex, P2MSOutputIndex,
P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex, P2SHAddressIndex, P2TRAddressIndex,
P2WPKHAddressIndex, P2WSHAddressIndex, QuarterIndex, SemesterIndex, TxInIndex, TxIndex,
TxOutIndex, UnknownOutputIndex, WeekIndex, YearIndex,
};
/// Aggregation dimension for querying metrics. Includes time-based (date, week, month, year),
@@ -46,10 +48,11 @@ pub enum Index {
YearIndex,
LoadedAddressIndex,
EmptyAddressIndex,
PairOutputIndex,
}
impl Index {
pub const fn all() -> [Self; 27] {
pub const fn all() -> [Self; 28] {
[
Self::DateIndex,
Self::DecadeIndex,
@@ -78,6 +81,7 @@ impl Index {
Self::YearIndex,
Self::LoadedAddressIndex,
Self::EmptyAddressIndex,
Self::PairOutputIndex,
]
}
@@ -110,6 +114,7 @@ impl Index {
Self::YearIndex => YearIndex::to_possible_strings(),
Self::LoadedAddressIndex => LoadedAddressIndex::to_possible_strings(),
Self::EmptyAddressIndex => EmptyAddressIndex::to_possible_strings(),
Self::PairOutputIndex => PairOutputIndex::to_possible_strings(),
}
}
@@ -188,6 +193,7 @@ impl TryFrom<&str> for Index {
v if (Self::EmptyAddressIndex).possible_values().contains(&v) => {
Self::EmptyAddressIndex
}
v if (Self::PairOutputIndex).possible_values().contains(&v) => Self::PairOutputIndex,
_ => return Err(Error::Parse(format!("Invalid index: {value}"))),
})
}
+10
View File
@@ -52,6 +52,7 @@ mod dollars;
mod emptyaddressdata;
mod emptyaddressindex;
mod emptyoutputindex;
mod etag;
mod feerate;
mod feeratepercentiles;
mod format;
@@ -78,16 +79,20 @@ mod metriccount;
mod metricdata;
mod metricparam;
mod metrics;
mod metricoutput;
mod metricselection;
mod metricselectionlegacy;
mod metricspaginated;
mod metricwithindex;
mod monthindex;
mod ohlc;
mod oracle_bins;
mod opreturnindex;
mod option_ext;
mod outpoint;
mod output;
mod outputtype;
mod pairoutputindex;
mod p2aaddressindex;
mod p2abytes;
mod p2msoutputindex;
@@ -216,6 +221,7 @@ pub use dollars::*;
pub use emptyaddressdata::*;
pub use emptyaddressindex::*;
pub use emptyoutputindex::*;
pub use etag::*;
pub use feerate::*;
pub use feeratepercentiles::*;
pub use format::*;
@@ -242,16 +248,20 @@ pub use metriccount::*;
pub use metricdata::*;
pub use metricparam::*;
pub use metrics::*;
pub use metricoutput::*;
pub use metricselection::*;
pub use metricselectionlegacy::*;
pub use metricspaginated::*;
pub use metricwithindex::*;
pub use monthindex::*;
pub use ohlc::*;
pub use oracle_bins::*;
pub use opreturnindex::*;
pub use option_ext::*;
pub use outpoint::*;
pub use output::*;
pub use outputtype::*;
pub use pairoutputindex::*;
pub use p2aaddressindex::*;
pub use p2abytes::*;
pub use p2msoutputindex::*;
+10 -7
View File
@@ -11,6 +11,8 @@ use vecdb::AnySerializableVec;
/// This type is not instantiated - use `MetricData::serialize()` to write JSON bytes directly.
#[derive(Debug, JsonSchema, Deserialize)]
pub struct MetricData<T = Value> {
/// Version of the metric data
pub version: u64,
/// Total number of data points in the metric
pub total: usize,
/// Start index (inclusive) of the returned range
@@ -22,22 +24,23 @@ pub struct MetricData<T = Value> {
}
impl MetricData {
/// Write metric data as JSON to buffer: `{"total":N,"start":N,"end":N,"data":[...]}`
/// Write metric data as JSON to buffer: `{"version":N,"total":N,"start":N,"end":N,"data":[...]}`
pub fn serialize(
vec: &dyn AnySerializableVec,
start: Option<usize>,
end: Option<usize>,
start: usize,
end: usize,
buf: &mut Vec<u8>,
) -> vecdb::Result<()> {
let version = u64::from(vec.version());
let total = vec.len();
let start_idx = start.unwrap_or(0);
let end_idx = end.unwrap_or(total).min(total);
let end = end.min(total);
let start = start.min(end);
write!(
buf,
r#"{{"total":{total},"start":{start_idx},"end":{end_idx},"data":"#
r#"{{"version":{version},"total":{total},"start":{start},"end":{end},"data":"#,
)?;
vec.write_json(start, end, buf)?;
vec.write_json(Some(start), Some(end), buf)?;
buf.push(b'}');
Ok(())
}
+33
View File
@@ -0,0 +1,33 @@
use crate::{Etag, Output, OutputLegacy};
/// Metric output with metadata for caching.
#[derive(Debug)]
pub struct MetricOutput {
pub output: Output,
pub version: u64,
pub total: usize,
pub start: usize,
pub end: usize,
}
impl MetricOutput {
pub fn etag(&self) -> Etag {
Etag::from_metric(self.version, self.total, self.start, self.end)
}
}
/// Deprecated: Legacy metric output with metadata for caching.
#[derive(Debug)]
pub struct MetricOutputLegacy {
pub output: OutputLegacy,
pub version: u64,
pub total: usize,
pub start: usize,
pub end: usize,
}
impl MetricOutputLegacy {
pub fn etag(&self) -> Etag {
Etag::from_metric(self.version, self.total, self.start, self.end)
}
}
+2 -1
View File
@@ -10,9 +10,10 @@ use super::Metric;
#[derive(Debug, Deref, JsonSchema)]
#[schemars(
with = "String",
example = &"date,price_close",
example = &"price_close",
example = &"price_close,market_cap",
example = &"realized_price,nvt_ratio,mvrv"
example = &"realized_price,market_cap,mvrv"
)]
pub struct Metrics(Vec<Metric>);
+150
View File
@@ -0,0 +1,150 @@
use std::{fmt::Display, mem::size_of};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::SeqAccess, de::Visitor};
use vecdb::{Bytes, Formattable};
use crate::Sats;
/// Number of bins for the phase histogram
pub const PHASE_BINS: usize = 100;
/// Phase histogram: counts per bin for frac(log10(sats))
///
/// Used for on-chain price discovery. Each bin represents 1% of the
/// log10 fractional range [0, 1). Values are u16 (max 65535 per bin).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OracleBins {
pub bins: [u16; PHASE_BINS],
}
impl Default for OracleBins {
fn default() -> Self {
Self::ZERO
}
}
impl Display for OracleBins {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "OracleBins(peak={})", self.peak_bin())
}
}
impl Serialize for OracleBins {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.bins.as_slice().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for OracleBins {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct BinsVisitor;
impl<'de> Visitor<'de> for BinsVisitor {
type Value = OracleBins;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "an array of {} u16 values", PHASE_BINS)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut bins = [0u16; PHASE_BINS];
for (i, bin) in bins.iter_mut().enumerate() {
*bin = seq
.next_element()?
.ok_or_else(|| serde::de::Error::invalid_length(i, &self))?;
}
Ok(OracleBins { bins })
}
}
deserializer.deserialize_seq(BinsVisitor)
}
}
impl JsonSchema for OracleBins {
fn schema_name() -> std::borrow::Cow<'static, str> {
"OracleBins".into()
}
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
// Represent as array of u16 values
Vec::<u16>::json_schema(_gen)
}
}
impl OracleBins {
pub const ZERO: Self = Self {
bins: [0; PHASE_BINS],
};
/// Get the bin index for a sats value
/// Filters: min 1k sats, max 100k BTC (matches Python 1e-5 to 1e5 BTC)
#[inline]
pub fn sats_to_bin(sats: Sats) -> Option<usize> {
if sats < Sats::_1K || sats > Sats::_100K_BTC {
return None;
}
let log_sats = f64::from(sats).log10();
let phase = log_sats.fract();
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
Some(((phase * PHASE_BINS as f64) as usize).min(PHASE_BINS - 1))
}
/// Add a count to the bin for this sats value
#[inline]
pub fn add(&mut self, sats: Sats) {
if let Some(bin) = Self::sats_to_bin(sats) {
self.bins[bin] = self.bins[bin].saturating_add(1);
}
}
/// Find the peak bin (index with highest count)
pub fn peak_bin(&self) -> usize {
self.bins
.iter()
.enumerate()
.max_by_key(|(_, count)| *count)
.map(|(idx, _)| idx)
.unwrap_or(0)
}
/// Get total count across all bins
pub fn total_count(&self) -> u32 {
self.bins.iter().map(|&c| c as u32).sum()
}
}
impl Bytes for OracleBins {
type Array = [u8; size_of::<Self>()];
fn to_bytes(&self) -> Self::Array {
let mut arr = [0u8; size_of::<Self>()];
for (i, &count) in self.bins.iter().enumerate() {
let bytes = count.to_le_bytes();
arr[i * 2] = bytes[0];
arr[i * 2 + 1] = bytes[1];
}
arr
}
fn from_bytes(bytes: &[u8]) -> vecdb::Result<Self> {
if bytes.len() < size_of::<Self>() {
return Err(vecdb::Error::WrongLength {
received: bytes.len(),
expected: size_of::<Self>(),
});
}
let mut bins = [0u16; PHASE_BINS];
for (i, bin) in bins.iter_mut().enumerate() {
*bin = u16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]);
}
Ok(Self { bins })
}
}
impl Formattable for OracleBins {
fn may_need_escaping() -> bool {
false
}
}
@@ -1,6 +1,6 @@
use brk_types::Format;
use crate::Format;
/// New format with MetricData metadata wrapper
/// Metric data output format
#[derive(Debug)]
pub enum Output {
Json(Vec<u8>),
@@ -19,7 +19,9 @@ impl Output {
pub fn default(format: Format) -> Self {
match format {
Format::CSV => Output::CSV(String::new()),
Format::JSON => Output::Json(br#"{"len":0,"from":0,"to":0,"data":[]}"#.to_vec()),
Format::JSON => {
Output::Json(br#"{"version":0,"total":0,"start":0,"end":0,"data":[]}"#.to_vec())
}
}
}
}
+121
View File
@@ -0,0 +1,121 @@
use std::ops::Add;
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex};
/// Index for 2-output transactions (oracle pair candidates)
///
/// This indexes all transactions with exactly 2 outputs, which are
/// candidates for the UTXOracle algorithm (payment + change pattern).
#[derive(
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Clone,
Copy,
Deref,
DerefMut,
Default,
Serialize,
Deserialize,
Pco,
JsonSchema,
Hash,
)]
pub struct PairOutputIndex(u32);
impl PairOutputIndex {
pub const ZERO: Self = Self(0);
pub fn new(index: u32) -> Self {
Self(index)
}
pub fn incremented(self) -> Self {
Self(*self + 1)
}
}
impl Add<usize> for PairOutputIndex {
type Output = Self;
fn add(self, rhs: usize) -> Self::Output {
Self(self.0 + rhs as u32)
}
}
impl CheckedSub<PairOutputIndex> for PairOutputIndex {
fn checked_sub(self, rhs: PairOutputIndex) -> Option<Self> {
self.0.checked_sub(rhs.0).map(PairOutputIndex::from)
}
}
impl From<u32> for PairOutputIndex {
#[inline]
fn from(value: u32) -> Self {
Self(value)
}
}
impl From<PairOutputIndex> for u32 {
#[inline]
fn from(value: PairOutputIndex) -> Self {
value.0
}
}
impl From<u64> for PairOutputIndex {
#[inline]
fn from(value: u64) -> Self {
Self(value as u32)
}
}
impl From<PairOutputIndex> for u64 {
#[inline]
fn from(value: PairOutputIndex) -> Self {
value.0 as u64
}
}
impl From<usize> for PairOutputIndex {
#[inline]
fn from(value: usize) -> Self {
Self(value as u32)
}
}
impl From<PairOutputIndex> for usize {
#[inline]
fn from(value: PairOutputIndex) -> Self {
value.0 as usize
}
}
impl PrintableIndex for PairOutputIndex {
fn to_string() -> &'static str {
"pairoutputindex"
}
fn to_possible_strings() -> &'static [&'static str] {
&["pairoutput", "pairoutputindex"]
}
}
impl std::fmt::Display for PairOutputIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut buf = itoa::Buffer::new();
let str = buf.format(self.0);
f.write_str(str)
}
}
impl Formattable for PairOutputIndex {
#[inline(always)]
fn may_need_escaping() -> bool {
false
}
}
+34
View File
@@ -69,6 +69,40 @@ impl Sats {
pub fn is_max(&self) -> bool {
*self == Self::MAX
}
/// Check if value is a "round" BTC amount (±0.1% of common round values).
/// Used to filter out non-price-related transactions.
/// Round amounts: 1k, 10k, 20k, 30k, 50k, 100k, 200k, 300k, 500k sats,
/// 0.01, 0.02, 0.03, 0.05, 0.1, 0.2, 0.3, 0.5, 1, 10 BTC
pub fn is_round_btc(&self) -> bool {
const ROUND_SATS: [u64; 19] = [
1_000, // 1k sats
10_000, // 10k sats
20_000, // 20k sats
30_000, // 30k sats
50_000, // 50k sats
100_000, // 100k sats (0.001 BTC)
200_000, // 200k sats
300_000, // 300k sats
500_000, // 500k sats
1_000_000, // 0.01 BTC
2_000_000, // 0.02 BTC
3_000_000, // 0.03 BTC
5_000_000, // 0.05 BTC
10_000_000, // 0.1 BTC
20_000_000, // 0.2 BTC
30_000_000, // 0.3 BTC
50_000_000, // 0.5 BTC
100_000_000, // 1 BTC
1_000_000_000, // 10 BTC
];
const TOLERANCE: f64 = 0.001; // 0.1%
let v = self.0 as f64;
ROUND_SATS
.iter()
.any(|&r| (v - r as f64).abs() <= r as f64 * TOLERANCE)
}
}
impl Add for Sats {
-3
View File
@@ -17,9 +17,6 @@ BTC_RPC_PORT=8332
# Enable price fetching from exchanges
BRK_FETCH=true
# Enable Model Context Protocol (MCP) for AI/LLM integration
BRK_MCP=true
# BRK data storage options
# Option 1: Use a Docker named volume (default, recommended)
# This is the default configuration - no changes needed.
-2
View File
@@ -63,7 +63,6 @@ cd docker && docker compose up -d
| `BTC_RPC_PASSWORD` | Bitcoin RPC password | - |
| `BRK_DATA_VOLUME` | Docker volume name for BRK data | `brk-data` |
| `BRK_FETCH` | Enable price fetching | `true` |
| `BRK_MCP` | Enable MCP for AI/LLM | `true` |
### Example .env File
@@ -80,7 +79,6 @@ BTC_RPC_PASSWORD=your_password
# BRK settings
BRK_FETCH=true
BRK_MCP=true
```
### Connecting to Bitcoin Core
-1
View File
@@ -38,7 +38,6 @@ services:
# BRK configuration
- BRKDIR=/home/brk/.brk
- FETCH=${BRK_FETCH:-true}
- MCP=${BRK_MCP:-true}
command:
- --bitcoindir
- /bitcoin
+1 -1
View File
@@ -26,7 +26,7 @@ Browse metrics and charts visually. Use it free at [Bitview](https://bitview.spa
Query thousands of metrics and blockchain data in JSON or CSV. Freely accessible at [Bitview](https://bitview.space/api).
[Documentation](https://bitview.space/api) · Clients: [JavaScript](https://www.npmjs.com/package/brk-client), [Python](https://pypi.org/project/brk-client), [Rust](https://crates.io/crates/brk_client) · [MCP](https://modelcontextprotocol.io)
[Documentation](https://bitview.space/api) · [JavaScript](https://www.npmjs.com/package/brk-client) · [Python](https://pypi.org/project/brk-client) · [Rust](https://crates.io/crates/brk_client)
### Self-host
-1
View File
@@ -67,7 +67,6 @@
- example: from -10,000 count 10, wont work if underlying vec isnt 10k or more long
- _LOGGER_
- BUG: remove colors from file
- _MCP_
- _PARSER_
- _SERVER_
- api
+321 -234
View File
@@ -308,7 +308,7 @@
* Aggregation dimension for querying metrics. Includes time-based (date, week, month, year),
* block-based (height, txindex), and address/output type indexes.
*
* @typedef {("dateindex"|"decadeindex"|"difficultyepoch"|"emptyoutputindex"|"halvingepoch"|"height"|"txinindex"|"monthindex"|"opreturnindex"|"txoutindex"|"p2aaddressindex"|"p2msoutputindex"|"p2pk33addressindex"|"p2pk65addressindex"|"p2pkhaddressindex"|"p2shaddressindex"|"p2traddressindex"|"p2wpkhaddressindex"|"p2wshaddressindex"|"quarterindex"|"semesterindex"|"txindex"|"unknownoutputindex"|"weekindex"|"yearindex"|"loadedaddressindex"|"emptyaddressindex")} Index
* @typedef {("dateindex"|"decadeindex"|"difficultyepoch"|"emptyoutputindex"|"halvingepoch"|"height"|"txinindex"|"monthindex"|"opreturnindex"|"txoutindex"|"p2aaddressindex"|"p2msoutputindex"|"p2pk33addressindex"|"p2pk65addressindex"|"p2pkhaddressindex"|"p2shaddressindex"|"p2traddressindex"|"p2wpkhaddressindex"|"p2wshaddressindex"|"quarterindex"|"semesterindex"|"txindex"|"unknownoutputindex"|"weekindex"|"yearindex"|"loadedaddressindex"|"emptyaddressindex"|"pairoutputindex")} Index
*/
/**
* Information about an available index and its query aliases
@@ -455,6 +455,7 @@
*
* @typedef {Cents} Open
*/
/** @typedef {number[]} OracleBins */
/** @typedef {number} OutPoint */
/**
* Type (P2PKH, P2WPKH, P2SH, P2TR, etc.)
@@ -492,6 +493,14 @@
* @typedef {Object} Pagination
* @property {?number=} page - Pagination index
*/
/**
* Index for 2-output transactions (oracle pair candidates)
*
* This indexes all transactions with exactly 2 outputs, which are
* candidates for the UTXOracle algorithm (payment + change pattern).
*
* @typedef {number} PairOutputIndex
*/
/**
* Block counts for different time periods
*
@@ -1092,6 +1101,7 @@ const _i29 = /** @type {const} */ (["weekindex"]);
const _i30 = /** @type {const} */ (["yearindex"]);
const _i31 = /** @type {const} */ (["loadedaddressindex"]);
const _i32 = /** @type {const} */ (["emptyaddressindex"]);
const _i33 = /** @type {const} */ (["pairoutputindex"]);
/**
* Generic metric pattern factory.
@@ -1214,6 +1224,9 @@ function createMetricPattern31(client, name) { return _mp(client, name, _i31); }
/** @template T @typedef {{ name: string, by: { readonly emptyaddressindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern32 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern32<T>} */
function createMetricPattern32(client, name) { return _mp(client, name, _i32); }
/** @template T @typedef {{ name: string, by: { readonly pairoutputindex: MetricEndpointBuilder<T> }, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }} MetricPattern33 */
/** @template T @param {BrkClientBase} client @param {string} name @returns {MetricPattern33<T>} */
function createMetricPattern33(client, name) { return _mp(client, name, _i33); }
// Reusable structural pattern factories
@@ -1631,59 +1644,6 @@ function createPrice111dSmaPattern(client, acc) {
};
}
/**
* @typedef {Object} ActivePriceRatioPattern
* @property {MetricPattern4<StoredF32>} ratio
* @property {MetricPattern4<StoredF32>} ratio1mSma
* @property {MetricPattern4<StoredF32>} ratio1wSma
* @property {Ratio1ySdPattern} ratio1ySd
* @property {Ratio1ySdPattern} ratio2ySd
* @property {Ratio1ySdPattern} ratio4ySd
* @property {MetricPattern4<StoredF32>} ratioPct1
* @property {MetricPattern4<Dollars>} ratioPct1Usd
* @property {MetricPattern4<StoredF32>} ratioPct2
* @property {MetricPattern4<Dollars>} ratioPct2Usd
* @property {MetricPattern4<StoredF32>} ratioPct5
* @property {MetricPattern4<Dollars>} ratioPct5Usd
* @property {MetricPattern4<StoredF32>} ratioPct95
* @property {MetricPattern4<Dollars>} ratioPct95Usd
* @property {MetricPattern4<StoredF32>} ratioPct98
* @property {MetricPattern4<Dollars>} ratioPct98Usd
* @property {MetricPattern4<StoredF32>} ratioPct99
* @property {MetricPattern4<Dollars>} ratioPct99Usd
* @property {Ratio1ySdPattern} ratioSd
*/
/**
* Create a ActivePriceRatioPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {ActivePriceRatioPattern}
*/
function createActivePriceRatioPattern(client, acc) {
return {
ratio: createMetricPattern4(client, acc),
ratio1mSma: createMetricPattern4(client, _m(acc, '1m_sma')),
ratio1wSma: createMetricPattern4(client, _m(acc, '1w_sma')),
ratio1ySd: createRatio1ySdPattern(client, _m(acc, '1y')),
ratio2ySd: createRatio1ySdPattern(client, _m(acc, '2y')),
ratio4ySd: createRatio1ySdPattern(client, _m(acc, '4y')),
ratioPct1: createMetricPattern4(client, _m(acc, 'pct1')),
ratioPct1Usd: createMetricPattern4(client, _m(acc, 'pct1_usd')),
ratioPct2: createMetricPattern4(client, _m(acc, 'pct2')),
ratioPct2Usd: createMetricPattern4(client, _m(acc, 'pct2_usd')),
ratioPct5: createMetricPattern4(client, _m(acc, 'pct5')),
ratioPct5Usd: createMetricPattern4(client, _m(acc, 'pct5_usd')),
ratioPct95: createMetricPattern4(client, _m(acc, 'pct95')),
ratioPct95Usd: createMetricPattern4(client, _m(acc, 'pct95_usd')),
ratioPct98: createMetricPattern4(client, _m(acc, 'pct98')),
ratioPct98Usd: createMetricPattern4(client, _m(acc, 'pct98_usd')),
ratioPct99: createMetricPattern4(client, _m(acc, 'pct99')),
ratioPct99Usd: createMetricPattern4(client, _m(acc, 'pct99_usd')),
ratioSd: createRatio1ySdPattern(client, acc),
};
}
/**
* @typedef {Object} PercentilesPattern
* @property {MetricPattern4<Dollars>} pct05
@@ -1737,6 +1697,59 @@ function createPercentilesPattern(client, acc) {
};
}
/**
* @typedef {Object} ActivePriceRatioPattern
* @property {MetricPattern4<StoredF32>} ratio
* @property {MetricPattern4<StoredF32>} ratio1mSma
* @property {MetricPattern4<StoredF32>} ratio1wSma
* @property {Ratio1ySdPattern} ratio1ySd
* @property {Ratio1ySdPattern} ratio2ySd
* @property {Ratio1ySdPattern} ratio4ySd
* @property {MetricPattern4<StoredF32>} ratioPct1
* @property {MetricPattern4<Dollars>} ratioPct1Usd
* @property {MetricPattern4<StoredF32>} ratioPct2
* @property {MetricPattern4<Dollars>} ratioPct2Usd
* @property {MetricPattern4<StoredF32>} ratioPct5
* @property {MetricPattern4<Dollars>} ratioPct5Usd
* @property {MetricPattern4<StoredF32>} ratioPct95
* @property {MetricPattern4<Dollars>} ratioPct95Usd
* @property {MetricPattern4<StoredF32>} ratioPct98
* @property {MetricPattern4<Dollars>} ratioPct98Usd
* @property {MetricPattern4<StoredF32>} ratioPct99
* @property {MetricPattern4<Dollars>} ratioPct99Usd
* @property {Ratio1ySdPattern} ratioSd
*/
/**
* Create a ActivePriceRatioPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {ActivePriceRatioPattern}
*/
function createActivePriceRatioPattern(client, acc) {
return {
ratio: createMetricPattern4(client, acc),
ratio1mSma: createMetricPattern4(client, _m(acc, '1m_sma')),
ratio1wSma: createMetricPattern4(client, _m(acc, '1w_sma')),
ratio1ySd: createRatio1ySdPattern(client, _m(acc, '1y')),
ratio2ySd: createRatio1ySdPattern(client, _m(acc, '2y')),
ratio4ySd: createRatio1ySdPattern(client, _m(acc, '4y')),
ratioPct1: createMetricPattern4(client, _m(acc, 'pct1')),
ratioPct1Usd: createMetricPattern4(client, _m(acc, 'pct1_usd')),
ratioPct2: createMetricPattern4(client, _m(acc, 'pct2')),
ratioPct2Usd: createMetricPattern4(client, _m(acc, 'pct2_usd')),
ratioPct5: createMetricPattern4(client, _m(acc, 'pct5')),
ratioPct5Usd: createMetricPattern4(client, _m(acc, 'pct5_usd')),
ratioPct95: createMetricPattern4(client, _m(acc, 'pct95')),
ratioPct95Usd: createMetricPattern4(client, _m(acc, 'pct95_usd')),
ratioPct98: createMetricPattern4(client, _m(acc, 'pct98')),
ratioPct98Usd: createMetricPattern4(client, _m(acc, 'pct98_usd')),
ratioPct99: createMetricPattern4(client, _m(acc, 'pct99')),
ratioPct99Usd: createMetricPattern4(client, _m(acc, 'pct99_usd')),
ratioSd: createRatio1ySdPattern(client, acc),
};
}
/**
* @typedef {Object} RelativePattern5
* @property {MetricPattern1<StoredF32>} negUnrealizedLossRelToMarketCap
@@ -2209,41 +2222,6 @@ function createAddrCountPattern(client, acc) {
};
}
/**
* @template T
* @typedef {Object} FullnessPattern
* @property {MetricPattern2<T>} average
* @property {MetricPattern11<T>} base
* @property {MetricPattern2<T>} max
* @property {MetricPattern6<T>} median
* @property {MetricPattern2<T>} min
* @property {MetricPattern6<T>} pct10
* @property {MetricPattern6<T>} pct25
* @property {MetricPattern6<T>} pct75
* @property {MetricPattern6<T>} pct90
*/
/**
* Create a FullnessPattern pattern node
* @template T
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {FullnessPattern<T>}
*/
function createFullnessPattern(client, acc) {
return {
average: createMetricPattern2(client, _m(acc, 'average')),
base: createMetricPattern11(client, acc),
max: createMetricPattern2(client, _m(acc, 'max')),
median: createMetricPattern6(client, _m(acc, 'median')),
min: createMetricPattern2(client, _m(acc, 'min')),
pct10: createMetricPattern6(client, _m(acc, 'pct10')),
pct25: createMetricPattern6(client, _m(acc, 'pct25')),
pct75: createMetricPattern6(client, _m(acc, 'pct75')),
pct90: createMetricPattern6(client, _m(acc, 'pct90')),
};
}
/**
* @template T
* @typedef {Object} FeeRatePattern
@@ -2279,6 +2257,41 @@ function createFeeRatePattern(client, acc) {
};
}
/**
* @template T
* @typedef {Object} FullnessPattern
* @property {MetricPattern2<T>} average
* @property {MetricPattern11<T>} base
* @property {MetricPattern2<T>} max
* @property {MetricPattern6<T>} median
* @property {MetricPattern2<T>} min
* @property {MetricPattern6<T>} pct10
* @property {MetricPattern6<T>} pct25
* @property {MetricPattern6<T>} pct75
* @property {MetricPattern6<T>} pct90
*/
/**
* Create a FullnessPattern pattern node
* @template T
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {FullnessPattern<T>}
*/
function createFullnessPattern(client, acc) {
return {
average: createMetricPattern2(client, _m(acc, 'average')),
base: createMetricPattern11(client, acc),
max: createMetricPattern2(client, _m(acc, 'max')),
median: createMetricPattern6(client, _m(acc, 'median')),
min: createMetricPattern2(client, _m(acc, 'min')),
pct10: createMetricPattern6(client, _m(acc, 'pct10')),
pct25: createMetricPattern6(client, _m(acc, 'pct25')),
pct75: createMetricPattern6(client, _m(acc, 'pct75')),
pct90: createMetricPattern6(client, _m(acc, 'pct90')),
};
}
/**
* @typedef {Object} _0satsPattern
* @property {ActivityPattern2} activity
@@ -2311,31 +2324,35 @@ function create_0satsPattern(client, acc) {
}
/**
* @typedef {Object} _100btcPattern
* @property {ActivityPattern2} activity
* @property {CostBasisPattern} costBasis
* @property {OutputsPattern} outputs
* @property {RealizedPattern} realized
* @property {RelativePattern} relative
* @property {SupplyPattern2} supply
* @property {UnrealizedPattern} unrealized
* @template T
* @typedef {Object} PhaseDailyCentsPattern
* @property {MetricPattern6<T>} average
* @property {MetricPattern6<T>} max
* @property {MetricPattern6<T>} median
* @property {MetricPattern6<T>} min
* @property {MetricPattern6<T>} pct10
* @property {MetricPattern6<T>} pct25
* @property {MetricPattern6<T>} pct75
* @property {MetricPattern6<T>} pct90
*/
/**
* Create a _100btcPattern pattern node
* Create a PhaseDailyCentsPattern pattern node
* @template T
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {_100btcPattern}
* @returns {PhaseDailyCentsPattern<T>}
*/
function create_100btcPattern(client, acc) {
function createPhaseDailyCentsPattern(client, acc) {
return {
activity: createActivityPattern2(client, acc),
costBasis: createCostBasisPattern(client, acc),
outputs: createOutputsPattern(client, _m(acc, 'utxo_count')),
realized: createRealizedPattern(client, acc),
relative: createRelativePattern(client, acc),
supply: createSupplyPattern2(client, _m(acc, 'supply')),
unrealized: createUnrealizedPattern(client, acc),
average: createMetricPattern6(client, _m(acc, 'average')),
max: createMetricPattern6(client, _m(acc, 'max')),
median: createMetricPattern6(client, _m(acc, 'median')),
min: createMetricPattern6(client, _m(acc, 'min')),
pct10: createMetricPattern6(client, _m(acc, 'pct10')),
pct25: createMetricPattern6(client, _m(acc, 'pct25')),
pct75: createMetricPattern6(client, _m(acc, 'pct75')),
pct90: createMetricPattern6(client, _m(acc, 'pct90')),
};
}
@@ -2368,35 +2385,6 @@ function createPeriodCagrPattern(client, acc) {
};
}
/**
* @typedef {Object} UnrealizedPattern
* @property {MetricPattern1<Dollars>} negUnrealizedLoss
* @property {MetricPattern1<Dollars>} netUnrealizedPnl
* @property {ActiveSupplyPattern} supplyInLoss
* @property {ActiveSupplyPattern} supplyInProfit
* @property {MetricPattern1<Dollars>} totalUnrealizedPnl
* @property {MetricPattern1<Dollars>} unrealizedLoss
* @property {MetricPattern1<Dollars>} unrealizedProfit
*/
/**
* Create a UnrealizedPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {UnrealizedPattern}
*/
function createUnrealizedPattern(client, acc) {
return {
negUnrealizedLoss: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss')),
netUnrealizedPnl: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl')),
supplyInLoss: createActiveSupplyPattern(client, _m(acc, 'supply_in_loss')),
supplyInProfit: createActiveSupplyPattern(client, _m(acc, 'supply_in_profit')),
totalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'total_unrealized_pnl')),
unrealizedLoss: createMetricPattern1(client, _m(acc, 'unrealized_loss')),
unrealizedProfit: createMetricPattern1(client, _m(acc, 'unrealized_profit')),
};
}
/**
* @typedef {Object} _10yTo12yPattern
* @property {ActivityPattern2} activity
@@ -2426,6 +2414,35 @@ function create_10yTo12yPattern(client, acc) {
};
}
/**
* @typedef {Object} UnrealizedPattern
* @property {MetricPattern1<Dollars>} negUnrealizedLoss
* @property {MetricPattern1<Dollars>} netUnrealizedPnl
* @property {ActiveSupplyPattern} supplyInLoss
* @property {ActiveSupplyPattern} supplyInProfit
* @property {MetricPattern1<Dollars>} totalUnrealizedPnl
* @property {MetricPattern1<Dollars>} unrealizedLoss
* @property {MetricPattern1<Dollars>} unrealizedProfit
*/
/**
* Create a UnrealizedPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {UnrealizedPattern}
*/
function createUnrealizedPattern(client, acc) {
return {
negUnrealizedLoss: createMetricPattern1(client, _m(acc, 'neg_unrealized_loss')),
netUnrealizedPnl: createMetricPattern1(client, _m(acc, 'net_unrealized_pnl')),
supplyInLoss: createActiveSupplyPattern(client, _m(acc, 'supply_in_loss')),
supplyInProfit: createActiveSupplyPattern(client, _m(acc, 'supply_in_profit')),
totalUnrealizedPnl: createMetricPattern1(client, _m(acc, 'total_unrealized_pnl')),
unrealizedLoss: createMetricPattern1(client, _m(acc, 'unrealized_loss')),
unrealizedProfit: createMetricPattern1(client, _m(acc, 'unrealized_profit')),
};
}
/**
* @typedef {Object} _0satsPattern2
* @property {ActivityPattern2} activity
@@ -2455,6 +2472,35 @@ function create_0satsPattern2(client, acc) {
};
}
/**
* @typedef {Object} _100btcPattern
* @property {ActivityPattern2} activity
* @property {CostBasisPattern} costBasis
* @property {OutputsPattern} outputs
* @property {RealizedPattern} realized
* @property {RelativePattern} relative
* @property {SupplyPattern2} supply
* @property {UnrealizedPattern} unrealized
*/
/**
* Create a _100btcPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {_100btcPattern}
*/
function create_100btcPattern(client, acc) {
return {
activity: createActivityPattern2(client, acc),
costBasis: createCostBasisPattern(client, acc),
outputs: createOutputsPattern(client, _m(acc, 'utxo_count')),
realized: createRealizedPattern(client, acc),
relative: createRelativePattern(client, acc),
supply: createSupplyPattern2(client, _m(acc, 'supply')),
unrealized: createUnrealizedPattern(client, acc),
};
}
/**
* @typedef {Object} _10yPattern
* @property {ActivityPattern2} activity
@@ -2535,23 +2581,23 @@ function createSplitPattern2(client, acc) {
}
/**
* @typedef {Object} _2015Pattern
* @property {MetricPattern4<Bitcoin>} bitcoin
* @property {MetricPattern4<Dollars>} dollars
* @property {MetricPattern4<Sats>} sats
* @typedef {Object} CoinbasePattern2
* @property {BlockCountPattern<Bitcoin>} bitcoin
* @property {BlockCountPattern<Dollars>} dollars
* @property {BlockCountPattern<Sats>} sats
*/
/**
* Create a _2015Pattern pattern node
* Create a CoinbasePattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {_2015Pattern}
* @returns {CoinbasePattern2}
*/
function create_2015Pattern(client, acc) {
function createCoinbasePattern2(client, acc) {
return {
bitcoin: createMetricPattern4(client, _m(acc, 'btc')),
dollars: createMetricPattern4(client, _m(acc, 'usd')),
sats: createMetricPattern4(client, acc),
bitcoin: createBlockCountPattern(client, _m(acc, 'btc')),
dollars: createBlockCountPattern(client, _m(acc, 'usd')),
sats: createBlockCountPattern(client, acc),
};
}
@@ -2576,27 +2622,6 @@ function createCoinbasePattern(client, acc) {
};
}
/**
* @typedef {Object} CoinbasePattern2
* @property {BlockCountPattern<Bitcoin>} bitcoin
* @property {BlockCountPattern<Dollars>} dollars
* @property {BlockCountPattern<Sats>} sats
*/
/**
* Create a CoinbasePattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {CoinbasePattern2}
*/
function createCoinbasePattern2(client, acc) {
return {
bitcoin: createBlockCountPattern(client, _m(acc, 'btc')),
dollars: createBlockCountPattern(client, _m(acc, 'usd')),
sats: createBlockCountPattern(client, acc),
};
}
/**
* @typedef {Object} SegwitAdoptionPattern
* @property {MetricPattern11<StoredF32>} base
@@ -2619,23 +2644,23 @@ function createSegwitAdoptionPattern(client, acc) {
}
/**
* @typedef {Object} CostBasisPattern2
* @property {MetricPattern1<Dollars>} max
* @property {MetricPattern1<Dollars>} min
* @property {PercentilesPattern} percentiles
* @typedef {Object} _2015Pattern
* @property {MetricPattern4<Bitcoin>} bitcoin
* @property {MetricPattern4<Dollars>} dollars
* @property {MetricPattern4<Sats>} sats
*/
/**
* Create a CostBasisPattern2 pattern node
* Create a _2015Pattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {CostBasisPattern2}
* @returns {_2015Pattern}
*/
function createCostBasisPattern2(client, acc) {
function create_2015Pattern(client, acc) {
return {
max: createMetricPattern1(client, _m(acc, 'max_cost_basis')),
min: createMetricPattern1(client, _m(acc, 'min_cost_basis')),
percentiles: createPercentilesPattern(client, _m(acc, 'cost_basis')),
bitcoin: createMetricPattern4(client, _m(acc, 'btc')),
dollars: createMetricPattern4(client, _m(acc, 'usd')),
sats: createMetricPattern4(client, acc),
};
}
@@ -2660,6 +2685,27 @@ function createActiveSupplyPattern(client, acc) {
};
}
/**
* @typedef {Object} CostBasisPattern2
* @property {MetricPattern1<Dollars>} max
* @property {MetricPattern1<Dollars>} min
* @property {PercentilesPattern} percentiles
*/
/**
* Create a CostBasisPattern2 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {CostBasisPattern2}
*/
function createCostBasisPattern2(client, acc) {
return {
max: createMetricPattern1(client, _m(acc, 'max_cost_basis')),
min: createMetricPattern1(client, _m(acc, 'min_cost_basis')),
percentiles: createPercentilesPattern(client, _m(acc, 'cost_basis')),
};
}
/**
* @typedef {Object} UnclaimedRewardsPattern
* @property {BitcoinPattern2<Bitcoin>} bitcoin
@@ -2681,6 +2727,25 @@ function createUnclaimedRewardsPattern(client, acc) {
};
}
/**
* @typedef {Object} RelativePattern4
* @property {MetricPattern1<StoredF64>} supplyInLossRelToOwnSupply
* @property {MetricPattern1<StoredF64>} supplyInProfitRelToOwnSupply
*/
/**
* Create a RelativePattern4 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {RelativePattern4}
*/
function createRelativePattern4(client, acc) {
return {
supplyInLossRelToOwnSupply: createMetricPattern1(client, _m(acc, 'loss_rel_to_own_supply')),
supplyInProfitRelToOwnSupply: createMetricPattern1(client, _m(acc, 'profit_rel_to_own_supply')),
};
}
/**
* @typedef {Object} CostBasisPattern
* @property {MetricPattern1<Dollars>} max
@@ -2719,25 +2784,6 @@ function createSupplyPattern2(client, acc) {
};
}
/**
* @typedef {Object} RelativePattern4
* @property {MetricPattern1<StoredF64>} supplyInLossRelToOwnSupply
* @property {MetricPattern1<StoredF64>} supplyInProfitRelToOwnSupply
*/
/**
* Create a RelativePattern4 pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {RelativePattern4}
*/
function createRelativePattern4(client, acc) {
return {
supplyInLossRelToOwnSupply: createMetricPattern1(client, _m(acc, 'loss_rel_to_own_supply')),
supplyInProfitRelToOwnSupply: createMetricPattern1(client, _m(acc, 'profit_rel_to_own_supply')),
};
}
/**
* @typedef {Object} _1dReturns1mSdPattern
* @property {MetricPattern4<StoredF32>} sd
@@ -2757,6 +2803,27 @@ function create_1dReturns1mSdPattern(client, acc) {
};
}
/**
* @template T
* @typedef {Object} SatsPattern
* @property {MetricPattern1<T>} ohlc
* @property {SplitPattern2<T>} split
*/
/**
* Create a SatsPattern pattern node
* @template T
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {SatsPattern<T>}
*/
function createSatsPattern(client, acc) {
return {
ohlc: createMetricPattern1(client, _m(acc, 'ohlc')),
split: createSplitPattern2(client, acc),
};
}
/**
* @template T
* @typedef {Object} BlockCountPattern
@@ -2800,23 +2867,19 @@ function createBitcoinPattern2(client, acc) {
}
/**
* @template T
* @typedef {Object} SatsPattern
* @property {MetricPattern1<T>} ohlc
* @property {SplitPattern2<T>} split
* @typedef {Object} OutputsPattern
* @property {MetricPattern1<StoredU64>} utxoCount
*/
/**
* Create a SatsPattern pattern node
* @template T
* Create a OutputsPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {SatsPattern<T>}
* @returns {OutputsPattern}
*/
function createSatsPattern(client, acc) {
function createOutputsPattern(client, acc) {
return {
ohlc: createMetricPattern1(client, _m(acc, 'ohlc_sats')),
split: createSplitPattern2(client, _m(acc, 'sats')),
utxoCount: createMetricPattern1(client, acc),
};
}
@@ -2837,23 +2900,6 @@ function createRealizedPriceExtraPattern(client, acc) {
};
}
/**
* @typedef {Object} OutputsPattern
* @property {MetricPattern1<StoredU64>} utxoCount
*/
/**
* Create a OutputsPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {OutputsPattern}
*/
function createOutputsPattern(client, acc) {
return {
utxoCount: createMetricPattern1(client, acc),
};
}
// Catalog tree typedefs
/**
@@ -4009,8 +4055,8 @@ function createOutputsPattern(client, acc) {
* @typedef {Object} MetricsTree_Price
* @property {MetricsTree_Price_Cents} cents
* @property {MetricsTree_Price_Oracle} oracle
* @property {SatsPattern<OHLCSats>} sats
* @property {MetricsTree_Price_Usd} usd
* @property {MetricsTree_Price_Sats} sats
* @property {SatsPattern<OHLCDollars>} usd
*/
/**
@@ -4029,16 +4075,24 @@ function createOutputsPattern(client, acc) {
/**
* @typedef {Object} MetricsTree_Price_Oracle
* @property {MetricPattern11<PairOutputIndex>} heightToFirstPairoutputindex
* @property {MetricPattern6<OHLCCents>} ohlcCents
* @property {MetricPattern6<OHLCDollars>} ohlcDollars
* @property {MetricPattern33<Sats>} output0Value
* @property {MetricPattern33<Sats>} output1Value
* @property {MetricPattern33<TxIndex>} pairoutputindexToTxindex
* @property {PhaseDailyCentsPattern<Cents>} phaseDailyCents
* @property {PhaseDailyCentsPattern<Dollars>} phaseDailyDollars
* @property {MetricPattern11<OracleBins>} phaseHistogram
* @property {MetricPattern11<Cents>} phasePriceCents
* @property {MetricPattern11<Cents>} priceCents
* @property {MetricPattern6<StoredU32>} txCount
*/
/**
* @typedef {Object} MetricsTree_Price_Usd
* @property {MetricPattern1<OHLCDollars>} ohlc
* @property {SplitPattern2<Dollars>} split
* @typedef {Object} MetricsTree_Price_Sats
* @property {MetricPattern1<OHLCSats>} ohlc
* @property {SplitPattern2<Sats>} split
*/
/**
@@ -4190,7 +4244,7 @@ function createOutputsPattern(client, acc) {
* @extends BrkClientBase
*/
class BrkClient extends BrkClientBase {
VERSION = "v0.1.0-alpha.2";
VERSION = "v0.1.0-alpha.3";
INDEXES = /** @type {const} */ ([
"dateindex",
@@ -4219,7 +4273,8 @@ class BrkClient extends BrkClientBase {
"weekindex",
"yearindex",
"loadedaddressindex",
"emptyaddressindex"
"emptyaddressindex",
"pairoutputindex"
]);
POOL_ID_TO_POOL_NAME = /** @type {const} */ ({
@@ -5990,16 +6045,24 @@ class BrkClient extends BrkClientBase {
},
},
oracle: {
heightToFirstPairoutputindex: createMetricPattern11(this, 'height_to_first_pairoutputindex'),
ohlcCents: createMetricPattern6(this, 'oracle_ohlc_cents'),
ohlcDollars: createMetricPattern6(this, 'oracle_ohlc'),
output0Value: createMetricPattern33(this, 'pair_output0_value'),
output1Value: createMetricPattern33(this, 'pair_output1_value'),
pairoutputindexToTxindex: createMetricPattern33(this, 'pairoutputindex_to_txindex'),
phaseDailyCents: createPhaseDailyCentsPattern(this, 'phase_daily'),
phaseDailyDollars: createPhaseDailyCentsPattern(this, 'phase_daily_dollars'),
phaseHistogram: createMetricPattern11(this, 'phase_histogram'),
phasePriceCents: createMetricPattern11(this, 'phase_price_cents'),
priceCents: createMetricPattern11(this, 'oracle_price_cents'),
txCount: createMetricPattern6(this, 'oracle_tx_count'),
},
sats: createSatsPattern(this, 'price'),
usd: {
ohlc: createMetricPattern1(this, 'price_ohlc'),
split: createSplitPattern2(this, 'price'),
sats: {
ohlc: createMetricPattern1(this, 'price_ohlc_sats'),
split: createSplitPattern2(this, 'price_sats'),
},
usd: createSatsPattern(this, 'price'),
},
scripts: {
count: {
@@ -6120,6 +6183,30 @@ class BrkClient extends BrkClientBase {
return _endpoint(this, metric, index);
}
/**
* OpenAPI specification
*
* Full OpenAPI 3.1 specification for this API.
*
* Endpoint: `GET /api.json`
* @returns {Promise<*>}
*/
async getOpenapi() {
return this.getJson(`/api.json`);
}
/**
* Trimmed OpenAPI specification
*
* Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information.
*
* Endpoint: `GET /api.trimmed.json`
* @returns {Promise<*>}
*/
async getOpenapiTrimmed() {
return this.getJson(`/api.trimmed.json`);
}
/**
* Address information
*
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"target": "ESNext",
"module": "ESNext",
"outDir": "/tmp/brk",
"lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"],
"skipLibCheck": true,
},
"exclude": ["assets", "./scripts/modules"],
}
+211 -124
View File
@@ -80,6 +80,7 @@ MonthIndex = int
# Opening price value for a time period
Open = Cents
OpReturnIndex = TypeIndex
OracleBins = List[int]
OutPoint = int
# Type (P2PKH, P2WPKH, P2SH, P2TR, etc.)
OutputType = Literal[
@@ -118,6 +119,11 @@ P2WPKHAddressIndex = TypeIndex
P2WPKHBytes = U8x20
P2WSHAddressIndex = TypeIndex
P2WSHBytes = U8x32
# Index for 2-output transactions (oracle pair candidates)
#
# This indexes all transactions with exactly 2 outputs, which are
# candidates for the UTXOracle algorithm (payment + change pattern).
PairOutputIndex = int
PoolSlug = Literal[
"unknown",
"blockfills",
@@ -340,6 +346,7 @@ Index = Literal[
"yearindex",
"loadedaddressindex",
"emptyaddressindex",
"pairoutputindex",
]
# Hierarchical tree node for organizing metrics into categories
TreeNode = Union[dict[str, "TreeNode"], "MetricLeafWithSchema"]
@@ -1679,6 +1686,7 @@ _i29 = ("weekindex",)
_i30 = ("yearindex",)
_i31 = ("loadedaddressindex",)
_i32 = ("emptyaddressindex",)
_i33 = ("pairoutputindex",)
def _ep(c: BrkClientBase, n: str, i: Index) -> MetricEndpointBuilder:
@@ -2511,6 +2519,29 @@ class MetricPattern32(Generic[T]):
return _ep(self.by._c, self._n, index) if index in _i32 else None
class _MetricPattern33By(Generic[T]):
def __init__(self, c: BrkClientBase, n: str):
self._c, self._n = c, n
def pairoutputindex(self) -> MetricEndpointBuilder[T]:
return _ep(self._c, self._n, "pairoutputindex")
class MetricPattern33(Generic[T]):
def __init__(self, c: BrkClientBase, n: str):
self._n, self.by = n, _MetricPattern33By(c, n)
@property
def name(self) -> str:
return self._n
def indexes(self) -> List[str]:
return list(_i33)
def get(self, index: Index) -> Optional[MetricEndpointBuilder[T]]:
return _ep(self.by._c, self._n, index) if index in _i33 else None
# Reusable structural pattern classes
@@ -3020,6 +3051,32 @@ class Price111dSmaPattern:
self.ratio_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, _m(acc, "ratio"))
class PercentilesPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.pct05: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct05"))
self.pct10: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct10"))
self.pct15: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct15"))
self.pct20: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct20"))
self.pct25: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct25"))
self.pct30: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct30"))
self.pct35: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct35"))
self.pct40: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct40"))
self.pct45: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct45"))
self.pct50: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct50"))
self.pct55: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct55"))
self.pct60: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct60"))
self.pct65: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct65"))
self.pct70: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct70"))
self.pct75: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct75"))
self.pct80: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct80"))
self.pct85: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct85"))
self.pct90: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct90"))
self.pct95: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct95"))
class ActivePriceRatioPattern:
"""Pattern struct for repeated tree structure."""
@@ -3074,32 +3131,6 @@ class ActivePriceRatioPattern:
self.ratio_sd: Ratio1ySdPattern = Ratio1ySdPattern(client, acc)
class PercentilesPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.pct05: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct05"))
self.pct10: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct10"))
self.pct15: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct15"))
self.pct20: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct20"))
self.pct25: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct25"))
self.pct30: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct30"))
self.pct35: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct35"))
self.pct40: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct40"))
self.pct45: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct45"))
self.pct50: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct50"))
self.pct55: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct55"))
self.pct60: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct60"))
self.pct65: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct65"))
self.pct70: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct70"))
self.pct75: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct75"))
self.pct80: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct80"))
self.pct85: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct85"))
self.pct90: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct90"))
self.pct95: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "pct95"))
class RelativePattern5:
"""Pattern struct for repeated tree structure."""
@@ -3457,22 +3488,6 @@ class AddrCountPattern:
self.p2wsh: MetricPattern1[StoredU64] = MetricPattern1(client, _p("p2wsh", acc))
class FullnessPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.average: MetricPattern2[T] = MetricPattern2(client, _m(acc, "average"))
self.base: MetricPattern11[T] = MetricPattern11(client, acc)
self.max: MetricPattern2[T] = MetricPattern2(client, _m(acc, "max"))
self.median: MetricPattern6[T] = MetricPattern6(client, _m(acc, "median"))
self.min: MetricPattern2[T] = MetricPattern2(client, _m(acc, "min"))
self.pct10: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct10"))
self.pct25: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct25"))
self.pct75: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct75"))
self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct90"))
class FeeRatePattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
@@ -3489,6 +3504,22 @@ class FeeRatePattern(Generic[T]):
self.txindex: MetricPattern27[T] = MetricPattern27(client, acc)
class FullnessPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.average: MetricPattern2[T] = MetricPattern2(client, _m(acc, "average"))
self.base: MetricPattern11[T] = MetricPattern11(client, acc)
self.max: MetricPattern2[T] = MetricPattern2(client, _m(acc, "max"))
self.median: MetricPattern6[T] = MetricPattern6(client, _m(acc, "median"))
self.min: MetricPattern2[T] = MetricPattern2(client, _m(acc, "min"))
self.pct10: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct10"))
self.pct25: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct25"))
self.pct75: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct75"))
self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct90"))
class _0satsPattern:
"""Pattern struct for repeated tree structure."""
@@ -3506,18 +3537,19 @@ class _0satsPattern:
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class _100btcPattern:
class PhaseDailyCentsPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.activity: ActivityPattern2 = ActivityPattern2(client, acc)
self.cost_basis: CostBasisPattern = CostBasisPattern(client, acc)
self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, "utxo_count"))
self.realized: RealizedPattern = RealizedPattern(client, acc)
self.relative: RelativePattern = RelativePattern(client, acc)
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, "supply"))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
self.average: MetricPattern6[T] = MetricPattern6(client, _m(acc, "average"))
self.max: MetricPattern6[T] = MetricPattern6(client, _m(acc, "max"))
self.median: MetricPattern6[T] = MetricPattern6(client, _m(acc, "median"))
self.min: MetricPattern6[T] = MetricPattern6(client, _m(acc, "min"))
self.pct10: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct10"))
self.pct25: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct25"))
self.pct75: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct75"))
self.pct90: MetricPattern6[T] = MetricPattern6(client, _m(acc, "pct90"))
class PeriodCagrPattern:
@@ -3534,6 +3566,20 @@ class PeriodCagrPattern:
self._8y: MetricPattern4[StoredF32] = MetricPattern4(client, _p("8y", acc))
class _10yTo12yPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.activity: ActivityPattern2 = ActivityPattern2(client, acc)
self.cost_basis: CostBasisPattern2 = CostBasisPattern2(client, acc)
self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, "utxo_count"))
self.realized: RealizedPattern2 = RealizedPattern2(client, acc)
self.relative: RelativePattern2 = RelativePattern2(client, acc)
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, "supply"))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class UnrealizedPattern:
"""Pattern struct for repeated tree structure."""
@@ -3562,20 +3608,6 @@ class UnrealizedPattern:
)
class _10yTo12yPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.activity: ActivityPattern2 = ActivityPattern2(client, acc)
self.cost_basis: CostBasisPattern2 = CostBasisPattern2(client, acc)
self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, "utxo_count"))
self.realized: RealizedPattern2 = RealizedPattern2(client, acc)
self.relative: RelativePattern2 = RelativePattern2(client, acc)
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, "supply"))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class _0satsPattern2:
"""Pattern struct for repeated tree structure."""
@@ -3590,6 +3622,20 @@ class _0satsPattern2:
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class _100btcPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.activity: ActivityPattern2 = ActivityPattern2(client, acc)
self.cost_basis: CostBasisPattern = CostBasisPattern(client, acc)
self.outputs: OutputsPattern = OutputsPattern(client, _m(acc, "utxo_count"))
self.realized: RealizedPattern = RealizedPattern(client, acc)
self.relative: RelativePattern = RelativePattern(client, acc)
self.supply: SupplyPattern2 = SupplyPattern2(client, _m(acc, "supply"))
self.unrealized: UnrealizedPattern = UnrealizedPattern(client, acc)
class _10yPattern:
"""Pattern struct for repeated tree structure."""
@@ -3637,26 +3683,6 @@ class SplitPattern2(Generic[T]):
self.open: MetricPattern1[T] = MetricPattern1(client, _m(acc, "open"))
class _2015Pattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: MetricPattern4[Bitcoin] = MetricPattern4(client, _m(acc, "btc"))
self.dollars: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "usd"))
self.sats: MetricPattern4[Sats] = MetricPattern4(client, acc)
class CoinbasePattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: BitcoinPattern = BitcoinPattern(client, _m(acc, "btc"))
self.dollars: DollarsPattern[Dollars] = DollarsPattern(client, _m(acc, "usd"))
self.sats: DollarsPattern[Sats] = DollarsPattern(client, acc)
class CoinbasePattern2:
"""Pattern struct for repeated tree structure."""
@@ -3671,6 +3697,16 @@ class CoinbasePattern2:
self.sats: BlockCountPattern[Sats] = BlockCountPattern(client, acc)
class CoinbasePattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: BitcoinPattern = BitcoinPattern(client, _m(acc, "btc"))
self.dollars: DollarsPattern[Dollars] = DollarsPattern(client, _m(acc, "usd"))
self.sats: DollarsPattern[Sats] = DollarsPattern(client, acc)
class SegwitAdoptionPattern:
"""Pattern struct for repeated tree structure."""
@@ -3683,6 +3719,26 @@ class SegwitAdoptionPattern:
self.sum: MetricPattern2[StoredF32] = MetricPattern2(client, _m(acc, "sum"))
class _2015Pattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: MetricPattern4[Bitcoin] = MetricPattern4(client, _m(acc, "btc"))
self.dollars: MetricPattern4[Dollars] = MetricPattern4(client, _m(acc, "usd"))
self.sats: MetricPattern4[Sats] = MetricPattern4(client, acc)
class ActiveSupplyPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: MetricPattern1[Bitcoin] = MetricPattern1(client, _m(acc, "btc"))
self.dollars: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, "usd"))
self.sats: MetricPattern1[Sats] = MetricPattern1(client, acc)
class CostBasisPattern2:
"""Pattern struct for repeated tree structure."""
@@ -3699,16 +3755,6 @@ class CostBasisPattern2:
)
class ActiveSupplyPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.bitcoin: MetricPattern1[Bitcoin] = MetricPattern1(client, _m(acc, "btc"))
self.dollars: MetricPattern1[Dollars] = MetricPattern1(client, _m(acc, "usd"))
self.sats: MetricPattern1[Sats] = MetricPattern1(client, acc)
class UnclaimedRewardsPattern:
"""Pattern struct for repeated tree structure."""
@@ -3721,6 +3767,19 @@ class UnclaimedRewardsPattern:
self.sats: BlockCountPattern[Sats] = BlockCountPattern(client, acc)
class RelativePattern4:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.supply_in_loss_rel_to_own_supply: MetricPattern1[StoredF64] = (
MetricPattern1(client, _m(acc, "loss_rel_to_own_supply"))
)
self.supply_in_profit_rel_to_own_supply: MetricPattern1[StoredF64] = (
MetricPattern1(client, _m(acc, "profit_rel_to_own_supply"))
)
class CostBasisPattern:
"""Pattern struct for repeated tree structure."""
@@ -3745,19 +3804,6 @@ class SupplyPattern2:
self.total: ActiveSupplyPattern = ActiveSupplyPattern(client, acc)
class RelativePattern4:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.supply_in_loss_rel_to_own_supply: MetricPattern1[StoredF64] = (
MetricPattern1(client, _m(acc, "loss_rel_to_own_supply"))
)
self.supply_in_profit_rel_to_own_supply: MetricPattern1[StoredF64] = (
MetricPattern1(client, _m(acc, "profit_rel_to_own_supply"))
)
class _1dReturns1mSdPattern:
"""Pattern struct for repeated tree structure."""
@@ -3767,6 +3813,15 @@ class _1dReturns1mSdPattern:
self.sma: MetricPattern4[StoredF32] = MetricPattern4(client, _m(acc, "sma"))
class SatsPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.ohlc: MetricPattern1[T] = MetricPattern1(client, _m(acc, "ohlc"))
self.split: SplitPattern2[T] = SplitPattern2(client, acc)
class BlockCountPattern(Generic[T]):
"""Pattern struct for repeated tree structure."""
@@ -3789,13 +3844,12 @@ class BitcoinPattern2(Generic[T]):
self.sum: MetricPattern1[T] = MetricPattern1(client, acc)
class SatsPattern(Generic[T]):
class OutputsPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.ohlc: MetricPattern1[T] = MetricPattern1(client, _m(acc, "ohlc_sats"))
self.split: SplitPattern2[T] = SplitPattern2(client, _m(acc, "sats"))
self.utxo_count: MetricPattern1[StoredU64] = MetricPattern1(client, acc)
class RealizedPriceExtraPattern:
@@ -3806,14 +3860,6 @@ class RealizedPriceExtraPattern:
self.ratio: MetricPattern4[StoredF32] = MetricPattern4(client, acc)
class OutputsPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.utxo_count: MetricPattern1[StoredU64] = MetricPattern1(client, acc)
# Metrics tree classes
@@ -5913,12 +5959,36 @@ class MetricsTree_Price_Oracle:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ""):
self.height_to_first_pairoutputindex: MetricPattern11[PairOutputIndex] = (
MetricPattern11(client, "height_to_first_pairoutputindex")
)
self.ohlc_cents: MetricPattern6[OHLCCents] = MetricPattern6(
client, "oracle_ohlc_cents"
)
self.ohlc_dollars: MetricPattern6[OHLCDollars] = MetricPattern6(
client, "oracle_ohlc"
)
self.output0_value: MetricPattern33[Sats] = MetricPattern33(
client, "pair_output0_value"
)
self.output1_value: MetricPattern33[Sats] = MetricPattern33(
client, "pair_output1_value"
)
self.pairoutputindex_to_txindex: MetricPattern33[TxIndex] = MetricPattern33(
client, "pairoutputindex_to_txindex"
)
self.phase_daily_cents: PhaseDailyCentsPattern[Cents] = PhaseDailyCentsPattern(
client, "phase_daily"
)
self.phase_daily_dollars: PhaseDailyCentsPattern[Dollars] = (
PhaseDailyCentsPattern(client, "phase_daily_dollars")
)
self.phase_histogram: MetricPattern11[OracleBins] = MetricPattern11(
client, "phase_histogram"
)
self.phase_price_cents: MetricPattern11[Cents] = MetricPattern11(
client, "phase_price_cents"
)
self.price_cents: MetricPattern11[Cents] = MetricPattern11(
client, "oracle_price_cents"
)
@@ -5927,12 +5997,12 @@ class MetricsTree_Price_Oracle:
)
class MetricsTree_Price_Usd:
class MetricsTree_Price_Sats:
"""Metrics tree node."""
def __init__(self, client: BrkClientBase, base_path: str = ""):
self.ohlc: MetricPattern1[OHLCDollars] = MetricPattern1(client, "price_ohlc")
self.split: SplitPattern2[Dollars] = SplitPattern2(client, "price")
self.ohlc: MetricPattern1[OHLCSats] = MetricPattern1(client, "price_ohlc_sats")
self.split: SplitPattern2[Sats] = SplitPattern2(client, "price_sats")
class MetricsTree_Price:
@@ -5941,8 +6011,8 @@ class MetricsTree_Price:
def __init__(self, client: BrkClientBase, base_path: str = ""):
self.cents: MetricsTree_Price_Cents = MetricsTree_Price_Cents(client)
self.oracle: MetricsTree_Price_Oracle = MetricsTree_Price_Oracle(client)
self.sats: SatsPattern[OHLCSats] = SatsPattern(client, "price")
self.usd: MetricsTree_Price_Usd = MetricsTree_Price_Usd(client)
self.sats: MetricsTree_Price_Sats = MetricsTree_Price_Sats(client)
self.usd: SatsPattern[OHLCDollars] = SatsPattern(client, "price")
class MetricsTree_Scripts_Count:
@@ -6225,7 +6295,7 @@ class MetricsTree:
class BrkClient(BrkClientBase):
"""Main BRK client with metrics tree and API methods."""
VERSION = "v0.1.0-alpha.2"
VERSION = "v0.1.0-alpha.3"
INDEXES = [
"dateindex",
@@ -6255,6 +6325,7 @@ class BrkClient(BrkClientBase):
"yearindex",
"loadedaddressindex",
"emptyaddressindex",
"pairoutputindex",
]
POOL_ID_TO_POOL_NAME = {
@@ -6870,6 +6941,22 @@ class BrkClient(BrkClientBase):
"""
return MetricEndpointBuilder(self, metric, index)
def get_openapi(self) -> Any:
"""OpenAPI specification.
Full OpenAPI 3.1 specification for this API.
Endpoint: `GET /api.json`"""
return self.get_json("/api.json")
def get_openapi_trimmed(self) -> Any:
"""Trimmed OpenAPI specification.
Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information.
Endpoint: `GET /api.trimmed.json`"""
return self.get_json("/api.trimmed.json")
def get_address(self, address: Address) -> AddressStats:
"""Address information.
-1
View File
@@ -21,7 +21,6 @@ CRATES=(
brk_computer
brk_query
brk_bindgen
brk_mcp
brk_server
brk_client
brk
+12
View File
@@ -43,6 +43,18 @@ export function createMarketSection(ctx) {
unit: Unit.usd,
colors: [colors.cyan, colors.purple],
}),
line({
metric: price.oracle.phaseDailyDollars.median,
name: "Oracle2 median",
unit: Unit.usd,
color: colors.blue,
}),
line({
metric: price.oracle.phaseDailyDollars.average,
name: "Oracle2 average",
unit: Unit.usd,
color: colors.yellow,
}),
],
}),
},
+3 -5
View File
@@ -87,7 +87,7 @@ function useMetricEndpoint(endpoint) {
* @param {number} [to]
* @returns {RangeState<T>}
*/
function range(from = -10000, to) {
function range(from, to) {
const key = `${from}-${to ?? ""}`;
const existing = ranges.get(key);
if (existing) return existing;
@@ -111,13 +111,11 @@ function useMetricEndpoint(endpoint) {
* @param {number} [start=-10000]
* @param {number} [end]
*/
async fetch(start = -10000, end) {
async fetch(start, end) {
const r = range(start, end);
r.loading.set(true);
try {
const result = await endpoint
.slice(start, end)
.fetch(r.response.set);
const result = await endpoint.slice(start, end).fetch(r.response.set);
return result;
} finally {
r.loading.set(false);