global: snapshot

This commit is contained in:
nym21
2026-01-14 16:38:53 +01:00
parent ddb1db7a8e
commit d75c2a881b
226 changed files with 7776 additions and 20942 deletions
+2 -3
View File
@@ -6,12 +6,11 @@ homepage.workspace = true
repository.workspace = true
edition.workspace = true
version.workspace = true
build = "build.rs"
[features]
full = [
"bencher",
"binder",
"bindgen",
"client",
"computer",
"error",
@@ -31,7 +30,7 @@ full = [
"types",
]
bencher = ["brk_bencher"]
binder = ["brk_bindgen"]
bindgen = ["brk_bindgen"]
client = ["brk_client"]
computer = ["brk_computer"]
error = ["brk_error"]
-4
View File
@@ -61,7 +61,3 @@ Feature flags match crate names without the `brk_` prefix. Use `full` to enable
| [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
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
+2 -2
View File
@@ -4,9 +4,9 @@
#[doc(inline)]
pub use brk_bencher as bencher;
#[cfg(feature = "binder")]
#[cfg(feature = "bindgen")]
#[doc(inline)]
pub use brk_bindgen as binder;
pub use brk_bindgen as bindgen;
#[cfg(feature = "client")]
#[doc(inline)]
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
brk_error = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -7,7 +7,6 @@ license.workspace = true
homepage.workspace = true
repository.workspace = true
publish = false
build = "build.rs"
[dependencies]
plotters = "0.3.7"
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
brk_cohort = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
@@ -110,7 +110,7 @@ class BrkError extends Error {{
* @typedef {{Object}} MetricPattern
* @property {{string}} name - The metric name
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters. Access via .by.dateindex or .by['dateindex']
* @property {{() => Index[]}} indexes - Get the list of available indexes
* @property {{() => readonly Index[]}} indexes - Get the list of available indexes
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
*/
@@ -333,14 +333,14 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
// Generate index array constants (e.g., _i1 = ["dateindex", "height"])
for (i, pattern) in patterns.iter().enumerate() {
write!(output, "const _i{} = [", i + 1).unwrap();
write!(output, "const _i{} = /** @type {{const}} */ ([", i + 1).unwrap();
for (j, index) in pattern.indexes.iter().enumerate() {
if j > 0 {
write!(output, ", ").unwrap();
}
write!(output, "\"{}\"", index.serialize_long()).unwrap();
}
writeln!(output, "];").unwrap();
writeln!(output, "]);").unwrap();
}
writeln!(output).unwrap();
@@ -352,11 +352,10 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
* @template T
* @param {{BrkClientBase}} client
* @param {{string}} name - The metric vec name
* @param {{readonly string[]}} indexes - The supported indexes
* @returns {{MetricPattern<T>}}
* @param {{readonly Index[]}} indexes - The supported indexes
*/
function _mp(client, name, indexes) {{
const by = {{}};
const by = /** @type {{any}} */ ({{}});
for (const idx of indexes) {{
Object.defineProperty(by, idx, {{
get() {{ return _endpoint(client, name, idx); }},
@@ -368,6 +367,7 @@ function _mp(client, name, indexes) {{
name,
by,
indexes() {{ return indexes; }},
/** @param {{Index}} index */
get(index) {{ return indexes.includes(index) ? _endpoint(client, name, index) : undefined; }}
}};
}}
@@ -392,7 +392,7 @@ function _mp(client, name, indexes) {{
writeln!(
output,
"/** @template T @typedef {{{{ name: string, by: {}, indexes: () => Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }}}} {} */",
"/** @template T @typedef {{{{ name: string, by: {}, indexes: () => readonly Index[], get: (index: Index) => MetricEndpointBuilder<T>|undefined }}}} {} */",
by_type, pattern.name
)
.unwrap();
+6 -7
View File
@@ -2,7 +2,10 @@ use std::{collections::BTreeMap, io};
use crate::ref_to_type_name;
use oas3::Spec;
use oas3::spec::{ObjectOrReference, ObjectSchema, Operation, ParameterIn, PathItem, Schema, SchemaType, SchemaTypeSet};
use oas3::spec::{
ObjectOrReference, ObjectSchema, Operation, ParameterIn, PathItem, Schema, SchemaType,
SchemaTypeSet,
};
use serde_json::Value;
/// Type schema extracted from OpenAPI components
@@ -156,7 +159,7 @@ pub fn extract_endpoints(spec: &Spec) -> Vec<Endpoint> {
for (path, path_item) in paths {
for (method, operation) in get_operations(path_item) {
if let Some(endpoint) = extract_endpoint(path, &method, operation) {
if let Some(endpoint) = extract_endpoint(path, method, operation) {
endpoints.push(endpoint);
}
}
@@ -218,11 +221,7 @@ fn extract_path_parameters(path: &str, operation: &Operation) -> Vec<Parameter>
// Extract parameter names from the path in order (e.g., "/api/metric/{metric}/{index}" -> ["metric", "index"])
let path_order: Vec<&str> = path
.split('/')
.filter_map(|segment| {
segment
.strip_prefix('{')
.and_then(|s| s.strip_suffix('}'))
})
.filter_map(|segment| segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
.collect();
// Get all path parameters from the operation
-5
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
brk_alloc = { workspace = true }
@@ -23,15 +22,11 @@ brk_rpc = { workspace = true }
brk_server = { workspace = true }
clap = { version = "4.5.54", features = ["derive", "string"] }
color-eyre = { workspace = true }
importmap = "0.1.3"
# importmap = { path = "../../../importmap" }
tracing = { workspace = true }
minreq = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
toml = "0.9.11"
vecdb = { workspace = true }
zip = { version = "7.0.0", default-features = false, features = ["deflate"] }
[[bin]]
name = "brk"
+25 -5
View File
@@ -21,6 +21,30 @@ Run a full BRK instance: index the blockchain, compute metrics, serve the API, a
cargo install --locked brk_cli
```
For pre-release versions (alpha, beta, rc), specify the version explicitly:
```bash
# Find the latest version
cargo search brk_cli
# Install a specific version
cargo install --locked brk_cli --version "VERSION"
```
See [crates.io/crates/brk_cli/versions](https://crates.io/crates/brk_cli/versions) for all available versions.
For better performance, build with native CPU optimizations:
```bash
RUSTFLAGS="-C target-cpu=native" cargo install --locked brk_cli
```
## Requirements
- Bitcoin Core with accessible `blk*.dat` files
- ~400 GB disk space
- 12+ GB RAM recommended
## Usage
```bash
@@ -45,11 +69,7 @@ brk --help
## Performance
| Machine | Time | Disk | Peak Disk | Memory | Peak Memory |
|---------|------|------|-----------|--------|-------------|
| MBP M3 Pro (36GB, internal SSD) | 5.2h | 341 GB | 415 GB | 6.4 GB | 12 GB |
Full benchmark data: [`https://github.com/bitcoinresearchkit/benches/tree/main/brk`](/benches/brk)
See [brk_computer](https://docs.rs/brk_computer) for full pipeline benchmarks.
## Built On
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
+6 -29
View File
@@ -9,9 +9,8 @@ use brk_rpc::{Auth, Client};
use clap::Parser;
use serde::{Deserialize, Deserializer, Serialize};
use crate::{default_brk_path, dot_brk_path, website::Website};
use crate::{default_brk_path, dot_brk_path, fix_user_path, website::Website};
const DOWNLOADS: &str = "downloads";
#[derive(Parser, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[command(version, about)]
@@ -234,58 +233,36 @@ Finally, you can run the program with '-h' for help."
self.bitcoindir
.as_ref()
.map_or_else(Client::default_bitcoin_path, |s| {
Self::fix_user_path(s.as_ref())
fix_user_path(s.as_ref())
})
}
pub fn blocksdir(&self) -> PathBuf {
self.blocksdir.as_ref().map_or_else(
|| self.bitcoindir().join("blocks"),
|blocksdir| Self::fix_user_path(blocksdir.as_str()),
|blocksdir| fix_user_path(blocksdir.as_str()),
)
}
pub fn brkdir(&self) -> PathBuf {
self.brkdir
.as_ref()
.map_or_else(default_brk_path, |s| Self::fix_user_path(s.as_ref()))
.map_or_else(default_brk_path, |s| fix_user_path(s.as_ref()))
}
pub fn harsdir(&self) -> PathBuf {
self.brkdir().join("hars")
}
pub fn downloads_dir(&self) -> PathBuf {
dot_brk_path().join(DOWNLOADS)
}
fn path_cookiefile(&self) -> PathBuf {
self.rpccookiefile.as_ref().map_or_else(
|| self.bitcoindir().join(".cookie"),
|p| Self::fix_user_path(p.as_str()),
|p| fix_user_path(p.as_str()),
)
}
fn fix_user_path(path: &str) -> PathBuf {
let fix = move |pattern: &str| {
if path.starts_with(pattern) {
let path = &path
.replace(&format!("{pattern}/"), "")
.replace(pattern, "");
let home = std::env::var("HOME").unwrap();
Some(Path::new(&home).join(path))
} else {
None
}
};
fix("~").unwrap_or_else(|| fix("$HOME").unwrap_or_else(|| PathBuf::from(&path)))
}
pub fn website(&self) -> Website {
self.website.unwrap_or(Website::Bitview)
self.website.clone().unwrap_or_default()
}
pub fn fetch(&self) -> bool {
+22 -82
View File
@@ -2,7 +2,6 @@
use std::{
fs,
io::Cursor,
path::PathBuf,
thread::{self, sleep},
time::Duration,
@@ -16,8 +15,7 @@ use brk_iterator::Blocks;
use brk_mempool::Mempool;
use brk_query::AsyncQuery;
use brk_reader::Reader;
use brk_server::{Server, VERSION};
use importmap::ImportMap;
use brk_server::{Server, WebsiteSource};
use tracing::info;
use vecdb::Exit;
@@ -25,7 +23,7 @@ mod config;
mod paths;
mod website;
use crate::{config::Config, paths::*};
use crate::{config::Config, paths::*, website::Website};
pub fn main() -> color_eyre::Result<()> {
// Can't increase main thread's stack size, thus we need to use another thread
@@ -80,89 +78,22 @@ pub fn run() -> color_eyre::Result<()> {
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool));
let website = config.website();
let downloads_path = config.downloads_dir();
let data_path = config.brkdir();
let future = async move {
// Try to find local dev directories - check cwd and parent directories
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
let mut dir = std::env::current_dir().ok()?;
loop {
let websites = dir.join("websites");
let modules = dir.join("modules");
if websites.exists() && modules.exists() {
return Some((websites, modules));
}
// Stop at workspace root (crates/ indicates we're there)
if dir.join("crates").exists() {
return None;
}
dir = dir.parent()?.to_path_buf();
}
};
let dev_dirs = find_dev_dirs();
let is_dev = dev_dirs.is_some();
let bundle_path = if website.is_some() {
let websites_path = if let Some((websites, _modules)) = dev_dirs {
websites
} else {
let downloaded_brk_path = downloads_path.join(format!("brk-{VERSION}"));
let downloaded_websites_path = downloaded_brk_path.join("websites");
if !fs::exists(&downloaded_websites_path)? {
info!("Downloading source from Github...");
let url = format!(
"https://github.com/bitcoinresearchkit/brk/archive/refs/tags/v{VERSION}.zip",
);
let response = minreq::get(url).with_timeout(60).send()?;
let bytes = response.as_bytes();
let cursor = Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).unwrap();
zip.extract(downloads_path).unwrap();
}
downloaded_websites_path
};
Some(websites_path.join(website.to_folder_name()))
} else {
None
};
// Generate import map for cache busting (disabled in dev mode)
if let Some(ref path) = bundle_path {
let map = if is_dev {
ImportMap::empty()
} else {
match ImportMap::scan(path, "") {
Ok(map) => map,
Err(e) => {
tracing::error!("Failed to generate importmap: {e}");
ImportMap::empty()
}
}
};
let html_path = path.join("index.html");
if let Ok(html) = fs::read_to_string(&html_path)
&& let Some(updated) = map.update_html(&html)
{
let _ = fs::write(&html_path, updated);
if !is_dev {
info!("Updated importmap in index.html");
}
let website_source = match config.website() {
Website::Enabled(false) => WebsiteSource::Disabled,
Website::Path(p) => WebsiteSource::Filesystem(p),
Website::Enabled(true) => {
// Prefer local filesystem if available, otherwise use embedded
match find_local_website_dir() {
Some(path) => WebsiteSource::Filesystem(path),
None => WebsiteSource::Embedded,
}
}
};
let server = Server::new(&query, data_path, bundle_path);
let future = async move {
let server = Server::new(&query, data_path, website_source);
tokio::spawn(async move {
server.serve(true).await.unwrap();
@@ -201,3 +132,12 @@ pub fn run() -> color_eyre::Result<()> {
}
}
}
/// Path to website directory relative to this crate (only valid at dev machine)
const DEV_WEBSITE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../website");
/// Returns local website path if it exists (dev mode)
fn find_local_website_dir() -> Option<PathBuf> {
let path = PathBuf::from(DEV_WEBSITE_DIR);
path.exists().then_some(path)
}
+9
View File
@@ -12,3 +12,12 @@ pub fn dot_brk_log_path() -> PathBuf {
pub fn default_brk_path() -> PathBuf {
dot_brk_path()
}
pub fn fix_user_path(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/").or(path.strip_prefix("$HOME/"))
&& let Ok(home) = std::env::var("HOME")
{
return PathBuf::from(home).join(rest);
}
PathBuf::from(path)
}
+28 -21
View File
@@ -1,28 +1,35 @@
use clap::ValueEnum;
use std::{path::PathBuf, str::FromStr};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
use crate::paths::fix_user_path;
/// Website configuration:
/// - `true` or omitted: serve embedded website
/// - `false`: disable website serving
/// - `"/path/to/website"`: serve custom website from path
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Website {
None,
Bitview,
Custom,
Enabled(bool),
Path(PathBuf),
}
impl Website {
pub fn is_none(&self) -> bool {
self == &Self::None
}
pub fn is_some(&self) -> bool {
!self.is_none()
}
pub fn to_folder_name(self) -> &'static str {
match self {
Self::Custom => "custom",
Self::Bitview => "bitview",
Self::None => unreachable!(),
}
impl Default for Website {
fn default() -> Self {
Self::Enabled(true)
}
}
impl FromStr for Website {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Self::Enabled(true),
"false" | "0" | "no" | "off" => Self::Enabled(false),
_ => Self::Path(fix_user_path(s)),
})
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
keywords = ["bitcoin", "blockchain", "analytics", "on-chain"]
categories = ["api-bindings", "cryptography::cryptocurrencies"]
+2 -6
View File
@@ -17,7 +17,7 @@ brk_client = "0.1"
use brk_client::{BrkClient, Index};
fn main() -> brk_client::Result<()> {
let client = BrkClient::new("http://localhost:3000");
let client = BrkClient::new("http://localhost:3110");
// Blockchain data (mempool.space compatible)
let block = client.get_block_by_height(800000)?;
@@ -47,11 +47,7 @@ fn main() -> brk_client::Result<()> {
use brk_client::{BrkClient, BrkClientOptions};
let client = BrkClient::with_options(BrkClientOptions {
base_url: "http://localhost:3000".to_string(),
base_url: "http://localhost:3110".to_string(),
timeout_secs: 60,
});
```
## License
MIT
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
+331 -331
View File
@@ -1585,6 +1585,40 @@ impl BitcoinPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct ClassAveragePricePattern<T> {
pub _2015: MetricPattern4<T>,
pub _2016: MetricPattern4<T>,
pub _2017: MetricPattern4<T>,
pub _2018: MetricPattern4<T>,
pub _2019: MetricPattern4<T>,
pub _2020: MetricPattern4<T>,
pub _2021: MetricPattern4<T>,
pub _2022: MetricPattern4<T>,
pub _2023: MetricPattern4<T>,
pub _2024: MetricPattern4<T>,
pub _2025: MetricPattern4<T>,
}
impl<T: DeserializeOwned> ClassAveragePricePattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
_2015: MetricPattern4::new(client.clone(), _m(&acc, "2015_average_price")),
_2016: MetricPattern4::new(client.clone(), _m(&acc, "2016_average_price")),
_2017: MetricPattern4::new(client.clone(), _m(&acc, "2017_average_price")),
_2018: MetricPattern4::new(client.clone(), _m(&acc, "2018_average_price")),
_2019: MetricPattern4::new(client.clone(), _m(&acc, "2019_average_price")),
_2020: MetricPattern4::new(client.clone(), _m(&acc, "2020_average_price")),
_2021: MetricPattern4::new(client.clone(), _m(&acc, "2021_average_price")),
_2022: MetricPattern4::new(client.clone(), _m(&acc, "2022_average_price")),
_2023: MetricPattern4::new(client.clone(), _m(&acc, "2023_average_price")),
_2024: MetricPattern4::new(client.clone(), _m(&acc, "2024_average_price")),
_2025: MetricPattern4::new(client.clone(), _m(&acc, "2025_average_price")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct DollarsPattern<T> {
pub average: MetricPattern2<T>,
@@ -1620,35 +1654,33 @@ impl<T: DeserializeOwned> DollarsPattern<T> {
}
/// Pattern struct for repeated tree structure.
pub struct ClassAveragePricePattern<T> {
pub _2015: MetricPattern4<T>,
pub _2016: MetricPattern4<T>,
pub _2017: MetricPattern4<T>,
pub _2018: MetricPattern4<T>,
pub _2019: MetricPattern4<T>,
pub _2020: MetricPattern4<T>,
pub _2021: MetricPattern4<T>,
pub _2022: MetricPattern4<T>,
pub _2023: MetricPattern4<T>,
pub _2024: MetricPattern4<T>,
pub _2025: MetricPattern4<T>,
pub struct RelativePattern2 {
pub neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub supply_in_loss_rel_to_own_supply: MetricPattern1<StoredF64>,
pub supply_in_profit_rel_to_own_supply: MetricPattern1<StoredF64>,
pub unrealized_loss_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub unrealized_profit_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
}
impl<T: DeserializeOwned> ClassAveragePricePattern<T> {
impl RelativePattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
_2015: MetricPattern4::new(client.clone(), _m(&acc, "2015_returns")),
_2016: MetricPattern4::new(client.clone(), _m(&acc, "2016_returns")),
_2017: MetricPattern4::new(client.clone(), _m(&acc, "2017_returns")),
_2018: MetricPattern4::new(client.clone(), _m(&acc, "2018_returns")),
_2019: MetricPattern4::new(client.clone(), _m(&acc, "2019_returns")),
_2020: MetricPattern4::new(client.clone(), _m(&acc, "2020_returns")),
_2021: MetricPattern4::new(client.clone(), _m(&acc, "2021_returns")),
_2022: MetricPattern4::new(client.clone(), _m(&acc, "2022_returns")),
_2023: MetricPattern4::new(client.clone(), _m(&acc, "2023_returns")),
_2024: MetricPattern4::new(client.clone(), _m(&acc, "2024_returns")),
_2025: MetricPattern4::new(client.clone(), _m(&acc, "2025_returns")),
neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_market_cap")),
neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_total_unrealized_pnl")),
net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_market_cap")),
net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_total_unrealized_pnl")),
supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_loss_rel_to_own_supply")),
supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_own_supply")),
unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_market_cap")),
unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_total_unrealized_pnl")),
unrealized_profit_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_market_cap")),
unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_total_unrealized_pnl")),
}
}
}
@@ -1685,38 +1717,6 @@ impl RelativePattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct RelativePattern2 {
pub neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub supply_in_loss_rel_to_own_supply: MetricPattern1<StoredF64>,
pub supply_in_profit_rel_to_own_supply: MetricPattern1<StoredF64>,
pub unrealized_loss_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
pub unrealized_profit_rel_to_own_market_cap: MetricPattern1<StoredF32>,
pub unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1<StoredF32>,
}
impl RelativePattern2 {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
neg_unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_market_cap")),
neg_unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "neg_unrealized_loss_rel_to_own_total_unrealized_pnl")),
net_unrealized_pnl_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_market_cap")),
net_unrealized_pnl_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "net_unrealized_pnl_rel_to_own_total_unrealized_pnl")),
supply_in_loss_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_loss_rel_to_own_supply")),
supply_in_profit_rel_to_own_supply: MetricPattern1::new(client.clone(), _m(&acc, "supply_in_profit_rel_to_own_supply")),
unrealized_loss_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_market_cap")),
unrealized_loss_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_loss_rel_to_own_total_unrealized_pnl")),
unrealized_profit_rel_to_own_market_cap: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_market_cap")),
unrealized_profit_rel_to_own_total_unrealized_pnl: MetricPattern1::new(client.clone(), _m(&acc, "unrealized_profit_rel_to_own_total_unrealized_pnl")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CountPattern2<T> {
pub average: MetricPattern1<T>,
@@ -1779,36 +1779,6 @@ impl AddrCountPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct FeeRatePattern<T> {
pub average: MetricPattern1<T>,
pub max: MetricPattern1<T>,
pub median: MetricPattern11<T>,
pub min: MetricPattern1<T>,
pub pct10: MetricPattern11<T>,
pub pct25: MetricPattern11<T>,
pub pct75: MetricPattern11<T>,
pub pct90: MetricPattern11<T>,
pub txindex: MetricPattern27<T>,
}
impl<T: DeserializeOwned> FeeRatePattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
average: MetricPattern1::new(client.clone(), _m(&acc, "average")),
max: MetricPattern1::new(client.clone(), _m(&acc, "max")),
median: MetricPattern11::new(client.clone(), _m(&acc, "median")),
min: MetricPattern1::new(client.clone(), _m(&acc, "min")),
pct10: MetricPattern11::new(client.clone(), _m(&acc, "pct10")),
pct25: MetricPattern11::new(client.clone(), _m(&acc, "pct25")),
pct75: MetricPattern11::new(client.clone(), _m(&acc, "pct75")),
pct90: MetricPattern11::new(client.clone(), _m(&acc, "pct90")),
txindex: MetricPattern27::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct FullnessPattern<T> {
pub average: MetricPattern2<T>,
@@ -1839,6 +1809,36 @@ impl<T: DeserializeOwned> FullnessPattern<T> {
}
}
/// Pattern struct for repeated tree structure.
pub struct FeeRatePattern<T> {
pub average: MetricPattern1<T>,
pub max: MetricPattern1<T>,
pub median: MetricPattern11<T>,
pub min: MetricPattern1<T>,
pub pct10: MetricPattern11<T>,
pub pct25: MetricPattern11<T>,
pub pct75: MetricPattern11<T>,
pub pct90: MetricPattern11<T>,
pub txindex: MetricPattern27<T>,
}
impl<T: DeserializeOwned> FeeRatePattern<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
average: MetricPattern1::new(client.clone(), _m(&acc, "average")),
max: MetricPattern1::new(client.clone(), _m(&acc, "max")),
median: MetricPattern11::new(client.clone(), _m(&acc, "median")),
min: MetricPattern1::new(client.clone(), _m(&acc, "min")),
pct10: MetricPattern11::new(client.clone(), _m(&acc, "pct10")),
pct25: MetricPattern11::new(client.clone(), _m(&acc, "pct25")),
pct75: MetricPattern11::new(client.clone(), _m(&acc, "pct75")),
pct90: MetricPattern11::new(client.clone(), _m(&acc, "pct90")),
txindex: MetricPattern27::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _0satsPattern {
pub activity: ActivityPattern2,
@@ -1868,79 +1868,27 @@ 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 PeriodCagrPattern {
pub _10y: MetricPattern4<StoredF32>,
pub _2y: MetricPattern4<StoredF32>,
pub _3y: MetricPattern4<StoredF32>,
pub _4y: MetricPattern4<StoredF32>,
pub _5y: MetricPattern4<StoredF32>,
pub _6y: MetricPattern4<StoredF32>,
pub _8y: MetricPattern4<StoredF32>,
}
impl _100btcPattern {
impl PeriodCagrPattern {
/// 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 _10yTo12yPattern {
pub activity: ActivityPattern2,
pub cost_basis: CostBasisPattern2,
pub outputs: OutputsPattern,
pub realized: RealizedPattern2,
pub relative: RelativePattern2,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _10yTo12yPattern {
/// 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: CostBasisPattern2::new(client.clone(), acc.clone()),
outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: RealizedPattern2::new(client.clone(), acc.clone()),
relative: RelativePattern2::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,
pub cost_basis: CostBasisPattern,
pub outputs: OutputsPattern,
pub realized: RealizedPattern4,
pub relative: RelativePattern,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _10yPattern {
/// 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: RealizedPattern4::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()),
_10y: MetricPattern4::new(client.clone(), _p("10y", &acc)),
_2y: MetricPattern4::new(client.clone(), _p("2y", &acc)),
_3y: MetricPattern4::new(client.clone(), _p("3y", &acc)),
_4y: MetricPattern4::new(client.clone(), _p("4y", &acc)),
_5y: MetricPattern4::new(client.clone(), _p("5y", &acc)),
_6y: MetricPattern4::new(client.clone(), _p("6y", &acc)),
_8y: MetricPattern4::new(client.clone(), _p("8y", &acc)),
}
}
}
@@ -1998,27 +1946,79 @@ impl _0satsPattern2 {
}
/// Pattern struct for repeated tree structure.
pub struct PeriodCagrPattern {
pub _10y: MetricPattern4<StoredF32>,
pub _2y: MetricPattern4<StoredF32>,
pub _3y: MetricPattern4<StoredF32>,
pub _4y: MetricPattern4<StoredF32>,
pub _5y: MetricPattern4<StoredF32>,
pub _6y: MetricPattern4<StoredF32>,
pub _8y: MetricPattern4<StoredF32>,
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 PeriodCagrPattern {
impl _100btcPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
_10y: MetricPattern4::new(client.clone(), _p("10y", &acc)),
_2y: MetricPattern4::new(client.clone(), _p("2y", &acc)),
_3y: MetricPattern4::new(client.clone(), _p("3y", &acc)),
_4y: MetricPattern4::new(client.clone(), _p("4y", &acc)),
_5y: MetricPattern4::new(client.clone(), _p("5y", &acc)),
_6y: MetricPattern4::new(client.clone(), _p("6y", &acc)),
_8y: MetricPattern4::new(client.clone(), _p("8y", &acc)),
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,
pub cost_basis: CostBasisPattern,
pub outputs: OutputsPattern,
pub realized: RealizedPattern4,
pub relative: RelativePattern,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _10yPattern {
/// 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: RealizedPattern4::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 _10yTo12yPattern {
pub activity: ActivityPattern2,
pub cost_basis: CostBasisPattern2,
pub outputs: OutputsPattern,
pub realized: RealizedPattern2,
pub relative: RelativePattern2,
pub supply: SupplyPattern2,
pub unrealized: UnrealizedPattern,
}
impl _10yTo12yPattern {
/// 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: CostBasisPattern2::new(client.clone(), acc.clone()),
outputs: OutputsPattern::new(client.clone(), _m(&acc, "utxo_count")),
realized: RealizedPattern2::new(client.clone(), acc.clone()),
relative: RelativePattern2::new(client.clone(), acc.clone()),
supply: SupplyPattern2::new(client.clone(), _m(&acc, "supply")),
unrealized: UnrealizedPattern::new(client.clone(), acc.clone()),
}
}
}
@@ -2066,91 +2066,19 @@ impl<T: DeserializeOwned> SplitPattern2<T> {
}
/// Pattern struct for repeated tree structure.
pub struct CostBasisPattern2 {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
pub percentiles: PercentilesPattern,
pub struct ActiveSupplyPattern {
pub bitcoin: MetricPattern1<Bitcoin>,
pub dollars: MetricPattern1<Dollars>,
pub sats: MetricPattern1<Sats>,
}
impl CostBasisPattern2 {
impl ActiveSupplyPattern {
/// 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 SegwitAdoptionPattern {
pub base: MetricPattern11<StoredF32>,
pub cumulative: MetricPattern2<StoredF32>,
pub sum: MetricPattern2<StoredF32>,
}
impl SegwitAdoptionPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
base: MetricPattern11::new(client.clone(), acc.clone()),
cumulative: MetricPattern2::new(client.clone(), _m(&acc, "cumulative")),
sum: MetricPattern2::new(client.clone(), _m(&acc, "sum")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct UnclaimedRewardsPattern {
pub bitcoin: BitcoinPattern2<Bitcoin>,
pub dollars: BlockCountPattern<Dollars>,
pub sats: BlockCountPattern<Sats>,
}
impl UnclaimedRewardsPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
bitcoin: BitcoinPattern2::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 CoinbasePattern {
pub bitcoin: BitcoinPattern,
pub dollars: DollarsPattern<Dollars>,
pub sats: DollarsPattern<Sats>,
}
impl CoinbasePattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
bitcoin: BitcoinPattern::new(client.clone(), _m(&acc, "btc")),
dollars: DollarsPattern::new(client.clone(), _m(&acc, "usd")),
sats: DollarsPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _2015Pattern {
pub bitcoin: MetricPattern4<Bitcoin>,
pub dollars: MetricPattern4<Dollars>,
pub sats: MetricPattern4<Sats>,
}
impl _2015Pattern {
/// 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: MetricPattern1::new(client.clone(), _m(&acc, "btc")),
dollars: MetricPattern1::new(client.clone(), _m(&acc, "usd")),
sats: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
@@ -2174,35 +2102,107 @@ impl CoinbasePattern2 {
}
/// Pattern struct for repeated tree structure.
pub struct ActiveSupplyPattern {
pub bitcoin: MetricPattern1<Bitcoin>,
pub dollars: MetricPattern1<Dollars>,
pub sats: MetricPattern1<Sats>,
pub struct _2015Pattern {
pub bitcoin: MetricPattern4<Bitcoin>,
pub dollars: MetricPattern4<Dollars>,
pub sats: MetricPattern4<Sats>,
}
impl ActiveSupplyPattern {
impl _2015Pattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
bitcoin: MetricPattern1::new(client.clone(), _m(&acc, "btc")),
dollars: MetricPattern1::new(client.clone(), _m(&acc, "usd")),
sats: MetricPattern1::new(client.clone(), acc.clone()),
bitcoin: MetricPattern4::new(client.clone(), _m(&acc, "btc")),
dollars: MetricPattern4::new(client.clone(), _m(&acc, "usd")),
sats: MetricPattern4::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct _1dReturns1mSdPattern {
pub sd: MetricPattern4<StoredF32>,
pub sma: MetricPattern4<StoredF32>,
pub struct CoinbasePattern {
pub bitcoin: BitcoinPattern,
pub dollars: DollarsPattern<Dollars>,
pub sats: DollarsPattern<Sats>,
}
impl _1dReturns1mSdPattern {
impl CoinbasePattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
sd: MetricPattern4::new(client.clone(), _m(&acc, "sd")),
sma: MetricPattern4::new(client.clone(), _m(&acc, "sma")),
bitcoin: BitcoinPattern::new(client.clone(), _m(&acc, "btc")),
dollars: DollarsPattern::new(client.clone(), _m(&acc, "usd")),
sats: DollarsPattern::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct UnclaimedRewardsPattern {
pub bitcoin: BitcoinPattern2<Bitcoin>,
pub dollars: BlockCountPattern<Dollars>,
pub sats: BlockCountPattern<Sats>,
}
impl UnclaimedRewardsPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
bitcoin: BitcoinPattern2::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>,
pub cumulative: MetricPattern2<StoredF32>,
pub sum: MetricPattern2<StoredF32>,
}
impl SegwitAdoptionPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
base: MetricPattern11::new(client.clone(), acc.clone()),
cumulative: MetricPattern2::new(client.clone(), _m(&acc, "cumulative")),
sum: MetricPattern2::new(client.clone(), _m(&acc, "sum")),
}
}
}
/// 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 CostBasisPattern {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
}
impl CostBasisPattern {
/// 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")),
}
}
}
@@ -2240,49 +2240,17 @@ impl RelativePattern4 {
}
/// Pattern struct for repeated tree structure.
pub struct CostBasisPattern {
pub max: MetricPattern1<Dollars>,
pub min: MetricPattern1<Dollars>,
pub struct _1dReturns1mSdPattern {
pub sd: MetricPattern4<StoredF32>,
pub sma: MetricPattern4<StoredF32>,
}
impl CostBasisPattern {
impl _1dReturns1mSdPattern {
/// 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")),
}
}
}
/// 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 BitcoinPattern2<T> {
pub cumulative: MetricPattern2<T>,
pub sum: MetricPattern1<T>,
}
impl<T: DeserializeOwned> BitcoinPattern2<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
cumulative: MetricPattern2::new(client.clone(), _m(&acc, "cumulative")),
sum: MetricPattern1::new(client.clone(), acc.clone()),
sd: MetricPattern4::new(client.clone(), _m(&acc, "sd")),
sma: MetricPattern4::new(client.clone(), _m(&acc, "sma")),
}
}
}
@@ -2303,6 +2271,38 @@ impl<T: DeserializeOwned> BlockCountPattern<T> {
}
}
/// 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_sats")),
split: SplitPattern2::new(client.clone(), _m(&acc, "sats")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct BitcoinPattern2<T> {
pub cumulative: MetricPattern2<T>,
pub sum: MetricPattern1<T>,
}
impl<T: DeserializeOwned> BitcoinPattern2<T> {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
cumulative: MetricPattern2::new(client.clone(), _m(&acc, "cumulative")),
sum: MetricPattern1::new(client.clone(), acc.clone()),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct OutputsPattern {
pub utxo_count: MetricPattern1<StoredU64>,
@@ -9300,8 +9300,8 @@ impl MetricsTree_Market_Ath {
/// Metrics tree node.
pub struct MetricsTree_Market_Dca {
pub class_average_price: MetricsTree_Market_Dca_ClassAveragePrice,
pub class_returns: ClassAveragePricePattern<StoredF32>,
pub class_average_price: ClassAveragePricePattern<Dollars>,
pub class_returns: MetricsTree_Market_Dca_ClassReturns,
pub class_stack: MetricsTree_Market_Dca_ClassStack,
pub period_average_price: PeriodAveragePricePattern<Dollars>,
pub period_cagr: PeriodCagrPattern,
@@ -9313,8 +9313,8 @@ pub struct MetricsTree_Market_Dca {
impl MetricsTree_Market_Dca {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
class_average_price: MetricsTree_Market_Dca_ClassAveragePrice::new(client.clone(), format!("{base_path}_class_average_price")),
class_returns: ClassAveragePricePattern::new(client.clone(), "dca_class".to_string()),
class_average_price: ClassAveragePricePattern::new(client.clone(), "dca_class".to_string()),
class_returns: MetricsTree_Market_Dca_ClassReturns::new(client.clone(), format!("{base_path}_class_returns")),
class_stack: MetricsTree_Market_Dca_ClassStack::new(client.clone(), format!("{base_path}_class_stack")),
period_average_price: PeriodAveragePricePattern::new(client.clone(), "dca_average_price".to_string()),
period_cagr: PeriodCagrPattern::new(client.clone(), "dca_cagr".to_string()),
@@ -9326,34 +9326,34 @@ impl MetricsTree_Market_Dca {
}
/// Metrics tree node.
pub struct MetricsTree_Market_Dca_ClassAveragePrice {
pub _2015: MetricPattern4<Dollars>,
pub _2016: MetricPattern4<Dollars>,
pub _2017: MetricPattern4<Dollars>,
pub _2018: MetricPattern4<Dollars>,
pub _2019: MetricPattern4<Dollars>,
pub _2020: MetricPattern4<Dollars>,
pub _2021: MetricPattern4<Dollars>,
pub _2022: MetricPattern4<Dollars>,
pub _2023: MetricPattern4<Dollars>,
pub _2024: MetricPattern4<Dollars>,
pub _2025: MetricPattern4<Dollars>,
pub struct MetricsTree_Market_Dca_ClassReturns {
pub _2015: MetricPattern4<StoredF32>,
pub _2016: MetricPattern4<StoredF32>,
pub _2017: MetricPattern4<StoredF32>,
pub _2018: MetricPattern4<StoredF32>,
pub _2019: MetricPattern4<StoredF32>,
pub _2020: MetricPattern4<StoredF32>,
pub _2021: MetricPattern4<StoredF32>,
pub _2022: MetricPattern4<StoredF32>,
pub _2023: MetricPattern4<StoredF32>,
pub _2024: MetricPattern4<StoredF32>,
pub _2025: MetricPattern4<StoredF32>,
}
impl MetricsTree_Market_Dca_ClassAveragePrice {
impl MetricsTree_Market_Dca_ClassReturns {
pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {
Self {
_2015: MetricPattern4::new(client.clone(), "dca_class_2015_average_price".to_string()),
_2016: MetricPattern4::new(client.clone(), "dca_class_2016_average_price".to_string()),
_2017: MetricPattern4::new(client.clone(), "dca_class_2017_average_price".to_string()),
_2018: MetricPattern4::new(client.clone(), "dca_class_2018_average_price".to_string()),
_2019: MetricPattern4::new(client.clone(), "dca_class_2019_average_price".to_string()),
_2020: MetricPattern4::new(client.clone(), "dca_class_2020_average_price".to_string()),
_2021: MetricPattern4::new(client.clone(), "dca_class_2021_average_price".to_string()),
_2022: MetricPattern4::new(client.clone(), "dca_class_2022_average_price".to_string()),
_2023: MetricPattern4::new(client.clone(), "dca_class_2023_average_price".to_string()),
_2024: MetricPattern4::new(client.clone(), "dca_class_2024_average_price".to_string()),
_2025: MetricPattern4::new(client.clone(), "dca_class_2025_average_price".to_string()),
_2015: MetricPattern4::new(client.clone(), "dca_class_2015_returns".to_string()),
_2016: MetricPattern4::new(client.clone(), "dca_class_2016_returns".to_string()),
_2017: MetricPattern4::new(client.clone(), "dca_class_2017_returns".to_string()),
_2018: MetricPattern4::new(client.clone(), "dca_class_2018_returns".to_string()),
_2019: MetricPattern4::new(client.clone(), "dca_class_2019_returns".to_string()),
_2020: MetricPattern4::new(client.clone(), "dca_class_2020_returns".to_string()),
_2021: MetricPattern4::new(client.clone(), "dca_class_2021_returns".to_string()),
_2022: MetricPattern4::new(client.clone(), "dca_class_2022_returns".to_string()),
_2023: MetricPattern4::new(client.clone(), "dca_class_2023_returns".to_string()),
_2024: MetricPattern4::new(client.clone(), "dca_class_2024_returns".to_string()),
_2025: MetricPattern4::new(client.clone(), "dca_class_2025_returns".to_string()),
}
}
}
@@ -11757,7 +11757,7 @@ impl MetricsTree_Price_Oracle {
Self {
ohlc_cents: MetricPattern6::new(client.clone(), "oracle_ohlc_cents".to_string()),
ohlc_dollars: MetricPattern6::new(client.clone(), "oracle_ohlc".to_string()),
price_cents: MetricPattern11::new(client.clone(), "orange_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()),
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
brk_error = { workspace = true }
+8 -6
View File
@@ -4,13 +4,13 @@ UTXO and address cohort filtering for on-chain analytics.
## What It Enables
Slice the UTXO set and address population by age, amount, output type, halving epoch, or holder classification (STH/LTH). Build complex cohorts by combining filters for metrics like "realized cap of 1+ BTC UTXOs older than 155 days."
Slice the UTXO set and address population by age, amount, output type, halving epoch, or holder classification (STH/LTH). Build complex cohorts by combining filters for metrics like "realized cap of 1+ BTC UTXOs older than 150 days."
## Key Features
- **Age-based**: `TimeFilter::GreaterOrEqual(155)`, `TimeFilter::Range(30..90)`, `TimeFilter::LowerThan(7)`
- **Age-based**: `TimeFilter::GreaterOrEqual(hours)`, `TimeFilter::Range(hours..hours)`, `TimeFilter::LowerThan(hours)`
- **Amount-based**: `AmountFilter::GreaterOrEqual(Sats::_1BTC)`, `AmountFilter::Range(Sats::_100K..Sats::_1M)`
- **Term classification**: `Term::Sth` (short-term holders, <155 days), `Term::Lth` (long-term holders)
- **Term classification**: `Term::Sth` (short-term holders, <150 days), `Term::Lth` (long-term holders)
- **Epoch filters**: Group by halving epoch
- **Type filters**: Segment by output type (P2PKH, P2TR, etc.)
- **Context-aware naming**: Automatic prefix generation (`utxos_`, `addrs_`) based on cohort context
@@ -25,6 +25,7 @@ pub enum Filter {
Time(TimeFilter), // Age-based
Amount(AmountFilter), // Value-based
Epoch(HalvingEpoch), // Halving epoch
Year(Year), // Calendar year
Type(OutputType), // P2PKH, P2TR, etc.
}
```
@@ -32,15 +33,16 @@ pub enum Filter {
## Core API
```rust,ignore
let filter = Filter::Time(TimeFilter::GreaterOrEqual(155));
// TimeFilter values are in hours (e.g., 3600 hours = 150 days)
let filter = Filter::Time(TimeFilter::GreaterOrEqual(3600));
// Check membership
filter.contains_time(200); // true
filter.contains_time(4000); // true (4000 hours > 3600 hours)
filter.contains_amount(sats);
// Generate metric names (via CohortContext)
let ctx = CohortContext::Utxo;
ctx.full_name(&filter, "min_age_155d"); // "utxos_min_age_155d"
ctx.full_name(&filter, "min_age_150d"); // "utxos_min_age_150d"
```
## Built On
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
bitcoin = { workspace = true }
+9 -8
View File
@@ -19,14 +19,14 @@ Compute 1000+ on-chain metrics from indexed blockchain data: supply breakdowns,
## Core API
```rust,ignore
let mut computer = Computer::forced_import(&outputs_path, &indexer, fetcher)?;
let mut computer = Computer::forced_import(&outputs_path, &indexer, Some(fetcher))?;
// Compute all metrics for new blocks
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
// Access computed data
let supply = computer.distribution.utxo_cohorts.all.metrics.supply.height_to_supply.get(height)?;
let realized_cap = computer.distribution.utxo_cohorts.all.metrics.realized.height_to_realized_cap.get(height)?;
// Access computed data via traversable vecs
let supply = computer.distribution.utxo_cohorts.all.metrics.supply.total.sats.height;
let realized_cap = computer.distribution.utxo_cohorts.all.metrics.realized.unwrap().realized_cap.height;
```
## Metric Categories
@@ -45,9 +45,9 @@ let realized_cap = computer.distribution.utxo_cohorts.all.metrics.realized.heigh
## Cohort System
UTXO and address cohorts support filtering by:
- **Age**: STH (<150d), LTH (≥150d), age bands (1d, 1w, 1m, 3m, 6m, 1y, 2y, ...)
- **Age**: STH (<150d), LTH (≥150d), 21 age bands (<1h, 1h-1d, 1d-1w, 1w-1m, 1m-2m, ..., 6m-1y, 1y-2y, ..., 12y-15y, 15y+)
- **Amount**: 0-0.001 BTC, 0.001-0.01, ..., 10k+ BTC
- **Type**: P2PKH, P2SH, P2WPKH, P2WSH, P2TR
- **Type**: P2PK, P2PKH, P2MS, P2SH, P2WPKH, P2WSH, P2TR, P2A
- **Epoch**: By halving epoch
## Performance
@@ -58,9 +58,10 @@ Full pipeline benchmarks (indexer + computer):
| Machine | Time | Disk | Peak Disk | Memory | Peak Memory |
|---------|------|------|-----------|--------|-------------|
| MBP M3 Pro (36GB, internal SSD) | 5.2h | 341 GB | 415 GB | 6.4 GB | 12 GB |
| MBP M3 Pro (36GB, internal SSD) | 4.4h | 345 GB | 348 GB | 3.3 GB | 11 GB |
| Mac Mini M4 (16GB, external SSD) | 7h | 344 GB | 346 GB | 4 GB | 10 GB |
Full benchmark data: [`https://github.com/bitcoinresearchkit/benches/tree/main/brk`](/benches/brk)
Full benchmark data: [bitcoinresearchkit/benches](https://github.com/bitcoinresearchkit/benches/tree/main/brk)
## Recommended: mimalloc v3
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
@@ -187,12 +187,14 @@ impl Vecs {
continue;
}
// Check witness size (SegWit era only, activated Aug 2017)
// Check witness size per input (SegWit era only, activated Aug 2017)
// Pre-SegWit transactions have no witness data
// Python checks each input's witness ≤ 500 bytes; we approximate with average
if cached_year >= 2017 {
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);
if *total_size - *base_size > 500 {
let witness_size = *total_size - *base_size;
if witness_size / *input_count as u32 > 500 {
continue;
}
}
@@ -379,10 +381,9 @@ impl Vecs {
}
};
self.ohlc_cents
.truncate_push_at(dateindex.to_usize(), ohlc)?;
self.ohlc_cents.truncate_push(dateindex, ohlc)?;
self.tx_count
.truncate_push_at(dateindex.to_usize(), StoredU32::from(tx_count))?;
.truncate_push(dateindex, StoredU32::from(tx_count))?;
}
// Write daily data
+13 -11
View File
@@ -48,11 +48,12 @@ impl OracleConfig {
blocks_per_window: 144, // ~1 day
min_tx_count: 1000,
},
// 2017+: Modern era ($1,000 - $1,000,000+)
// 2017+: Modern era ($10,000 - $500,000)
// Matches Python's slide range of -141 to 201
_ => Self {
min_price_cents: 100_000, // $1,000
max_price_cents: 100_000_000, // $1,000,000
blocks_per_window: 144, // ~1 day
min_price_cents: 1_000_000, // $10,000 (gives max_slide = 200)
max_price_cents: 50_000_000, // $500,000 (gives min_slide ≈ -140)
blocks_per_window: 144, // ~1 day
min_tx_count: 2000,
},
}
@@ -90,9 +91,10 @@ mod tests {
#[test]
fn test_config_for_year() {
// 2017+ config matches Python: $10,000 to $500,000
let c2020 = OracleConfig::for_year(2020);
assert_eq!(c2020.min_price_cents, 100_000);
assert_eq!(c2020.max_price_cents, 100_000_000);
assert_eq!(c2020.min_price_cents, 1_000_000);
assert_eq!(c2020.max_price_cents, 50_000_000);
let c2015 = OracleConfig::for_year(2015);
assert_eq!(c2015.min_price_cents, 10_000);
@@ -101,13 +103,13 @@ mod tests {
#[test]
fn test_slide_range() {
// 2024 config: $1,000 to $1,000,000
// 2024 config: $10,000 to $500,000 (matches Python's -141 to 201)
let config = OracleConfig::for_year(2024);
let (min, max) = config.slide_range();
// $1,000,000 = 10^8 cents → slide = (7-8)*200 = -200
// $1,000 = 10^5 cents → slide = (7-5)*200 = 400
assert_eq!(min, -200);
assert_eq!(max, 400);
// $500,000 = 5*10^7 cents → slide = (7-7.699)*200 -140
// $10,000 = 10^6 cents → slide = (7-6)*200 = 200
assert!((-141..=-139).contains(&min)); // ~-140
assert_eq!(max, 200);
// 2015 config: $100 to $20,000
let config = OracleConfig::for_year(2015);
@@ -11,10 +11,12 @@ pub const MAX_LOG_BTC: f64 = 2.0; // 10^2 BTC = 100 BTC
pub const NUM_DECADES: usize = 8; // -6 to +2
pub const TOTAL_BINS: usize = NUM_DECADES * BINS_PER_DECADE; // 1600 bins
/// Minimum output value to consider (10,000 sats = 0.0001 BTC)
pub const MIN_OUTPUT_SATS: Sats = Sats::_10K;
/// Maximum output value to consider (10 BTC)
pub const MAX_OUTPUT_SATS: Sats = Sats::_10BTC;
/// Minimum output value to consider (~1,000 sats = 0.00001 BTC)
/// Matches Python: zeros bins 0-200 which is 10^-5 BTC
pub const MIN_OUTPUT_SATS: Sats = Sats::_1K;
/// Maximum output value to consider (100 BTC)
/// Matches Python: zeros bins 1601+ which is ~10^2 BTC
pub const MAX_OUTPUT_SATS: Sats = Sats::_100BTC;
/// Round BTC bin indices that should be smoothed to avoid false positives
/// These are bins where round BTC amounts would naturally cluster
@@ -5,8 +5,11 @@ use vecdb::{BytesVec, Database, ImportableVec, IterableCloneableVec, LazyVecFrom
use super::Vecs;
impl Vecs {
pub fn forced_import(db: &Database, version: Version) -> Result<Self> {
let price_cents = PcoVec::forced_import(db, "orange_price_cents", version)?;
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;
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)?;
+85 -69
View File
@@ -12,68 +12,66 @@ use super::histogram::{BINS_PER_DECADE, Histogram, TOTAL_BINS};
/// Number of parallel chunks for stencil sliding
const PARALLEL_CHUNKS: i32 = 4;
/// USD spike stencil entries: (bin offset from $100 center, weight)
/// USD spike stencil entries: (bin offset from center_bin, weight)
/// These represent the expected frequency of round USD amounts in transactions
/// Offset formula: log10(USD/100) * 200 bins/decade
/// Companion spikes at ±2 bins from main spike (Rust 200 bins/decade ≈ Python's ±1 at 180 bins/decade)
/// Matches Python's 29 entries from utxo_oracle.py lines 1013-1041
/// Positions derived from Python's empirical data (utxo_oracle.py lines 1013-1041)
/// Offset = python_stencil_index - 402 (since Python stencil starts at bin 199, center is 601)
const SPIKE_STENCIL: &[(i32, f64)] = &[
// $1 (single)
(-400, 0.00130),
// $5 (single)
(-260, 0.00168),
// $10 (main + companion)
(-200, 0.00347),
(-198, 0.00199),
// $15 (single)
(-165, 0.00191),
// $20 (main + companion)
(-140, 0.00334),
(-138, 0.00259),
// $30 (main + companion)
(-105, 0.00258),
(-103, 0.00273),
// $50 (main + 2 companions)
// $1 (single) - Python index 40
(-362, 0.00130),
// $5 (single) - Python index 141
(-261, 0.00168),
// $10 (main + companion) - Python indices 201-202
(-201, 0.00347),
(-200, 0.00199),
// $15 (single) - Python index 236
(-166, 0.00191),
// $20 (main + companion) - Python indices 261-262
(-141, 0.00334),
(-140, 0.00259),
// $30 (main + companion) - Python indices 296-297
(-106, 0.00258),
(-105, 0.00273),
// $50 (main + 2 companions) - Python indices 340-342
(-62, 0.00308),
(-60, 0.00561),
(-58, 0.00309),
// $100 (main + 3 companions) - center
(-61, 0.00561),
(-60, 0.00309),
// $100 (main + 3 companions) - Python indices 400-403
(-2, 0.00292),
(0, 0.00617),
(2, 0.00442),
(4, 0.00263),
// $150 (single)
(35, 0.00286),
// $200 (main + companion)
(60, 0.00410),
(62, 0.00335),
// $300 (main + companion)
(95, 0.00252),
(97, 0.00278),
// $500 (single)
(140, 0.00379),
// $1000 (main + companion)
(200, 0.00369),
(202, 0.00239),
// $1500 (single)
(235, 0.00128),
// $2000 (main + companion)
(260, 0.00165),
(262, 0.00140),
// $5000 (single)
(340, 0.00115),
// $10000 (single)
(400, 0.00083),
(-1, 0.00617),
(0, 0.00442),
(1, 0.00263),
// $150 (single) - Python index 436
(34, 0.00286),
// $200 (main + companion) - Python indices 461-462
(59, 0.00410),
(60, 0.00335),
// $300 (main + companion) - Python indices 496-497
(94, 0.00252),
(95, 0.00278),
// $500 (single) - Python index 541
(139, 0.00379),
// $1000 (main + companion) - Python indices 601-602
(199, 0.00369),
(200, 0.00239),
// $1500 (single) - Python index 636
(234, 0.00128),
// $2000 (main + companion) - Python indices 661-662
(259, 0.00165),
(260, 0.00140),
// $5000 (single) - Python index 741
(339, 0.00115),
// $10000 (single) - Python index 801
(399, 0.00083),
];
/// Width of the smooth stencil in bins (Gaussian sigma)
/// Python uses std_dev=201 with 803 bins. Our histogram has 1600 bins (2x),
/// so we use 201 * (1600/803) ≈ 400 bins sigma equivalent
const SMOOTH_WIDTH: f64 = 400.0;
/// Both Python and Rust use 200 bins per decade, so sigma is the same
const SMOOTH_WIDTH: f64 = 201.0;
/// Linear term coefficient for smooth stencil (per Python: 0.0000005 * x)
/// Scaled for our larger histogram: 0.0000005 * (803/1600) ≈ 0.00000025
const SMOOTH_LINEAR_COEF: f64 = 0.00000025;
/// NOT scaled - the linear term uses window position (0-802), same as Python
const SMOOTH_LINEAR_COEF: f64 = 0.0000005;
/// Weight given to smooth stencil vs spike stencil
const SMOOTH_WEIGHT: f64 = 0.65;
@@ -84,6 +82,12 @@ const SPIKE_WEIGHT: f64 = 1.0;
/// This avoids computing exp() billions of times
const SMOOTH_RANGE: usize = 800;
/// Gaussian center bin offset from spike center
/// Python's Gaussian has mean=411 in 803-element stencil
/// Stencil starts at bin 199, so Gaussian centers at bin 199+411=610
/// Spike center is at bin 601, so Gaussian is offset by +9 bins
const GAUSSIAN_CENTER_OFFSET: i32 = 9;
/// Lazily initialized Gaussian weight lookup table
fn gaussian_weights() -> &'static [f64; SMOOTH_RANGE + 1] {
use std::sync::OnceLock;
@@ -110,16 +114,6 @@ fn gaussian_weights() -> &'static [f64; SMOOTH_RANGE + 1] {
pub fn find_best_price(histogram: &Histogram, min_slide: i32, max_slide: i32) -> Option<Cents> {
let bins = histogram.bins();
// Pre-compute the linear term sum (constant for all slide positions)
// linear_sum = Σ bins[i] * SMOOTH_LINEAR_COEF * i
let linear_sum: f64 = bins
.iter()
.copied()
.enumerate()
.filter(|(_, v)| *v > 0.0)
.map(|(i, v)| v * SMOOTH_LINEAR_COEF * i as f64)
.sum();
// Collect non-zero bins: Vec for Gaussian (needs iteration), HashMap for spike (needs lookup)
let non_zero_bins: Vec<(usize, f64)> = bins
.iter()
@@ -147,7 +141,7 @@ pub fn find_best_price(histogram: &Histogram, min_slide: i32, max_slide: i32) ->
let mut local_total = 0.0;
for slide in chunk_start..=chunk_end {
let score = compute_score_fast(&non_zero_bins, &bin_map, linear_sum, slide);
let score = compute_score_fast(&non_zero_bins, &bin_map, slide);
local_total += score;
if score > local_best_score {
local_best_score = score;
@@ -170,8 +164,8 @@ pub fn find_best_price(histogram: &Histogram, min_slide: i32, max_slide: i32) ->
);
// Compute neighbor scores for sub-bin interpolation (matches Python behavior)
let neighbor_up_score = compute_score_fast(&non_zero_bins, &bin_map, linear_sum, best_position + 1);
let neighbor_down_score = compute_score_fast(&non_zero_bins, &bin_map, linear_sum, best_position - 1);
let neighbor_up_score = compute_score_fast(&non_zero_bins, &bin_map, best_position + 1);
let neighbor_down_score = compute_score_fast(&non_zero_bins, &bin_map, best_position - 1);
// Find best neighbor
let (best_neighbor_offset, neighbor_score) = if neighbor_up_score > neighbor_down_score {
@@ -204,7 +198,6 @@ pub fn find_best_price(histogram: &Histogram, min_slide: i32, max_slide: i32) ->
fn compute_score_fast(
non_zero_bins: &[(usize, f64)],
bin_map: &FxHashMap<usize, f64>,
linear_sum: f64,
slide: i32,
) -> f64 {
let spike_score = compute_spike_score_hash(bin_map, slide);
@@ -212,17 +205,40 @@ fn compute_score_fast(
// Python: smooth weight only applied for slide < 150
if slide < 150 {
let gaussian_score = compute_gaussian_score_sparse(non_zero_bins, slide);
let linear_score = compute_linear_score_sparse(non_zero_bins, slide);
// Combine Gaussian and linear parts of smooth score
let smooth_score = 0.0015 * gaussian_score + linear_sum;
let smooth_score = 0.0015 * gaussian_score + linear_score;
SMOOTH_WEIGHT * smooth_score + SPIKE_WEIGHT * spike_score
} else {
SPIKE_WEIGHT * spike_score
}
}
/// Compute the linear part of the smooth stencil (per-slide, matches Python)
/// Python: sum(shifted_curve[n] * 0.0000005 * n) where n is window position (0-802)
fn compute_linear_score_sparse(non_zero_bins: &[(usize, f64)], slide: i32) -> f64 {
// Window starts at left_p001 + slide = (center_bin - 402) + slide = 199 + slide
// Python: left_p001 = center_p001 - int((803+1)/2) = 601 - 402 = 199
let window_start = 199 + slide;
let window_end = window_start + 803; // 803 elements like Python's stencil
let mut score = 0.0;
for &(i, bin_value) in non_zero_bins {
let bin_idx = i as i32;
if bin_idx >= window_start && bin_idx < window_end {
let window_pos = bin_idx - window_start;
score += bin_value * SMOOTH_LINEAR_COEF * window_pos as f64;
}
}
score
}
/// Compute just the Gaussian part of the smooth stencil (sparse iteration)
/// Note: Gaussian center is offset from spike center by GAUSSIAN_CENTER_OFFSET
fn compute_gaussian_score_sparse(non_zero_bins: &[(usize, f64)], slide: i32) -> f64 {
let center = center_bin() as i32 + slide;
// Python's Gaussian is centered at bin 610 (not 601), so we add the offset
let center = center_bin() as i32 + GAUSSIAN_CENTER_OFFSET + slide;
let weights = gaussian_weights();
let mut score = 0.0;
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
bitcoin = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
brk_error = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
bitcoin = { workspace = true }
+12 -10
View File
@@ -24,23 +24,24 @@ let mut indexer = Indexer::forced_import(&outputs_dir)?;
let starting_indexes = indexer.index(&blocks, &client, &exit)?;
// Access indexed data
let height = indexer.stores.txidprefix_to_txindex.get(&txid_prefix)?;
let blockhash = indexer.vecs.block.height_to_blockhash.get(height)?;
let txindex = indexer.stores.txidprefix_to_txindex.get(&txid_prefix)?;
let blockhash = indexer.vecs.blocks.blockhash.get(height)?;
```
## Data Structures
**Vecs** (append-only vectors):
- Block: `height_to_blockhash`, `height_to_timestamp`, `height_to_difficulty`
- Transaction: `txindex_to_txid`, `txindex_to_height`, `txindex_to_base_size`
- Input/Output: `txinindex_to_outpoint`, `txoutindex_to_value`, `txoutindex_to_outputtype`
- Address: Per-type `typeindex_to_addressbytes`
- `blocks`: `blockhash`, `timestamp`, `difficulty`, `total_size`, `weight`
- `transactions`: `txid`, `first_txinindex`, `first_txoutindex`
- `inputs`: `outpoint`, `txindex`
- `outputs`: `value`, `outputtype`, `typeindex`, `txindex`
- `addresses`: Per-type `p2pkhbytes`, `p2shbytes`, `p2wpkhbytes`, etc.
**Stores** (key-value lookups):
- `txidprefix_to_txindex` - TXID lookup via 10-byte prefix
- `blockhashprefix_to_height` - Block lookup via 4-byte prefix
- `addresshash_to_addressindex` - Address lookup per type
- `addressindex_to_unspent_outpoints` - Live UTXO set per address
- `addresstype_to_addresshash_to_addressindex` - Address lookup per type
- `addresstype_to_addressindex_and_unspentoutpoint` - Live UTXO set per address
## Processing Pipeline
@@ -55,10 +56,10 @@ let blockhash = indexer.vecs.block.height_to_blockhash.get(height)?;
| Machine | Time | Disk | Peak Disk | Memory | Peak Memory |
|---------|------|------|-----------|--------|-------------|
| MBP M3 Pro (36GB, internal SSD) | 3.1h | 233 GB | 307 GB | 5.5 GB | 11 GB |
| MBP M3 Pro (36GB, internal SSD) | 3h | 247 GB | 314 GB | 5.2 GB | 11 GB |
| Mac Mini M4 (16GB, external SSD) | 4.9h | 233 GB | 303 GB | 5.4 GB | 11 GB |
Full benchmark data: [`https://github.com/bitcoinresearchkit/benches/tree/main/brk_indexer`](/benches/brk_indexer)
Full benchmark data: [bitcoinresearchkit/benches](https://github.com/bitcoinresearchkit/benches/tree/main/brk_indexer)
## Recommended: mimalloc v3
@@ -66,6 +67,7 @@ Use [mimalloc v3](https://crates.io/crates/mimalloc) as the global allocator to
## Built On
- `vecdb` for append-only vectors
- `brk_cohort` for address type handling
- `brk_iterator` for block iteration
- `brk_store` for key-value storage
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
brk_error = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
jiff = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
axum = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
brk_error = { workspace = true }
+5 -5
View File
@@ -36,10 +36,10 @@ let tracker = mempool.get_addresses();
Returns `RecommendedFees` with sat/vB rates for different confirmation targets:
- `fastest_fee` - Next block
- `half_hour_fee` - ~3 blocks
- `hour_fee` - ~6 blocks
- `economy_fee` - ~144 blocks
- `fastest_fee` - Next block (index 0)
- `half_hour_fee` - ~3 blocks (index 2)
- `hour_fee` - ~6 blocks (index 5)
- `economy_fee` - ~8 blocks (index 7, last projected block)
- `minimum_fee` - Relay minimum
## Block Projection
@@ -48,7 +48,7 @@ Builds projected blocks by:
1. Constructing transaction dependency graph
2. Calculating effective fee rates (including ancestors)
3. Selecting transactions greedily by ancestor-aware fee rate
4. Partitioning into ~4MB virtual blocks
4. Partitioning into 1MB vsize blocks
## Built On
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
+1 -2
View File
@@ -6,7 +6,6 @@ edition.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[features]
tokio = ["dep:tokio"]
@@ -24,6 +23,6 @@ brk_types = { workspace = true }
derive_more = { workspace = true }
jiff = { workspace = true }
# quickmatch = { path = "../../../quickmatch" }
quickmatch = "0.1.8"
quickmatch = "0.2.0"
tokio = { workspace = true, optional = true }
vecdb = { workspace = true }
+11 -12
View File
@@ -25,20 +25,19 @@ let height = query.height();
// Metric queries
let data = query.search_and_format(MetricSelection {
vecids: vec!["supply".into()],
metrics: vec!["supply".into()],
index: Index::Height,
range: DataRange::Last(100),
..Default::default()
range: DataRangeFormat::default(),
})?;
// Block queries
let info = query.block_info(Height::new(840_000))?;
let info = query.block_by_height(Height::new(840_000))?;
// Transaction queries
let tx = query.transaction(&txid)?;
let tx = query.transaction(txid.into())?;
// Address queries
let stats = query.address_stats(&address)?;
let stats = query.address(address)?;
```
## Query Types
@@ -46,11 +45,11 @@ let stats = query.address_stats(&address)?;
| Domain | Methods |
|--------|---------|
| Metrics | `metrics`, `search_and_format`, `metric_to_indexes` |
| Blocks | `block_info`, `block_txs`, `block_status`, `block_timestamp` |
| Transactions | `transaction`, `tx_status`, `tx_merkle_proof` |
| Addresses | `address_stats`, `address_txs`, `address_utxos` |
| Mining | `difficulty_adjustments`, `hashrate`, `pools`, `epochs` |
| Mempool | `mempool_info`, `mempool_fees`, `mempool_projected_blocks` |
| 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` |
| Mining | `difficulty_adjustments`, `hashrate`, `mining_pools`, `reward_stats` |
| Mempool | `mempool_info`, `recommended_fees`, `mempool_blocks` |
## Async Usage
@@ -58,7 +57,7 @@ let stats = query.address_stats(&address)?;
let async_query = AsyncQuery::build(&reader, &indexer, &computer, mempool);
// Run blocking queries in thread pool
let result = async_query.run(|q| q.block_info(height)).await;
let result = async_query.run(|q| q.block_by_height(height)).await;
// Access inner Query
let height = async_query.inner().height();
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -8,7 +8,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
bitcoin = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
bitcoin = { workspace = true }
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
+8
View File
@@ -24,6 +24,7 @@ brk_rpc = { workspace = true }
brk_types = { workspace = true }
brk_traversable = { workspace = true }
derive_more = { workspace = true }
include_dir = "0.7"
vecdb = { workspace = true }
jiff = { workspace = true }
quick_cache = "0.6.18"
@@ -34,5 +35,12 @@ tokio = { workspace = true }
tracing = { workspace = true }
tower-http = { version = "0.6.8", features = ["compression-full", "trace"] }
[build-dependencies]
# importmap = { path = "../../../importmap" }
importmap = "0.2"
[dev-dependencies]
brk_mempool = { workspace = true }
[package.metadata.cargo-machete]
ignored = ["importmap"]
+9 -7
View File
@@ -8,7 +8,7 @@ Serve BRK data via REST API with OpenAPI documentation, response caching, MCP en
## Key Features
- **OpenAPI spec**: Auto-generated documentation at `/api/openapi.json`
- **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
- **Static files**: Optional web interface hosting
@@ -17,7 +17,8 @@ Serve BRK data via REST API with OpenAPI documentation, response caching, MCP en
## Core API
```rust,ignore
let server = Server::new(&async_query, Some(files_path));
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
```
@@ -25,17 +26,18 @@ server.serve(true).await?; // true enables MCP endpoint
| Path | Description |
|------|-------------|
| `/api/blocks/{height}` | Block info, transactions, status |
| `/api/txs/{txid}` | Transaction details, status, merkle proof |
| `/api/addresses/{addr}` | Address stats, transactions, UTXOs |
| `/api/block-height/{height}` | Block by height |
| `/api/block/{hash}` | Block info, transactions, status |
| `/api/tx/{txid}` | Transaction details, status, hex |
| `/api/address/{addr}` | Address stats, transactions, UTXOs |
| `/api/metrics` | Metric catalog and data queries |
| `/api/mining/*` | Hashrate, difficulty, pools, epochs |
| `/api/v1/mining/*` | Hashrate, difficulty, pools, rewards |
| `/api/mempool/*` | Fee estimates, projected blocks |
| `/mcp` | MCP endpoint (if enabled) |
## Caching
Uses ETag-based caching with stale-while-revalidate semantics:
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
+19 -5
View File
@@ -1,8 +1,22 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
use std::{env, path::Path};
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
fn main() {
let is_dev = env::var("PROFILE").is_ok_and(|p| p == "debug");
// Generate importmap for website (updates index.html in place)
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let website_path = Path::new(&manifest_dir).join("../../website");
println!("cargo:rerun-if-changed=../../website");
if website_path.exists() {
// Skip importmap hashing in dev mode (files change often)
let map = if is_dev {
importmap::ImportMap::empty()
} else {
importmap::ImportMap::scan(&website_path, "").unwrap_or_else(|_| importmap::ImportMap::empty())
};
let _ = map.update_html_file(&website_path.join("index.html"));
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ use brk_mempool::Mempool;
use brk_query::AsyncQuery;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_server::Server;
use brk_server::{Server, WebsiteSource};
use tracing::info;
use vecdb::Exit;
@@ -54,7 +54,7 @@ fn run() -> Result<()> {
// Option 1: block_on to run and properly propagate errors
runtime.block_on(async move {
let server = Server::new(&query, outputs_dir, None);
let server = Server::new(&query, outputs_dir, WebsiteSource::Disabled);
let handle = tokio::spawn(async move { server.serve(true).await });
+159 -104
View File
@@ -10,121 +10,63 @@ use axum::{
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use brk_error::{Error, Result};
use brk_error::Result;
use quick_cache::sync::GuardResult;
use tracing::{error, info};
use crate::{AppState, HeaderMapExtended, ModifiedState, ResponseExtended};
use crate::{
AppState, EMBEDDED_WEBSITE, HeaderMapExtended, ModifiedState, ResponseExtended, WebsiteSource,
};
pub async fn file_handler(
headers: HeaderMap,
State(state): State<AppState>,
path: extract::Path<String>,
) -> Response {
any_handler(headers, state, Some(path))
any_handler(headers, state, Some(path.0))
}
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Response {
any_handler(headers, state, None)
}
fn any_handler(
headers: HeaderMap,
state: AppState,
path: Option<extract::Path<String>>,
) -> Response {
let files_path = state.files_path.as_ref().unwrap();
if let Some(path) = path.as_ref() {
// Sanitize path components to prevent traversal attacks
let sanitized: String = path
.0
.split('/')
.filter(|component| !component.is_empty() && *component != "." && *component != "..")
.collect::<Vec<_>>()
.join("/");
let mut path = files_path.join(&sanitized);
// Canonicalize and verify the path stays within the project root
// (allows symlinks to modules/ which is outside the website directory)
if let Ok(canonical) = path.canonicalize()
&& let Ok(canonical_base) = files_path.canonicalize()
{
// Allow paths within files_path OR within project root (2 levels up)
let project_root = canonical_base.parent().and_then(|p| p.parent());
let allowed = canonical.starts_with(&canonical_base)
|| project_root.is_some_and(|root| canonical.starts_with(root));
if !allowed {
let mut response: Response<Body> =
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
}
}
// Strip hash from import-mapped URLs (e.g., foo.abc12345.js -> foo.js)
if !path.exists()
&& let Some(unhashed) = strip_importmap_hash(&path)
&& unhashed.exists()
{
path = unhashed;
}
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let mut response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
"File doesn't exist".to_string(),
)
.into_response();
response.headers_mut().insert_cors();
return response;
} else {
path = files_path.join("index.html");
}
}
path_to_response(&headers, &state, &path)
} else {
path_to_response(&headers, &state, &files_path.join("index.html"))
}
}
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
match path_to_response_(headers, state, path) {
Ok(response) => response,
Err(error) => {
let mut response =
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
response.headers_mut().insert_cors();
response
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Response {
match &state.website {
WebsiteSource::Disabled => unreachable!("routes not added when disabled"),
WebsiteSource::Embedded => embedded_handler(&state, path),
WebsiteSource::Filesystem(files_path) => {
filesystem_handler(headers, &state, files_path, path)
}
}
}
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
let (modified, date) = headers.check_if_modified_since(path)?;
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
return Ok(Response::new_not_modified());
}
/// Sanitize path to prevent traversal attacks
fn sanitize_path(path: &str) -> String {
path.split('/')
.filter(|c| !c.is_empty() && *c != "." && *c != "..")
.collect::<Vec<_>>()
.join("/")
}
let serialized_path = path.to_str().unwrap();
/// Check if path requires revalidation (HTML files, service worker)
fn must_revalidate(path: &Path) -> bool {
path.extension().is_some_and(|ext| ext == "html")
|| path
.to_str()
.is_some_and(|p| p.ends_with("service-worker.js"))
}
let must_revalidate = path
.extension()
.is_some_and(|extension| extension == "html")
|| serialized_path.ends_with("service-worker.js");
/// Build response with proper headers and caching
fn build_response(state: &AppState, path: &Path, content: Vec<u8>, cache_key: &str) -> Response {
let must_revalidate = must_revalidate(path);
// Use cache for non-HTML files in release mode
let guard_res = if !cfg!(debug_assertions) && !must_revalidate {
Some(state.cache.get_value_or_guard(
&path.to_str().unwrap().to_owned(),
Some(Duration::from_millis(50)),
))
Some(
state
.cache
.get_value_or_guard(&cache_key.to_owned(), Some(Duration::from_millis(50))),
)
} else {
None
};
@@ -132,19 +74,10 @@ fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Resu
let mut response = if let Some(GuardResult::Value(v)) = guard_res {
Response::new(Body::from(v))
} else {
let content = fs::read(path).unwrap_or_else(|error| {
error!("{error}");
let path = path.to_str().unwrap();
info!("Can't read file {path}");
panic!("")
});
if let Some(GuardResult::Guard(g)) = guard_res {
g.insert(content.clone().into())
.map_err(|_| Error::QuickCacheError)?;
let _ = g.insert(content.clone().into());
}
Response::new(content.into())
Response::new(Body::from(content))
};
let headers = response.headers_mut();
@@ -157,7 +90,129 @@ fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Resu
headers.insert_cache_control_immutable();
}
headers.insert_last_modified(date);
response
}
fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
let path = path.unwrap_or_else(|| "index.html".to_string());
let sanitized = sanitize_path(&path);
// Try to get file, with importmap hash stripping and SPA fallback
let file = EMBEDDED_WEBSITE
.get_file(&sanitized)
.or_else(|| {
strip_importmap_hash(Path::new(&sanitized))
.and_then(|unhashed| EMBEDDED_WEBSITE.get_file(unhashed.to_str()?))
})
.or_else(|| {
// If no extension, serve index.html (SPA routing)
if Path::new(&sanitized).extension().is_none() {
EMBEDDED_WEBSITE.get_file("index.html")
} else {
None
}
});
let Some(file) = file else {
let mut response: Response<Body> =
(StatusCode::NOT_FOUND, "File not found".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
};
build_response(
state,
Path::new(file.path()),
file.contents().to_vec(),
&file.path().to_string_lossy(),
)
}
fn filesystem_handler(
headers: HeaderMap,
state: &AppState,
files_path: &Path,
path: Option<String>,
) -> Response {
let path = if let Some(path) = path {
let sanitized = sanitize_path(&path);
let mut path = files_path.join(&sanitized);
// Canonicalize and verify the path stays within the project root
// (allows symlinks to modules/ which is outside the website directory)
if let Ok(canonical) = path.canonicalize()
&& let Ok(canonical_base) = files_path.canonicalize()
{
let project_root = canonical_base.parent().and_then(|p| p.parent());
let allowed = canonical.starts_with(&canonical_base)
|| project_root.is_some_and(|root| canonical.starts_with(root));
if !allowed {
let mut response: Response<Body> =
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
response.headers_mut().insert_cors();
return response;
}
}
// Strip hash from import-mapped URLs
if !path.exists()
&& let Some(unhashed) = strip_importmap_hash(&path)
&& unhashed.exists()
{
path = unhashed;
}
// SPA fallback
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let mut response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
"File doesn't exist".to_string(),
)
.into_response();
response.headers_mut().insert_cors();
return response;
} else {
path = files_path.join("index.html");
}
}
path
} else {
files_path.join("index.html")
};
path_to_response(&headers, state, &path)
}
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
match path_to_response_(headers, state, path) {
Ok(response) => response,
Err(error) => {
let mut response =
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
response.headers_mut().insert_cors();
response
}
}
}
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
let (modified, date) = headers.check_if_modified_since(path)?;
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
return Ok(Response::new_not_modified());
}
let content = fs::read(path).unwrap_or_else(|error| {
error!("{error}");
let path = path.to_str().unwrap();
info!("Can't read file {path}");
panic!("")
});
let cache_key = path.to_str().unwrap();
let mut response = build_response(state, path, content, cache_key);
response.headers_mut().insert_last_modified(date);
Ok(response)
}
+4 -6
View File
@@ -1,21 +1,19 @@
use std::path::PathBuf;
use aide::axum::ApiRouter;
use axum::{response::Redirect, routing::get};
use super::AppState;
use super::{AppState, WebsiteSource};
mod file;
use file::{file_handler, index_handler};
pub trait FilesRoutes {
fn add_files_routes(self, path: Option<&PathBuf>) -> Self;
fn add_files_routes(self, website: &WebsiteSource) -> Self;
}
impl FilesRoutes for ApiRouter<AppState> {
fn add_files_routes(self, path: Option<&PathBuf>) -> Self {
if path.is_some() {
fn add_files_routes(self, website: &WebsiteSource) -> Self {
if website.is_enabled() {
self.route("/{*path}", get(file_handler))
.route("/", get(index_handler))
} else {
+21 -3
View File
@@ -15,11 +15,29 @@ use axum::{
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;
use tokio::net::TcpListener;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tracing::{error, info};
/// Embedded website assets
pub static EMBEDDED_WEBSITE: Dir = include_dir!("$CARGO_MANIFEST_DIR/../../website");
/// Source for serving the website
#[derive(Debug, Clone)]
pub enum WebsiteSource {
Disabled,
Embedded,
Filesystem(PathBuf),
}
impl WebsiteSource {
pub fn is_enabled(&self) -> bool {
!matches!(self, Self::Disabled)
}
}
mod api;
pub mod cache;
mod extended;
@@ -37,12 +55,12 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub struct Server(AppState);
impl Server {
pub fn new(query: &AsyncQuery, data_path: PathBuf, files_path: Option<PathBuf>) -> Self {
pub fn new(query: &AsyncQuery, data_path: PathBuf, website: WebsiteSource) -> Self {
Self(AppState {
client: query.client().clone(),
query: query.clone(),
data_path,
files_path,
website,
cache: Arc::new(Cache::new(5_000)),
started_at: jiff::Timestamp::now(),
started_instant: Instant::now(),
@@ -87,7 +105,7 @@ impl Server {
let vecs = state.query.inner().vecs();
let router = ApiRouter::new()
.add_api_routes()
.add_files_routes(state.files_path.as_ref())
.add_files_routes(&state.website)
.route(
"/discord",
get(Redirect::temporary("https://discord.gg/WACpShCB7M")),
+2 -2
View File
@@ -13,7 +13,7 @@ use quick_cache::sync::Cache;
use serde::Serialize;
use crate::{
CacheParams, CacheStrategy,
CacheParams, CacheStrategy, WebsiteSource,
extended::{ResponseExtended, ResultExtended},
};
@@ -22,7 +22,7 @@ pub struct AppState {
#[deref]
pub query: AsyncQuery,
pub data_path: PathBuf,
pub files_path: Option<PathBuf>,
pub website: WebsiteSource,
pub cache: Arc<Cache<String, Bytes>>,
pub client: Client,
pub started_at: Timestamp,
-1
View File
@@ -8,7 +8,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
brk_error = { workspace = true }
+1 -1
View File
@@ -19,7 +19,7 @@ Persist and query Bitcoin index data (address→outputs, txid→height, etc.) wi
```rust,ignore
let store: Store<Txid, Height> = Store::import(
&db, &path, "txid_to_height",
Version::new(1), Mode::default(), Kind::Random
Version::new(1), Mode::Any, Kind::Random
)?;
store.insert(txid, height);
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[features]
derive = ["brk_traversable_derive"]
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[lib]
proc-macro = true
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}
-1
View File
@@ -6,7 +6,6 @@ edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[dependencies]
bitcoin = { workspace = true }
+2 -2
View File
@@ -9,8 +9,8 @@ Work with Bitcoin primitives (heights, satoshis, addresses, transactions) throug
## Key Features
- **Bitcoin primitives**: `Height`, `Sats`, `Txid`, `BlockHash`, `Outpoint` with full arithmetic and conversion support
- **Address types**: All output types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR, P2PK, P2A, OP_RETURN) with address index variants
- **Time indexes**: `DateIndex`, `WeekIndex`, `MonthIndex`, `QuarterIndex`, `YearIndex`, `DecadeIndex` with cross-index conversion
- **Address types**: All output types (P2PK33, P2PK65, P2PKH, P2MS, P2SH, P2WPKH, P2WSH, P2TR, P2A, OP_RETURN) with address index variants
- **Time indexes**: `DateIndex`, `WeekIndex`, `MonthIndex`, `QuarterIndex`, `SemesterIndex`, `YearIndex`, `DecadeIndex` with cross-index conversion
- **Protocol types**: `DifficultyEpoch`, `HalvingEpoch`, `TxVersion`, `RawLocktime`
- **Financial types**: `Dollars`, `Cents`, `OHLC` (Open/High/Low/Close)
- **Serialization**: Serde + JSON Schema generation via schemars
-8
View File
@@ -1,8 +0,0 @@
fn main() {
let profile = std::env::var("PROFILE").unwrap_or_default();
if profile == "release" {
println!("cargo:rustc-flag=-C");
println!("cargo:rustc-flag=target-cpu=native");
}
}