global: snapshot

This commit is contained in:
nym21
2026-01-12 11:39:44 +01:00
parent 8fe0af349d
commit 1b9e18f98b
33 changed files with 7603 additions and 7968 deletions
+1
View File
@@ -17,6 +17,7 @@ _*
/*.py
/api.json
/*.json
/*.html
# Logs
*.log*
Generated
+1
View File
@@ -489,6 +489,7 @@ dependencies = [
"brk_types",
"minreq",
"serde",
"serde_json",
]
[[package]]
+50 -28
View File
@@ -2,44 +2,66 @@
Umbrella crate for the Bitcoin Research Kit.
## What It Enables
Single dependency to access any BRK component. Enable only what you need via feature flags.
[crates.io](https://crates.io/crates/brk) | [docs.rs](https://docs.rs/brk)
## Usage
Single dependency to access any BRK component. Enable only what you need via feature flags.
```toml
[dependencies]
brk = { version = "0.x", features = ["query", "types"] }
brk = { version = "0.1", features = ["query", "types"] }
```
```rust,ignore
```rust
use brk::query::Query;
use brk::types::Height;
```
## Feature Flags
Feature flags match crate names without the `brk_` prefix. Use `full` to enable all.
| Feature | Crate | Description |
|---------|-------|-------------|
| `bencher` | `brk_bencher` | Benchmarking utilities |
| `binder` | `brk_binder` | Client code generation |
| `client` | `brk_client` | Generated Rust API client |
| `computer` | `brk_computer` | Metric computation |
| `error` | `brk_error` | Error types |
| `fetcher` | `brk_fetcher` | Price data fetching |
| `cohort` | `brk_cohort` | Cohort filtering |
| `indexer` | `brk_indexer` | Blockchain indexing |
| `iterator` | `brk_iterator` | Block iteration |
| `logger` | `brk_logger` | Logging setup |
| `mcp` | `brk_mcp` | MCP server |
| `mempool` | `brk_mempool` | Mempool monitoring |
| `query` | `brk_query` | Query interface |
| `reader` | `brk_reader` | Raw block reading |
| `rpc` | `brk_rpc` | Bitcoin RPC client |
| `server` | `brk_server` | HTTP API server |
| `store` | `brk_store` | Key-value storage |
| `traversable` | `brk_traversable` | Data traversal |
| `types` | `brk_types` | Domain types |
## Crates
Use `full` to enable all features.
**Core Pipeline**
| Crate | Description |
|-------|-------------|
| [brk_reader](https://docs.rs/brk_reader) | Read blocks from `blk*.dat` with parallel parsing and XOR decoding |
| [brk_indexer](https://docs.rs/brk_indexer) | Index transactions, addresses, and UTXOs |
| [brk_computer](https://docs.rs/brk_computer) | Compute derived metrics (realized cap, MVRV, SOPR, cohorts, etc.) |
| [brk_mempool](https://docs.rs/brk_mempool) | Monitor mempool, estimate fees, project upcoming blocks |
| [brk_query](https://docs.rs/brk_query) | Query interface for indexed and computed data |
| [brk_server](https://docs.rs/brk_server) | REST API with OpenAPI docs |
**Data & Storage**
| Crate | Description |
|-------|-------------|
| [brk_types](https://docs.rs/brk_types) | Domain types: `Height`, `Sats`, `Txid`, addresses, etc. |
| [brk_store](https://docs.rs/brk_store) | Key-value storage (fjall wrapper) |
| [brk_fetcher](https://docs.rs/brk_fetcher) | Fetch price data from exchanges |
| [brk_rpc](https://docs.rs/brk_rpc) | Bitcoin Core RPC client |
| [brk_iterator](https://docs.rs/brk_iterator) | Unified block iteration with automatic source selection |
| [brk_cohort](https://docs.rs/brk_cohort) | UTXO and address cohort filtering |
| [brk_traversable](https://docs.rs/brk_traversable) | Navigate hierarchical data structures |
**Clients & Integration**
| Crate | Description |
|-------|-------------|
| [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**
| Crate | Description |
|-------|-------------|
| [brk_cli](https://docs.rs/brk_cli) | CLI binary (`cargo install --locked brk_cli`) |
| [brk_error](https://docs.rs/brk_error) | Error types |
| [brk_logger](https://docs.rs/brk_logger) | Logging infrastructure |
| [brk_bencher](https://docs.rs/brk_bencher) | Benchmarking utilities |
## License
MIT
@@ -2,7 +2,7 @@
use std::fmt::Write;
use crate::{Endpoint, Parameter, generators::MANUAL_GENERIC_TYPES, to_camel_case};
use crate::{Endpoint, Parameter, generators::{MANUAL_GENERIC_TYPES, write_description}, to_camel_case};
/// Generate API methods for the BrkClient class.
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
@@ -28,9 +28,13 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " *").unwrap();
writeln!(output, " * {}", desc).unwrap();
write_description(output, desc, " * ", " *");
}
// Add endpoint path
writeln!(output, " *").unwrap();
writeln!(output, " * Endpoint: `{} {}`", endpoint.method.to_uppercase(), endpoint.path).unwrap();
if !endpoint.path_params.is_empty() || !endpoint.query_params.is_empty() {
writeln!(output, " *").unwrap();
}
@@ -23,15 +23,22 @@ pub fn generate_base_client(output: &mut String) {
* @typedef {{Object}} BrkClientOptions
* @property {{string}} baseUrl - Base URL for the API
* @property {{number}} [timeout] - Request timeout in milliseconds
* @property {{string|boolean}} [cache] - Enable browser cache with default name (true), custom name (string), or disable (false). No effect in Node.js. Default: true
*/
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
const _runIdle = (/** @type {{VoidFunction}} */ fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
const _defaultCacheName = '__BRK_CLIENT__';
/** @type {{Promise<Cache | null>}} */
const _cachePromise = _isBrowser
? caches.open('__BRK_CLIENT__').catch(() => null)
: Promise.resolve(null);
/**
* @param {{string|boolean|undefined}} cache
* @returns {{Promise<Cache | null>}}
*/
const _openCache = (cache) => {{
if (!_isBrowser || cache === false) return Promise.resolve(null);
const name = typeof cache === 'string' ? cache : _defaultCacheName;
return caches.open(name).catch(() => null);
}};
/**
* Custom error class for BRK client errors
@@ -112,6 +119,8 @@ class BrkClientBase {{
const isString = typeof options === 'string';
this.baseUrl = isString ? options : options.baseUrl;
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
/** @type {{Promise<Cache | null>}} */
this._cachePromise = _openCache(isString ? undefined : options.cache);
}}
/**
@@ -136,7 +145,7 @@ class BrkClientBase {{
async getJson(path, onUpdate) {{
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
const url = `${{base}}${{path}}`;
const cache = await _cachePromise;
const cache = await this._cachePromise;
const cachedRes = await cache?.match(url);
const cachedJson = cachedRes ? await cachedRes.json() : null;
+14
View File
@@ -7,6 +7,8 @@
//! - `api.rs` - API method generation
//! - `mod.rs` - Entry point
use std::fmt::Write;
pub mod javascript;
pub mod python;
pub mod rust;
@@ -17,3 +19,15 @@ pub use rust::generate_rust_client;
/// Types that are manually defined as generics in client code, not from schema.
pub const MANUAL_GENERIC_TYPES: &[&str] = &["MetricData", "MetricEndpoint"];
/// Write a multi-line description with the given prefix for each line.
/// `empty_prefix` is used for blank lines (e.g., " *" without trailing space).
pub fn write_description(output: &mut String, desc: &str, prefix: &str, empty_prefix: &str) {
for line in desc.lines() {
if line.is_empty() {
writeln!(output, "{}", empty_prefix).unwrap();
} else {
writeln!(output, "{}{}", prefix, line).unwrap();
}
}
}
@@ -2,7 +2,7 @@
use std::fmt::Write;
use crate::{Endpoint, Parameter, escape_python_keyword, generators::MANUAL_GENERIC_TYPES, to_snake_case};
use crate::{Endpoint, Parameter, escape_python_keyword, generators::{MANUAL_GENERIC_TYPES, write_description}, to_snake_case};
use super::client::generate_class_constants;
use super::types::js_type_to_python;
@@ -69,16 +69,31 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
(Some(summary), Some(desc)) if summary != desc => {
writeln!(output, " \"\"\"{}.", summary.trim_end_matches('.')).unwrap();
writeln!(output).unwrap();
writeln!(output, " {}\"\"\"", desc).unwrap();
write_description(output, desc, " ", "");
}
(Some(summary), _) => {
writeln!(output, " \"\"\"{}\"\"\"", summary).unwrap();
writeln!(output, " \"\"\"{}", summary).unwrap();
}
(None, Some(desc)) => {
writeln!(output, " \"\"\"{}\"\"\"", desc).unwrap();
// First line includes opening quotes
let mut lines = desc.lines();
if let Some(first) = lines.next() {
writeln!(output, " \"\"\"{}", first).unwrap();
}
for line in lines {
if line.is_empty() {
writeln!(output).unwrap();
} else {
writeln!(output, " {}", line).unwrap();
}
}
}
(None, None) => {
write!(output, " \"\"\"").unwrap();
}
(None, None) => {}
}
writeln!(output).unwrap();
writeln!(output, " Endpoint: `{} {}`\"\"\"", endpoint.method.to_uppercase(), endpoint.path).unwrap();
// Build path
let path = build_path_template(&endpoint.path, &endpoint.path_params);
@@ -2,7 +2,7 @@
use std::fmt::Write;
use crate::{Endpoint, VERSION, to_snake_case};
use crate::{Endpoint, VERSION, generators::write_description, to_snake_case};
use super::types::js_type_to_rust;
@@ -78,8 +78,11 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " ///").unwrap();
writeln!(output, " /// {}", desc).unwrap();
write_description(output, desc, " /// ", " ///");
}
// Add endpoint path
writeln!(output, " ///").unwrap();
writeln!(output, " /// Endpoint: `{} {}`", endpoint.method.to_uppercase(), endpoint.path).unwrap();
let params = build_method_params(endpoint);
writeln!(
+1
View File
@@ -15,3 +15,4 @@ brk_cohort = { workspace = true }
brk_types = { workspace = true }
minreq = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+56
View File
@@ -1 +1,57 @@
# brk_client
Rust client for the [Bitcoin Research Kit](https://github.com/bitcoinresearchkit/brk) API.
[crates.io](https://crates.io/crates/brk_client) | [docs.rs](https://docs.rs/brk_client)
## Installation
```toml
[dependencies]
brk_client = "0.1"
```
## Quick Start
```rust
use brk_client::{BrkClient, Index};
fn main() -> brk_client::Result<()> {
let client = BrkClient::new("http://localhost:3000");
// Blockchain data (mempool.space compatible)
let block = client.get_block_by_height(800000)?;
let tx = client.get_tx("abc123...")?;
let address = client.get_address("bc1q...")?;
// Metrics API - typed, chainable
let prices = client.metrics()
.price.usd.split.close
.by.dateindex()
.range(Some(-30), None)?; // Last 30 days
// Generic metric fetching
let data = client.get_metric(
"price_close".into(),
Index::DateIndex,
Some(-30), None, None, None,
)?;
Ok(())
}
```
## Configuration
```rust
use brk_client::{BrkClient, BrkClientOptions};
let client = BrkClient::with_options(BrkClientOptions {
base_url: "http://localhost:3000".to_string(),
timeout_secs: 60,
});
```
## License
MIT
+1 -1
View File
@@ -60,7 +60,7 @@ fn main() -> brk_client::Result<()> {
println!("Last 3 circulating supply values: {:?}", circulating);
// Using generic metric fetching
let metricdata = client.get_metric_by_index(
let metricdata = client.get_metric(
Metric::from("price_close"),
Index::DateIndex,
Some(-3),
File diff suppressed because it is too large Load Diff
+5
View File
@@ -124,6 +124,9 @@ pub enum Error {
#[error("Fetch failed after retries: {0}")]
FetchFailed(String),
#[error("HTTP {status}: {url}")]
HttpStatus { status: u16, url: String },
#[error("Version mismatch at {path:?}: expected {expected}, found {found}")]
VersionMismatch {
path: PathBuf,
@@ -141,6 +144,8 @@ impl Error {
match self {
Error::Minreq(e) => is_minreq_error_permanent(e),
Error::IO(e) => is_io_error_permanent(e),
// 403 Forbidden suggests IP/geo blocking; 429 and 5xx are transient
Error::HttpStatus { status, .. } => *status == 403,
// Other errors are data/parsing related, not network - treat as transient
_ => false,
}
+5 -5
View File
@@ -11,7 +11,7 @@ use serde_json::Value;
use tracing::info;
use crate::{
PriceSource, default_retry,
PriceSource, check_response, default_retry,
ohlc::{compute_ohlc_from_range, date_from_timestamp, ohlc_from_array, timestamp_from_ms},
};
@@ -73,8 +73,8 @@ impl Binance {
default_retry(|_| {
let url = Self::url("interval=1m&limit=1000");
info!("Fetching {url} ...");
let json: Value =
serde_json::from_slice(minreq::get(url).with_timeout(30).send()?.as_bytes())?;
let bytes = check_response(minreq::get(&url).with_timeout(30).send()?, &url)?;
let json: Value = serde_json::from_slice(&bytes)?;
Self::parse_ohlc_array(&json)
})
}
@@ -96,8 +96,8 @@ impl Binance {
default_retry(|_| {
let url = Self::url("interval=1d");
info!("Fetching {url} ...");
let json: Value =
serde_json::from_slice(minreq::get(url).with_timeout(30).send()?.as_bytes())?;
let bytes = check_response(minreq::get(&url).with_timeout(30).send()?, &url)?;
let json: Value = serde_json::from_slice(&bytes)?;
Self::parse_date_ohlc_array(&json)
})
}
+5 -5
View File
@@ -8,7 +8,7 @@ use brk_types::{
use serde_json::Value;
use tracing::info;
use crate::{PriceSource, default_retry};
use crate::{PriceSource, check_response, default_retry};
#[derive(Default, Clone)]
#[allow(clippy::upper_case_acronyms)]
@@ -49,8 +49,8 @@ impl BRK {
);
info!("Fetching {url} ...");
let body: Value =
serde_json::from_slice(minreq::get(url).with_timeout(30).send()?.as_bytes())?;
let bytes = check_response(minreq::get(&url).with_timeout(30).send()?, &url)?;
let body: Value = serde_json::from_slice(&bytes)?;
body.as_array()
.ok_or(Error::Parse("Expected JSON array".into()))?
@@ -90,8 +90,8 @@ impl BRK {
);
info!("Fetching {url}...");
let body: Value =
serde_json::from_slice(minreq::get(url).with_timeout(30).send()?.as_bytes())?;
let bytes = check_response(minreq::get(&url).with_timeout(30).send()?, &url)?;
let body: Value = serde_json::from_slice(&bytes)?;
body.as_array()
.ok_or(Error::Parse("Expected JSON array".into()))?
+5 -5
View File
@@ -6,7 +6,7 @@ use serde_json::Value;
use tracing::info;
use crate::{
PriceSource, default_retry,
PriceSource, check_response, default_retry,
ohlc::{compute_ohlc_from_range, date_from_timestamp, ohlc_from_array, timestamp_from_secs},
};
@@ -39,8 +39,8 @@ impl Kraken {
default_retry(|_| {
let url = Self::url(1);
info!("Fetching {url} ...");
let json: Value =
serde_json::from_slice(minreq::get(url).with_timeout(30).send()?.as_bytes())?;
let bytes = check_response(minreq::get(&url).with_timeout(30).send()?, &url)?;
let json: Value = serde_json::from_slice(&bytes)?;
Self::parse_ohlc_response(&json)
})
}
@@ -61,8 +61,8 @@ impl Kraken {
default_retry(|_| {
let url = Self::url(1440);
info!("Fetching {url} ...");
let json: Value =
serde_json::from_slice(minreq::get(url).with_timeout(30).send()?.as_bytes())?;
let bytes = check_response(minreq::get(&url).with_timeout(30).send()?, &url)?;
let json: Value = serde_json::from_slice(&bytes)?;
Self::parse_date_ohlc_response(&json)
})
}
+13
View File
@@ -22,6 +22,19 @@ pub use source::{PriceSource, TrackedSource};
const MAX_RETRIES: usize = 12 * 60; // 12 hours of retrying
/// Check HTTP response status and return bytes or error
pub fn check_response(response: minreq::Response, url: &str) -> Result<Vec<u8>> {
let status = response.status_code as u16;
if (200..300).contains(&status) {
Ok(response.into_bytes())
} else {
Err(Error::HttpStatus {
status,
url: url.to_string(),
})
}
}
#[derive(Clone)]
pub struct Fetcher {
pub binance: Option<TrackedSource<Binance>>,
+2 -1
View File
@@ -98,8 +98,8 @@ impl<T: PriceSource> TrackedSource<T> {
self.name(),
self.cooldown.as_secs()
);
self.unhealthy_since = Some(Instant::now());
}
self.unhealthy_since = Some(Instant::now());
}
Err(_) => {} // Transient - no change
}
@@ -137,5 +137,6 @@ impl<T: PriceSource> PriceSource for TrackedSource<T> {
fn clear(&mut self) {
self.source.clear();
self.reset_health();
}
}
+12 -6
View File
@@ -32,9 +32,10 @@ impl AddressRoutes for ApiRouter<AppState> {
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address(path.address)).await
}, |op| op
.id("get_address")
.addresses_tag()
.summary("Address information")
.description("Retrieve comprehensive information about a Bitcoin address including balance, transaction history, UTXOs, and estimated investment metrics. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR, etc.).")
.description("Retrieve address information including balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address)*")
.ok_response::<AddressStats>()
.not_modified()
.bad_request()
@@ -52,9 +53,10 @@ impl AddressRoutes for ApiRouter<AppState> {
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(path.address, params.after_txid, params.limit)).await
}, |op| op
.id("get_address_txs")
.addresses_tag()
.summary("Address transaction IDs")
.description("Get transaction IDs for an address, newest first. Use after_txid for pagination.")
.description("Get transaction IDs for an address, newest first. Use after_txid for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*")
.ok_response::<Vec<Txid>>()
.not_modified()
.bad_request()
@@ -71,9 +73,10 @@ impl AddressRoutes for ApiRouter<AppState> {
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_utxos(path.address)).await
}, |op| op
.id("get_address_utxos")
.addresses_tag()
.summary("Address UTXOs")
.description("Get unspent transaction outputs for an address.")
.description("Get unspent transaction outputs (UTXOs) for an address. Returns txid, vout, value, and confirmation status for each UTXO.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-utxo)*")
.ok_response::<Vec<Utxo>>()
.not_modified()
.bad_request()
@@ -91,9 +94,10 @@ impl AddressRoutes for ApiRouter<AppState> {
// Mempool txs for an address - use MaxAge since it's volatile
state.cached_json(&headers, CacheStrategy::MaxAge(5), move |q| q.address_mempool_txids(path.address)).await
}, |op| op
.id("get_address_mempool_txs")
.addresses_tag()
.summary("Address mempool transactions")
.description("Get unconfirmed transaction IDs for an address from the mempool (up to 50).")
.description("Get unconfirmed transaction IDs for an address from the mempool (up to 50).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-mempool)*")
.ok_response::<Vec<Txid>>()
.bad_request()
.not_found()
@@ -110,9 +114,10 @@ impl AddressRoutes for ApiRouter<AppState> {
| {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.address_txids(path.address, params.after_txid, 25)).await
}, |op| op
.id("get_address_confirmed_txs")
.addresses_tag()
.summary("Address confirmed transactions")
.description("Get confirmed transaction IDs for an address, 25 per page. Use ?after_txid=<txid> for pagination.")
.description("Get confirmed transaction IDs for an address, 25 per page. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*")
.ok_response::<Vec<Txid>>()
.not_modified()
.bad_request()
@@ -129,9 +134,10 @@ impl AddressRoutes for ApiRouter<AppState> {
| {
state.cached_json(&headers, CacheStrategy::Static, move |_q| Ok(AddressValidation::from_address(&path.address))).await
}, |op| op
.id("validate_address")
.addresses_tag()
.summary("Validate address")
.description("Validate a Bitcoin address and get information about its type and scriptPubKey.")
.description("Validate a Bitcoin address and get information about its type and scriptPubKey.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)*")
.ok_response::<AddressValidation>()
.not_modified()
),
+30 -20
View File
@@ -28,9 +28,10 @@ impl BlockRoutes for ApiRouter<AppState> {
.await
},
|op| {
op.blocks_tag()
op.id("get_blocks")
.blocks_tag()
.summary("Recent blocks")
.description("Retrieve the last 10 blocks. Returns block metadata for each block.")
.description("Retrieve the last 10 blocks. Returns block metadata for each block.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*")
.ok_response::<Vec<BlockInfo>>()
.not_modified()
.server_error()
@@ -46,10 +47,11 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block(&path.hash)).await
},
|op| {
op.blocks_tag()
op.id("get_block")
.blocks_tag()
.summary("Block information")
.description(
"Retrieve block information by block hash. Returns block metadata including height, timestamp, difficulty, size, weight, and transaction count.",
"Retrieve block information by block hash. Returns block metadata including height, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block)*",
)
.ok_response::<BlockInfo>()
.not_modified()
@@ -68,10 +70,11 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_status(&path.hash)).await
},
|op| {
op.blocks_tag()
op.id("get_block_status")
.blocks_tag()
.summary("Block status")
.description(
"Retrieve the status of a block. Returns whether the block is in the best chain and, if so, its height and the hash of the next block.",
"Retrieve the status of a block. Returns whether the block is in the best chain and, if so, its height and the hash of the next block.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-status)*",
)
.ok_response::<BlockStatus>()
.not_modified()
@@ -90,10 +93,11 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_height(path.height)).await
},
|op| {
op.blocks_tag()
op.id("get_block_by_height")
.blocks_tag()
.summary("Block by height")
.description(
"Retrieve block information by block height. Returns block metadata including hash, timestamp, difficulty, size, weight, and transaction count.",
"Retrieve block information by block height. Returns block metadata including hash, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-height)*",
)
.ok_response::<BlockInfo>()
.not_modified()
@@ -112,10 +116,11 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.blocks(Some(path.height))).await
},
|op| {
op.blocks_tag()
op.id("get_blocks_from_height")
.blocks_tag()
.summary("Blocks from height")
.description(
"Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.",
"Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*",
)
.ok_response::<Vec<BlockInfo>>()
.not_modified()
@@ -133,10 +138,11 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txids(&path.hash)).await
},
|op| {
op.blocks_tag()
op.id("get_block_txids")
.blocks_tag()
.summary("Block transaction IDs")
.description(
"Retrieve all transaction IDs in a block by block hash.",
"Retrieve all transaction IDs in a block. Returns an array of txids in block order.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transaction-ids)*",
)
.ok_response::<Vec<Txid>>()
.not_modified()
@@ -155,10 +161,11 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_txs(&path.hash, path.start_index)).await
},
|op| {
op.blocks_tag()
op.id("get_block_txs")
.blocks_tag()
.summary("Block transactions (paginated)")
.description(&format!(
"Retrieve transactions in a block by block hash, starting from the specified index. Returns up to {} transactions at a time.",
"Retrieve transactions in a block by block hash, starting from the specified index. Returns up to {} transactions at a time.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transactions)*",
BLOCK_TXS_PAGE_SIZE
))
.ok_response::<Vec<Transaction>>()
@@ -178,10 +185,11 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_text(&headers, CacheStrategy::Height, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await
},
|op| {
op.blocks_tag()
op.id("get_block_txid")
.blocks_tag()
.summary("Transaction ID at index")
.description(
"Retrieve a single transaction ID at a specific index within a block. Returns plain text txid.",
"Retrieve a single transaction ID at a specific index within a block. Returns plain text txid.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transaction-id)*",
)
.ok_response::<Txid>()
.not_modified()
@@ -200,9 +208,10 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.block_by_timestamp(path.timestamp)).await
},
|op| {
op.blocks_tag()
op.id("get_block_by_timestamp")
.blocks_tag()
.summary("Block by timestamp")
.description("Find the block closest to a given UNIX timestamp.")
.description("Find the block closest to a given UNIX timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-timestamp)*")
.ok_response::<BlockTimestamp>()
.not_modified()
.bad_request()
@@ -220,10 +229,11 @@ impl BlockRoutes for ApiRouter<AppState> {
state.cached_bytes(&headers, CacheStrategy::Height, move |q| q.block_raw(&path.hash)).await
},
|op| {
op.blocks_tag()
op.id("get_block_raw")
.blocks_tag()
.summary("Raw block")
.description(
"Returns the raw block data in binary format.",
"Returns the raw block data in binary format.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-raw)*",
)
.ok_response::<Vec<u8>>()
.not_modified()
+12 -8
View File
@@ -26,9 +26,10 @@ impl MempoolRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_info()).await
},
|op| {
op.mempool_tag()
op.id("get_mempool")
.mempool_tag()
.summary("Mempool statistics")
.description("Get current mempool statistics including transaction count, total vsize, and total fees.")
.description("Get current mempool statistics including transaction count, total vsize, and total fees.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool)*")
.ok_response::<MempoolInfo>()
.server_error()
},
@@ -41,9 +42,10 @@ impl MempoolRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_txids()).await
},
|op| {
op.mempool_tag()
op.id("get_mempool_txids")
.mempool_tag()
.summary("Mempool transaction IDs")
.description("Get all transaction IDs currently in the mempool.")
.description("Get all transaction IDs currently in the mempool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-transaction-ids)*")
.ok_response::<Vec<Txid>>()
.server_error()
},
@@ -56,9 +58,10 @@ impl MempoolRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::MaxAge(3), |q| q.recommended_fees()).await
},
|op| {
op.mempool_tag()
op.id("get_recommended_fees")
.mempool_tag()
.summary("Recommended fees")
.description("Get recommended fee rates for different confirmation targets based on current mempool state.")
.description("Get recommended fee rates for different confirmation targets based on current mempool state.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
.ok_response::<RecommendedFees>()
.server_error()
},
@@ -71,9 +74,10 @@ impl MempoolRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::MaxAge(5), |q| q.mempool_blocks()).await
},
|op| {
op.mempool_tag()
op.id("get_mempool_blocks")
.mempool_tag()
.summary("Projected mempool blocks")
.description("Get projected blocks from the mempool for fee estimation. Each block contains statistics about transactions that would be included if a block were mined now.")
.description("Get projected blocks from the mempool for fee estimation. Each block contains statistics about transactions that would be included if a block were mined now.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*")
.ok_response::<Vec<MempoolBlock>>()
.server_error()
},
+14 -5
View File
@@ -37,10 +37,12 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metrics_catalog().clone())).await
},
|op| op
.id("get_metrics_tree")
.metrics_tag()
.summary("Metrics catalog")
.description(
"Returns the complete hierarchical catalog of available metrics organized as a tree structure. Metrics are grouped by categories and subcategories. Best viewed in an interactive JSON viewer (e.g., Firefox's built-in JSON viewer) for easy navigation of the nested structure."
"Returns the complete hierarchical catalog of available metrics organized as a tree structure. \
Metrics are grouped by categories and subcategories."
)
.ok_response::<TreeNode>()
.not_modified(),
@@ -56,9 +58,10 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metric_count())).await
},
|op| op
.id("get_metrics_count")
.metrics_tag()
.summary("Metric count")
.description("Current metric count")
.description("Returns the number of metrics available per index type.")
.ok_response::<Vec<MetricCount>>()
.not_modified(),
),
@@ -73,6 +76,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.indexes().to_vec())).await
},
|op| op
.id("get_indexes")
.metrics_tag()
.summary("List available indexes")
.description(
@@ -93,9 +97,10 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.metrics(pagination))).await
},
|op| op
.id("list_metrics")
.metrics_tag()
.summary("Metrics list")
.description("Paginated list of available metrics")
.description("Paginated flat list of all available metric names. Use `page` query param for pagination.")
.ok_response::<PaginatedMetrics>()
.not_modified(),
),
@@ -112,6 +117,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.match_metric(&path.metric, query.limit))).await
},
|op| op
.id("search_metrics")
.metrics_tag()
.summary("Search metrics")
.description("Fuzzy search for metrics by name. Supports partial matches and typos.")
@@ -136,10 +142,11 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
}).await
},
|op| op
.id("get_metric_info")
.metrics_tag()
.summary("Get supported indexes for a metric")
.description(
"Returns the list of indexes are supported by the specified metric. \
"Returns the list of indexes supported by the specified metric. \
For example, `realized_price` might be available on dateindex, weekindex, and monthindex."
)
.ok_response::<Vec<Index>>()
@@ -166,6 +173,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
.await
},
|op| op
.id("get_metric")
.metrics_tag()
.summary("Get metric data")
.description(
@@ -183,11 +191,12 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
get_with(
bulk::handler,
|op| op
.id("get_metrics")
.metrics_tag()
.summary("Bulk metric data")
.description(
"Fetch multiple metrics in a single request. Supports filtering by index and date range. \
Returns an array of MetricData objects."
Returns an array of MetricData objects. For a single metric, use `get_metric` instead."
)
.ok_response::<Vec<MetricData>>()
.csv_response()
+54 -41
View File
@@ -32,9 +32,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, |q| q.difficulty_adjustment()).await
},
|op| {
op.mining_tag()
op.id("get_difficulty_adjustment")
.mining_tag()
.summary("Difficulty adjustment")
.description("Get current difficulty adjustment information including progress through the current epoch, estimated retarget date, and difficulty change prediction.")
.description("Get current difficulty adjustment information including progress through the current epoch, estimated retarget date, and difficulty change prediction.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustment)*")
.ok_response::<DifficultyAdjustment>()
.not_modified()
.server_error()
@@ -49,9 +50,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.all_pools())).await
},
|op| {
op.mining_tag()
op.id("get_pools")
.mining_tag()
.summary("List all mining pools")
.description("Get list of all known mining pools with their identifiers.")
.description("Get list of all known mining pools with their identifiers.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)*")
.ok_response::<Vec<PoolInfo>>()
.not_modified()
.server_error()
@@ -65,9 +67,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with(format!("{:?}", path.time_period)), move |q| q.mining_pools(path.time_period)).await
},
|op| {
op.mining_tag()
op.id("get_pool_stats")
.mining_tag()
.summary("Mining pool statistics")
.description("Get mining pool statistics for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y")
.description("Get mining pool statistics for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)*")
.ok_response::<PoolsSummary>()
.not_modified()
.server_error()
@@ -81,9 +84,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with(path.slug), move |q| q.pool_detail(path.slug)).await
},
|op| {
op.mining_tag()
op.id("get_pool")
.mining_tag()
.summary("Mining pool details")
.description("Get detailed information about a specific mining pool including block counts and shares for different time periods.")
.description("Get detailed information about a specific mining pool including block counts and shares for different time periods.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool)*")
.ok_response::<PoolDetail>()
.not_modified()
.not_found()
@@ -98,9 +102,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with("hashrate"), |q| q.hashrate(None)).await
},
|op| {
op.mining_tag()
op.id("get_hashrate")
.mining_tag()
.summary("Network hashrate (all time)")
.description("Get network hashrate and difficulty data for all time.")
.description("Get network hashrate and difficulty data for all time.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)*")
.ok_response::<HashrateSummary>()
.not_modified()
.server_error()
@@ -114,9 +119,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with(format!("hashrate-{:?}", path.time_period)), move |q| q.hashrate(Some(path.time_period))).await
},
|op| {
op.mining_tag()
op.id("get_hashrate_by_period")
.mining_tag()
.summary("Network hashrate")
.description("Get network hashrate and difficulty data for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y")
.description("Get network hashrate and difficulty data for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)*")
.ok_response::<HashrateSummary>()
.not_modified()
.server_error()
@@ -130,9 +136,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with("diff-adj"), |q| q.difficulty_adjustments(None)).await
},
|op| {
op.mining_tag()
op.id("get_difficulty_adjustments")
.mining_tag()
.summary("Difficulty adjustments (all time)")
.description("Get historical difficulty adjustments. Returns array of [timestamp, height, difficulty, change_percent].")
.description("Get historical difficulty adjustments including timestamp, block height, difficulty value, and percentage change.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)*")
.ok_response::<Vec<DifficultyAdjustmentEntry>>()
.not_modified()
.server_error()
@@ -146,9 +153,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with(format!("diff-adj-{:?}", path.time_period)), move |q| q.difficulty_adjustments(Some(path.time_period))).await
},
|op| {
op.mining_tag()
op.id("get_difficulty_adjustments_by_period")
.mining_tag()
.summary("Difficulty adjustments")
.description("Get historical difficulty adjustments for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y. Returns array of [timestamp, height, difficulty, change_percent].")
.description("Get historical difficulty adjustments for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)*")
.ok_response::<Vec<DifficultyAdjustmentEntry>>()
.not_modified()
.server_error()
@@ -162,9 +170,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with(format!("fees-{:?}", path.time_period)), move |q| q.block_fees(path.time_period)).await
},
|op| {
op.mining_tag()
op.id("get_block_fees")
.mining_tag()
.summary("Block fees")
.description("Get average block fees for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y")
.description("Get average block fees for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)*")
.ok_response::<Vec<BlockFeesEntry>>()
.not_modified()
.server_error()
@@ -178,32 +187,34 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with(format!("rewards-{:?}", path.time_period)), move |q| q.block_rewards(path.time_period)).await
},
|op| {
op.mining_tag()
op.id("get_block_rewards")
.mining_tag()
.summary("Block rewards")
.description("Get average block rewards (coinbase = subsidy + fees) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y")
.description("Get average block rewards (coinbase = subsidy + fees) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)*")
.ok_response::<Vec<BlockRewardsEntry>>()
.not_modified()
.server_error()
},
),
)
// TODO: Disabled - dateindex doesn't have percentile fields (see block_fee_rates.rs)
// .api_route(
// "/api/v1/mining/blocks/fee-rates/{time_period}",
// get_with(
// async |headers: HeaderMap, Path(path): Path<TimePeriodParam>, State(state): State<AppState>| {
// state.cached_json(&headers, CacheStrategy::height_with(format!("feerates-{:?}", path.time_period)), move |q| q.block_fee_rates(path.time_period)).await
// },
// |op| {
// op.mining_tag()
// .summary("Block fee rates")
// .description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y")
// .ok_response::<Vec<BlockFeeRatesEntry>>()
// .not_modified()
// .server_error()
// },
// ),
// )
.api_route(
"/api/v1/mining/blocks/fee-rates/{time_period}",
get_with(
async |Path(_path): Path<TimePeriodParam>| {
axum::Json(serde_json::json!({
"status": "wip",
"message": "This endpoint is work in progress. Percentile fields are not yet available."
}))
},
|op| {
op.id("get_block_fee_rates")
.mining_tag()
.summary("Block fee rates (WIP)")
.description("**Work in progress.** Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*")
.ok_response::<serde_json::Value>()
},
),
)
.api_route(
"/api/v1/mining/blocks/sizes-weights/{time_period}",
get_with(
@@ -211,9 +222,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with(format!("sizes-{:?}", path.time_period)), move |q| q.block_sizes_weights(path.time_period)).await
},
|op| {
op.mining_tag()
op.id("get_block_sizes_weights")
.mining_tag()
.summary("Block sizes and weights")
.description("Get average block sizes and weights for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y")
.description("Get average block sizes and weights for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)*")
.ok_response::<BlockSizesWeights>()
.not_modified()
.server_error()
@@ -227,9 +239,10 @@ impl MiningRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::height_with(format!("reward-stats-{}", path.block_count)), move |q| q.reward_stats(path.block_count)).await
},
|op| {
op.mining_tag()
op.id("get_reward_stats")
.mining_tag()
.summary("Mining reward statistics")
.description("Get mining reward statistics for the last N blocks including total rewards, fees, and transaction count.")
.description("Get mining reward statistics for the last N blocks including total rewards, fees, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-reward-stats)*")
.ok_response::<RewardStats>()
.not_modified()
.server_error()
+4 -2
View File
@@ -58,7 +58,8 @@ impl ApiRoutes for ApiRouter<AppState> {
.await
},
|op| {
op.server_tag()
op.id("get_version")
.server_tag()
.summary("API version")
.description("Returns the current version of the API server")
.ok_response::<String>()
@@ -77,7 +78,8 @@ impl ApiRoutes for ApiRouter<AppState> {
})
},
|op| {
op.server_tag()
op.id("get_health")
.server_tag()
.summary("Health check")
.description("Returns the health status of the API server")
.ok_response::<Health>()
+44 -9
View File
@@ -1,4 +1,4 @@
use aide::openapi::{Info, OpenApi, Tag};
use aide::openapi::{Contact, Info, License, OpenApi, Tag};
//
// https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html
@@ -18,10 +18,40 @@ pub fn create_openapi() -> OpenApi {
let info = Info {
title: "Bitcoin Research Kit".to_string(),
description: Some(
"API for querying Bitcoin blockchain data including addresses, transactions, and chain statistics. This API provides low-level access to indexed blockchain data with advanced analytics capabilities."
r#"API for querying Bitcoin blockchain data and on-chain metrics.
### Features
- **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
### Client Libraries
- [JavaScript/TypeScript](https://www.npmjs.com/package/brk-client)
- [Python](https://pypi.org/project/brk-client/)
- [Rust](https://crates.io/crates/brk_client)
### Links
- [GitHub](https://github.com/bitcoinresearchkit/brk)
- [Bitview](https://bitview.space) - Web app built on this API"#
.to_string(),
),
version: format!("v{VERSION}"),
contact: Some(Contact {
name: Some("Bitcoin Research Kit".to_string()),
url: Some("https://github.com/bitcoinresearchkit/brk".to_string()),
email: Some("hello@bitcoinresearchkit.org".to_string()),
..Contact::default()
}),
license: Some(License {
name: "MIT".to_string(),
url: Some(
"https://github.com/bitcoinresearchkit/brk/blob/main/docs/LICENSE.md".to_string(),
),
..License::default()
}),
..Info::default()
};
@@ -29,8 +59,8 @@ pub fn create_openapi() -> OpenApi {
Tag {
name: "Metrics".to_string(),
description: Some(
"Access Bitcoin network metrics and time-series data. Query historical statistics \
across various indexes with JSON or CSV output."
"Access thousands of Bitcoin network metrics and time-series data. Query historical statistics \
across various indexes (date, week, month, block height) with JSON or CSV output."
.to_string(),
),
..Default::default()
@@ -39,7 +69,8 @@ pub fn create_openapi() -> OpenApi {
name: "Blocks".to_string(),
description: Some(
"Retrieve block data by hash or height. Access block headers, transaction lists, \
and raw block bytes."
and raw block bytes.\n\n\
*[Mempool.space](https://mempool.space/docs/api/rest) compatible (WIP).*"
.to_string(),
),
..Default::default()
@@ -48,7 +79,8 @@ pub fn create_openapi() -> OpenApi {
name: "Transactions".to_string(),
description: Some(
"Retrieve transaction data by txid. Access full transaction details, confirmation \
status, raw hex, and output spend information."
status, raw hex, and output spend information.\n\n\
*[Mempool.space](https://mempool.space/docs/api/rest) compatible (WIP).*"
.to_string(),
),
..Default::default()
@@ -57,7 +89,8 @@ pub fn create_openapi() -> OpenApi {
name: "Addresses".to_string(),
description: Some(
"Query Bitcoin address data including balances, transaction history, and UTXOs. \
Supports all address types: P2PKH, P2SH, P2WPKH, P2WSH, and P2TR."
Supports all address types: P2PKH, P2SH, P2WPKH, P2WSH, and P2TR.\n\n\
*[Mempool.space](https://mempool.space/docs/api/rest) compatible (WIP).*"
.to_string(),
),
..Default::default()
@@ -66,7 +99,8 @@ pub fn create_openapi() -> OpenApi {
name: "Mempool".to_string(),
description: Some(
"Monitor unconfirmed transactions and fee estimates. Get mempool statistics, \
transaction IDs, and recommended fee rates for different confirmation targets."
transaction IDs, and recommended fee rates for different confirmation targets.\n\n\
*[Mempool.space](https://mempool.space/docs/api/rest) compatible (WIP).*"
.to_string(),
),
..Default::default()
@@ -75,7 +109,8 @@ pub fn create_openapi() -> OpenApi {
name: "Mining".to_string(),
description: Some(
"Mining statistics including pool distribution, hashrate, difficulty adjustments, \
block rewards, and fee rates across configurable time periods."
block rewards, and fee rates across configurable time periods.\n\n\
*[Mempool.space](https://mempool.space/docs/api/rest) compatible (WIP).*"
.to_string(),
),
..Default::default()
+10 -5
View File
@@ -31,10 +31,11 @@ impl TxRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction(txid)).await
},
|op| op
.id("get_tx")
.transactions_tag()
.summary("Transaction information")
.description(
"Retrieve complete transaction data by transaction ID (txid). Returns the full transaction details including inputs, outputs, and metadata. The transaction data is read directly from the blockchain data files.",
"Retrieve complete transaction data by transaction ID (txid). Returns inputs, outputs, fee, size, and confirmation status.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction)*",
)
.ok_response::<Transaction>()
.not_modified()
@@ -54,10 +55,11 @@ impl TxRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.transaction_status(txid)).await
},
|op| op
.id("get_tx_status")
.transactions_tag()
.summary("Transaction status")
.description(
"Retrieve the confirmation status of a transaction. Returns whether the transaction is confirmed and, if so, the block height, hash, and timestamp.",
"Retrieve the confirmation status of a transaction. Returns whether the transaction is confirmed and, if so, the block height, hash, and timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-status)*",
)
.ok_response::<TxStatus>()
.not_modified()
@@ -77,10 +79,11 @@ impl TxRoutes for ApiRouter<AppState> {
state.cached_text(&headers, CacheStrategy::Height, move |q| q.transaction_hex(txid)).await
},
|op| op
.id("get_tx_hex")
.transactions_tag()
.summary("Transaction hex")
.description(
"Retrieve the raw transaction as a hex-encoded string. Returns the serialized transaction in hexadecimal format.",
"Retrieve the raw transaction as a hex-encoded string. Returns the serialized transaction in hexadecimal format.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-hex)*",
)
.ok_response::<Hex>()
.not_modified()
@@ -101,10 +104,11 @@ impl TxRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspend(txid, path.vout)).await
},
|op| op
.id("get_tx_outspend")
.transactions_tag()
.summary("Output spend status")
.description(
"Get the spending status of a transaction output. Returns whether the output has been spent and, if so, the spending transaction details.",
"Get the spending status of a transaction output. Returns whether the output has been spent and, if so, the spending transaction details.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-outspend)*",
)
.ok_response::<TxOutspend>()
.not_modified()
@@ -124,10 +128,11 @@ impl TxRoutes for ApiRouter<AppState> {
state.cached_json(&headers, CacheStrategy::Height, move |q| q.outspends(txid)).await
},
|op| op
.id("get_tx_outspends")
.transactions_tag()
.summary("All output spend statuses")
.description(
"Get the spending status of all outputs in a transaction. Returns an array with the spend status for each output.",
"Get the spending status of all outputs in a transaction. Returns an array with the spend status for each output.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-outspends)*",
)
.ok_response::<Vec<TxOutspend>>()
.not_modified()
+99
View File
@@ -0,0 +1,99 @@
# Architecture
## Overview
```
blk*.dat ──▶ Reader ──┐
├──▶ Indexer ──▶ Computer ──┐
RPC Client ──┤ ├──▶ Query ──▶ Server
└──▶ Mempool ───────────────┘
```
## Components
### Reader (`brk_reader`)
Parses Bitcoin Core's `blk*.dat` files directly, bypassing RPC for historical data. Supports parallel parsing and handles XOR-encoded blocks (Bitcoin Core 28+).
### RPC Client (`brk_rpc`)
Connects to Bitcoin Core for real-time data: new blocks, mempool transactions, and fee estimates. Thread-safe with automatic retries.
### Indexer (`brk_indexer`)
Builds lookup tables from parsed blocks:
- Transaction index (txid → block position)
- Address index (address → transactions, UTXOs)
- UTXO set tracking
- Output type classification (P2PKH, P2WPKH, P2TR, etc.)
### Computer (`brk_computer`)
Derives analytics from indexed data:
- Market metrics: realized cap, MVRV, SOPR, NVT
- Supply metrics: circulating, liquid, illiquid
- UTXO cohorts: by age, size, type
- Address cohorts: by balance, activity
- Pricing models: thermocap, realized price bands
Metrics are computed across multiple time resolutions (daily, weekly, monthly, by block height).
### Mempool (`brk_mempool`)
Monitors unconfirmed transactions:
- Fee rate distribution and estimation
- Projected block templates
- Address mempool activity
### Query (`brk_query`)
Unified interface to all data sources:
- Block and transaction lookups
- Address balances and history
- Computed metrics with range queries
- Mempool state
### Server (`brk_server`)
REST API exposing Query functionality:
- OpenAPI documentation (Scalar UI)
- JSON and CSV output formats
- ETag caching
- mempool.space compatible endpoints
## Data Flow
**Initial sync:**
1. Reader parses all `blk*.dat` files in parallel
2. Indexer processes blocks sequentially, building indexes
3. Computer derives metrics from indexed data
4. Server starts accepting requests
**Ongoing operation:**
1. RPC client polls for new blocks
2. Reader fetches block data
3. Indexer updates indexes
4. Computer recalculates affected metrics
5. Mempool monitors transaction pool
## Storage
Data is stored in `~/.brk/` (configurable):
```
~/.brk/
├── indexer/ # Transaction and address indexes (fjall)
├── computer/ # Computed metrics (vecdb)
└── config.toml # Configuration
```
Disk usage scales with blockchain size. Full index with metrics: ~400 GB.
## Dependencies
Built on:
- [`rust-bitcoin`](https://github.com/rust-bitcoin/rust-bitcoin) - Bitcoin primitives
- [`fjall`](https://github.com/fjall-rs/fjall) - LSM-tree storage
- [`vecdb`](https://github.com/anydb-rs/anydb) - Vector storage
- [`axum`](https://github.com/tokio-rs/axum) - HTTP server
- [`aide`](https://github.com/tamasfe/aide) - OpenAPI generation
+1 -1
View File
@@ -14,7 +14,7 @@ Requirements:
- ~400 GB disk space
- 12+ GB RAM recommended
See the [CLI README](../crates/brk_cli/README.md) for configuration options.
See the [CLI documentation](https://docs.rs/brk_cli) for configuration options.
## Professional Hosting
+30 -104
View File
@@ -1,84 +1,43 @@
# Bitcoin Research Kit
<div align="center">
**Open-source Bitcoin analytics infrastructure.**
<p>
<strong>A suite of Rust crates for working with Bitcoin data.</strong>
</p>
[![MIT Licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/bitcoinresearchkit/brk/blob/main/docs/LICENSE.md)
[![Crates.io](https://img.shields.io/crates/v/brk.svg)](https://crates.io/crates/brk)
[![docs.rs](https://img.shields.io/docsrs/brk)](https://docs.rs/brk)
[![Discord](https://img.shields.io/discord/1350431684562124850?logo=discord)](https://discord.gg/WACpShCB7M)
<p>
<a href="https://github.com/bitcoinresearchkit/brk/blob/main/docs/LICENSE.md"><img alt="MIT Licensed" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
<a href="https://crates.io/crates/brk"><img alt="Crates.io" src="https://img.shields.io/crates/v/brk.svg"/></a>
<a href="https://docs.rs/brk"><img alt="docs.rs" src="https://img.shields.io/docsrs/brk"/></a>
<a href="https://discord.gg/WACpShCB7M"><img alt="Discord" src="https://img.shields.io/discord/1350431684562124850?logo=discord"/></a>
</p>
[Homepage](https://bitcoinresearchkit.org) · [**Bitview**](https://bitview.space) · [API Reference](https://bitcoinresearchkit.org/api)
</div>
---
[Homepage](https://bitcoinresearchkit.org) · [API Docs](https://bitcoinresearchkit.org/api) · [Charts](https://bitview.space) · [Changelog](https://github.com/bitcoinresearchkit/brk/blob/main/docs/CHANGELOG.md)
BRK parses, indexes, and analyzes Bitcoin blockchain data. It combines on-chain analytics (like [Glassnode](https://glassnode.com)), block exploration (like [mempool.space](https://mempool.space)), and address indexing (like [electrs](https://github.com/romanz/electrs)) into a single self-hostable package.
## About
## See It In Action
BRK is a toolkit for parsing, indexing, and analyzing Bitcoin blockchain data. It combines functionality similar to [Glassnode](https://glassnode.com) (on-chain analytics), [mempool.space](https://mempool.space) (block explorer), and [electrs](https://github.com/romanz/electrs) (address indexing) into a single self-hostable package.
[**Bitview**](https://bitview.space) is a web application built entirely on BRK. It offers interactive charts for exploring Bitcoin on-chain metrics—price models, supply dynamics, holder behavior, network activity, and more. Browse it to see what's possible with the data BRK provides.
- **Parse** blocks directly from Bitcoin Core's data files
- **Index** transactions, addresses, and UTXOs
- **Compute** derived metrics across multiple time resolutions
- **Monitor** mempool with fee estimation and projected block building
- **Serve** data via REST API and web interface
- **Query** addresses, transactions, blocks, and analytics
## What It Provides
The crates can be used together as a complete solution, or independently for specific needs.
**On-Chain Metrics** — Thousands of derived metrics: market indicators (realized cap, MVRV, SOPR, NVT), supply analysis (circulating, liquid, illiquid), holder cohorts (by balance, age, address type), and pricing models. This is what sets BRK apart from typical block explorers.
Built on [`rust-bitcoin`], [`vecdb`], and [`fjall`].
**Blockchain Data** — Blocks, transactions, addresses, UTXOs. The API follows mempool.space's format for compatibility with existing tools.
## Crates
**Multiple Indexes** — Query data by date, block height, halving epoch, address type, UTXO age, and more. Enables flexible time-series queries and cohort analysis.
**Entry Points**
**Mempool** — Real-time fee estimation, projected blocks, unconfirmed transaction tracking.
| Crate | Purpose |
|-------|---------|
| [`brk`](./crates/brk) | Umbrella crate, re-exports all components via feature flags |
| [`brk_cli`](./crates/brk_cli) | CLI binary (`cargo install --locked brk_cli`) |
**REST API** — JSON and CSV output with OpenAPI documentation.
**Core**
**MCP Server** — Model Context Protocol integration for AI assistants and LLMs.
| Crate | Purpose |
|-------|---------|
| [`brk_reader`](./crates/brk_reader) | Read blocks from `blk*.dat` with parallel parsing and XOR decoding |
| [`brk_indexer`](./crates/brk_indexer) | Index transactions, addresses, and UTXOs |
| [`brk_computer`](./crates/brk_computer) | Compute derived metrics (realized cap, MVRV, SOPR, cohorts, etc.) |
| [`brk_mempool`](./crates/brk_mempool) | Monitor mempool, estimate fees, project upcoming blocks |
| [`brk_query`](./crates/brk_query) | Query interface for indexed and computed data |
| [`brk_server`](./crates/brk_server) | REST API with OpenAPI docs |
## Get Started
**Data & Storage**
**Use the Public API** — Access data without running infrastructure. Client libraries available for [JavaScript](https://www.npmjs.com/package/brk-client), [Python](https://pypi.org/project/brk-client/), and [Rust](https://crates.io/crates/brk_client). See the [API reference](https://bitcoinresearchkit.org/api) for endpoints.
| Crate | Purpose |
|-------|---------|
| [`brk_types`](./crates/brk_types) | Domain types: `Height`, `Sats`, `Txid`, addresses, etc. |
| [`brk_store`](./crates/brk_store) | Key-value storage (fjall wrapper) |
| [`brk_fetcher`](./crates/brk_fetcher) | Fetch price data from exchanges |
| [`brk_rpc`](./crates/brk_rpc) | Bitcoin Core RPC client |
| [`brk_iterator`](./crates/brk_iterator) | Unified block iteration with automatic source selection |
| [`brk_grouper`](./crates/brk_grouper) | UTXO and address cohort filtering |
| [`brk_traversable`](./crates/brk_traversable) | Navigate hierarchical data structures |
**Self-Host** — Run your own instance with Bitcoin Core. Install via `cargo install --locked brk_cli` or use [Docker](https://github.com/bitcoinresearchkit/brk/tree/main/docker). Requires ~400 GB disk and 12+ GB RAM. See the [hosting guide](./HOSTING.md).
**Clients & Integration**
| Crate | Purpose |
|-------|---------|
| [`brk_mcp`](./crates/brk_mcp) | Model Context Protocol server for LLM integration |
| [`brk_binder`](./crates/brk_binder) | Generate typed clients (Rust, JavaScript, Python) |
| [`brk_client`](./crates/brk_client) | Generated Rust API client |
**Internal**
| Crate | Purpose |
|-------|---------|
| [`brk_error`](./crates/brk_error) | Error types |
| [`brk_logger`](./crates/brk_logger) | Logging infrastructure |
| [`brk_bencher`](./crates/brk_bencher) | Benchmarking utilities |
**Use as a Library** — Build custom tools with the Rust crates. Use individual components or the [umbrella crate](https://docs.rs/brk). See [architecture](./ARCHITECTURE.md) for how they fit together.
## Architecture
@@ -89,51 +48,18 @@ blk*.dat ──▶ Reader ──┐
└──▶ Mempool ───────────────┘
```
- `Reader` parses `blk*.dat` files directly
- `RPC Client` connects to Bitcoin Core
- `Indexer` builds lookup tables from blocks
- `Computer` derives metrics from indexed data
- `Mempool` tracks unconfirmed transactions
- `Query` provides unified access to all data
- `Server` exposes `Query` as REST API
**Reader** parses Bitcoin Core's block files. **Indexer** builds lookup tables. **Computer** derives metrics. **Mempool** tracks unconfirmed transactions. **Query** provides unified data access. **Server** exposes the REST API.
## Usage
[Detailed architecture](./ARCHITECTURE.md) · [All crates](https://docs.rs/brk)
**As a library:**
## Links
```rust
use brk::{reader::Reader, indexer::Indexer, computer::Computer};
let reader = Reader::new(blocks_dir, &rpc);
let indexer = Indexer::forced_import(&brk_dir)?;
let computer = Computer::forced_import(&brk_dir, &indexer, None)?;
```
**As a CLI:** See [`brk_cli`](./crates/brk_cli)
**Public API:** [bitcoinresearchkit.org/api](https://bitcoinresearchkit.org/api)
## Documentation
- [Changelog](./docs/CHANGELOG.md)
- [TODO](./docs/TODO.md)
- [Hosting](./docs/HOSTING.md)
- [Support](./docs/SUPPORT.md)
## Contributing
Contributions are welcome. See [open issues](https://github.com/bitcoinresearchkit/brk/issues).
Join the discussion on [Discord](https://discord.gg/WACpShCB7M) or [Nostr](https://primal.net/p/nprofile1qqsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03sqxu2e6).
## Acknowledgments
Development supported by [OpenSats](https://opensats.org/).
- [Changelog](./CHANGELOG.md)
- [Support](./SUPPORT.md)
- [Contributing](https://github.com/bitcoinresearchkit/brk/issues)
- Community: [Discord](https://discord.gg/WACpShCB7M) · [Nostr](https://primal.net/p/nprofile1qqsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03sqxu2e6)
- Development supported by [OpenSats](https://opensats.org/)
## License
[MIT](https://github.com/bitcoinresearchkit/brk/blob/main/docs/LICENSE.md)
[`rust-bitcoin`]: https://github.com/rust-bitcoin/rust-bitcoin
[`vecdb`]: https://github.com/anydb-rs/anydb
[`fjall`]: https://github.com/fjall-rs/fjall
[MIT](./LICENSE.md)
+3457 -2443
View File
File diff suppressed because it is too large Load Diff
+30 -3
View File
@@ -1,8 +1,8 @@
# brk_client
# brk-client
Python client for the [Bitcoin Research Kit](https://bitcoinresearchkit.org) - a suite of tools to extract, compute and display data stored on a Bitcoin Core node.
Python client for the [Bitcoin Research Kit](https://github.com/bitcoinresearchkit/brk) API.
[Documentation](/DOCS.md)
[PyPI](https://pypi.org/project/brk-client/) | [API Reference](https://github.com/bitcoinresearchkit/brk/blob/main/packages/brk_client/DOCS.md)
## Installation
@@ -10,6 +10,33 @@ Python client for the [Bitcoin Research Kit](https://bitcoinresearchkit.org) - a
pip install brk-client
```
## Quick Start
```python
from brk_client import BrkClient
client = BrkClient("http://localhost:3000")
# Blockchain data (mempool.space compatible)
block = client.get_block_by_height(800000)
tx = client.get_tx("abc123...")
address = client.get_address("bc1q...")
# Metrics API - typed, chainable
prices = client.metrics.price.usd.split.close \
.by.dateindex() \
.range(-30) # Last 30 days
# Generic metric fetching
data = client.get_metric("price_close", "dateindex", -30)
```
## Configuration
```python
client = BrkClient("http://localhost:3000", timeout=60.0)
```
## License
MIT
File diff suppressed because it is too large Load Diff
+24 -24
View File
@@ -1558,12 +1558,12 @@
</script>
<!-- IMPORTMAP -->
<link rel="modulepreload" href="/scripts/chart/index.891e1546.js">
<link rel="modulepreload" href="/scripts/chart/index.024e5d6b.js">
<link rel="modulepreload" href="/scripts/chart/oklch.21450255.js">
<link rel="modulepreload" href="/scripts/entry.fe229b42.js">
<link rel="modulepreload" href="/scripts/entry.7b7383d1.js">
<link rel="modulepreload" href="/scripts/lazy.1ae52534.js">
<link rel="modulepreload" href="/scripts/main.22a5bd79.js">
<link rel="modulepreload" href="/scripts/modules/brk-client/index.f69b33ed.js">
<link rel="modulepreload" href="/scripts/modules/brk-client/index.c18ba682.js">
<link rel="modulepreload" href="/scripts/modules/brk-client/tests/basic.b92ff866.js">
<link rel="modulepreload" href="/scripts/modules/brk-client/tests/tree.ba9474f7.js">
<link rel="modulepreload" href="/scripts/modules/lean-qr/2.6.1/index.09195c13.mjs">
@@ -1577,21 +1577,21 @@
<link rel="modulepreload" href="/scripts/modules/solidjs-signals/0.6.3/dist/prod.2f80e335.js">
<link rel="modulepreload" href="/scripts/modules/solidjs-signals/0.8.5/dist/prod.8ae56250.js">
<link rel="modulepreload" href="/scripts/options/_partial_old.62bf3faa.js">
<link rel="modulepreload" href="/scripts/options/chain.3d99e062.js">
<link rel="modulepreload" href="/scripts/options/chain.c173ace8.js">
<link rel="modulepreload" href="/scripts/options/cohorts/address.2e8a42cb.js">
<link rel="modulepreload" href="/scripts/options/cohorts/data.f7f24b88.js">
<link rel="modulepreload" href="/scripts/options/cohorts/data.11381d83.js">
<link rel="modulepreload" href="/scripts/options/cohorts/index.b0e57c9d.js">
<link rel="modulepreload" href="/scripts/options/cohorts/shared.0602caa3.js">
<link rel="modulepreload" href="/scripts/options/cohorts/shared.87b9837c.js">
<link rel="modulepreload" href="/scripts/options/cohorts/utxo.f0e69857.js">
<link rel="modulepreload" href="/scripts/options/cointime.a69ba0e7.js">
<link rel="modulepreload" href="/scripts/options/cointime.e25633d9.js">
<link rel="modulepreload" href="/scripts/options/colors/cohorts.262d4551.js">
<link rel="modulepreload" href="/scripts/options/colors/index.a54dc83f.js">
<link rel="modulepreload" href="/scripts/options/colors/misc.bee7dbee.js">
<link rel="modulepreload" href="/scripts/options/constants.f5b525fe.js">
<link rel="modulepreload" href="/scripts/options/context.85881310.js">
<link rel="modulepreload" href="/scripts/options/full.cbb89ca5.js">
<link rel="modulepreload" href="/scripts/options/constants.16dfce27.js">
<link rel="modulepreload" href="/scripts/options/context.8bf2932e.js">
<link rel="modulepreload" href="/scripts/options/full.11772605.js">
<link rel="modulepreload" href="/scripts/options/market/averages.d95aa3e1.js">
<link rel="modulepreload" href="/scripts/options/market/index.6896a1b5.js">
<link rel="modulepreload" href="/scripts/options/market/index.e3b750d6.js">
<link rel="modulepreload" href="/scripts/options/market/indicators/bands.81b19b83.js">
<link rel="modulepreload" href="/scripts/options/market/indicators/index.70e9b3e4.js">
<link rel="modulepreload" href="/scripts/options/market/indicators/momentum.48e71442.js">
@@ -1604,7 +1604,7 @@
<link rel="modulepreload" href="/scripts/options/series.5a2a34ed.js">
<link rel="modulepreload" href="/scripts/options/types.64db5149.js">
<link rel="modulepreload" href="/scripts/options/unused.24a71427.js">
<link rel="modulepreload" href="/scripts/panes/chart/index.3d231714.js">
<link rel="modulepreload" href="/scripts/panes/chart/index.947ceee8.js">
<link rel="modulepreload" href="/scripts/panes/chart/screenshot.adc8da89.js">
<link rel="modulepreload" href="/scripts/panes/explorer.91a5a9ae.js">
<link rel="modulepreload" href="/scripts/panes/nav.0338dc4b.js">
@@ -1629,12 +1629,12 @@
<script type="importmap">
{
"imports": {
"/scripts/chart/index.js": "/scripts/chart/index.891e1546.js",
"/scripts/chart/index.js": "/scripts/chart/index.024e5d6b.js",
"/scripts/chart/oklch.js": "/scripts/chart/oklch.21450255.js",
"/scripts/entry.js": "/scripts/entry.fe229b42.js",
"/scripts/entry.js": "/scripts/entry.7b7383d1.js",
"/scripts/lazy.js": "/scripts/lazy.1ae52534.js",
"/scripts/main.js": "/scripts/main.22a5bd79.js",
"/scripts/modules/brk-client/index.js": "/scripts/modules/brk-client/index.f69b33ed.js",
"/scripts/modules/brk-client/index.js": "/scripts/modules/brk-client/index.c18ba682.js",
"/scripts/modules/brk-client/tests/basic.js": "/scripts/modules/brk-client/tests/basic.b92ff866.js",
"/scripts/modules/brk-client/tests/tree.js": "/scripts/modules/brk-client/tests/tree.ba9474f7.js",
"/scripts/modules/lean-qr/2.6.1/index.mjs": "/scripts/modules/lean-qr/2.6.1/index.09195c13.mjs",
@@ -1648,21 +1648,21 @@
"/scripts/modules/solidjs-signals/0.6.3/dist/prod.js": "/scripts/modules/solidjs-signals/0.6.3/dist/prod.2f80e335.js",
"/scripts/modules/solidjs-signals/0.8.5/dist/prod.js": "/scripts/modules/solidjs-signals/0.8.5/dist/prod.8ae56250.js",
"/scripts/options/_partial_old.js": "/scripts/options/_partial_old.62bf3faa.js",
"/scripts/options/chain.js": "/scripts/options/chain.3d99e062.js",
"/scripts/options/chain.js": "/scripts/options/chain.c173ace8.js",
"/scripts/options/cohorts/address.js": "/scripts/options/cohorts/address.2e8a42cb.js",
"/scripts/options/cohorts/data.js": "/scripts/options/cohorts/data.f7f24b88.js",
"/scripts/options/cohorts/data.js": "/scripts/options/cohorts/data.11381d83.js",
"/scripts/options/cohorts/index.js": "/scripts/options/cohorts/index.b0e57c9d.js",
"/scripts/options/cohorts/shared.js": "/scripts/options/cohorts/shared.0602caa3.js",
"/scripts/options/cohorts/shared.js": "/scripts/options/cohorts/shared.87b9837c.js",
"/scripts/options/cohorts/utxo.js": "/scripts/options/cohorts/utxo.f0e69857.js",
"/scripts/options/cointime.js": "/scripts/options/cointime.a69ba0e7.js",
"/scripts/options/cointime.js": "/scripts/options/cointime.e25633d9.js",
"/scripts/options/colors/cohorts.js": "/scripts/options/colors/cohorts.262d4551.js",
"/scripts/options/colors/index.js": "/scripts/options/colors/index.a54dc83f.js",
"/scripts/options/colors/misc.js": "/scripts/options/colors/misc.bee7dbee.js",
"/scripts/options/constants.js": "/scripts/options/constants.f5b525fe.js",
"/scripts/options/context.js": "/scripts/options/context.85881310.js",
"/scripts/options/full.js": "/scripts/options/full.cbb89ca5.js",
"/scripts/options/constants.js": "/scripts/options/constants.16dfce27.js",
"/scripts/options/context.js": "/scripts/options/context.8bf2932e.js",
"/scripts/options/full.js": "/scripts/options/full.11772605.js",
"/scripts/options/market/averages.js": "/scripts/options/market/averages.d95aa3e1.js",
"/scripts/options/market/index.js": "/scripts/options/market/index.6896a1b5.js",
"/scripts/options/market/index.js": "/scripts/options/market/index.e3b750d6.js",
"/scripts/options/market/indicators/bands.js": "/scripts/options/market/indicators/bands.81b19b83.js",
"/scripts/options/market/indicators/index.js": "/scripts/options/market/indicators/index.70e9b3e4.js",
"/scripts/options/market/indicators/momentum.js": "/scripts/options/market/indicators/momentum.48e71442.js",
@@ -1675,7 +1675,7 @@
"/scripts/options/series.js": "/scripts/options/series.5a2a34ed.js",
"/scripts/options/types.js": "/scripts/options/types.64db5149.js",
"/scripts/options/unused.js": "/scripts/options/unused.24a71427.js",
"/scripts/panes/chart/index.js": "/scripts/panes/chart/index.3d231714.js",
"/scripts/panes/chart/index.js": "/scripts/panes/chart/index.947ceee8.js",
"/scripts/panes/chart/screenshot.js": "/scripts/panes/chart/screenshot.adc8da89.js",
"/scripts/panes/explorer.js": "/scripts/panes/explorer.91a5a9ae.js",
"/scripts/panes/nav.js": "/scripts/panes/nav.0338dc4b.js",