mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 06:01:57 -07:00
global: snapshot
This commit is contained in:
Generated
+7
-190
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -14,3 +14,4 @@ brk_types = { workspace = true }
|
||||
oas3 = "0.20"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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 ¶ms.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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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` |
|
||||
|
||||
@@ -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(¶ms)?;
|
||||
|
||||
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, ¶ms.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(¶ms)?;
|
||||
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, ¶ms.range)
|
||||
Ok(MetricOutput {
|
||||
output,
|
||||
version,
|
||||
total,
|
||||
start,
|
||||
end,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn metric_to_index_to_vec(&self) -> &BTreeMap<&str, IndexToVec<'_>> {
|
||||
|
||||
@@ -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(¶ms)?;
|
||||
|
||||
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, ¶ms.range)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
@@ -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")
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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, ¶ms)
|
||||
}
|
||||
|
||||
fn new_text_cached(value: &str, params: &CacheParams) -> Self {
|
||||
let mut response = Response::builder()
|
||||
.body(value.to_string().into())
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"))),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -38,7 +38,6 @@ services:
|
||||
# BRK configuration
|
||||
- BRKDIR=/home/brk/.brk
|
||||
- FETCH=${BRK_FETCH:-true}
|
||||
- MCP=${BRK_MCP:-true}
|
||||
command:
|
||||
- --bitcoindir
|
||||
- /bitcoin
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
- example: from -10,000 count 10, won’t work if underlying vec isn’t 10k or more long
|
||||
- _LOGGER_
|
||||
- BUG: remove colors from file
|
||||
- _MCP_
|
||||
- _PARSER_
|
||||
- _SERVER_
|
||||
- api
|
||||
|
||||
+321
-234
@@ -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
|
||||
*
|
||||
|
||||
+3944
-3723
File diff suppressed because it is too large
Load Diff
@@ -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"],
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ CRATES=(
|
||||
brk_computer
|
||||
brk_query
|
||||
brk_bindgen
|
||||
brk_mcp
|
||||
brk_server
|
||||
brk_client
|
||||
brk
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user