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
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
rustflags = ["-C", "target-cpu=native"]

97
Cargo.lock generated
View File

@@ -134,15 +134,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -471,14 +462,11 @@ dependencies = [
"brk_server",
"clap",
"color-eyre",
"importmap",
"minreq",
"serde",
"tokio",
"toml",
"tracing",
"vecdb",
"zip",
]
[[package]]
@@ -748,6 +736,8 @@ dependencies = [
"brk_traversable",
"brk_types",
"derive_more",
"importmap",
"include_dir",
"jiff",
"quick_cache",
"schemars",
@@ -1249,17 +1239,6 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -1450,7 +1429,6 @@ checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
dependencies = [
"crc32fast",
"miniz_oxide",
"zlib-rs",
]
[[package]]
@@ -1954,9 +1932,9 @@ dependencies = [
[[package]]
name = "importmap"
version = "0.1.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a45c4c5e8b4d0a6aed90f3a14125bd3af3bca73d49d44e8f8b764311903b5213"
checksum = "f650142f4c83e122ae10a49dbaa88593dc4b20158124409856adbae847775938"
dependencies = [
"rapidhash",
"serde",
@@ -1964,6 +1942,25 @@ dependencies = [
"walkdir",
]
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "indenter"
version = "0.3.4"
@@ -2644,9 +2641,9 @@ dependencies = [
[[package]]
name = "quickmatch"
version = "0.1.8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da298bd4f885924ca16faab70af76893797eb0164dfa4e86a14ffd983b8cb14"
checksum = "e6f3930eacf56a69f668e37113288d810cc83a7af7afde8d7bf81d73148a5db7"
dependencies = [
"rustc-hash",
]
@@ -2745,9 +2742,9 @@ dependencies = [
[[package]]
name = "rawdb"
version = "0.5.9"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb8999fc8f80521c13091f98e1a24bde2448b29c0637f008dc470ed84d58f703"
checksum = "12f4f316acfc1844da8b3c84163aabd98f155df82d3b57ce4075dda550e1752d"
dependencies = [
"libc",
"log",
@@ -3689,9 +3686,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]]
name = "vecdb"
version = "0.5.9"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a31985bee527adda1601eab9c72de0a567be83a08e80e646b79afb77ab14970"
checksum = "28d56bd525ca319c5772dcc91b40caa6947dfb6c56b63c4a9b2268ee24254c05"
dependencies = [
"ctrlc",
"log",
@@ -3710,9 +3707,9 @@ dependencies = [
[[package]]
name = "vecdb_derive"
version = "0.5.9"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61716cbeb322dcc9838cea11b533cea278f9592b2ef2ce520595849833cfee1c"
checksum = "17c227a593e7a08c3d00babaa81aec624052d2c6a16ba127fb8f491f273c9751"
dependencies = [
"quote",
"syn",
@@ -4206,44 +4203,12 @@ dependencies = [
"syn",
]
[[package]]
name = "zip"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd8a47718a4ee5fe78e07667cd36f3de80e7c2bfe727c7074245ffc7303c037"
dependencies = [
"arbitrary",
"crc32fast",
"flate2",
"indexmap",
"memchr",
"zopfli",
]
[[package]]
name = "zlib-rs"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
[[package]]
name = "zmij"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.3"

View File

@@ -79,7 +79,7 @@ serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_ord
smallvec = "1.15.1"
tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
tracing = { version = "0.1", default-features = false, features = ["std"] }
vecdb = { version = "0.5.9", features = ["derive", "serde_json", "pco", "schemars"] }
vecdb = { version = "0.5.10", features = ["derive", "serde_json", "pco", "schemars"] }
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
[workspace.metadata.release]

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"]

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

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");
}
}

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)]

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 }

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");
}
}

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"

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");
}
}

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 }

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");
}
}

View File

@@ -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();

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

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"

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

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");
}
}

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 {

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)
}

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)
}

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)),
})
}
}

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"]

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

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");
}
}

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()),
}
}

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 }

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

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");
}
}

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 }

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

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");
}
}

View File

@@ -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

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);

View File

@@ -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

View File

@@ -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)?;

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;

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 }

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");
}
}

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 }

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");
}
}

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 }

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

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");
}
}

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 }

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");
}
}

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 }

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");
}
}

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 }

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");
}
}

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 }

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

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");
}
}

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 }

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();

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");
}
}

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 }

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");
}
}

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 }

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");
}
}

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"]

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

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"));
}
}

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 });

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)
}

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 {

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")),

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,

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 }

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);

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");
}
}

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"]

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");
}
}

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

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");
}
}

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 }

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

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");
}
}

View File

@@ -2,19 +2,7 @@
## Self-Hosting
BRK is designed to be self-hosted. Install and run with:
```bash
cargo install --locked brk_cli
brk --bitcoindir ~/.bitcoin --brkdir ~/.brk
```
Requirements:
- Bitcoin Core with accessible `blk*.dat` files
- ~400 GB disk space
- 12+ GB RAM recommended
See the [CLI documentation](https://docs.rs/brk_cli) for configuration options.
BRK is designed to be self-hosted. See the [brk_cli documentation](https://docs.rs/brk_cli) for installation, requirements, and configuration options.
## Professional Hosting

View File

@@ -35,7 +35,7 @@ BRK parses, indexes, and analyzes Bitcoin blockchain data. It combines on-chain
**Use the Public API** — Access data without running infrastructure. Client libraries available for [JavaScript](https://www.npmjs.com/package/brk-client), [Python](https://pypi.org/project/brk-client/), and [Rust](https://crates.io/crates/brk_client). See the [API reference](https://bitcoinresearchkit.org/api) for endpoints.
**Self-Host** — Run your own instance with Bitcoin Core. Install via `cargo install --locked brk_cli` or use [Docker](https://github.com/bitcoinresearchkit/brk/tree/main/docker). Requires ~400 GB disk and 12+ GB RAM. See the [hosting guide](./HOSTING.md).
**Self-Host** — Run your own instance with Bitcoin Core. Install via [`brk_cli`](https://docs.rs/brk_cli) or use [Docker](https://github.com/bitcoinresearchkit/brk/tree/main/docker). See the [hosting guide](./HOSTING.md).
**Use as a Library** — Build custom tools with the Rust crates. Use individual components or the [umbrella crate](https://docs.rs/brk). See [architecture](./ARCHITECTURE.md) for how they fit together.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
*.js

View File

@@ -1,354 +0,0 @@
// @ts-nocheck
interface Options {
/**
* Width in pixels to be applied to node before rendering.
*/
width?: number;
/**
* Height in pixels to be applied to node before rendering.
*/
height?: number;
/**
* A number between `0` and `1` indicating image quality (e.g. 0.92 => 92%) of the JPEG image.
*/
quality?: number;
/**
* A string indicating the image format. The default type is image/png; that type is also used if the given type isn't supported.
*/
type?: string;
/**
* The pixel ratio of captured image.
*
* DPI = 96 * scale
*
* default: 1
*/
scale?: number;
/**
* A string value for the background color, any valid CSS color value.
*/
backgroundColor?: string | null;
/**
* An object whose properties to be copied to node's style before rendering.
*/
style?: Partial<CSSStyleDeclaration> | null;
/**
* A function taking DOM node as argument. Should return `true` if passed
* node should be included in the output. Excluding node means excluding
* it's children as well.
*/
filter?: ((el: Node) => boolean) | null;
/**
* Maximum canvas size (pixels).
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
*/
maximumCanvasSize?: number;
/**
* Load media timeout and fetch remote asset timeout (millisecond).
*
* default: 30000
*/
timeout?: number;
/**
* Embed assets progress.
*/
progress?: ((current: number, total: number) => void) | null;
/**
* Enable debug mode to view the execution time log.
*/
debug?: boolean;
/**
* Custom implementation to get image data for a custom URL.
* This can be helpful for Capacitor or Cordova when using
* native fetch to bypass CORS issues.
*
* If returns a string, will completely bypass any `Options.fetch`
* settings with your custom implementation.
*
* If returns false, will fall back to normal fetch implementation
*
* @param url
* @returns A data URL for the image
*/
fetchFn?: ((url: string) => Promise<string | false>) | null;
/**
* The options of fetch resources.
*/
fetch?: {
/**
* The second parameter of `window.fetch` RequestInit
*
* default: {
* cache: 'force-cache',
* }
*/
requestInit?: RequestInit;
/**
* Set to `true` to append the current time as a query string to URL
* requests to enable cache busting.
*
* default: false
*/
bypassingCache?: boolean | RegExp;
/**
* A data URL for a placeholder image that will be used when fetching
* an image fails. Defaults to an empty string and will render empty
* areas for failed images.
*
* default: data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
*/
placeholderImage?: string | ((cloned: HTMLImageElement | SVGImageElement) => string | Promise<string>);
};
/**
* The options of fonts download and embed.
*/
font?: false | {
/**
* Font minify
*/
minify?: (font: ArrayBuffer, subset: string) => ArrayBuffer;
/**
* The preferred font format. If specified all other font formats are ignored.
*/
preferredFormat?: 'woff' | 'woff2' | 'truetype' | 'opentype' | 'embedded-opentype' | 'svg' | string;
/**
* A CSS string to specify for font embeds. If specified only this CSS will
* be present in the resulting image.
*/
cssText?: string;
};
/**
* All enabled features
*
* default: true
*/
features?: boolean | {
/**
* Copy scrollbar css styles
*
* default: true
*/
copyScrollbar?: boolean;
/**
* Remove abnormal attributes to cloned node (for normalize XML)
*
* default: true
*/
removeAbnormalAttributes?: boolean;
/**
* Remove control characters (for normalize XML)
*
* default: true
*/
removeControlCharacter?: boolean;
/**
* Fix svg+xml image decode (for Safari、Firefox)
*
* default: true
*/
fixSvgXmlDecode?: boolean;
/**
* Render scrolled children with scrolled content
*
* default: false
*/
restoreScrollPosition?: boolean;
};
/**
* Canvas `drawImage` interval
* is used to fix errors in decoding images in Safari、Firefox
*
* default: 100
*/
drawImageInterval?: number;
/**
* Web Worker script url
*/
workerUrl?: string | null;
/**
* Web Worker number
*/
workerNumber?: number;
/**
* Triggered after each node is cloned
*/
onCloneEachNode?: ((cloned: Node) => void | Promise<void>) | null;
/**
* Triggered after a node is cloned
*/
onCloneNode?: ((cloned: Node) => void | Promise<void>) | null;
/**
* Triggered after a node is embed
*/
onEmbedNode?: ((cloned: Node) => void | Promise<void>) | null;
/**
* Triggered after a ForeignObjectSvg is created
*/
onCreateForeignObjectSvg?: ((svg: SVGSVGElement) => void | Promise<void>) | null;
/**
* An array of style property names.
* Can be used to manually specify which style properties are
* included when cloning nodes.
* This can be useful for performance-critical scenarios.
*/
includeStyleProperties?: string[] | null;
}
interface Request {
type: 'image' | 'text';
resolve?: (response: string) => void;
reject?: (error: Error) => void;
response: Promise<string>;
}
interface InternalContext<T extends Node> {
/**
* FLAG
*/
__CONTEXT__: true;
/**
* Logger
*/
log: {
time: (label: string) => void;
timeEnd: (label: string) => void;
warn: (...args: any[]) => void;
};
/**
* Node
*/
node: T;
/**
* Owner document
*/
ownerDocument?: Document;
/**
* Owner window
*/
ownerWindow?: Window;
/**
* DPI
*
* scale === 1 ? null : 96 * scale
*/
dpi: number | null;
/**
* The `style` element under the root `svg` element
*/
svgStyleElement?: HTMLStyleElement;
/**
* The `defs` element under the root `svg` element
*/
svgDefsElement?: SVGDefsElement;
/**
* The `svgStyleElement` class styles
*
* Map<cssText, class[]>
*/
svgStyles: Map<string, string[]>;
/**
* The map of default `getComputedStyle` for all tagnames
*/
defaultComputedStyles: Map<string, Map<string, any>>;
/**
* The IFrame sandbox used to get the `defaultComputedStyles`
*/
sandbox?: HTMLIFrameElement;
/**
* Web Workers
*/
workers: Worker[];
/**
* The map of `font-family` values for all cloend elements
*/
fontFamilies: Map<string, Set<string>>;
/**
* Map<CssUrl, DataUrl>
*/
fontCssTexts: Map<string, string>;
/**
* `headers.accept` to use when `window.fetch` fetches images
*/
acceptOfImage: string;
/**
* All requests for `fetch`
*/
requests: Map<string, Request>;
/**
* Canvas multiple draw image fix svg+xml image decoding in Safari and Firefox
*/
drawImageCount: number;
/**
* Wait for all tasks embedded in
*/
tasks: Promise<void>[];
/**
* Automatically destroy context
*/
autoDestruct: boolean;
/**
* Is enable
*
* @param key
*/
isEnable: (key: string) => boolean;
/**
* [cloning phase] To get the node style set by the user
*/
currentNodeStyle?: Map<string, [string, string]>;
currentParentNodeStyle?: Map<string, [string, string]>;
/**
* [cloning phase] shadowDOM root list
*/
shadowRoots: ShadowRoot[];
}
type Context<T extends Node = Node> = InternalContext<T> & Required<Options>;
declare function domToBlob<T extends Node>(node: T, options?: Options): Promise<Blob>;
declare function domToBlob<T extends Node>(context: Context<T>): Promise<Blob>;
declare function domToCanvas<T extends Node>(node: T, options?: Options): Promise<HTMLCanvasElement>;
declare function domToCanvas<T extends Node>(context: Context<T>): Promise<HTMLCanvasElement>;
declare function domToDataUrl<T extends Node>(node: T, options?: Options): Promise<string>;
declare function domToDataUrl<T extends Node>(context: Context<T>): Promise<string>;
declare function domToForeignObjectSvg<T extends Node>(node: T, options?: Options): Promise<SVGElement>;
declare function domToForeignObjectSvg<T extends Node>(context: Context<T>): Promise<SVGElement>;
declare function domToImage<T extends Node>(node: T, options?: Options): Promise<HTMLImageElement>;
declare function domToImage<T extends Node>(context: Context<T>): Promise<HTMLImageElement>;
declare function domToJpeg<T extends Node>(node: T, options?: Options): Promise<string>;
declare function domToJpeg<T extends Node>(context: Context<T>): Promise<string>;
declare function domToPixel<T extends Node>(node: T, options?: Options): Promise<Uint8ClampedArray>;
declare function domToPixel<T extends Node>(context: Context<T>): Promise<Uint8ClampedArray>;
declare function domToPng<T extends Node>(node: T, options?: Options): Promise<string>;
declare function domToPng<T extends Node>(context: Context<T>): Promise<string>;
declare function domToSvg<T extends Node>(node: T, options?: Options): Promise<string>;
declare function domToSvg<T extends Node>(context: Context<T>): Promise<string>;
declare function domToWebp<T extends Node>(node: T, options?: Options): Promise<string>;
declare function domToWebp<T extends Node>(context: Context<T>): Promise<string>;
declare function createContext<T extends Node>(node: T, options?: Options & {
autoDestruct?: boolean;
}): Promise<Context<T>>;
declare function destroyContext(context: Context): void;
type Media = HTMLVideoElement | HTMLImageElement | SVGImageElement;
interface LoadMediaOptions {
ownerDocument?: Document;
timeout?: number;
onError?: (error: Error) => void;
onWarn?: (...args: any[]) => void;
}
declare function loadMedia<T extends Media>(media: T, options?: LoadMediaOptions): Promise<T>;
declare function loadMedia(media: string, options?: LoadMediaOptions): Promise<HTMLImageElement>;
declare function waitUntilLoad(node: Node, options?: LoadMediaOptions): Promise<void>;
export { type Context, type Options, createContext, destroyContext, domToBlob, domToCanvas, domToDataUrl, domToForeignObjectSvg, domToImage, domToJpeg, domToPixel, domToPng, domToSvg, domToWebp, loadMedia, waitUntilLoad };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
function changeJpegDpi(uint8Array, dpi) {
uint8Array[13] = 1;
uint8Array[14] = dpi >> 8;

View File

@@ -16,6 +16,7 @@ export interface SignalOptions<T> {
equals?: ((prev: T, next: T) => boolean) | false;
pureWrite?: boolean;
unobserved?: () => void;
lazy?: boolean;
}
export interface RawSignal<T> {
id?: string;
@@ -64,6 +65,7 @@ export interface Computed<T> extends RawSignal<T>, Owner {
_nextHeap: Computed<any> | undefined;
_prevHeap: Computed<any>;
_fn: (prev?: T) => T;
_inFlight: Promise<T> | AsyncIterable<T> | null;
_child: FirewallSignal<any> | null;
_notifyQueue?: (statusFlagsChanged: boolean, prevStatusFlags: number) => void;
}
@@ -74,16 +76,11 @@ export interface Root extends Owner {
}
export declare let context: Owner | null;
export declare function recompute(el: Computed<any>, create?: boolean): void;
export declare function handleAsync<T>(el: Computed<T>, result: T | Promise<T> | AsyncIterable<T>, setter?: (value: T) => void): T;
export declare function dispose(node: Computed<unknown>): void;
export declare function getNextChildId(owner: Owner): string;
export declare function computed<T>(fn: (prev?: T) => T): Computed<T>;
export declare function computed<T>(fn: (prev: T) => T, initialValue?: T, options?: SignalOptions<T>): Computed<T>;
export declare function asyncComputed<T>(asyncFn: (prev?: T, refreshing?: boolean) => T | Promise<T> | AsyncIterable<T>): Computed<T> & {
_refresh: () => void;
};
export declare function asyncComputed<T>(asyncFn: (prev: T, refreshing?: boolean) => T | Promise<T> | AsyncIterable<T>, initialValue: T, options?: SignalOptions<T>): Computed<T> & {
_refresh: () => void;
};
export declare function computed<T>(fn: (prev?: T) => T | Promise<T> | AsyncIterable<T>): Computed<T>;
export declare function computed<T>(fn: (prev: T) => T | Promise<T> | AsyncIterable<T>, initialValue?: T, options?: SignalOptions<T>): Computed<T>;
export declare function signal<T>(v: T, options?: SignalOptions<T>): Signal<T>;
export declare function signal<T>(v: T, options?: SignalOptions<T>, firewall?: Computed<any>): FirewallSignal<T>;
export declare function isEqual<T>(a: T, b: T): boolean;
@@ -121,3 +118,5 @@ export declare function runWithOwner<T>(owner: Owner | null, fn: () => T): T;
export declare function staleValues<T>(fn: () => T, set?: boolean): T;
export declare function pending<T>(fn: () => T): T;
export declare function isPending(fn: () => any): boolean;
export declare function refresh<T>(fn: () => T): T;
export declare function isRefreshing(): boolean;

View File

@@ -1,6 +1,6 @@
export { ContextNotFoundError, NoOwnerError, NotReadyError } from "./error.js";
export { createContext, getContext, setContext, type Context, type ContextRecord } from "./context.js";
export { getObserver, isEqual, untrack, getOwner, runWithOwner, createOwner, createRoot, computed, dispose, signal, asyncComputed, read, setSignal, onCleanup, getNextChildId, isPending, pending, staleValues, type Owner, type Computed, type Root, type Signal, type SignalOptions } from "./core.js";
export { getObserver, isEqual, untrack, getOwner, runWithOwner, createOwner, createRoot, computed, dispose, signal, read, setSignal, onCleanup, getNextChildId, isPending, pending, refresh, isRefreshing, staleValues, handleAsync, type Owner, type Computed, type Root, type Signal, type SignalOptions } from "./core.js";
export { effect, type Effect } from "./effect.js";
export { flush, Queue, type IQueue, type QueueCallback } from "./scheduler.js";
export * from "./constants.js";

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