mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-27 22:44:30 -07:00
Compare commits
96 Commits
v0.2.5
...
v0.3.0-beta.4
| Author | SHA1 | Date | |
|---|---|---|---|
| f5c50e69fc | |||
| 9709c2040d | |||
| 3faa989691 | |||
| 84e924b77e | |||
| c5b16e7048 | |||
| c1335cec31 | |||
| bdc3ba1df6 | |||
| 6afce0bbdc | |||
| 327873d010 | |||
| 08175009d2 | |||
| a5d3be465e | |||
| fd2b93367d | |||
| 2a93f51e81 | |||
| 008143ff00 | |||
| d340855c8b | |||
| 78d6d9d6f1 | |||
| 5cc85b0619 | |||
| 7433ce0d0e | |||
| 75a97b4da9 | |||
| c23e0f2a3c | |||
| 08ba4ad996 | |||
| 39da441d14 | |||
| 904ec93668 | |||
| 4cd8d9eb56 | |||
| 283baca848 | |||
| 765261648d | |||
| c3cef71aa3 | |||
| 18d9c166d8 | |||
| 286256ebf0 | |||
| 12aae503c9 | |||
| 95e5168244 | |||
| 5fd9fff9cf | |||
| db5b3887f9 | |||
| 5a3e1b4e6e | |||
| 21a0226a19 | |||
| c5c49f62d1 | |||
| dac66c988d | |||
| 303d168681 | |||
| 1ddb3385e2 | |||
| eb75274dbf | |||
| 3a7887348c | |||
| 0a4cb0601f | |||
| 861e29277c | |||
| c76b149ef9 | |||
| 4c4c6fc840 | |||
| 0c14dfe924 | |||
| 17e531b4ee | |||
| f022f62cce | |||
| e91f1386b1 | |||
| 02f543af38 | |||
| 20c96fb551 | |||
| acd3d6f425 | |||
| 2b15a24b6d | |||
| 7fac0bc613 | |||
| 62f51761ee | |||
| 5340cc288e | |||
| befe3c8fb7 | |||
| 41ec24c81e | |||
| 42b497ff65 | |||
| 01d908a560 | |||
| 42debcce80 | |||
| 8bc993eceb | |||
| 366ac33e23 | |||
| b5a7023bd3 | |||
| 883b38c77c | |||
| 59c767a9e2 | |||
| 9b5bb848f7 | |||
| 5bf06530ce | |||
| 768e6870cb | |||
| 79829ddd53 | |||
| 78082801c6 | |||
| 50771ddccc | |||
| 3a8a9ddecc | |||
| 6cd45c1f1f | |||
| 1a2db43cf5 | |||
| 4840e564f4 | |||
| 744dce932c | |||
| 8dfc1bc932 | |||
| d92cf43c57 | |||
| 099699872e | |||
| 5099903043 | |||
| 982fe47a33 | |||
| 65d5fadd13 | |||
| b55f5255ad | |||
| 83edef4806 | |||
| d4936d889a | |||
| c938cc8eae | |||
| 0558834eef | |||
| 098950fdde | |||
| 91e68a1d1e | |||
| 7172ddb247 | |||
| 96f2e058f7 | |||
| 8782944191 | |||
| ae26db6df2 | |||
| d038141a8a | |||
| f6960c61d6 |
@@ -39,6 +39,7 @@ flamegraph.svg
|
||||
|
||||
# AI
|
||||
.claude/settings*
|
||||
!CLAUDE.md
|
||||
|
||||
# Expand
|
||||
expand.rs
|
||||
|
||||
Generated
+189
-227
File diff suppressed because it is too large
Load Diff
+36
-33
@@ -4,7 +4,7 @@ members = ["crates/*"]
|
||||
package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node"
|
||||
package.license = "MIT"
|
||||
package.edition = "2024"
|
||||
package.version = "0.2.5"
|
||||
package.version = "0.3.0-beta.4"
|
||||
package.homepage = "https://bitcoinresearchkit.org"
|
||||
package.repository = "https://github.com/bitcoinresearchkit/brk"
|
||||
package.readme = "README.md"
|
||||
@@ -36,45 +36,47 @@ inherits = "release"
|
||||
debug = true
|
||||
|
||||
[workspace.dependencies]
|
||||
aide = { version = "0.16.0-alpha.3", features = ["axum-json", "axum-query"] }
|
||||
axum = { version = "0.8.8", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
|
||||
aide = { version = "0.16.0-alpha.4", features = ["axum-json", "axum-query"] }
|
||||
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
|
||||
bitcoin = { version = "0.32.8", features = ["serde"] }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
brk_alloc = { version = "0.2.5", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.2.5", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.2.5", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.2.5", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.2.5", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.2.5", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.2.5", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.2.5", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.2.5", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.2.5", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.2.5", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.2.5", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.2.5", path = "crates/brk_mempool" }
|
||||
brk_oracle = { version = "0.2.5", path = "crates/brk_oracle" }
|
||||
brk_query = { version = "0.2.5", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.2.5", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.2.5", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.2.5", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.2.5", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.2.5", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.2.5", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.2.5", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.2.5", path = "crates/brk_website" }
|
||||
brk_alloc = { version = "0.3.0-beta.4", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.3.0-beta.4", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.3.0-beta.4", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.3.0-beta.4", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.3.0-beta.4", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.3.0-beta.4", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.3.0-beta.4", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.3.0-beta.4", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.3.0-beta.4", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.3.0-beta.4", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.3.0-beta.4", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.3.0-beta.4", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.3.0-beta.4", path = "crates/brk_mempool" }
|
||||
brk_oracle = { version = "0.3.0-beta.4", path = "crates/brk_oracle" }
|
||||
brk_query = { version = "0.3.0-beta.4", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.3.0-beta.4", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.3.0-beta.4", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.3.0-beta.4", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.3.0-beta.4", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.3.0-beta.4", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.3.0-beta.4", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.3.0-beta.4", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.3.0-beta.4", path = "crates/brk_website" }
|
||||
byteview = "0.10.1"
|
||||
color-eyre = "0.6.5"
|
||||
corepc-client = { package = "brk-corepc-client", version = "0.11.0", features = ["client-sync"] }
|
||||
corepc-jsonrpc = { package = "brk-corepc-jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
|
||||
corepc-client = { package = "brk-corepc-client", version = "0.11.1", features = ["client-sync"] }
|
||||
# corepc-client = { package = "brk-corepc-client", path = "../corepc/client", features = ["client-sync"] }
|
||||
corepc-jsonrpc = { package = "brk-corepc-jsonrpc", version = "0.19.1", features = ["simple_http"], default-features = false }
|
||||
# corepc-jsonrpc = { package = "brk-corepc-jsonrpc", path = "../corepc/jsonrpc", features = ["simple_http"], default-features = false }
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = "3.1.2"
|
||||
indexmap = { version = "2.13.0", features = ["serde"] }
|
||||
fjall = "=3.0.4"
|
||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.23", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
owo-colors = "4.3.0"
|
||||
parking_lot = "0.12.5"
|
||||
pco = "1.0.1"
|
||||
rayon = "1.11.0"
|
||||
rayon = "1.12.0"
|
||||
rustc-hash = "2.1.2"
|
||||
schemars = { version = "1.2.1", features = ["indexmap2"] }
|
||||
serde = "1.0.228"
|
||||
@@ -82,12 +84,12 @@ serde_bytes = "0.11.19"
|
||||
serde_derive = "1.0.228"
|
||||
serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_order"] }
|
||||
smallvec = "1.15.1"
|
||||
tokio = { version = "1.50.0", features = ["rt-multi-thread"] }
|
||||
tokio = { version = "1.52.1", features = ["rt-multi-thread"] }
|
||||
tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
|
||||
tower-layer = "0.3"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
ureq = { version = "3.3.0", features = ["json"] }
|
||||
vecdb = { version = "0.9.0", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
vecdb = { version = "0.10.1", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
|
||||
[workspace.metadata.release]
|
||||
@@ -95,6 +97,7 @@ shared-version = true
|
||||
tag-name = "v{{version}}"
|
||||
pre-release-commit-message = "release: v{{version}}"
|
||||
tag-message = "release: v{{version}}"
|
||||
allow-branch = ["main", "next"]
|
||||
|
||||
[workspace.metadata.dist]
|
||||
cargo-dist-version = "0.30.2"
|
||||
|
||||
@@ -8,5 +8,5 @@ homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
libmimalloc-sys = { version = "0.1.44", features = ["extended"] }
|
||||
mimalloc = { version = "0.1.48", features = ["v3"] }
|
||||
libmimalloc-sys = { version = "0.1.47", features = ["extended"] }
|
||||
mimalloc = { version = "0.1.50" }
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
//! This module detects repeating tree structures and analyzes them
|
||||
//! using the bottom-up name deconstruction algorithm.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
};
|
||||
|
||||
use brk_types::{TreeNode, extract_json_type};
|
||||
|
||||
@@ -111,7 +114,7 @@ pub fn detect_structural_patterns(
|
||||
// Also collects node bases for each tree path
|
||||
let node_bases = analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
|
||||
|
||||
patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len()));
|
||||
patterns.sort_by_key(|p| Reverse(p.fields.len()));
|
||||
(patterns, concrete_to_pattern, type_mappings, node_bases)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ impl ClientConstants {
|
||||
|
||||
let pools = pools();
|
||||
let mut sorted_pools: Vec<_> = pools.iter().collect();
|
||||
sorted_pools.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
sorted_pools.sort_by_key(|p| p.name.to_lowercase());
|
||||
let pool_map: BTreeMap<PoolSlug, &'static str> =
|
||||
sorted_pools.iter().map(|p| (p.slug(), p.name)).collect();
|
||||
|
||||
|
||||
@@ -69,31 +69,49 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{{ signal?: AbortSignal, onUpdate?: (value: {}) => void }}}} [options]",
|
||||
return_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(output, " async {}({}) {{", method_name, params).unwrap();
|
||||
let params_with_opts = if params.is_empty() {
|
||||
"{ signal, onUpdate } = {}".to_string()
|
||||
} else {
|
||||
format!("{}, {{ signal, onUpdate }} = {{}}", params)
|
||||
};
|
||||
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
|
||||
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
let fetch_call = if endpoint.returns_json() {
|
||||
"this.getJson(path, { signal, onUpdate })"
|
||||
} else {
|
||||
"this.getText(path, { signal, onUpdate })"
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(output, " return this.getJson(`{}`);", path).unwrap();
|
||||
writeln!(output, " const path = `{}`;", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " const params = new URLSearchParams();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
let ident = sanitize_ident(¶m.name);
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" params.set('{}', String({}));",
|
||||
param.name, param.name
|
||||
param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if ({} !== undefined) params.set('{}', String({}));",
|
||||
param.name, param.name, param.name
|
||||
ident, param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -105,17 +123,13 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if (format === 'csv') {{").unwrap();
|
||||
writeln!(output, " return this.getText(path);").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " return this.getJson(path);").unwrap();
|
||||
} else {
|
||||
writeln!(output, " return this.getJson(path);").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if (format === 'csv') return this.getText(path, {{ signal, onUpdate }});").unwrap();
|
||||
}
|
||||
writeln!(output, " return {};", fetch_call).unwrap();
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
}
|
||||
@@ -127,14 +141,19 @@ fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
params.push(param.name.clone());
|
||||
params.push(sanitize_ident(¶m.name));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
params.push(param.name.clone());
|
||||
params.push(sanitize_ident(¶m.name));
|
||||
}
|
||||
params.join(", ")
|
||||
}
|
||||
|
||||
/// Strip characters invalid in JS identifiers (e.g. `[]` from `txId[]`).
|
||||
fn sanitize_ident(name: &str) -> String {
|
||||
name.replace(['[', ']'], "")
|
||||
}
|
||||
|
||||
fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
|
||||
let mut result = path.to_string();
|
||||
for param in path_params {
|
||||
|
||||
@@ -22,6 +22,20 @@ pub fn generate_base_client(output: &mut String) {
|
||||
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
|
||||
const _runIdle = (/** @type {{VoidFunction}} */ fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
|
||||
const _defaultCacheName = '__BRK_CLIENT__';
|
||||
/** @param {{*}} v */
|
||||
const _addCamelGetters = (v) => {{
|
||||
if (Array.isArray(v)) {{ v.forEach(_addCamelGetters); return v; }}
|
||||
if (v && typeof v === 'object' && v.constructor === Object) {{
|
||||
for (const k in v) {{
|
||||
if (k.includes('_')) {{
|
||||
const c = k.replace(/_([a-z])/g, (_, l) => l.toUpperCase());
|
||||
if (!(c in v)) Object.defineProperty(v, c, {{ get() {{ return this[k]; }} }});
|
||||
}}
|
||||
_addCamelGetters(v[k]);
|
||||
}}
|
||||
}}
|
||||
return v;
|
||||
}};
|
||||
|
||||
/**
|
||||
* @param {{string|boolean|undefined}} cache
|
||||
@@ -390,23 +404,28 @@ class BrkClientBase {{
|
||||
|
||||
/**
|
||||
* @param {{string}} path
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<Response>}}
|
||||
*/
|
||||
async get(path) {{
|
||||
async get(path, {{ signal }} = {{}}) {{
|
||||
const url = `${{this.baseUrl}}${{path}}`;
|
||||
const res = await fetch(url, {{ signal: AbortSignal.timeout(this.timeout) }});
|
||||
const signals = [AbortSignal.timeout(this.timeout)];
|
||||
if (signal) signals.push(signal);
|
||||
const res = await fetch(url, {{ signal: AbortSignal.any(signals) }});
|
||||
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
|
||||
return res;
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request - races cache vs network, first to resolve calls onUpdate
|
||||
* Make a GET request - races cache vs network, first to resolve calls onUpdate.
|
||||
* Shared implementation backing `getJson` and `getText`.
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available (may be called twice: cache then network)
|
||||
* @param {{(res: Response) => Promise<T>}} parse - Response body reader
|
||||
* @param {{{{ onUpdate?: (value: T) => void, signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
async getJson(path, onUpdate) {{
|
||||
async _getCached(path, parse, {{ onUpdate, signal }} = {{}}) {{
|
||||
const url = `${{this.baseUrl}}${{path}}`;
|
||||
const cache = this._cache ?? await this._cachePromise;
|
||||
|
||||
@@ -418,51 +437,61 @@ class BrkClientBase {{
|
||||
const cachePromise = cache?.match(url).then(async (res) => {{
|
||||
cachedRes = res ?? null;
|
||||
if (!res) return null;
|
||||
const json = await res.json();
|
||||
const value = await parse(res);
|
||||
if (!resolved && onUpdate) {{
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
onUpdate(value);
|
||||
}}
|
||||
return json;
|
||||
return value;
|
||||
}});
|
||||
|
||||
const networkPromise = this.get(path).then(async (res) => {{
|
||||
const networkPromise = this.get(path, {{ signal }}).then(async (res) => {{
|
||||
const cloned = res.clone();
|
||||
const json = await res.json();
|
||||
const value = await parse(res);
|
||||
// Skip update if ETag matches and cache already delivered
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) {{
|
||||
if (!resolved && onUpdate) {{
|
||||
resolved = true;
|
||||
onUpdate(json);
|
||||
onUpdate(value);
|
||||
}}
|
||||
return json;
|
||||
return value;
|
||||
}}
|
||||
resolved = true;
|
||||
if (onUpdate) {{
|
||||
onUpdate(json);
|
||||
}}
|
||||
if (onUpdate) onUpdate(value);
|
||||
if (cache) _runIdle(() => cache.put(url, cloned));
|
||||
return json;
|
||||
return value;
|
||||
}});
|
||||
|
||||
try {{
|
||||
return await networkPromise;
|
||||
}} catch (e) {{
|
||||
// Network failed - wait for cache
|
||||
const cachedJson = await cachePromise?.catch(() => null);
|
||||
if (cachedJson) return cachedJson;
|
||||
const cachedValue = await cachePromise?.catch(() => null);
|
||||
if (cachedValue != null) return cachedValue;
|
||||
throw e;
|
||||
}}
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request and return raw text (for CSV responses)
|
||||
* Make a GET request expecting a JSON response. Cached and supports `onUpdate`.
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{{{ onUpdate?: (value: T) => void, signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
getJson(path, options) {{
|
||||
return this._getCached(path, async (res) => _addCamelGetters(await res.json()), options);
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request expecting a text response (text/plain, text/csv, ...).
|
||||
* Cached and supports `onUpdate`, same as `getJson`.
|
||||
* @param {{string}} path
|
||||
* @param {{{{ onUpdate?: (value: string) => void, signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<string>}}
|
||||
*/
|
||||
async getText(path) {{
|
||||
const res = await this.get(path);
|
||||
return res.text();
|
||||
getText(path, options) {{
|
||||
return this._getCached(path, (res) => res.text(), options);
|
||||
}}
|
||||
|
||||
/**
|
||||
@@ -474,7 +503,7 @@ class BrkClientBase {{
|
||||
*/
|
||||
async _fetchSeriesData(path, onUpdate) {{
|
||||
const wrappedOnUpdate = onUpdate ? (/** @type {{SeriesData<T>}} */ raw) => onUpdate(_wrapSeriesData(raw)) : undefined;
|
||||
const raw = await this.getJson(path, wrappedOnUpdate);
|
||||
const raw = await this.getJson(path, {{ onUpdate: wrappedOnUpdate }});
|
||||
return _wrapSeriesData(raw);
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -108,15 +108,14 @@ pub fn generate_main_client(
|
||||
writeln!(output, " constructor(options) {{").unwrap();
|
||||
writeln!(output, " super(options);").unwrap();
|
||||
writeln!(output, " /** @type {{SeriesTree}} */").unwrap();
|
||||
writeln!(output, " this.series = this._buildTree('');").unwrap();
|
||||
writeln!(output, " this.series = this._buildTree();").unwrap();
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
|
||||
writeln!(output, " /**").unwrap();
|
||||
writeln!(output, " * @private").unwrap();
|
||||
writeln!(output, " * @param {{string}} basePath").unwrap();
|
||||
writeln!(output, " * @returns {{SeriesTree}}").unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
writeln!(output, " _buildTree(basePath) {{").unwrap();
|
||||
writeln!(output, " _buildTree() {{").unwrap();
|
||||
writeln!(output, " return {{").unwrap();
|
||||
let mut generated = BTreeSet::new();
|
||||
generate_tree_initializer(
|
||||
|
||||
@@ -101,7 +101,7 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_python)
|
||||
.unwrap_or_else(|| "Any".to_string()),
|
||||
.unwrap_or_else(|| "str".to_string()),
|
||||
);
|
||||
|
||||
let return_type = if endpoint.supports_csv {
|
||||
@@ -159,11 +159,17 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
// Build path
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
let fetch_method = if endpoint.returns_json() {
|
||||
"get_json"
|
||||
} else {
|
||||
"get_text"
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
if endpoint.path_params.is_empty() {
|
||||
writeln!(output, " return self.get_json('{}')", path).unwrap();
|
||||
writeln!(output, " return self.{}('{}')", fetch_method, path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " return self.get_json(f'{}')", path).unwrap();
|
||||
writeln!(output, " return self.{}(f'{}')", fetch_method, path).unwrap();
|
||||
}
|
||||
} else {
|
||||
writeln!(output, " params = []").unwrap();
|
||||
@@ -197,9 +203,9 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
if endpoint.supports_csv {
|
||||
writeln!(output, " if format == 'csv':").unwrap();
|
||||
writeln!(output, " return self.get_text(path)").unwrap();
|
||||
writeln!(output, " return self.get_json(path)").unwrap();
|
||||
writeln!(output, " return self.{}(path)", fetch_method).unwrap();
|
||||
} else {
|
||||
writeln!(output, " return self.get_json(path)").unwrap();
|
||||
writeln!(output, " return self.{}(path)", fetch_method).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_rust)
|
||||
.unwrap_or_else(|| "serde_json::Value".to_string());
|
||||
.unwrap_or_else(|| "String".to_string());
|
||||
|
||||
let return_type = if endpoint.supports_csv {
|
||||
format!("FormatResponse<{}>", base_return_type)
|
||||
@@ -132,29 +132,43 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
.unwrap();
|
||||
|
||||
let (path, index_arg) = build_path_template(endpoint);
|
||||
let fetch_method = if endpoint.returns_json() {
|
||||
"get_json"
|
||||
} else {
|
||||
"get_text"
|
||||
};
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.get_json(&format!(\"{}\"{}))",
|
||||
path, index_arg
|
||||
" self.base.{}(&format!(\"{}\"{}))",
|
||||
fetch_method, path, index_arg
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(output, " let mut query = Vec::new();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
if param.required {
|
||||
let ident = sanitize_ident(¶m.name);
|
||||
let is_array = param.param_type.ends_with("[]");
|
||||
if is_array {
|
||||
writeln!(
|
||||
output,
|
||||
" for v in {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
ident, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" query.push(format!(\"{}={{}}\", {}));",
|
||||
param.name, param.name
|
||||
param.name, ident
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
param.name, param.name
|
||||
ident, param.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -177,12 +191,13 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
writeln!(output, " }} else {{").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.get_json(&path).map(FormatResponse::Json)"
|
||||
" self.base.{}(&path).map(FormatResponse::Json)",
|
||||
fetch_method
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
} else {
|
||||
writeln!(output, " self.base.get_json(&path)").unwrap();
|
||||
writeln!(output, " self.base.{}(&path)", fetch_method).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,26 +213,35 @@ fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
let rust_type = param_type_to_rust(¶m.param_type);
|
||||
params.push(format!(", {}: {}", param.name, rust_type));
|
||||
params.push(format!(", {}: {}", sanitize_ident(¶m.name), rust_type));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
let rust_type = param_type_to_rust(¶m.param_type);
|
||||
let name = sanitize_ident(¶m.name);
|
||||
if param.required {
|
||||
params.push(format!(", {}: {}", param.name, rust_type));
|
||||
params.push(format!(", {}: {}", name, rust_type));
|
||||
} else {
|
||||
params.push(format!(", {}: Option<{}>", param.name, rust_type));
|
||||
params.push(format!(", {}: Option<{}>", name, rust_type));
|
||||
}
|
||||
}
|
||||
params.join("")
|
||||
}
|
||||
|
||||
/// Strip characters invalid in Rust identifiers (e.g. `[]` from `txId[]`).
|
||||
fn sanitize_ident(name: &str) -> String {
|
||||
name.replace(['[', ']'], "")
|
||||
}
|
||||
|
||||
/// Convert parameter type to Rust type for function signatures.
|
||||
fn param_type_to_rust(param_type: &str) -> String {
|
||||
if let Some(inner) = param_type.strip_suffix("[]") {
|
||||
return format!("&[{}]", param_type_to_rust(inner));
|
||||
}
|
||||
match param_type {
|
||||
"string" | "*" => "&str".to_string(),
|
||||
"integer" | "number" => "i64".to_string(),
|
||||
"boolean" => "bool".to_string(),
|
||||
other => other.to_string(), // Domain types like Index, SeriesName, Format
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ impl Endpoint {
|
||||
self.method == "GET" && !self.deprecated
|
||||
}
|
||||
|
||||
/// Returns true if this endpoint returns JSON (has a response_type extracted from application/json).
|
||||
pub fn returns_json(&self) -> bool {
|
||||
self.response_type.is_some()
|
||||
}
|
||||
|
||||
/// Returns the operation ID or generates one from the path.
|
||||
/// The returned string uses the raw case from the spec (typically camelCase).
|
||||
pub fn operation_name(&self) -> String {
|
||||
|
||||
@@ -74,6 +74,9 @@ pub fn escape_python_keyword(name: &str) -> String {
|
||||
"try", "while", "with", "yield",
|
||||
];
|
||||
|
||||
// Strip characters invalid in identifiers (e.g. `[]` from `txId[]`)
|
||||
let name = name.replace(['[', ']'], "");
|
||||
|
||||
// Prefix with underscore if starts with digit
|
||||
let name = if name.starts_with(|c: char| c.is_ascii_digit()) {
|
||||
format!("_{}", name)
|
||||
|
||||
@@ -13,7 +13,6 @@ brk_alloc = { workspace = true }
|
||||
brk_computer = { workspace = true }
|
||||
brk_error = { workspace = true, features = ["tokio", "vecdb"] }
|
||||
brk_indexer = { workspace = true }
|
||||
brk_iterator = { workspace = true }
|
||||
brk_logger = { workspace = true }
|
||||
brk_mempool = { workspace = true }
|
||||
brk_query = { workspace = true }
|
||||
@@ -26,7 +25,7 @@ owo-colors = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = "1.1.0"
|
||||
toml = "1.1.2"
|
||||
vecdb = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
# BRK CLI
|
||||
|
||||
Command-line interface for running a Bitcoin Research Kit instance.
|
||||
Run your own Bitcoin Research Kit instance. One binary, one command. Full sync in ~4-7h depending on hardware. ~44% disk overhead vs 250% for mempool/electrs.
|
||||
|
||||
## Demo
|
||||
|
||||
- [bitview.space](https://bitview.space) - web interface
|
||||
- [bitview.space/api](https://bitview.space/api) - API docs
|
||||
[bitview.space](https://bitview.space) is the official free hosted instance.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ use brk_alloc::Mimalloc;
|
||||
use brk_computer::Computer;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_mempool::Mempool;
|
||||
use brk_query::AsyncQuery;
|
||||
use brk_reader::Reader;
|
||||
@@ -37,8 +36,6 @@ pub fn main() -> anyhow::Result<()> {
|
||||
|
||||
let reader = Reader::new(config.blocksdir(), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&config.brkdir())?;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -52,7 +49,7 @@ pub fn main() -> anyhow::Result<()> {
|
||||
info!("Indexing {blocks_behind} blocks before starting server...");
|
||||
info!("---");
|
||||
sleep(Duration::from_secs(10));
|
||||
indexer.index(&blocks, &client, &exit)?;
|
||||
indexer.index(&reader, &client, &exit)?;
|
||||
drop(indexer);
|
||||
Mimalloc::collect();
|
||||
indexer = Indexer::forced_import(&config.brkdir())?;
|
||||
@@ -102,14 +99,14 @@ pub fn main() -> anyhow::Result<()> {
|
||||
let total_start = Instant::now();
|
||||
|
||||
let starting_indexes = if cfg!(debug_assertions) {
|
||||
indexer.checked_index(&blocks, &client, &exit)?
|
||||
indexer.checked_index(&reader, &client, &exit)?
|
||||
} else {
|
||||
indexer.index(&blocks, &client, &exit)?
|
||||
indexer.index(&reader, &client, &exit)?
|
||||
};
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, starting_indexes, &exit)?;
|
||||
|
||||
info!("Total time: {:?}", total_start.elapsed());
|
||||
info!("Waiting for new blocks...");
|
||||
|
||||
+2429
-5309
File diff suppressed because it is too large
Load Diff
@@ -14,3 +14,6 @@ brk_traversable = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["vecdb"]
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::OutputType;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use super::{SpendableType, UnspendableType};
|
||||
use super::{Filter, SpendableType, UnspendableType};
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub const OP_RETURN: &str = "op_return";
|
||||
|
||||
#[derive(Default, Clone, Debug, Traversable)]
|
||||
pub struct ByType<T> {
|
||||
#[traversable(flatten)]
|
||||
pub spendable: SpendableType<T>,
|
||||
#[traversable(flatten)]
|
||||
pub unspendable: UnspendableType<T>,
|
||||
}
|
||||
|
||||
impl<T> ByType<T> {
|
||||
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
|
||||
where
|
||||
F: FnMut(Filter, &'static str) -> Result<T, E>,
|
||||
{
|
||||
Ok(Self {
|
||||
spendable: SpendableType::try_new(&mut create)?,
|
||||
unspendable: UnspendableType {
|
||||
op_return: create(Filter::Type(OutputType::OpReturn), OP_RETURN)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self, output_type: OutputType) -> &T {
|
||||
match output_type {
|
||||
OutputType::P2PK65 => &self.spendable.p2pk65,
|
||||
@@ -44,6 +62,49 @@ impl<T> ByType<T> {
|
||||
OutputType::OpReturn => &mut self.unspendable.op_return,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.spendable
|
||||
.iter()
|
||||
.chain(std::iter::once(&self.unspendable.op_return))
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||
self.spendable
|
||||
.iter_mut()
|
||||
.chain(std::iter::once(&mut self.unspendable.op_return))
|
||||
}
|
||||
|
||||
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
let Self {
|
||||
spendable,
|
||||
unspendable,
|
||||
} = self;
|
||||
spendable
|
||||
.par_iter_mut()
|
||||
.chain([&mut unspendable.op_return].into_par_iter())
|
||||
}
|
||||
|
||||
pub fn iter_typed(&self) -> impl Iterator<Item = (OutputType, &T)> {
|
||||
self.spendable
|
||||
.iter_typed()
|
||||
.chain(std::iter::once((
|
||||
OutputType::OpReturn,
|
||||
&self.unspendable.op_return,
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn iter_typed_mut(&mut self) -> impl Iterator<Item = (OutputType, &mut T)> {
|
||||
self.spendable
|
||||
.iter_typed_mut()
|
||||
.chain(std::iter::once((
|
||||
OutputType::OpReturn,
|
||||
&mut self.unspendable.op_return,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Add for ByType<T>
|
||||
|
||||
@@ -153,6 +153,6 @@ impl<T> Loss<T> {
|
||||
.into_iter()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.map(move |(n, threshold)| (threshold, &ranges[len - 1 - n..]))
|
||||
.map(move |(n, threshold)| (threshold, &ranges[len - 2 - n..]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,6 @@ impl<T> Profit<T> {
|
||||
.into_iter()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.map(move |(n, threshold)| (threshold, &ranges[..n + 1]))
|
||||
.map(move |(n, threshold)| (threshold, &ranges[..n + 2]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,23 @@ impl<T> SpendableType<T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self, output_type: OutputType) -> &T {
|
||||
match output_type {
|
||||
OutputType::P2PK65 => &self.p2pk65,
|
||||
OutputType::P2PK33 => &self.p2pk33,
|
||||
OutputType::P2PKH => &self.p2pkh,
|
||||
OutputType::P2MS => &self.p2ms,
|
||||
OutputType::P2SH => &self.p2sh,
|
||||
OutputType::P2WPKH => &self.p2wpkh,
|
||||
OutputType::P2WSH => &self.p2wsh,
|
||||
OutputType::P2TR => &self.p2tr,
|
||||
OutputType::P2A => &self.p2a,
|
||||
OutputType::Unknown => &self.unknown,
|
||||
OutputType::Empty => &self.empty,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, output_type: OutputType) -> &mut T {
|
||||
match output_type {
|
||||
OutputType::P2PK65 => &mut self.p2pk65,
|
||||
|
||||
@@ -14,11 +14,8 @@ brk_error = { workspace = true, features = ["vecdb"] }
|
||||
brk_cohort = { workspace = true }
|
||||
brk_indexer = { workspace = true }
|
||||
brk_oracle = { workspace = true }
|
||||
brk_iterator = { workspace = true }
|
||||
brk_logger = { workspace = true }
|
||||
brk_reader = { workspace = true }
|
||||
brk_rpc = { workspace = true, features = ["corepc"] }
|
||||
brk_store = { workspace = true }
|
||||
brk_traversable = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
@@ -33,6 +30,7 @@ smallvec = { workspace = true }
|
||||
vecdb = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
brk_reader = { workspace = true }
|
||||
brk_alloc = { workspace = true }
|
||||
brk_bencher = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::{
|
||||
use brk_alloc::Mimalloc;
|
||||
use brk_computer::Computer;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use vecdb::Exit;
|
||||
@@ -31,8 +30,6 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
let exit = Exit::new();
|
||||
@@ -42,7 +39,7 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
let chain_height = client.get_last_height()?;
|
||||
let indexed_height = indexer.vecs.starting_height();
|
||||
if u32::from(chain_height).saturating_sub(u32::from(indexed_height)) > 1000 {
|
||||
indexer.checked_index(&blocks, &client, &exit)?;
|
||||
indexer.checked_index(&reader, &client, &exit)?;
|
||||
drop(indexer);
|
||||
Mimalloc::collect();
|
||||
indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
@@ -52,11 +49,11 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
loop {
|
||||
let i = Instant::now();
|
||||
let starting_indexes = indexer.checked_index(&blocks, &client, &exit)?;
|
||||
let starting_indexes = indexer.checked_index(&reader, &client, &exit)?;
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, starting_indexes, &exit)?;
|
||||
dbg!(i.elapsed());
|
||||
sleep(Duration::from_secs(10));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use brk_bencher::Bencher;
|
||||
use brk_computer::Computer;
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use tracing::{debug, info};
|
||||
@@ -28,8 +27,6 @@ pub fn main() -> Result<()> {
|
||||
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
let mut computer = Computer::forced_import(&outputs_benches_dir, &indexer)?;
|
||||
@@ -47,13 +44,13 @@ pub fn main() -> Result<()> {
|
||||
});
|
||||
|
||||
let i = Instant::now();
|
||||
let starting_indexes = indexer.index(&blocks, &client, &exit)?;
|
||||
let starting_indexes = indexer.index(&reader, &client, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
let i = Instant::now();
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, starting_indexes, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
// We want to benchmark the drop too
|
||||
|
||||
@@ -9,7 +9,6 @@ use brk_alloc::Mimalloc;
|
||||
use brk_bencher::Bencher;
|
||||
use brk_computer::Computer;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_iterator::Blocks;
|
||||
use brk_reader::Reader;
|
||||
use brk_rpc::{Auth, Client};
|
||||
use tracing::{debug, info};
|
||||
@@ -45,15 +44,13 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
|
||||
|
||||
let blocks = Blocks::new(&client, &reader);
|
||||
|
||||
let mut indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
// Pre-run indexer if too far behind, then drop and reimport to reduce memory
|
||||
let chain_height = client.get_last_height()?;
|
||||
let indexed_height = indexer.vecs.starting_height();
|
||||
if chain_height.saturating_sub(*indexed_height) > 1000 {
|
||||
indexer.index(&blocks, &client, &exit)?;
|
||||
indexer.index(&reader, &client, &exit)?;
|
||||
drop(indexer);
|
||||
Mimalloc::collect();
|
||||
indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
@@ -63,13 +60,13 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
loop {
|
||||
let i = Instant::now();
|
||||
let starting_indexes = indexer.index(&blocks, &client, &exit)?;
|
||||
let starting_indexes = indexer.index(&reader, &client, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
Mimalloc::collect();
|
||||
|
||||
let i = Instant::now();
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
computer.compute(&indexer, starting_indexes, &exit)?;
|
||||
info!("Done in {:?}", i.elapsed());
|
||||
|
||||
sleep(Duration::from_secs(60));
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
BlockCountTarget1m, BlockCountTarget1w, BlockCountTarget1y, BlockCountTarget24h,
|
||||
CachedWindowStarts, ConstantVecs, PerBlockCumulativeRolling, Windows,
|
||||
ConstantVecs, PerBlockCumulativeRolling, WindowStartVec, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
target: Windows {
|
||||
|
||||
@@ -30,7 +30,7 @@ impl Vecs {
|
||||
|
||||
self.blocks_to_retarget.height.compute_transform(
|
||||
starting_indexes.height,
|
||||
&indexes.height.identity,
|
||||
&indexes.height.epoch,
|
||||
|(h, ..)| (h, StoredU32::from(h.left_before_next_diff_adj())),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -20,10 +20,10 @@ impl Vecs {
|
||||
) -> Result<Self> {
|
||||
let v2 = Version::TWO;
|
||||
|
||||
let hashrate = LazyPerBlock::from_height_source::<DifficultyToHashF64>(
|
||||
let hashrate = LazyPerBlock::from_height_source::<DifficultyToHashF64, _>(
|
||||
"difficulty_hashrate",
|
||||
version,
|
||||
indexer.vecs.blocks.difficulty.read_only_boxed_clone(),
|
||||
indexer.vecs.blocks.difficulty.read_only_clone(),
|
||||
indexes,
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ impl Vecs {
|
||||
Ok(Self {
|
||||
value: Resolutions::forced_import(
|
||||
"difficulty",
|
||||
indexer.vecs.blocks.difficulty.read_only_boxed_clone(),
|
||||
indexer.vecs.blocks.difficulty.read_only_clone(),
|
||||
version,
|
||||
indexes,
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Vecs {
|
||||
|
||||
self.blocks_to_halving.height.compute_transform(
|
||||
starting_indexes.height,
|
||||
&indexes.height.identity,
|
||||
&indexes.height.halving,
|
||||
|(h, ..)| (h, StoredU32::from(h.left_before_next_halving())),
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -24,11 +24,11 @@ impl Vecs {
|
||||
let version = parent_version;
|
||||
|
||||
let lookback = LookbackVecs::forced_import(&db, version)?;
|
||||
let cached_starts = &lookback.cached_window_starts;
|
||||
let count = CountVecs::forced_import(&db, version, indexes, cached_starts)?;
|
||||
let interval = IntervalVecs::forced_import(&db, version, indexes, cached_starts)?;
|
||||
let size = SizeVecs::forced_import(&db, version, indexes, cached_starts)?;
|
||||
let weight = WeightVecs::forced_import(&db, version, indexes, cached_starts, &size)?;
|
||||
let cached_starts = lookback.cached_window_starts();
|
||||
let count = CountVecs::forced_import(&db, version, indexes, &cached_starts)?;
|
||||
let interval = IntervalVecs::forced_import(&db, version, indexes, &cached_starts)?;
|
||||
let size = SizeVecs::forced_import(&db, version, indexes, &cached_starts)?;
|
||||
let weight = WeightVecs::forced_import(&db, version, indexes, &cached_starts, &size)?;
|
||||
let difficulty = DifficultyVecs::forced_import(&db, version, indexer, indexes)?;
|
||||
let halving = HalvingVecs::forced_import(&db, version, indexes)?;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use vecdb::Database;
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, PerBlockRollingAverage},
|
||||
internal::{PerBlockRollingAverage, WindowStartVec, Windows},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
@@ -13,7 +13,7 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let interval = PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
|
||||
@@ -8,5 +8,5 @@ use crate::internal::PerBlockRollingAverage;
|
||||
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub PerBlockRollingAverage<Timestamp, M>,
|
||||
#[traversable(flatten)] pub PerBlockRollingAverage<Timestamp, Timestamp, M>,
|
||||
);
|
||||
|
||||
@@ -8,17 +8,15 @@ use vecdb::{
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, WindowStarts, Windows},
|
||||
internal::{WindowStartVec, WindowStarts, Windows},
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
#[traversable(skip)]
|
||||
pub cached_window_starts: CachedWindowStarts,
|
||||
pub _1h: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _24h: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 1d
|
||||
pub _24h: CachedVec<M::Stored<EagerVec<PcoVec<Height, Height>>>>, // 1d
|
||||
pub _3d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _1w: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 7d
|
||||
pub _1w: CachedVec<M::Stored<EagerVec<PcoVec<Height, Height>>>>, // 7d
|
||||
pub _8d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _9d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _12d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
@@ -26,7 +24,7 @@ pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub _2w: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 14d
|
||||
pub _21d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _26d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _1m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 30d
|
||||
pub _1m: CachedVec<M::Stored<EagerVec<PcoVec<Height, Height>>>>, // 30d
|
||||
pub _34d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _55d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _2m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 60d
|
||||
@@ -43,7 +41,7 @@ pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub _9m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 270d
|
||||
pub _350d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
|
||||
pub _12m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 360d
|
||||
pub _1y: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 365d
|
||||
pub _1y: CachedVec<M::Stored<EagerVec<PcoVec<Height, Height>>>>, // 365d
|
||||
pub _14m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 420d
|
||||
pub _2y: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 730d
|
||||
pub _26m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 780d
|
||||
@@ -106,19 +104,11 @@ impl Vecs {
|
||||
let _14y = ImportableVec::forced_import(db, "height_14y_ago", version)?;
|
||||
let _26y = ImportableVec::forced_import(db, "height_26y_ago", version)?;
|
||||
|
||||
let cached_window_starts = CachedWindowStarts(Windows {
|
||||
_24h: CachedVec::new(&_24h),
|
||||
_1w: CachedVec::new(&_1w),
|
||||
_1m: CachedVec::new(&_1m),
|
||||
_1y: CachedVec::new(&_1y),
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
cached_window_starts,
|
||||
_1h,
|
||||
_24h,
|
||||
_24h: CachedVec::wrap(_24h),
|
||||
_3d,
|
||||
_1w,
|
||||
_1w: CachedVec::wrap(_1w),
|
||||
_8d,
|
||||
_9d,
|
||||
_12d,
|
||||
@@ -126,7 +116,7 @@ impl Vecs {
|
||||
_2w,
|
||||
_21d,
|
||||
_26d,
|
||||
_1m,
|
||||
_1m: CachedVec::wrap(_1m),
|
||||
_34d,
|
||||
_55d,
|
||||
_2m,
|
||||
@@ -143,7 +133,7 @@ impl Vecs {
|
||||
_9m,
|
||||
_350d,
|
||||
_12m,
|
||||
_1y,
|
||||
_1y: CachedVec::wrap(_1y),
|
||||
_14m,
|
||||
_2y,
|
||||
_26m,
|
||||
@@ -161,8 +151,8 @@ impl Vecs {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn window_starts(&self) -> WindowStarts<'_> {
|
||||
WindowStarts {
|
||||
pub fn cached_window_starts(&self) -> Windows<&WindowStartVec> {
|
||||
Windows {
|
||||
_24h: &self._24h,
|
||||
_1w: &self._1w,
|
||||
_1m: &self._1m,
|
||||
@@ -170,11 +160,20 @@ impl Vecs {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_starts(&self) -> WindowStarts<'_> {
|
||||
WindowStarts {
|
||||
_24h: &self._24h.inner,
|
||||
_1w: &self._1w.inner,
|
||||
_1m: &self._1m.inner,
|
||||
_1y: &self._1y.inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_vec(&self, days: usize) -> &EagerVec<PcoVec<Height, Height>> {
|
||||
match days {
|
||||
1 => &self._24h,
|
||||
1 => &self._24h.inner,
|
||||
3 => &self._3d,
|
||||
7 => &self._1w,
|
||||
7 => &self._1w.inner,
|
||||
8 => &self._8d,
|
||||
9 => &self._9d,
|
||||
12 => &self._12d,
|
||||
@@ -182,7 +181,7 @@ impl Vecs {
|
||||
14 => &self._2w,
|
||||
21 => &self._21d,
|
||||
26 => &self._26d,
|
||||
30 => &self._1m,
|
||||
30 => &self._1m.inner,
|
||||
34 => &self._34d,
|
||||
55 => &self._55d,
|
||||
60 => &self._2m,
|
||||
@@ -199,7 +198,7 @@ impl Vecs {
|
||||
270 => &self._9m,
|
||||
350 => &self._350d,
|
||||
360 => &self._12m,
|
||||
365 => &self._1y,
|
||||
365 => &self._1y.inner,
|
||||
420 => &self._14m,
|
||||
730 => &self._2y,
|
||||
780 => &self._26m,
|
||||
@@ -225,9 +224,9 @@ impl Vecs {
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.compute_rolling_start_hours(indexes, starting_indexes, exit, 1, |s| &mut s._1h)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 1, |s| &mut s._24h)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 1, |s| &mut s._24h.inner)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 3, |s| &mut s._3d)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 7, |s| &mut s._1w)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 7, |s| &mut s._1w.inner)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 8, |s| &mut s._8d)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 9, |s| &mut s._9d)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 12, |s| &mut s._12d)?;
|
||||
@@ -235,7 +234,7 @@ impl Vecs {
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 14, |s| &mut s._2w)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 21, |s| &mut s._21d)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 26, |s| &mut s._26d)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 30, |s| &mut s._1m)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 30, |s| &mut s._1m.inner)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 34, |s| &mut s._34d)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 55, |s| &mut s._55d)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 60, |s| &mut s._2m)?;
|
||||
@@ -252,7 +251,7 @@ impl Vecs {
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 270, |s| &mut s._9m)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 350, |s| &mut s._350d)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 360, |s| &mut s._12m)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 365, |s| &mut s._1y)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 365, |s| &mut s._1y.inner)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 420, |s| &mut s._14m)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 730, |s| &mut s._2y)?;
|
||||
self.compute_rolling_start(indexes, starting_indexes, exit, 780, |s| &mut s._26m)?;
|
||||
|
||||
@@ -5,7 +5,7 @@ use vecdb::Database;
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, PerBlockFull, PerBlockRolling},
|
||||
internal::{PerBlockFull, PerBlockRolling, WindowStartVec, Windows},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
@@ -13,7 +13,7 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
vbytes: PerBlockFull::forced_import(
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::Vecs;
|
||||
use crate::{
|
||||
blocks::SizeVecs,
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, LazyPerBlockRolling, PercentVec, VBytesToWeight},
|
||||
internal::{LazyPerBlockRolling, PercentVec, VBytesToWeight, WindowStartVec, Windows},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
@@ -14,7 +14,7 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
size: &SizeVecs,
|
||||
) -> Result<Self> {
|
||||
let weight = LazyPerBlockRolling::from_per_block_full::<VBytesToWeight>(
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
CachedWindowStarts, LazyPerBlock, OneMinusF64, PerBlock, PerBlockCumulativeRolling,
|
||||
LazyPerBlock, OneMinusF64, PerBlock, PerBlockCumulativeRolling, WindowStartVec, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let liveliness = PerBlock::forced_import(db, "liveliness", version, indexes)?;
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ use super::{
|
||||
ValueVecs, Vecs,
|
||||
};
|
||||
|
||||
use crate::internal::CachedWindowStarts;
|
||||
use crate::internal::{WindowStartVec, Windows};
|
||||
|
||||
impl Vecs {
|
||||
pub(crate) fn forced_import(
|
||||
parent_path: &Path,
|
||||
parent_version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let db = open_db(parent_path, DB_NAME, 250_000)?;
|
||||
let version = parent_version;
|
||||
|
||||
@@ -3,7 +3,7 @@ use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{indexes, internal::AmountPerBlock};
|
||||
use crate::{indexes, internal::ValuePerBlock};
|
||||
|
||||
impl Vecs {
|
||||
pub(crate) fn forced_import(
|
||||
@@ -12,8 +12,8 @@ impl Vecs {
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
vaulted: AmountPerBlock::forced_import(db, "vaulted_supply", version, indexes)?,
|
||||
active: AmountPerBlock::forced_import(db, "active_supply", version, indexes)?,
|
||||
vaulted: ValuePerBlock::forced_import(db, "vaulted_supply", version, indexes)?,
|
||||
active: ValuePerBlock::forced_import(db, "active_supply", version, indexes)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use brk_traversable::Traversable;
|
||||
use vecdb::{Rw, StorageMode};
|
||||
|
||||
use crate::internal::AmountPerBlock;
|
||||
use crate::internal::ValuePerBlock;
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct Vecs<M: StorageMode = Rw> {
|
||||
pub vaulted: AmountPerBlock<M>,
|
||||
pub active: AmountPerBlock<M>,
|
||||
pub vaulted: ValuePerBlock<M>,
|
||||
pub active: ValuePerBlock<M>,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use vecdb::Database;
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, PerBlockCumulativeRolling},
|
||||
internal::{PerBlockCumulativeRolling, WindowStartVec, Windows},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
@@ -13,7 +13,7 @@ impl Vecs {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
destroyed: PerBlockCumulativeRolling::forced_import(
|
||||
|
||||
@@ -7,19 +7,20 @@
|
||||
//! | `receiving` | Unique addresses that received this block |
|
||||
//! | `sending` | Unique addresses that sent this block |
|
||||
//! | `reactivated` | Addresses that were empty and now have funds |
|
||||
//! | `both` | Addresses that both sent AND received same block |
|
||||
//! | `bidirectional` | Addresses that both sent AND received in same block |
|
||||
//! | `active` | Distinct addresses involved (sent ∪ received) |
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU32, Version};
|
||||
use brk_types::{Height, StoredU32, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, PerBlockRollingAverage},
|
||||
internal::{PerBlockRollingAverage, WindowStartVec, Windows},
|
||||
};
|
||||
|
||||
/// Per-block activity counts - reset each block.
|
||||
@@ -28,7 +29,7 @@ pub struct BlockActivityCounts {
|
||||
pub reactivated: u32,
|
||||
pub sending: u32,
|
||||
pub receiving: u32,
|
||||
pub both: u32,
|
||||
pub bidirectional: u32,
|
||||
}
|
||||
|
||||
impl BlockActivityCounts {
|
||||
@@ -56,7 +57,7 @@ impl AddrTypeToActivityCounts {
|
||||
total.reactivated += counts.reactivated;
|
||||
total.sending += counts.sending;
|
||||
total.receiving += counts.receiving;
|
||||
total.both += counts.both;
|
||||
total.bidirectional += counts.bidirectional;
|
||||
}
|
||||
total
|
||||
}
|
||||
@@ -65,45 +66,61 @@ impl AddrTypeToActivityCounts {
|
||||
/// Activity count vectors for a single category (e.g., one address type or "all").
|
||||
#[derive(Traversable)]
|
||||
pub struct ActivityCountVecs<M: StorageMode = Rw> {
|
||||
pub reactivated: PerBlockRollingAverage<StoredU32, M>,
|
||||
pub sending: PerBlockRollingAverage<StoredU32, M>,
|
||||
pub receiving: PerBlockRollingAverage<StoredU32, M>,
|
||||
pub both: PerBlockRollingAverage<StoredU32, M>,
|
||||
pub reactivated: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
pub sending: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
pub receiving: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
pub bidirectional: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
/// Distinct addresses involved in this block (sent ∪ received),
|
||||
/// computed at push time as `sending + receiving - bidirectional`
|
||||
/// via inclusion-exclusion. For per-type instances this is
|
||||
/// per-type. For the `all` aggregate it's the cross-type total.
|
||||
pub active: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
}
|
||||
|
||||
impl ActivityCountVecs {
|
||||
/// `prefix` is prepended to each field's disk name. Use `""` for the
|
||||
/// "all" aggregate and `"{type}_"` for per-address-type instances.
|
||||
/// Field names are suffixed with `_addrs` so the final disk series
|
||||
/// are e.g. `active_addrs`, `p2tr_bidirectional_addrs`.
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
reactivated: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{name}_reactivated"),
|
||||
&format!("{prefix}reactivated_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
sending: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{name}_sending"),
|
||||
&format!("{prefix}sending_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
receiving: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{name}_receiving"),
|
||||
&format!("{prefix}receiving_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
both: PerBlockRollingAverage::forced_import(
|
||||
bidirectional: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{name}_both"),
|
||||
&format!("{prefix}bidirectional_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
active: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{prefix}active_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
@@ -117,7 +134,8 @@ impl ActivityCountVecs {
|
||||
.len()
|
||||
.min(self.sending.block.len())
|
||||
.min(self.receiving.block.len())
|
||||
.min(self.both.block.len())
|
||||
.min(self.bidirectional.block.len())
|
||||
.min(self.active.block.len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
@@ -125,9 +143,10 @@ impl ActivityCountVecs {
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
[
|
||||
&mut self.reactivated.block as &mut dyn AnyStoredVec,
|
||||
&mut self.sending.block as &mut dyn AnyStoredVec,
|
||||
&mut self.receiving.block as &mut dyn AnyStoredVec,
|
||||
&mut self.both.block as &mut dyn AnyStoredVec,
|
||||
&mut self.sending.block,
|
||||
&mut self.receiving.block,
|
||||
&mut self.bidirectional.block,
|
||||
&mut self.active.block,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
@@ -136,7 +155,8 @@ impl ActivityCountVecs {
|
||||
self.reactivated.block.reset()?;
|
||||
self.sending.block.reset()?;
|
||||
self.receiving.block.reset()?;
|
||||
self.both.block.reset()?;
|
||||
self.bidirectional.block.reset()?;
|
||||
self.active.block.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -145,14 +165,19 @@ impl ActivityCountVecs {
|
||||
self.reactivated.block.push(counts.reactivated.into());
|
||||
self.sending.block.push(counts.sending.into());
|
||||
self.receiving.block.push(counts.receiving.into());
|
||||
self.both.block.push(counts.both.into());
|
||||
self.bidirectional
|
||||
.block
|
||||
.push(counts.bidirectional.into());
|
||||
let active = counts.sending + counts.receiving - counts.bidirectional;
|
||||
self.active.block.push(active.into());
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(&mut self, max_from: Height, exit: &Exit) -> Result<()> {
|
||||
self.reactivated.compute_rest(max_from, exit)?;
|
||||
self.sending.compute_rest(max_from, exit)?;
|
||||
self.receiving.compute_rest(max_from, exit)?;
|
||||
self.both.compute_rest(max_from, exit)?;
|
||||
self.bidirectional.compute_rest(max_from, exit)?;
|
||||
self.active.compute_rest(max_from, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -171,16 +196,15 @@ impl From<ByAddrType<ActivityCountVecs>> for AddrTypeToActivityCountVecs {
|
||||
impl AddrTypeToActivityCountVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self::from(ByAddrType::<ActivityCountVecs>::new_with_name(
|
||||
|type_name| {
|
||||
ActivityCountVecs::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{name}"),
|
||||
&format!("{type_name}_"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
@@ -205,7 +229,8 @@ impl AddrTypeToActivityCountVecs {
|
||||
vecs.push(&mut type_vecs.reactivated.block);
|
||||
vecs.push(&mut type_vecs.sending.block);
|
||||
vecs.push(&mut type_vecs.receiving.block);
|
||||
vecs.push(&mut type_vecs.both.block);
|
||||
vecs.push(&mut type_vecs.bidirectional.block);
|
||||
vecs.push(&mut type_vecs.active.block);
|
||||
}
|
||||
vecs.into_par_iter()
|
||||
}
|
||||
@@ -243,16 +268,14 @@ pub struct AddrActivityVecs<M: StorageMode = Rw> {
|
||||
impl AddrActivityVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
all: ActivityCountVecs::forced_import(db, name, version, indexes, cached_starts)?,
|
||||
all: ActivityCountVecs::forced_import(db, "", version, indexes, cached_starts)?,
|
||||
by_addr_type: AddrTypeToActivityCountVecs::forced_import(
|
||||
db,
|
||||
name,
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPointsSigned32, StoredI64, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, LazyRollingDeltasFromHeight},
|
||||
internal::{LazyRollingDeltasFromHeight, WindowStartVec, Windows, WithAddrTypes},
|
||||
};
|
||||
|
||||
use super::AddrCountsVecs;
|
||||
|
||||
type AddrDelta = LazyRollingDeltasFromHeight<StoredU64, StoredI64, BasisPointsSigned32>;
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct DeltaVecs {
|
||||
pub all: AddrDelta,
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: ByAddrType<AddrDelta>,
|
||||
}
|
||||
#[derive(Clone, Deref, DerefMut, Traversable)]
|
||||
pub struct DeltaVecs(#[traversable(flatten)] pub WithAddrTypes<AddrDelta>);
|
||||
|
||||
impl DeltaVecs {
|
||||
pub(crate) fn new(
|
||||
version: Version,
|
||||
addr_count: &AddrCountsVecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Self {
|
||||
let version = version + Version::TWO;
|
||||
@@ -45,6 +41,6 @@ impl DeltaVecs {
|
||||
)
|
||||
});
|
||||
|
||||
Self { all, by_addr_type }
|
||||
Self(WithAddrTypes { all, by_addr_type })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Exposed address count tracking — running counters of how many addresses
|
||||
//! are currently in (or have ever been in) the exposed set, per address type
|
||||
//! plus an aggregated `all`. See the parent [`super`] module for the
|
||||
//! definition of "exposed" and how it varies by address type.
|
||||
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use state::AddrTypeToExposedAddrCount;
|
||||
pub use vecs::ExposedAddrCountAllVecs;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Indexes, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::indexes;
|
||||
|
||||
/// Exposed address counts: funded (currently at-risk) and total (ever at-risk).
|
||||
#[derive(Traversable)]
|
||||
pub struct ExposedAddrCountsVecs<M: StorageMode = Rw> {
|
||||
pub funded: ExposedAddrCountAllVecs<M>,
|
||||
pub total: ExposedAddrCountAllVecs<M>,
|
||||
}
|
||||
|
||||
impl ExposedAddrCountsVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
funded: ExposedAddrCountAllVecs::forced_import(
|
||||
db,
|
||||
"exposed_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
total: ExposedAddrCountAllVecs::forced_import(
|
||||
db,
|
||||
"total_exposed_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.funded
|
||||
.min_stateful_len()
|
||||
.min(self.total.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.funded
|
||||
.par_iter_height_mut()
|
||||
.chain(self.total.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.funded.reset_height()?;
|
||||
self.total.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
|
||||
self.funded.compute_rest(starting_indexes, exit)?;
|
||||
self.total.compute_rest(starting_indexes, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::{Height, StoredU64};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::internal::PerBlock;
|
||||
|
||||
use super::vecs::ExposedAddrCountAllVecs;
|
||||
|
||||
/// Runtime counter for exposed address counts per address type.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToExposedAddrCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToExposedAddrCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ExposedAddrCountAllVecs, Height)> for AddrTypeToExposedAddrCount {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&ExposedAddrCountAllVecs, Height)) -> Self {
|
||||
if let Some(prev_height) = starting_height.decremented() {
|
||||
let read = |v: &PerBlock<StoredU64>| -> u64 {
|
||||
v.height.collect_one(prev_height).unwrap().into()
|
||||
};
|
||||
Self(ByAddrType {
|
||||
p2pk65: read(&vecs.by_addr_type.p2pk65),
|
||||
p2pk33: read(&vecs.by_addr_type.p2pk33),
|
||||
p2pkh: read(&vecs.by_addr_type.p2pkh),
|
||||
p2sh: read(&vecs.by_addr_type.p2sh),
|
||||
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
|
||||
p2wsh: read(&vecs.by_addr_type.p2wsh),
|
||||
p2tr: read(&vecs.by_addr_type.p2tr),
|
||||
p2a: read(&vecs.by_addr_type.p2a),
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
/// Exposed address count (`all` + per-type) for a single variant (funded or total).
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ExposedAddrCountAllVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
|
||||
);
|
||||
|
||||
impl ExposedAddrCountAllVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
|
||||
db, name, version, indexes,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//! Exposed address tracking (quantum / pubkey-exposure sense).
|
||||
//!
|
||||
//! An address is "exposed" once its public key is in the blockchain. Once
|
||||
//! exposed, any funds at that address are at cryptographic risk (e.g. from
|
||||
//! a quantum attacker capable of recovering the private key from the pubkey).
|
||||
//!
|
||||
//! When the pubkey gets exposed depends on the address type:
|
||||
//!
|
||||
//! - **P2PK33, P2PK65, P2TR**: the pubkey (or P2TR's tweaked output key) is
|
||||
//! directly in the locking script of the funding output. These addresses are
|
||||
//! exposed the moment they receive any funds.
|
||||
//! - **P2PKH, P2SH, P2WPKH, P2WSH**: the locking script contains a hash of
|
||||
//! the pubkey/script. The pubkey is only revealed when spending. Note that
|
||||
//! even the spending tx itself exposes the pubkey while the address still
|
||||
//! holds funds — during the mempool window between broadcast and confirmation,
|
||||
//! the pubkey is visible while the UTXO being spent is still unspent on-chain.
|
||||
//! So every spent address of these types has had at least one moment with
|
||||
//! funds at quantum risk.
|
||||
//! - **P2A**: anyone-can-spend, no pubkey at all. Excluded from both counters.
|
||||
//!
|
||||
//! Formally, with `is_funding_exposed` = `output_type.pubkey_exposed_at_funding()`:
|
||||
//! - `funded` (count): `(utxo_count > 0) AND (is_funding_exposed OR spent_txo_count >= 1)`
|
||||
//! - `total` (count): `(is_funding_exposed AND ever received) OR spent_txo_count >= 1`
|
||||
//! - `supply` (sats): sum of balances of addresses currently in the funded set
|
||||
//!
|
||||
//! For P2PK/P2TR types this means `total ≡ total_addr_count` and
|
||||
//! `funded ≡ funded_addr_count` (every address of those types is exposed by
|
||||
//! virtue of existing). For P2PKH/P2SH/P2WPKH/P2WSH it's the strict subset of
|
||||
//! addresses that have been spent from. The aggregate `all` exposed counter
|
||||
//! sums these, giving "Bitcoin addresses currently with funds at quantum risk".
|
||||
//!
|
||||
//! All metrics are tracked as running counters and require no extra fields
|
||||
//! on the address data — they're maintained via delta detection in
|
||||
//! `process_received` and `process_sent`.
|
||||
|
||||
mod count;
|
||||
mod supply;
|
||||
|
||||
pub use count::{AddrTypeToExposedAddrCount, ExposedAddrCountsVecs};
|
||||
pub use supply::{AddrTypeToExposedSupply, ExposedAddrSupplyVecs, ExposedSupplyShareVecs};
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Indexes, Sats, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{indexes, internal::RatioSatsBp16, prices};
|
||||
|
||||
/// Top-level container for all exposed address tracking: counts (funded +
|
||||
/// total), the funded supply, and share of supply.
|
||||
#[derive(Traversable)]
|
||||
pub struct ExposedAddrVecs<M: StorageMode = Rw> {
|
||||
pub count: ExposedAddrCountsVecs<M>,
|
||||
pub supply: ExposedAddrSupplyVecs<M>,
|
||||
#[traversable(wrap = "supply", rename = "share")]
|
||||
pub supply_share: ExposedSupplyShareVecs<M>,
|
||||
}
|
||||
|
||||
impl ExposedAddrVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: ExposedAddrCountsVecs::forced_import(db, version, indexes)?,
|
||||
supply: ExposedAddrSupplyVecs::forced_import(db, version, indexes)?,
|
||||
supply_share: ExposedSupplyShareVecs::forced_import(db, version, indexes)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.count
|
||||
.min_stateful_len()
|
||||
.min(self.supply.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.supply.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.count.reset_height()?;
|
||||
self.supply.reset_height()?;
|
||||
self.supply_share.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
prices: &prices::Vecs,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.count.compute_rest(starting_indexes, exit)?;
|
||||
self.supply
|
||||
.compute_rest(starting_indexes.height, prices, exit)?;
|
||||
|
||||
let max_from = starting_indexes.height;
|
||||
|
||||
self.supply_share
|
||||
.all
|
||||
.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&self.supply.all.sats.height,
|
||||
all_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
for ((_, share), ((_, exposed), (_, denom))) in self
|
||||
.supply_share
|
||||
.by_addr_type
|
||||
.iter_mut()
|
||||
.zip(self.supply.by_addr_type.iter().zip(type_supply_sats.iter()))
|
||||
{
|
||||
share.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&exposed.sats.height,
|
||||
*denom,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Exposed address supply (sats) tracking — running sum of balances held by
|
||||
//! addresses currently in the funded exposed set, per address type plus an
|
||||
//! aggregated `all`. See the parent [`super`] module for the definition of
|
||||
//! "exposed" and how it varies by address type.
|
||||
|
||||
mod share;
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use share::ExposedSupplyShareVecs;
|
||||
pub use state::AddrTypeToExposedSupply;
|
||||
pub use vecs::ExposedAddrSupplyVecs;
|
||||
@@ -0,0 +1,36 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PercentPerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
/// Share of exposed supply relative to total supply.
|
||||
///
|
||||
/// - `all`: exposed_supply / circulating_supply
|
||||
/// - Per-type: type's exposed_supply / type's total supply
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ExposedSupplyShareVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PercentPerBlock<BasisPoints16, M>>,
|
||||
);
|
||||
|
||||
impl ExposedSupplyShareVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(
|
||||
WithAddrTypes::<PercentPerBlock<BasisPoints16>>::forced_import(
|
||||
db,
|
||||
"exposed_supply_share",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::{Height, Sats};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::internal::ValuePerBlock;
|
||||
|
||||
use super::vecs::ExposedAddrSupplyVecs;
|
||||
|
||||
/// Runtime running counter for the total balance (sats) held by funded
|
||||
/// exposed addresses, per address type.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToExposedSupply(ByAddrType<Sats>);
|
||||
|
||||
impl AddrTypeToExposedSupply {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> Sats {
|
||||
self.0.values().copied().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ExposedAddrSupplyVecs, Height)> for AddrTypeToExposedSupply {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&ExposedAddrSupplyVecs, Height)) -> Self {
|
||||
if let Some(prev_height) = starting_height.decremented() {
|
||||
let read =
|
||||
|v: &ValuePerBlock| -> Sats { v.sats.height.collect_one(prev_height).unwrap() };
|
||||
Self(ByAddrType {
|
||||
p2pk65: read(&vecs.by_addr_type.p2pk65),
|
||||
p2pk33: read(&vecs.by_addr_type.p2pk33),
|
||||
p2pkh: read(&vecs.by_addr_type.p2pkh),
|
||||
p2sh: read(&vecs.by_addr_type.p2sh),
|
||||
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
|
||||
p2wsh: read(&vecs.by_addr_type.p2wsh),
|
||||
p2tr: read(&vecs.by_addr_type.p2tr),
|
||||
p2a: read(&vecs.by_addr_type.p2a),
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ValuePerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
/// Exposed address supply (sats/btc/cents/usd) — `all` + per-address-type.
|
||||
/// Tracks the total balance held by addresses currently in the funded
|
||||
/// exposed set. Sats are pushed stateful per block; cents/usd are derived
|
||||
/// post-hoc from sats × spot price.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ExposedAddrSupplyVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<ValuePerBlock<M>>,
|
||||
);
|
||||
|
||||
impl ExposedAddrSupplyVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<ValuePerBlock>::forced_import(
|
||||
db,
|
||||
"exposed_supply",
|
||||
version,
|
||||
indexes,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ mod activity;
|
||||
mod addr_count;
|
||||
mod data;
|
||||
mod delta;
|
||||
mod exposed;
|
||||
mod indexes;
|
||||
mod new_addr_count;
|
||||
mod reused;
|
||||
mod total_addr_count;
|
||||
mod type_map;
|
||||
|
||||
@@ -11,7 +13,9 @@ pub use activity::{AddrActivityVecs, AddrTypeToActivityCounts};
|
||||
pub use addr_count::{AddrCountsVecs, AddrTypeToAddrCount};
|
||||
pub use data::AddrsDataVecs;
|
||||
pub use delta::DeltaVecs;
|
||||
pub use exposed::{AddrTypeToExposedAddrCount, AddrTypeToExposedSupply, ExposedAddrVecs,};
|
||||
pub use indexes::AnyAddrIndexesVecs;
|
||||
pub use new_addr_count::NewAddrCountVecs;
|
||||
pub use reused::{AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, ReusedAddrVecs};
|
||||
pub use total_addr_count::TotalAddrCountVecs;
|
||||
pub use type_map::{AddrTypeToTypeIndexMap, AddrTypeToVec, HeightToAddrTypeToVec};
|
||||
|
||||
@@ -1,50 +1,35 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, PerBlockCumulativeRolling},
|
||||
internal::{PerBlockCumulativeRolling, WindowStartVec, Windows, WithAddrTypes},
|
||||
};
|
||||
|
||||
use super::TotalAddrCountVecs;
|
||||
|
||||
/// New address count per block (global + per-type)
|
||||
#[derive(Traversable)]
|
||||
pub struct NewAddrCountVecs<M: StorageMode = Rw> {
|
||||
pub all: PerBlockCumulativeRolling<StoredU64, StoredU64, M>,
|
||||
/// New address count per block (global + per-type).
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct NewAddrCountVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: ByAddrType<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
|
||||
}
|
||||
pub WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
|
||||
);
|
||||
|
||||
impl NewAddrCountVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let all = PerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
"new_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?;
|
||||
|
||||
let by_addr_type = ByAddrType::new_with_name(|name| {
|
||||
PerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{name}_new_addr_count"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self { all, by_addr_type })
|
||||
Ok(Self(WithAddrTypes::<
|
||||
PerBlockCumulativeRolling<StoredU64, StoredU64>,
|
||||
>::forced_import(
|
||||
db, "new_addr_count", version, indexes, cached_starts
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
@@ -53,11 +38,12 @@ impl NewAddrCountVecs {
|
||||
total_addr_count: &TotalAddrCountVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.all.compute(max_from, exit, |height_vec| {
|
||||
self.0.all.compute(max_from, exit, |height_vec| {
|
||||
Ok(height_vec.compute_change(max_from, &total_addr_count.all.height, 1, exit)?)
|
||||
})?;
|
||||
|
||||
for ((_, new), (_, total)) in self
|
||||
.0
|
||||
.by_addr_type
|
||||
.iter_mut()
|
||||
.zip(total_addr_count.by_addr_type.iter())
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Reused address count tracking — running counters of how many addresses
|
||||
//! are currently in (or have ever been in) the reused set, per address type
|
||||
//! plus an aggregated `all`. See the parent [`super`] module for the
|
||||
//! definition of "reused".
|
||||
//!
|
||||
//! Two counters are exposed:
|
||||
//! - `funded`: addresses currently funded AND with `funded_txo_count > 1`
|
||||
//! - `total`: addresses that have ever satisfied `funded_txo_count > 1` (monotonic)
|
||||
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use state::AddrTypeToReusedAddrCount;
|
||||
pub use vecs::ReusedAddrCountAllVecs;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Indexes, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::indexes;
|
||||
|
||||
/// Reused address counts: funded (currently with balance) and total (ever reused).
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrCountsVecs<M: StorageMode = Rw> {
|
||||
pub funded: ReusedAddrCountAllVecs<M>,
|
||||
pub total: ReusedAddrCountAllVecs<M>,
|
||||
}
|
||||
|
||||
impl ReusedAddrCountsVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
funded: ReusedAddrCountAllVecs::forced_import(
|
||||
db,
|
||||
"reused_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
total: ReusedAddrCountAllVecs::forced_import(
|
||||
db,
|
||||
"total_reused_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.funded
|
||||
.min_stateful_len()
|
||||
.min(self.total.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.funded
|
||||
.par_iter_height_mut()
|
||||
.chain(self.total.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.funded.reset_height()?;
|
||||
self.total.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
|
||||
self.funded.compute_rest(starting_indexes, exit)?;
|
||||
self.total.compute_rest(starting_indexes, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_types::{Height, StoredU64};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::internal::PerBlock;
|
||||
|
||||
use super::vecs::ReusedAddrCountAllVecs;
|
||||
|
||||
/// Runtime counter for reused address counts per address type.
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToReusedAddrCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToReusedAddrCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ReusedAddrCountAllVecs, Height)> for AddrTypeToReusedAddrCount {
|
||||
#[inline]
|
||||
fn from((vecs, starting_height): (&ReusedAddrCountAllVecs, Height)) -> Self {
|
||||
if let Some(prev_height) = starting_height.decremented() {
|
||||
let read = |v: &PerBlock<StoredU64>| -> u64 {
|
||||
v.height.collect_one(prev_height).unwrap().into()
|
||||
};
|
||||
Self(ByAddrType {
|
||||
p2pk65: read(&vecs.by_addr_type.p2pk65),
|
||||
p2pk33: read(&vecs.by_addr_type.p2pk33),
|
||||
p2pkh: read(&vecs.by_addr_type.p2pkh),
|
||||
p2sh: read(&vecs.by_addr_type.p2sh),
|
||||
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
|
||||
p2wsh: read(&vecs.by_addr_type.p2wsh),
|
||||
p2tr: read(&vecs.by_addr_type.p2tr),
|
||||
p2a: read(&vecs.by_addr_type.p2a),
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
/// Reused address count (`all` + per-type) for a single variant (funded or total).
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct ReusedAddrCountAllVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
|
||||
);
|
||||
|
||||
impl ReusedAddrCountAllVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
|
||||
db, name, version, indexes,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Per-block reused-address event tracking. Holds both the output-side
|
||||
//! ("an output landed on a previously-used address") and input-side
|
||||
//! ("an input spent from an address in the reused set") event counters.
|
||||
//! See [`vecs::ReusedAddrEventsVecs`] for the full description of each
|
||||
//! metric.
|
||||
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use state::AddrTypeToReusedAddrEventCount;
|
||||
pub use vecs::ReusedAddrEventsVecs;
|
||||
@@ -0,0 +1,28 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
/// Per-block running counter of reused-address events, per address type.
|
||||
/// Shared runtime container for both output-side events
|
||||
/// (`output_to_reused_addr_count`, outputs landing on addresses that
|
||||
/// had already received ≥ 1 prior output) and input-side events
|
||||
/// (`input_from_reused_addr_count`, inputs spending from addresses
|
||||
/// with lifetime `funded_txo_count > 1`). Reset at the start of each
|
||||
/// block (no disk recovery needed since per-block flow is
|
||||
/// reconstructed deterministically from `process_received` /
|
||||
/// `process_sent`).
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToReusedAddrEventCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToReusedAddrEventCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn reset(&mut self) {
|
||||
for v in self.0.values_mut() {
|
||||
*v = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, Indexes, OutputType, StoredF32, StoredU32, StoredU64, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
indexes, inputs,
|
||||
internal::{
|
||||
PerBlockCumulativeRolling, PerBlockRollingAverage, PercentCumulativeRolling,
|
||||
WindowStartVec, Windows, WithAddrTypes,
|
||||
},
|
||||
outputs,
|
||||
};
|
||||
|
||||
use super::state::AddrTypeToReusedAddrEventCount;
|
||||
|
||||
/// Per-block reused-address event metrics. Holds three families of
|
||||
/// signals: output-level (use), input-level (spend), and address-level
|
||||
/// (active in block).
|
||||
///
|
||||
/// `output_to_reused_addr_count`: every output landing on an address that had
|
||||
/// already received at least one prior output anywhere in its lifetime,
|
||||
/// i.e. an output-level reuse event. Outputs are not deduplicated per
|
||||
/// address within a block: an address receiving N outputs in one block
|
||||
/// that had `before` lifetime outputs contributes
|
||||
/// `max(0, N - max(0, 1 - before))` events. Only the very first output
|
||||
/// an address ever sees is excluded. Every subsequent output counts,
|
||||
/// matching the standard "% of outputs to previously-used addresses"
|
||||
/// reuse ratio reported by external sources. `output_to_reused_addr_share`
|
||||
/// uses `outputs::ByTypeVecs::output_count` (all 12 output types) as
|
||||
/// denominator. `spendable_output_to_reused_addr_share` uses the
|
||||
/// op_return-excluded 11-type aggregate (`spendable_output_count`).
|
||||
///
|
||||
/// `input_from_reused_addr_count`: every input spending from an address
|
||||
/// whose lifetime `funded_txo_count > 1` at the time of the spend (i.e.
|
||||
/// the address is in the same reused set tracked by
|
||||
/// `reused_addr_count`). Every input is checked independently. If a
|
||||
/// single address has multiple inputs in one block each one counts.
|
||||
/// This is a *stable-predicate* signal about the sending address, not
|
||||
/// an output-level repeat event: the first spend from a reused address
|
||||
/// counts just as much as the tenth. Denominator
|
||||
/// (`input_from_reused_addr_share`): `inputs::ByTypeVecs::input_count` (11
|
||||
/// spendable types, where `p2ms`, `unknown`, `empty` count as true
|
||||
/// negatives).
|
||||
///
|
||||
/// `active_reused_addr_count` / `active_reused_addr_share`: block-level
|
||||
/// *address* signals (single aggregate, not per-type).
|
||||
/// `active_reused_addr_count` is the count of distinct addresses
|
||||
/// involved in this block (sent ∪ received) that satisfy `is_reused()`
|
||||
/// after the block's events, populated inline in `process_received`
|
||||
/// (each receiver, post-receive) and in `process_sent` (each
|
||||
/// first-encounter sender, deduped against `received_addrs` so
|
||||
/// addresses that did both aren't double-counted).
|
||||
/// `active_reused_addr_share` is the per-block ratio
|
||||
/// `reused / active * 100` as a percentage in `[0, 100]` (or `0.0` for
|
||||
/// empty blocks). The denominator (distinct active addrs per block)
|
||||
/// lives on `ActivityCountVecs::active` (`addrs.activity.all.active`),
|
||||
/// derived from `sending + receiving - bidirectional`. Both fields
|
||||
/// use `PerBlockRollingAverage` so their lazy 24h/1w/1m/1y series are
|
||||
/// rolling *averages* of the per-block values. Sums and cumulatives of
|
||||
/// distinct-address counts would be misleading because the same
|
||||
/// address can appear in multiple blocks.
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrEventsVecs<M: StorageMode = Rw> {
|
||||
pub output_to_reused_addr_count:
|
||||
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
|
||||
pub output_to_reused_addr_share: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
|
||||
pub spendable_output_to_reused_addr_share: PercentCumulativeRolling<BasisPoints16, M>,
|
||||
pub input_from_reused_addr_count:
|
||||
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
|
||||
pub input_from_reused_addr_share: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
|
||||
pub active_reused_addr_count: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
pub active_reused_addr_share: PerBlockRollingAverage<StoredF32, StoredF32, M>,
|
||||
}
|
||||
|
||||
impl ReusedAddrEventsVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let import_count = |name: &str| {
|
||||
WithAddrTypes::<PerBlockCumulativeRolling<StoredU64, StoredU64>>::forced_import(
|
||||
db,
|
||||
name,
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
};
|
||||
let import_percent = |name: &str| -> Result<WithAddrTypes<
|
||||
PercentCumulativeRolling<BasisPoints16>,
|
||||
>> {
|
||||
Ok(WithAddrTypes {
|
||||
all: PercentCumulativeRolling::forced_import(db, name, version, indexes)?,
|
||||
by_addr_type: ByAddrType::new_with_name(|type_name| {
|
||||
PercentCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
})?,
|
||||
})
|
||||
};
|
||||
|
||||
let output_to_reused_addr_count = import_count("output_to_reused_addr_count")?;
|
||||
let output_to_reused_addr_share = import_percent("output_to_reused_addr_share")?;
|
||||
let spendable_output_to_reused_addr_share = PercentCumulativeRolling::forced_import(
|
||||
db,
|
||||
"spendable_output_to_reused_addr_share",
|
||||
version,
|
||||
indexes,
|
||||
)?;
|
||||
let input_from_reused_addr_count = import_count("input_from_reused_addr_count")?;
|
||||
let input_from_reused_addr_share = import_percent("input_from_reused_addr_share")?;
|
||||
|
||||
let active_reused_addr_count = PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
"active_reused_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?;
|
||||
let active_reused_addr_share = PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
"active_reused_addr_share",
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
output_to_reused_addr_count,
|
||||
output_to_reused_addr_share,
|
||||
spendable_output_to_reused_addr_share,
|
||||
input_from_reused_addr_count,
|
||||
input_from_reused_addr_share,
|
||||
active_reused_addr_count,
|
||||
active_reused_addr_share,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.output_to_reused_addr_count
|
||||
.min_stateful_len()
|
||||
.min(self.input_from_reused_addr_count.min_stateful_len())
|
||||
.min(self.active_reused_addr_count.block.len())
|
||||
.min(self.active_reused_addr_share.block.len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.output_to_reused_addr_count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.input_from_reused_addr_count.par_iter_height_mut())
|
||||
.chain([
|
||||
&mut self.active_reused_addr_count.block as &mut dyn AnyStoredVec,
|
||||
&mut self.active_reused_addr_share.block as &mut dyn AnyStoredVec,
|
||||
])
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.output_to_reused_addr_count.reset_height()?;
|
||||
self.input_from_reused_addr_count.reset_height()?;
|
||||
self.active_reused_addr_count.block.reset()?;
|
||||
self.active_reused_addr_share.block.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(
|
||||
&mut self,
|
||||
uses: &AddrTypeToReusedAddrEventCount,
|
||||
spends: &AddrTypeToReusedAddrEventCount,
|
||||
active_addr_count: u32,
|
||||
active_reused_addr_count: u32,
|
||||
) {
|
||||
self.output_to_reused_addr_count
|
||||
.push_height(uses.sum(), uses.values().copied());
|
||||
self.input_from_reused_addr_count
|
||||
.push_height(spends.sum(), spends.values().copied());
|
||||
self.active_reused_addr_count
|
||||
.block
|
||||
.push(StoredU32::from(active_reused_addr_count));
|
||||
// Stored as a percentage in [0, 100] to match the rest of the
|
||||
// codebase (Unit.percentage on the website expects 0..100). The
|
||||
// `active_addr_count` denominator lives on `ActivityCountVecs`
|
||||
// (`addrs.activity.all.active`), passed in here so we can
|
||||
// compute the per-block ratio inline.
|
||||
let share = if active_addr_count > 0 {
|
||||
100.0 * (active_reused_addr_count as f32 / active_addr_count as f32)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.active_reused_addr_share
|
||||
.block
|
||||
.push(StoredF32::from(share));
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
outputs_by_type: &outputs::ByTypeVecs,
|
||||
inputs_by_type: &inputs::ByTypeVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.output_to_reused_addr_count
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
self.input_from_reused_addr_count
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
self.active_reused_addr_count
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
self.active_reused_addr_share
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
|
||||
self.output_to_reused_addr_share.all.compute_count_ratio(
|
||||
&self.output_to_reused_addr_count.all,
|
||||
&outputs_by_type.output_count.all,
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
self.spendable_output_to_reused_addr_share.compute_count_ratio(
|
||||
&self.output_to_reused_addr_count.all,
|
||||
&outputs_by_type.spendable_output_count,
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
self.input_from_reused_addr_share.all.compute_count_ratio(
|
||||
&self.input_from_reused_addr_count.all,
|
||||
&inputs_by_type.input_count.all,
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
for otype in OutputType::ADDR_TYPES {
|
||||
self.output_to_reused_addr_share
|
||||
.by_addr_type
|
||||
.get_mut_unwrap(otype)
|
||||
.compute_count_ratio(
|
||||
self.output_to_reused_addr_count.by_addr_type.get_unwrap(otype),
|
||||
outputs_by_type.output_count.by_type.get(otype),
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
self.input_from_reused_addr_share
|
||||
.by_addr_type
|
||||
.get_mut_unwrap(otype)
|
||||
.compute_count_ratio(
|
||||
self.input_from_reused_addr_count.by_addr_type.get_unwrap(otype),
|
||||
inputs_by_type.input_count.by_type.get(otype),
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Reused address tracking.
|
||||
//!
|
||||
//! An address is "reused" if its lifetime `funded_txo_count > 1`, i.e.
|
||||
//! it has received more than one output across its lifetime. This is
|
||||
//! the simplest output-multiplicity proxy for address linkability.
|
||||
//!
|
||||
//! Two facets are tracked here:
|
||||
//! - [`count`]: how many distinct addresses are currently reused
|
||||
//! (funded) and how many have *ever* been reused (total). Per address
|
||||
//! type plus an aggregated `all`.
|
||||
//! - [`events`]: per-block address-reuse event counts on both sides.
|
||||
//! Output-side (`output_to_reused_addr_count`, outputs landing on
|
||||
//! addresses that had already received ≥ 1 prior output) and
|
||||
//! input-side (`input_from_reused_addr_count`, inputs spending from
|
||||
//! addresses with lifetime `funded_txo_count > 1`). Each count is
|
||||
//! paired with a percent over the matching block-level output/input
|
||||
//! total.
|
||||
|
||||
mod count;
|
||||
mod events;
|
||||
|
||||
pub use count::{AddrTypeToReusedAddrCount, ReusedAddrCountsVecs};
|
||||
pub use events::{AddrTypeToReusedAddrEventCount, ReusedAddrEventsVecs};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Indexes, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes, inputs,
|
||||
internal::{WindowStartVec, Windows},
|
||||
outputs,
|
||||
};
|
||||
|
||||
/// Top-level container for all reused address tracking: counts (funded +
|
||||
/// total) plus per-block reuse events (output-side + input-side).
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrVecs<M: StorageMode = Rw> {
|
||||
pub count: ReusedAddrCountsVecs<M>,
|
||||
pub events: ReusedAddrEventsVecs<M>,
|
||||
}
|
||||
|
||||
impl ReusedAddrVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: ReusedAddrCountsVecs::forced_import(db, version, indexes)?,
|
||||
events: ReusedAddrEventsVecs::forced_import(db, version, indexes, cached_starts)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.count
|
||||
.min_stateful_len()
|
||||
.min(self.events.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.events.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.count.reset_height()?;
|
||||
self.events.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
outputs_by_type: &outputs::ByTypeVecs,
|
||||
inputs_by_type: &inputs::ByTypeVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.count.compute_rest(starting_indexes, exit)?;
|
||||
self.events.compute_rest(
|
||||
starting_indexes,
|
||||
outputs_by_type,
|
||||
inputs_by_type,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::{indexes, internal::PerBlock};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{PerBlock, WithAddrTypes},
|
||||
};
|
||||
|
||||
use super::AddrCountsVecs;
|
||||
|
||||
/// Total address count (global + per-type) with all derived indexes
|
||||
#[derive(Traversable)]
|
||||
pub struct TotalAddrCountVecs<M: StorageMode = Rw> {
|
||||
pub all: PerBlock<StoredU64, M>,
|
||||
#[traversable(flatten)]
|
||||
pub by_addr_type: ByAddrType<PerBlock<StoredU64, M>>,
|
||||
}
|
||||
/// Total address count (global + per-type) with all derived indexes.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct TotalAddrCountVecs<M: StorageMode = Rw>(
|
||||
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
|
||||
);
|
||||
|
||||
impl TotalAddrCountVecs {
|
||||
pub(crate) fn forced_import(
|
||||
@@ -22,13 +23,12 @@ impl TotalAddrCountVecs {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let all = PerBlock::forced_import(db, "total_addr_count", version, indexes)?;
|
||||
|
||||
let by_addr_type: ByAddrType<PerBlock<StoredU64>> = ByAddrType::new_with_name(|name| {
|
||||
PerBlock::forced_import(db, &format!("{name}_total_addr_count"), version, indexes)
|
||||
})?;
|
||||
|
||||
Ok(Self { all, by_addr_type })
|
||||
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
|
||||
db,
|
||||
"total_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
)?))
|
||||
}
|
||||
|
||||
/// Eagerly compute total = addr_count + empty_addr_count.
|
||||
@@ -39,14 +39,14 @@ impl TotalAddrCountVecs {
|
||||
empty_addr_count: &AddrCountsVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.all.height.compute_add(
|
||||
self.0.all.height.compute_add(
|
||||
max_from,
|
||||
&addr_count.all.height,
|
||||
&empty_addr_count.all.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
for ((_, total), ((_, addr), (_, empty))) in self.by_addr_type.iter_mut().zip(
|
||||
for ((_, total), ((_, addr), (_, empty))) in self.0.by_addr_type.iter_mut().zip(
|
||||
addr_count
|
||||
.by_addr_type
|
||||
.iter()
|
||||
|
||||
@@ -67,14 +67,14 @@ pub(crate) fn process_funded_addrs(
|
||||
|
||||
// Pure pushes - no holes remain
|
||||
addrs_data.funded.reserve_pushed(pushes_iter.len());
|
||||
let mut next_index = addrs_data.funded.len();
|
||||
for (addr_type, type_index, data) in pushes_iter {
|
||||
for (next_index, (addr_type, type_index, data)) in
|
||||
(addrs_data.funded.len()..).zip(pushes_iter)
|
||||
{
|
||||
addrs_data.funded.push(data);
|
||||
result.get_mut(addr_type).unwrap().insert(
|
||||
type_index,
|
||||
AnyAddrIndex::from(FundedAddrIndex::from(next_index)),
|
||||
);
|
||||
next_index += 1;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
@@ -138,14 +138,14 @@ pub(crate) fn process_empty_addrs(
|
||||
|
||||
// Pure pushes - no holes remain
|
||||
addrs_data.empty.reserve_pushed(pushes_iter.len());
|
||||
let mut next_index = addrs_data.empty.len();
|
||||
for (addr_type, type_index, data) in pushes_iter {
|
||||
for (next_index, (addr_type, type_index, data)) in
|
||||
(addrs_data.empty.len()..).zip(pushes_iter)
|
||||
{
|
||||
addrs_data.empty.push(data);
|
||||
result.get_mut(addr_type).unwrap().insert(
|
||||
type_index,
|
||||
AnyAddrIndex::from(EmptyAddrIndex::from(next_index)),
|
||||
);
|
||||
next_index += 1;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -3,7 +3,10 @@ use brk_types::{Cents, Sats, TypeIndex};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::distribution::{
|
||||
addr::{AddrTypeToActivityCounts, AddrTypeToVec},
|
||||
addr::{
|
||||
AddrTypeToActivityCounts, AddrTypeToExposedAddrCount, AddrTypeToExposedSupply,
|
||||
AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, AddrTypeToVec,
|
||||
},
|
||||
cohorts::AddrCohorts,
|
||||
};
|
||||
|
||||
@@ -25,6 +28,13 @@ pub(crate) fn process_received(
|
||||
addr_count: &mut ByAddrType<u64>,
|
||||
empty_addr_count: &mut ByAddrType<u64>,
|
||||
activity_counts: &mut AddrTypeToActivityCounts,
|
||||
reused_addr_count: &mut AddrTypeToReusedAddrCount,
|
||||
total_reused_addr_count: &mut AddrTypeToReusedAddrCount,
|
||||
output_to_reused_addr_count: &mut AddrTypeToReusedAddrEventCount,
|
||||
active_reused_addr_count: &mut AddrTypeToReusedAddrEventCount,
|
||||
exposed_addr_count: &mut AddrTypeToExposedAddrCount,
|
||||
total_exposed_addr_count: &mut AddrTypeToExposedAddrCount,
|
||||
exposed_supply: &mut AddrTypeToExposedSupply,
|
||||
) {
|
||||
let max_type_len = received_data
|
||||
.iter()
|
||||
@@ -43,6 +53,13 @@ pub(crate) fn process_received(
|
||||
let type_addr_count = addr_count.get_mut(output_type).unwrap();
|
||||
let type_empty_count = empty_addr_count.get_mut(output_type).unwrap();
|
||||
let type_activity = activity_counts.get_mut_unwrap(output_type);
|
||||
let type_reused_count = reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_total_reused_count = total_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_output_to_reused_count = output_to_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_active_reused_count = active_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_exposed_count = exposed_addr_count.get_mut(output_type).unwrap();
|
||||
let type_total_exposed_count = total_exposed_addr_count.get_mut(output_type).unwrap();
|
||||
let type_exposed_supply = exposed_supply.get_mut(output_type).unwrap();
|
||||
|
||||
// Aggregate receives by address - each address processed exactly once
|
||||
for (type_index, value) in vec {
|
||||
@@ -57,6 +74,13 @@ pub(crate) fn process_received(
|
||||
// Track receiving activity - each address in receive aggregation
|
||||
type_activity.receiving += 1;
|
||||
|
||||
// Capture state BEFORE the receive mutates funded_txo_count
|
||||
let was_funded = addr_data.is_funded();
|
||||
let was_reused = addr_data.is_reused();
|
||||
let funded_txo_count_before = addr_data.funded_txo_count;
|
||||
let was_pubkey_exposed = addr_data.is_pubkey_exposed(output_type);
|
||||
let exposed_contribution_before = addr_data.exposed_supply_contribution(output_type);
|
||||
|
||||
match status {
|
||||
TrackingStatus::New => {
|
||||
*type_addr_count += 1;
|
||||
@@ -134,6 +158,62 @@ pub(crate) fn process_received(
|
||||
.receive_outputs(addr_data, recv.total_value, price, recv.output_count);
|
||||
}
|
||||
}
|
||||
|
||||
// Update reused counts based on the post-receive state
|
||||
let is_now_reused = addr_data.is_reused();
|
||||
if is_now_reused && !was_reused {
|
||||
// Newly crossed the reuse threshold this block
|
||||
*type_reused_count += 1;
|
||||
*type_total_reused_count += 1;
|
||||
} else if is_now_reused && !was_funded {
|
||||
// Already-reused address reactivating into the funded set
|
||||
*type_reused_count += 1;
|
||||
}
|
||||
|
||||
// Block-level "active reused address" count: each address
|
||||
// is processed exactly once here (via aggregation), so we
|
||||
// count it once iff it is reused after the block's receives.
|
||||
// The sender-side counterpart in process_sent dedupes
|
||||
// against `received_addrs` so addresses that did both
|
||||
// aren't double-counted.
|
||||
if is_now_reused {
|
||||
*type_active_reused_count += 1;
|
||||
}
|
||||
|
||||
// Per-block reused-use count: every individual output to this
|
||||
// address counts iff, at the moment the output arrives, the
|
||||
// address had already received at least one prior output
|
||||
// (i.e. it is an output-level "address reuse event"). With
|
||||
// aggregation, that means we skip the very first output the
|
||||
// address ever sees and count every subsequent one, so
|
||||
// `skipped` is `max(0, 1 - before)`.
|
||||
let skipped = 1u32.saturating_sub(funded_txo_count_before);
|
||||
let counted = recv.output_count.saturating_sub(skipped);
|
||||
*type_output_to_reused_count += u64::from(counted);
|
||||
|
||||
// Update exposed counts. The address's pubkey-exposure state
|
||||
// is unchanged by a receive (spent_txo_count unchanged), so we
|
||||
// can use the captured `was_pubkey_exposed` for both pre and post.
|
||||
// After the receive the address is always funded, so it's in the
|
||||
// funded exposed set iff its pubkey is exposed.
|
||||
//
|
||||
// Funded exposed enters when the address wasn't funded before but
|
||||
// is now AND its pubkey is exposed.
|
||||
// Total exposed (pk_exposed_at_funding types only) increments on
|
||||
// first-ever receive (status == TrackingStatus::New); for other
|
||||
// types it's incremented in process_sent on the first spend.
|
||||
if !was_funded && was_pubkey_exposed {
|
||||
*type_exposed_count += 1;
|
||||
}
|
||||
if output_type.pubkey_exposed_at_funding() && matches!(status, TrackingStatus::New) {
|
||||
*type_total_exposed_count += 1;
|
||||
}
|
||||
|
||||
// Update exposed supply via post-receive contribution delta.
|
||||
let exposed_contribution_after = addr_data.exposed_supply_contribution(output_type);
|
||||
// Receives can only add to balance and membership, so the delta
|
||||
// is always non-negative.
|
||||
*type_exposed_supply += exposed_contribution_after - exposed_contribution_before;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ use rustc_hash::FxHashSet;
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::distribution::{
|
||||
addr::{AddrTypeToActivityCounts, HeightToAddrTypeToVec},
|
||||
addr::{
|
||||
AddrTypeToActivityCounts, AddrTypeToExposedAddrCount, AddrTypeToExposedSupply,
|
||||
AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, HeightToAddrTypeToVec,
|
||||
},
|
||||
cohorts::AddrCohorts,
|
||||
compute::PriceRangeMax,
|
||||
};
|
||||
@@ -35,6 +38,12 @@ pub(crate) fn process_sent(
|
||||
addr_count: &mut ByAddrType<u64>,
|
||||
empty_addr_count: &mut ByAddrType<u64>,
|
||||
activity_counts: &mut AddrTypeToActivityCounts,
|
||||
reused_addr_count: &mut AddrTypeToReusedAddrCount,
|
||||
input_from_reused_addr_count: &mut AddrTypeToReusedAddrEventCount,
|
||||
active_reused_addr_count: &mut AddrTypeToReusedAddrEventCount,
|
||||
exposed_addr_count: &mut AddrTypeToExposedAddrCount,
|
||||
total_exposed_addr_count: &mut AddrTypeToExposedAddrCount,
|
||||
exposed_supply: &mut AddrTypeToExposedSupply,
|
||||
received_addrs: &ByAddrType<FxHashSet<TypeIndex>>,
|
||||
height_to_price: &[Cents],
|
||||
height_to_timestamp: &[Timestamp],
|
||||
@@ -57,12 +66,28 @@ pub(crate) fn process_sent(
|
||||
let type_addr_count = addr_count.get_mut(output_type).unwrap();
|
||||
let type_empty_count = empty_addr_count.get_mut(output_type).unwrap();
|
||||
let type_activity = activity_counts.get_mut_unwrap(output_type);
|
||||
let type_reused_count = reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_input_from_reused_count =
|
||||
input_from_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_active_reused_count = active_reused_addr_count.get_mut(output_type).unwrap();
|
||||
let type_exposed_count = exposed_addr_count.get_mut(output_type).unwrap();
|
||||
let type_total_exposed_count = total_exposed_addr_count.get_mut(output_type).unwrap();
|
||||
let type_exposed_supply = exposed_supply.get_mut(output_type).unwrap();
|
||||
let type_received = received_addrs.get(output_type);
|
||||
let type_seen = seen_senders.get_mut_unwrap(output_type);
|
||||
|
||||
for (type_index, value) in vec {
|
||||
let addr_data = lookup.get_for_send(output_type, type_index);
|
||||
|
||||
// "Input from a reused address" event: the sending
|
||||
// address is in the reused set (lifetime
|
||||
// funded_txo_count > 1). Checked once per input. The
|
||||
// spend itself doesn't touch funded_txo_count so the
|
||||
// predicate is stable before/after `cohort_state.send`.
|
||||
if addr_data.is_reused() {
|
||||
*type_input_from_reused_count += 1;
|
||||
}
|
||||
|
||||
let prev_balance = addr_data.balance();
|
||||
let new_balance = prev_balance.checked_sub(value).unwrap();
|
||||
|
||||
@@ -70,14 +95,29 @@ pub(crate) fn process_sent(
|
||||
if type_seen.insert(type_index) {
|
||||
type_activity.sending += 1;
|
||||
|
||||
// Track "both" - addresses that sent AND received this block
|
||||
if type_received.is_some_and(|s| s.contains(&type_index)) {
|
||||
type_activity.both += 1;
|
||||
let also_received = type_received.is_some_and(|s| s.contains(&type_index));
|
||||
// Track "bidirectional": addresses that sent AND
|
||||
// received this block.
|
||||
if also_received {
|
||||
type_activity.bidirectional += 1;
|
||||
}
|
||||
|
||||
// Block-level "active reused address" count: count
|
||||
// every distinct sender that's reused, but skip
|
||||
// those that also received this block (already
|
||||
// counted in process_received).
|
||||
if !also_received && addr_data.is_reused() {
|
||||
*type_active_reused_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let will_be_empty = addr_data.has_1_utxos();
|
||||
|
||||
// Capture exposed state BEFORE the spend mutates spent_txo_count.
|
||||
let was_pubkey_exposed = addr_data.is_pubkey_exposed(output_type);
|
||||
let exposed_contribution_before =
|
||||
addr_data.exposed_supply_contribution(output_type);
|
||||
|
||||
// Compute buckets once
|
||||
let prev_bucket = AmountBucket::from(prev_balance);
|
||||
let new_bucket = AmountBucket::from(new_balance);
|
||||
@@ -91,6 +131,28 @@ pub(crate) fn process_sent(
|
||||
.unwrap();
|
||||
|
||||
cohort_state.send(addr_data, value, current_price, prev_price, peak_price, age)?;
|
||||
// addr_data.spent_txo_count is now incremented by 1.
|
||||
|
||||
// Update exposed supply via post-spend contribution delta.
|
||||
let exposed_contribution_after = addr_data.exposed_supply_contribution(output_type);
|
||||
if exposed_contribution_after >= exposed_contribution_before {
|
||||
*type_exposed_supply +=
|
||||
exposed_contribution_after - exposed_contribution_before;
|
||||
} else {
|
||||
*type_exposed_supply -=
|
||||
exposed_contribution_before - exposed_contribution_after;
|
||||
}
|
||||
|
||||
// Update exposed counts on first-ever pubkey exposure.
|
||||
// For non-pk-exposed types this fires on the first spend; for
|
||||
// pk-exposed types it never fires here (was_pubkey_exposed was
|
||||
// already true at first receive in process_received).
|
||||
if !was_pubkey_exposed {
|
||||
*type_total_exposed_count += 1;
|
||||
if !will_be_empty {
|
||||
*type_exposed_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If crossing a bucket boundary, remove the (now-updated) address from old bucket
|
||||
if will_be_empty || crossing_boundary {
|
||||
@@ -101,6 +163,17 @@ pub(crate) fn process_sent(
|
||||
if will_be_empty {
|
||||
*type_addr_count -= 1;
|
||||
*type_empty_count += 1;
|
||||
// Reused addr leaving the funded reused set
|
||||
if addr_data.is_reused() {
|
||||
*type_reused_count -= 1;
|
||||
}
|
||||
// Exposed addr leaving the funded exposed set: was in set
|
||||
// iff its pubkey was exposed pre-spend (since it was funded
|
||||
// to be in process_sent in the first place), and now leaves
|
||||
// because it's empty.
|
||||
if was_pubkey_exposed {
|
||||
*type_exposed_count -= 1;
|
||||
}
|
||||
lookup.move_to_empty(output_type, type_index);
|
||||
} else if crossing_boundary {
|
||||
cohorts
|
||||
|
||||
@@ -3,12 +3,17 @@ use std::path::Path;
|
||||
use brk_cohort::{AddrGroups, AmountRange, Filter, Filtered, OverAmount, UnderAmount};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Indexes, StoredU64, Version};
|
||||
use brk_types::{Height, Indexes, Sats, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{distribution::DynCohortVecs, indexes, internal::CachedWindowStarts, prices};
|
||||
use crate::{
|
||||
distribution::DynCohortVecs,
|
||||
indexes,
|
||||
internal::{WindowStartVec, Windows},
|
||||
prices,
|
||||
};
|
||||
|
||||
use super::{super::traits::CohortVecs, vecs::AddrCohortVecs};
|
||||
|
||||
@@ -25,7 +30,7 @@ impl AddrCohorts {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
states_path: &Path,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
|
||||
@@ -104,12 +109,19 @@ impl AddrCohorts {
|
||||
&mut self,
|
||||
prices: &prices::Vecs,
|
||||
starting_indexes: &Indexes,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.0
|
||||
.par_iter_mut()
|
||||
.try_for_each(|v| v.compute_rest_part2(prices, starting_indexes, all_utxo_count, exit))
|
||||
self.0.par_iter_mut().try_for_each(|v| {
|
||||
v.compute_rest_part2(
|
||||
prices,
|
||||
starting_indexes,
|
||||
all_supply_sats,
|
||||
all_utxo_count,
|
||||
exit,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
|
||||
@@ -3,14 +3,14 @@ use std::path::Path;
|
||||
use brk_cohort::{CohortContext, Filter, Filtered};
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPointsSigned32, Cents, Height, Indexes, StoredI64, StoredU64, Version};
|
||||
use brk_types::{BasisPointsSigned32, Cents, Height, Indexes, Sats, StoredI64, StoredU64, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
distribution::state::{AddrCohortState, MinimalRealizedState},
|
||||
indexes,
|
||||
internal::{CachedWindowStarts, PerBlockWithDeltas},
|
||||
internal::{PerBlockWithDeltas, WindowStartVec, Windows},
|
||||
prices,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ impl AddrCohortVecs {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
states_path: Option<&Path>,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let full_name = CohortContext::Addr.full_name(&filter, name);
|
||||
|
||||
@@ -230,10 +230,16 @@ impl CohortVecs for AddrCohortVecs {
|
||||
&mut self,
|
||||
prices: &prices::Vecs,
|
||||
starting_indexes: &Indexes,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, all_utxo_count, exit)
|
||||
self.metrics.compute_rest_part2(
|
||||
prices,
|
||||
starting_indexes,
|
||||
all_supply_sats,
|
||||
all_utxo_count,
|
||||
exit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{Cents, Height, Indexes, StoredU64, Version};
|
||||
use brk_types::{Cents, Height, Indexes, Sats, StoredU64, Version};
|
||||
use vecdb::{Exit, ReadableVec};
|
||||
|
||||
use crate::prices;
|
||||
@@ -62,6 +62,7 @@ pub trait CohortVecs: DynCohortVecs {
|
||||
&mut self,
|
||||
prices: &prices::Vecs,
|
||||
starting_indexes: &Indexes,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
|
||||
exit: &Exit,
|
||||
) -> Result<()>;
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
state::UTXOCohortState,
|
||||
},
|
||||
indexes,
|
||||
internal::{AmountPerBlockCumulativeRolling, CachedWindowStarts},
|
||||
internal::{ValuePerBlockCumulativeRolling, WindowStartVec, Windows},
|
||||
prices,
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
|
||||
#[traversable(rename = "type")]
|
||||
pub type_: SpendableType<UTXOCohortVecs<TypeCohortMetrics<M>>>,
|
||||
pub profitability: ProfitabilityMetrics<M>,
|
||||
pub matured: AgeRange<AmountPerBlockCumulativeRolling<M>>,
|
||||
pub matured: AgeRange<ValuePerBlockCumulativeRolling<M>>,
|
||||
#[traversable(skip)]
|
||||
pub(super) caches: UTXOCohortsTransientState,
|
||||
}
|
||||
@@ -75,7 +75,7 @@ impl UTXOCohorts<Rw> {
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
states_path: &Path,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let v = version + VERSION;
|
||||
|
||||
@@ -264,8 +264,8 @@ impl UTXOCohorts<Rw> {
|
||||
let prefix = CohortContext::Utxo.prefix();
|
||||
let matured = AgeRange::try_new(&|_f: Filter,
|
||||
name: &'static str|
|
||||
-> Result<AmountPerBlockCumulativeRolling> {
|
||||
AmountPerBlockCumulativeRolling::forced_import(
|
||||
-> Result<ValuePerBlockCumulativeRolling> {
|
||||
ValuePerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{prefix}_{name}_matured_supply"),
|
||||
v,
|
||||
@@ -542,17 +542,14 @@ impl UTXOCohorts<Rw> {
|
||||
}
|
||||
|
||||
/// Second phase of post-processing: compute relative metrics.
|
||||
pub(crate) fn compute_rest_part2<HM>(
|
||||
pub(crate) fn compute_rest_part2(
|
||||
&mut self,
|
||||
blocks: &blocks::Vecs,
|
||||
prices: &prices::Vecs,
|
||||
starting_indexes: &Indexes,
|
||||
height_to_market_cap: &HM,
|
||||
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
HM: ReadableVec<Height, Dollars> + Sync,
|
||||
{
|
||||
) -> Result<()> {
|
||||
// Get under_1h value sources for adjusted computation (cloned to avoid borrow conflicts).
|
||||
let under_1h_value_created = self
|
||||
.age_range
|
||||
@@ -663,7 +660,7 @@ impl UTXOCohorts<Rw> {
|
||||
Box::new(|| {
|
||||
over_amount.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, au, exit)
|
||||
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
@@ -681,19 +678,19 @@ impl UTXOCohorts<Rw> {
|
||||
Box::new(|| {
|
||||
amount_range.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, au, exit)
|
||||
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
under_amount.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, au, exit)
|
||||
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
|
||||
})
|
||||
}),
|
||||
Box::new(|| {
|
||||
type_.par_iter_mut().try_for_each(|v| {
|
||||
v.metrics
|
||||
.compute_rest_part2(prices, starting_indexes, au, exit)
|
||||
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
|
||||
})
|
||||
}),
|
||||
];
|
||||
@@ -830,26 +827,26 @@ impl UTXOCohorts<Rw> {
|
||||
let mut sth_acc = RealizedFullAccum::default();
|
||||
let mut lth_acc = RealizedFullAccum::default();
|
||||
|
||||
let mut all_icap = (0u128, 0u128);
|
||||
let mut sth_icap = (0u128, 0u128);
|
||||
let mut lth_icap = (0u128, 0u128);
|
||||
let mut all_ccap = (0u128, 0u128);
|
||||
let mut sth_ccap = (0u128, 0u128);
|
||||
let mut lth_ccap = (0u128, 0u128);
|
||||
|
||||
for ar in age_range.iter_mut() {
|
||||
if let Some(state) = ar.state.as_mut() {
|
||||
all_acc.add(&state.realized);
|
||||
|
||||
let u = state.compute_unrealized_state(height_price);
|
||||
all_icap.0 += u.investor_cap_in_profit_raw;
|
||||
all_icap.1 += u.investor_cap_in_loss_raw;
|
||||
all_ccap.0 += u.capitalized_cap_in_profit_raw;
|
||||
all_ccap.1 += u.capitalized_cap_in_loss_raw;
|
||||
|
||||
if sth_filter.includes(&ar.metrics.filter) {
|
||||
sth_acc.add(&state.realized);
|
||||
sth_icap.0 += u.investor_cap_in_profit_raw;
|
||||
sth_icap.1 += u.investor_cap_in_loss_raw;
|
||||
sth_ccap.0 += u.capitalized_cap_in_profit_raw;
|
||||
sth_ccap.1 += u.capitalized_cap_in_loss_raw;
|
||||
} else {
|
||||
lth_acc.add(&state.realized);
|
||||
lth_icap.0 += u.investor_cap_in_profit_raw;
|
||||
lth_icap.1 += u.investor_cap_in_loss_raw;
|
||||
lth_ccap.0 += u.capitalized_cap_in_profit_raw;
|
||||
lth_ccap.1 += u.capitalized_cap_in_loss_raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -860,28 +857,28 @@ impl UTXOCohorts<Rw> {
|
||||
|
||||
all.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(all_icap.0));
|
||||
.capitalized_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(all_ccap.0));
|
||||
all.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(all_icap.1));
|
||||
.capitalized_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(all_ccap.1));
|
||||
sth.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(sth_icap.0));
|
||||
.capitalized_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(sth_ccap.0));
|
||||
sth.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(sth_icap.1));
|
||||
.capitalized_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(sth_ccap.1));
|
||||
lth.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(lth_icap.0));
|
||||
.capitalized_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(lth_ccap.0));
|
||||
lth.metrics
|
||||
.unrealized
|
||||
.investor_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(lth_icap.1));
|
||||
.capitalized_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(lth_ccap.1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path};
|
||||
|
||||
use brk_cohort::{Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES};
|
||||
use brk_cohort::{AGE_RANGE_NAMES, CohortContext, Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES};
|
||||
use rayon::prelude::*;
|
||||
use brk_error::Result;
|
||||
use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Sats};
|
||||
use brk_types::{BasisPoints16, Cents, CentsCompact, UrpdRaw, Date, Dollars, Sats};
|
||||
|
||||
use crate::distribution::metrics::{CostBasis, ProfitabilityMetrics};
|
||||
|
||||
@@ -57,6 +58,30 @@ impl UTXOCohorts {
|
||||
fn write_disk_distributions(&mut self, date: Date, states_path: &Path) -> Result<()> {
|
||||
let sth_filter = self.sth.metrics.filter.clone();
|
||||
|
||||
self.age_range
|
||||
.iter()
|
||||
.zip(AGE_RANGE_NAMES.iter())
|
||||
.collect::<Vec<_>>()
|
||||
.into_par_iter()
|
||||
.try_for_each(|(sub, name)| -> Result<()> {
|
||||
let Some(state) = sub.state.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut merged: Vec<(CentsCompact, Sats)> = Vec::new();
|
||||
for (&price, &sats) in state.cost_basis_map().iter() {
|
||||
let rounded = price.round_to_dollar(COST_BASIS_PRICE_DIGITS);
|
||||
if let Some(last) = merged.last_mut()
|
||||
&& last.0 == rounded
|
||||
{
|
||||
last.1 += sats;
|
||||
} else {
|
||||
merged.push((rounded, sats));
|
||||
}
|
||||
}
|
||||
let full = CohortContext::Utxo.prefixed(name.id);
|
||||
write_distribution(states_path, &full, date, merged)
|
||||
})?;
|
||||
|
||||
let maps: Vec<_> = self
|
||||
.age_range
|
||||
.iter()
|
||||
@@ -84,9 +109,13 @@ impl UTXOCohorts {
|
||||
|
||||
merge_k_way(&maps, &mut targets);
|
||||
|
||||
write_distribution(states_path, "all", date, targets.all.merged)?;
|
||||
write_distribution(states_path, TERM_NAMES.short.id, date, targets.sth.merged)?;
|
||||
write_distribution(states_path, TERM_NAMES.long.id, date, targets.lth.merged)?;
|
||||
[
|
||||
("all", targets.all.merged),
|
||||
(TERM_NAMES.short.id, targets.sth.merged),
|
||||
(TERM_NAMES.long.id, targets.lth.merged),
|
||||
]
|
||||
.into_par_iter()
|
||||
.try_for_each(|(name, merged)| write_distribution(states_path, name, date, merged))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -122,15 +151,15 @@ fn push_profitability(
|
||||
|
||||
fn write_distribution(
|
||||
states_path: &Path,
|
||||
name: &str,
|
||||
full_name: &str,
|
||||
date: Date,
|
||||
merged: Vec<(CentsCompact, Sats)>,
|
||||
) -> Result<()> {
|
||||
let dir = states_path.join(format!("utxo_{name}_cost_basis/by_date"));
|
||||
let dir = states_path.join(full_name).join("urpd");
|
||||
fs::create_dir_all(&dir)?;
|
||||
fs::write(
|
||||
dir.join(date.to_string()),
|
||||
CostBasisDistribution::serialize_iter(merged.into_iter())?,
|
||||
UrpdRaw::serialize_iter(merged.into_iter())?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ use brk_types::{
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::FxHashSet;
|
||||
use tracing::{debug, info};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec, unlikely};
|
||||
|
||||
use crate::{
|
||||
distribution::{
|
||||
addr::{AddrTypeToActivityCounts, AddrTypeToAddrCount},
|
||||
addr::{
|
||||
AddrTypeToActivityCounts, AddrTypeToAddrCount, AddrTypeToExposedAddrCount,
|
||||
AddrTypeToExposedSupply, AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount,
|
||||
},
|
||||
block::{
|
||||
AddrCache, InputsResult, process_inputs, process_outputs, process_received,
|
||||
process_sent,
|
||||
@@ -192,22 +195,49 @@ pub(crate) fn process_blocks(
|
||||
|
||||
// Track running totals - recover from previous height if resuming
|
||||
debug!("recovering addr_counts from height {}", starting_height);
|
||||
let (mut addr_counts, mut empty_addr_counts) = if starting_height > Height::ZERO {
|
||||
let addr_counts =
|
||||
AddrTypeToAddrCount::from((&vecs.addrs.funded.by_addr_type, starting_height));
|
||||
let empty_addr_counts =
|
||||
AddrTypeToAddrCount::from((&vecs.addrs.empty.by_addr_type, starting_height));
|
||||
(addr_counts, empty_addr_counts)
|
||||
let (
|
||||
mut addr_counts,
|
||||
mut empty_addr_counts,
|
||||
mut reused_addr_counts,
|
||||
mut total_reused_addr_counts,
|
||||
mut exposed_addr_counts,
|
||||
mut total_exposed_addr_counts,
|
||||
mut exposed_supply,
|
||||
) = if starting_height > Height::ZERO {
|
||||
(
|
||||
AddrTypeToAddrCount::from((&vecs.addrs.funded.by_addr_type, starting_height)),
|
||||
AddrTypeToAddrCount::from((&vecs.addrs.empty.by_addr_type, starting_height)),
|
||||
AddrTypeToReusedAddrCount::from((&vecs.addrs.reused.count.funded, starting_height)),
|
||||
AddrTypeToReusedAddrCount::from((&vecs.addrs.reused.count.total, starting_height)),
|
||||
AddrTypeToExposedAddrCount::from((&vecs.addrs.exposed.count.funded, starting_height)),
|
||||
AddrTypeToExposedAddrCount::from((&vecs.addrs.exposed.count.total, starting_height)),
|
||||
AddrTypeToExposedSupply::from((&vecs.addrs.exposed.supply, starting_height)),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
AddrTypeToAddrCount::default(),
|
||||
AddrTypeToAddrCount::default(),
|
||||
AddrTypeToReusedAddrCount::default(),
|
||||
AddrTypeToReusedAddrCount::default(),
|
||||
AddrTypeToExposedAddrCount::default(),
|
||||
AddrTypeToExposedAddrCount::default(),
|
||||
AddrTypeToExposedSupply::default(),
|
||||
)
|
||||
};
|
||||
debug!("addr_counts recovered");
|
||||
|
||||
// Track activity counts - reset each block
|
||||
let mut activity_counts = AddrTypeToActivityCounts::default();
|
||||
// Reused-addr event counts (receive + spend side). Per-block
|
||||
// flow, reset each block.
|
||||
let mut output_to_reused_addr_counts = AddrTypeToReusedAddrEventCount::default();
|
||||
let mut input_from_reused_addr_counts = AddrTypeToReusedAddrEventCount::default();
|
||||
// Distinct addresses active this block whose lifetime
|
||||
// funded_txo_count > 1 after this block's events. Incremented in
|
||||
// process_received for every receiver that ends up reused, and in
|
||||
// process_sent for every sender that's reused AND didn't also
|
||||
// receive this block (deduped via `received_addrs`).
|
||||
let mut active_reused_addr_counts = AddrTypeToReusedAddrEventCount::default();
|
||||
|
||||
debug!("creating AddrCache");
|
||||
let mut cache = AddrCache::new();
|
||||
@@ -226,6 +256,9 @@ pub(crate) fn process_blocks(
|
||||
.chain(vecs.addrs.funded.par_iter_height_mut())
|
||||
.chain(vecs.addrs.empty.par_iter_height_mut())
|
||||
.chain(vecs.addrs.activity.par_iter_height_mut())
|
||||
.chain(vecs.addrs.reused.par_iter_height_mut())
|
||||
.chain(vecs.addrs.exposed.par_iter_height_mut())
|
||||
.chain(vecs.addrs.avg_amount.par_iter_height_mut())
|
||||
.chain(rayon::iter::once(
|
||||
&mut vecs.coinblocks_destroyed.block as &mut dyn AnyStoredVec,
|
||||
))
|
||||
@@ -243,7 +276,11 @@ pub(crate) fn process_blocks(
|
||||
for height in starting_height.to_usize()..=last_height.to_usize() {
|
||||
let height = Height::from(height);
|
||||
|
||||
info!("Processing chain at {}...", height);
|
||||
if unlikely(height.is_multiple_of(100)) {
|
||||
info!("Processing chain at {}...", height);
|
||||
} else {
|
||||
debug!("Processing chain at {}...", height);
|
||||
}
|
||||
|
||||
// Get block metadata from pre-collected vecs
|
||||
let offset = height.to_usize() - start_usize;
|
||||
@@ -274,6 +311,9 @@ pub(crate) fn process_blocks(
|
||||
|
||||
// Reset per-block activity counts
|
||||
activity_counts.reset();
|
||||
output_to_reused_addr_counts.reset();
|
||||
input_from_reused_addr_counts.reset();
|
||||
active_reused_addr_counts.reset();
|
||||
|
||||
// Process outputs, inputs, and tick-tock in parallel via rayon::join.
|
||||
// Collection (build tx_index mappings + bulk mmap reads) is merged into the
|
||||
@@ -443,6 +483,13 @@ pub(crate) fn process_blocks(
|
||||
&mut addr_counts,
|
||||
&mut empty_addr_counts,
|
||||
&mut activity_counts,
|
||||
&mut reused_addr_counts,
|
||||
&mut total_reused_addr_counts,
|
||||
&mut output_to_reused_addr_counts,
|
||||
&mut active_reused_addr_counts,
|
||||
&mut exposed_addr_counts,
|
||||
&mut total_exposed_addr_counts,
|
||||
&mut exposed_supply,
|
||||
);
|
||||
|
||||
// Process sent inputs (addresses sending funds)
|
||||
@@ -455,6 +502,12 @@ pub(crate) fn process_blocks(
|
||||
&mut addr_counts,
|
||||
&mut empty_addr_counts,
|
||||
&mut activity_counts,
|
||||
&mut reused_addr_counts,
|
||||
&mut input_from_reused_addr_counts,
|
||||
&mut active_reused_addr_counts,
|
||||
&mut exposed_addr_counts,
|
||||
&mut total_exposed_addr_counts,
|
||||
&mut exposed_supply,
|
||||
&received_addrs,
|
||||
height_to_price_vec,
|
||||
height_to_timestamp_vec,
|
||||
@@ -477,6 +530,36 @@ pub(crate) fn process_blocks(
|
||||
.empty
|
||||
.push_height(empty_addr_counts.sum(), &empty_addr_counts);
|
||||
vecs.addrs.activity.push_height(&activity_counts);
|
||||
vecs.addrs.reused.count.funded.push_height(
|
||||
reused_addr_counts.sum(),
|
||||
reused_addr_counts.values().copied(),
|
||||
);
|
||||
vecs.addrs.reused.count.total.push_height(
|
||||
total_reused_addr_counts.sum(),
|
||||
total_reused_addr_counts.values().copied(),
|
||||
);
|
||||
let activity_totals = activity_counts.totals();
|
||||
let active_addr_count =
|
||||
activity_totals.sending + activity_totals.receiving - activity_totals.bidirectional;
|
||||
let active_reused = u32::try_from(active_reused_addr_counts.sum()).unwrap();
|
||||
vecs.addrs.reused.events.push_height(
|
||||
&output_to_reused_addr_counts,
|
||||
&input_from_reused_addr_counts,
|
||||
active_addr_count,
|
||||
active_reused,
|
||||
);
|
||||
vecs.addrs.exposed.count.funded.push_height(
|
||||
exposed_addr_counts.sum(),
|
||||
exposed_addr_counts.values().copied(),
|
||||
);
|
||||
vecs.addrs.exposed.count.total.push_height(
|
||||
total_exposed_addr_counts.sum(),
|
||||
total_exposed_addr_counts.values().copied(),
|
||||
);
|
||||
vecs.addrs
|
||||
.exposed
|
||||
.supply
|
||||
.push_height(exposed_supply.sum(), exposed_supply.values().copied());
|
||||
|
||||
let is_last_of_day = is_last_of_day[offset];
|
||||
let date_opt = is_last_of_day.then(|| Date::from(timestamp));
|
||||
@@ -549,7 +632,7 @@ fn push_cohort_states(
|
||||
height: Height,
|
||||
height_price: Cents,
|
||||
) {
|
||||
// Phase 1: push + unrealized (no reset yet — states still needed for aggregation)
|
||||
// Phase 1: push + unrealized (no reset yet; states still needed for aggregation)
|
||||
rayon::join(
|
||||
|| {
|
||||
utxo_cohorts.par_iter_separate_mut().for_each(|v| {
|
||||
|
||||
@@ -79,6 +79,8 @@ pub(crate) fn write(
|
||||
.chain(vecs.addrs.funded.par_iter_height_mut())
|
||||
.chain(vecs.addrs.empty.par_iter_height_mut())
|
||||
.chain(vecs.addrs.activity.par_iter_height_mut())
|
||||
.chain(vecs.addrs.reused.par_iter_height_mut())
|
||||
.chain(vecs.addrs.exposed.par_iter_height_mut())
|
||||
.chain(
|
||||
[
|
||||
&mut vecs.supply_state as &mut dyn AnyStoredVec,
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
metrics::ImportConfig,
|
||||
state::{CohortState, CostBasisOps, RealizedOps},
|
||||
},
|
||||
internal::{AmountPerBlockCumulativeRolling, PerBlockCumulativeRolling},
|
||||
internal::{ValuePerBlockCumulativeRolling, PerBlockCumulativeRolling},
|
||||
prices,
|
||||
};
|
||||
|
||||
@@ -24,9 +24,9 @@ pub struct ActivityCore<M: StorageMode = Rw> {
|
||||
|
||||
pub coindays_destroyed: PerBlockCumulativeRolling<StoredF64, StoredF64, M>,
|
||||
#[traversable(wrap = "transfer_volume", rename = "in_profit")]
|
||||
pub transfer_volume_in_profit: AmountPerBlockCumulativeRolling<M>,
|
||||
pub transfer_volume_in_profit: ValuePerBlockCumulativeRolling<M>,
|
||||
#[traversable(wrap = "transfer_volume", rename = "in_loss")]
|
||||
pub transfer_volume_in_loss: AmountPerBlockCumulativeRolling<M>,
|
||||
pub transfer_volume_in_loss: ValuePerBlockCumulativeRolling<M>,
|
||||
}
|
||||
|
||||
impl ActivityCore {
|
||||
|
||||
@@ -2,7 +2,7 @@ use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Indexes, StoredF32, StoredF64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{AnyStoredVec, Exit, ReadableCloneableVec, Rw, StorageMode};
|
||||
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::internal::{Identity, LazyPerBlock, PerBlock, Windows};
|
||||
|
||||
@@ -33,15 +33,10 @@ impl ActivityFull {
|
||||
let v1 = Version::ONE;
|
||||
let inner = ActivityCore::forced_import(cfg)?;
|
||||
|
||||
let coinyears_destroyed = LazyPerBlock::from_height_source::<Identity<StoredF64>>(
|
||||
let coinyears_destroyed = LazyPerBlock::from_height_source::<Identity<StoredF64>, _>(
|
||||
&cfg.name("coinyears_destroyed"),
|
||||
cfg.version + v1,
|
||||
inner
|
||||
.coindays_destroyed
|
||||
.sum
|
||||
._1y
|
||||
.height
|
||||
.read_only_boxed_clone(),
|
||||
inner.coindays_destroyed.sum._1y.height.clone(),
|
||||
cfg.indexes,
|
||||
);
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ use crate::{
|
||||
metrics::ImportConfig,
|
||||
state::{CohortState, CostBasisOps, RealizedOps},
|
||||
},
|
||||
internal::AmountPerBlockCumulativeRolling,
|
||||
internal::ValuePerBlockCumulativeRolling,
|
||||
prices,
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct ActivityMinimal<M: StorageMode = Rw> {
|
||||
pub transfer_volume: AmountPerBlockCumulativeRolling<M>,
|
||||
pub transfer_volume: ValuePerBlockCumulativeRolling<M>,
|
||||
}
|
||||
|
||||
impl ActivityMinimal {
|
||||
|
||||
@@ -2,8 +2,7 @@ use brk_cohort::Filter;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Cents, Dollars, Height, Indexes, Version};
|
||||
use vecdb::AnyStoredVec;
|
||||
use vecdb::{Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode};
|
||||
use vecdb::{AnyStoredVec, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
blocks,
|
||||
@@ -144,18 +143,23 @@ impl AllCohortMetrics {
|
||||
&self.unrealized.invested_capital.in_loss.cents.height,
|
||||
&self.supply.in_profit.sats.height,
|
||||
&self.supply.in_loss.sats.height,
|
||||
&self.unrealized.investor_cap_in_profit_raw,
|
||||
&self.unrealized.investor_cap_in_loss_raw,
|
||||
&self.unrealized.capitalized_cap_in_profit_raw,
|
||||
&self.unrealized.capitalized_cap_in_loss_raw,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.unrealized
|
||||
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
|
||||
|
||||
let own_supply_sats = self.supply.total.sats.height.read_only_clone();
|
||||
self.supply
|
||||
.compute_dominance(starting_indexes.height, &own_supply_sats, exit)?;
|
||||
|
||||
self.relative.compute(
|
||||
starting_indexes.height,
|
||||
&self.supply,
|
||||
&self.unrealized,
|
||||
&self.realized,
|
||||
height_to_market_cap,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -6,14 +6,13 @@ use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
distribution::metrics::{
|
||||
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, RelativeToAll,
|
||||
SupplyCore, UnrealizedCore,
|
||||
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
|
||||
UnrealizedCore,
|
||||
},
|
||||
prices,
|
||||
};
|
||||
|
||||
/// Basic cohort metrics: no extensions, with relative (rel_to_all).
|
||||
/// Used by: age_range cohorts.
|
||||
/// Basic cohort metrics: no extensions, used by age_range cohorts.
|
||||
#[derive(Traversable)]
|
||||
pub struct BasicCohortMetrics<M: StorageMode = Rw> {
|
||||
#[traversable(skip)]
|
||||
@@ -23,8 +22,6 @@ pub struct BasicCohortMetrics<M: StorageMode = Rw> {
|
||||
pub activity: Box<ActivityCore<M>>,
|
||||
pub realized: Box<RealizedCore<M>>,
|
||||
pub unrealized: Box<UnrealizedCore<M>>,
|
||||
#[traversable(flatten)]
|
||||
pub relative: Box<RelativeToAll<M>>,
|
||||
}
|
||||
|
||||
impl CohortMetricsBase for BasicCohortMetrics {
|
||||
@@ -51,8 +48,6 @@ impl BasicCohortMetrics {
|
||||
let unrealized = UnrealizedCore::forced_import(cfg)?;
|
||||
let realized = RealizedCore::forced_import(cfg)?;
|
||||
|
||||
let relative = RelativeToAll::forced_import(cfg)?;
|
||||
|
||||
Ok(Self {
|
||||
filter: cfg.filter.clone(),
|
||||
supply: Box::new(supply),
|
||||
@@ -60,7 +55,6 @@ impl BasicCohortMetrics {
|
||||
activity: Box::new(ActivityCore::forced_import(cfg)?),
|
||||
realized: Box::new(realized),
|
||||
unrealized: Box::new(unrealized),
|
||||
relative: Box::new(relative),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,8 +81,8 @@ impl BasicCohortMetrics {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.relative
|
||||
.compute(starting_indexes.height, &self.supply, all_supply_sats, exit)?;
|
||||
self.supply
|
||||
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
|
||||
|
||||
self.outputs
|
||||
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
|
||||
|
||||
@@ -6,8 +6,8 @@ use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
distribution::metrics::{
|
||||
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, RelativeToAll,
|
||||
SupplyCore, UnrealizedCore,
|
||||
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
|
||||
UnrealizedCore,
|
||||
},
|
||||
prices,
|
||||
};
|
||||
@@ -21,8 +21,6 @@ pub struct CoreCohortMetrics<M: StorageMode = Rw> {
|
||||
pub activity: Box<ActivityCore<M>>,
|
||||
pub realized: Box<RealizedCore<M>>,
|
||||
pub unrealized: Box<UnrealizedCore<M>>,
|
||||
#[traversable(flatten)]
|
||||
pub relative: Box<RelativeToAll<M>>,
|
||||
}
|
||||
|
||||
impl CoreCohortMetrics {
|
||||
@@ -34,7 +32,6 @@ impl CoreCohortMetrics {
|
||||
activity: Box::new(ActivityCore::forced_import(cfg)?),
|
||||
realized: Box::new(RealizedCore::forced_import(cfg)?),
|
||||
unrealized: Box::new(UnrealizedCore::forced_import(cfg)?),
|
||||
relative: Box::new(RelativeToAll::forced_import(cfg)?),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,8 +142,8 @@ impl CoreCohortMetrics {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.relative
|
||||
.compute(starting_indexes.height, &self.supply, all_supply_sats, exit)?;
|
||||
self.supply
|
||||
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
|
||||
|
||||
self.outputs
|
||||
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
|
||||
|
||||
@@ -120,20 +120,23 @@ impl ExtendedCohortMetrics {
|
||||
&self.unrealized.invested_capital.in_loss.cents.height,
|
||||
&self.supply.in_profit.sats.height,
|
||||
&self.supply.in_loss.sats.height,
|
||||
&self.unrealized.investor_cap_in_profit_raw,
|
||||
&self.unrealized.investor_cap_in_loss_raw,
|
||||
&self.unrealized.capitalized_cap_in_profit_raw,
|
||||
&self.unrealized.capitalized_cap_in_loss_raw,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.unrealized
|
||||
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
|
||||
|
||||
self.supply
|
||||
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
|
||||
|
||||
self.relative.compute(
|
||||
starting_indexes.height,
|
||||
&self.supply,
|
||||
&self.unrealized,
|
||||
&self.realized,
|
||||
height_to_market_cap,
|
||||
all_supply_sats,
|
||||
&self.supply.total.usd.height,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use brk_cohort::Filter;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Indexes, StoredU64};
|
||||
use brk_types::{Height, Indexes, Sats, StoredU64};
|
||||
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
@@ -112,6 +112,7 @@ impl MinimalCohortMetrics {
|
||||
&mut self,
|
||||
prices: &prices::Vecs,
|
||||
starting_indexes: &Indexes,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
@@ -129,6 +130,9 @@ impl MinimalCohortMetrics {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.supply
|
||||
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
|
||||
|
||||
self.outputs
|
||||
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use brk_cohort::Filter;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Indexes, StoredU64};
|
||||
use brk_types::{Height, Indexes, Sats, StoredU64};
|
||||
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
@@ -74,6 +74,7 @@ impl TypeCohortMetrics {
|
||||
&mut self,
|
||||
prices: &prices::Vecs,
|
||||
starting_indexes: &Indexes,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
@@ -91,6 +92,9 @@ impl TypeCohortMetrics {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.supply
|
||||
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
|
||||
|
||||
self.outputs
|
||||
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ use vecdb::{BytesVec, BytesVecValue, Database, ImportableVec};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
AmountPerBlock, AmountPerBlockCumulative, AmountPerBlockCumulativeRolling,
|
||||
CachedWindowStarts, CentsType, FiatPerBlock, FiatPerBlockCumulativeWithSums, NumericValue,
|
||||
PerBlock, PerBlockCumulativeRolling, PercentPerBlock, PercentRollingWindows, Price,
|
||||
ValuePerBlock, ValuePerBlockCumulative, ValuePerBlockCumulativeRolling, FiatType,
|
||||
FiatPerBlock, FiatPerBlockCumulativeWithSums, NumericValue, PerBlock,
|
||||
PerBlockCumulativeRolling, PercentPerBlock, PercentRollingWindows, Price,
|
||||
PriceWithRatioExtendedPerBlock, PriceWithRatioPerBlock, RatioPerBlock,
|
||||
RollingWindow24hPerBlock, RollingWindows, RollingWindowsFrom1w,
|
||||
RollingWindow24hPerBlock, RollingWindows, RollingWindowsFrom1w, WindowStartVec, Windows,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -35,8 +35,8 @@ macro_rules! impl_config_import {
|
||||
|
||||
// Non-generic types
|
||||
impl_config_import!(
|
||||
AmountPerBlock,
|
||||
AmountPerBlockCumulative,
|
||||
ValuePerBlock,
|
||||
ValuePerBlockCumulative,
|
||||
PriceWithRatioPerBlock,
|
||||
PriceWithRatioExtendedPerBlock,
|
||||
RatioPerBlock<BasisPoints32>,
|
||||
@@ -79,7 +79,7 @@ impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindow24hPerBlock<T>
|
||||
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
|
||||
}
|
||||
}
|
||||
impl ConfigImport for AmountPerBlockCumulativeRolling {
|
||||
impl ConfigImport for ValuePerBlockCumulativeRolling {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
Self::forced_import(
|
||||
cfg.db,
|
||||
@@ -90,7 +90,7 @@ impl ConfigImport for AmountPerBlockCumulativeRolling {
|
||||
)
|
||||
}
|
||||
}
|
||||
impl<C: CentsType> ConfigImport for FiatPerBlockCumulativeWithSums<C> {
|
||||
impl<C: FiatType> ConfigImport for FiatPerBlockCumulativeWithSums<C> {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
Self::forced_import(
|
||||
cfg.db,
|
||||
@@ -106,7 +106,7 @@ impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindowsFrom1w<T> {
|
||||
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
|
||||
}
|
||||
}
|
||||
impl<C: CentsType> ConfigImport for FiatPerBlock<C> {
|
||||
impl<C: FiatType> ConfigImport for FiatPerBlock<C> {
|
||||
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
|
||||
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ pub struct ImportConfig<'a> {
|
||||
pub full_name: &'a str,
|
||||
pub version: Version,
|
||||
pub indexes: &'a indexes::Vecs,
|
||||
pub cached_starts: &'a CachedWindowStarts,
|
||||
pub cached_starts: &'a Windows<&'a WindowStartVec>,
|
||||
}
|
||||
|
||||
impl<'a> ImportConfig<'a> {
|
||||
|
||||
@@ -153,8 +153,8 @@ impl CostBasis {
|
||||
invested_cap_in_loss: &impl ReadableVec<Height, Cents>,
|
||||
supply_in_profit_sats: &impl ReadableVec<Height, Sats>,
|
||||
supply_in_loss_sats: &impl ReadableVec<Height, Sats>,
|
||||
investor_cap_in_profit_raw: &impl ReadableVec<Height, CentsSquaredSats>,
|
||||
investor_cap_in_loss_raw: &impl ReadableVec<Height, CentsSquaredSats>,
|
||||
capitalized_cap_in_profit_raw: &impl ReadableVec<Height, CentsSquaredSats>,
|
||||
capitalized_cap_in_loss_raw: &impl ReadableVec<Height, CentsSquaredSats>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.in_profit.per_coin.cents.height.compute_transform3(
|
||||
@@ -193,29 +193,29 @@ impl CostBasis {
|
||||
)?;
|
||||
self.in_profit.per_dollar.cents.height.compute_transform3(
|
||||
starting_indexes.height,
|
||||
investor_cap_in_profit_raw,
|
||||
capitalized_cap_in_profit_raw,
|
||||
invested_cap_in_profit,
|
||||
spot,
|
||||
|(h, investor_cap, invested_cents, spot, ..)| {
|
||||
|(h, capitalized_cap, invested_cents, spot, ..)| {
|
||||
let invested_raw = invested_cents.as_u128() * Sats::ONE_BTC_U128;
|
||||
if invested_raw == 0 {
|
||||
return (h, spot);
|
||||
}
|
||||
(h, Cents::new((investor_cap.inner() / invested_raw) as u64))
|
||||
(h, Cents::new((capitalized_cap.inner() / invested_raw) as u64))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
self.in_loss.per_dollar.cents.height.compute_transform3(
|
||||
starting_indexes.height,
|
||||
investor_cap_in_loss_raw,
|
||||
capitalized_cap_in_loss_raw,
|
||||
invested_cap_in_loss,
|
||||
spot,
|
||||
|(h, investor_cap, invested_cents, spot, ..)| {
|
||||
|(h, capitalized_cap, invested_cents, spot, ..)| {
|
||||
let invested_raw = invested_cents.as_u128() * Sats::ONE_BTC_U128;
|
||||
if invested_raw == 0 {
|
||||
return (h, spot);
|
||||
}
|
||||
(h, Cents::new((investor_cap.inner() / invested_raw) as u64))
|
||||
(h, Cents::new((capitalized_cap.inner() / invested_raw) as u64))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
@@ -132,8 +132,8 @@ pub use profitability::ProfitabilityMetrics;
|
||||
pub use realized::{
|
||||
AdjustedSopr, RealizedCore, RealizedFull, RealizedFullAccum, RealizedLike, RealizedMinimal,
|
||||
};
|
||||
pub use relative::{RelativeForAll, RelativeToAll, RelativeWithExtended};
|
||||
pub use supply::{SupplyBase, SupplyCore};
|
||||
pub use relative::{RelativeForAll, RelativeWithExtended};
|
||||
pub use supply::{AvgAmountMetrics, SupplyBase, SupplyCore};
|
||||
pub use unrealized::{
|
||||
UnrealizedBasic, UnrealizedCore, UnrealizedFull, UnrealizedLike, UnrealizedMinimal,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
AmountPerBlock, AmountPerBlockWithDeltas, CachedWindowStarts, PerBlock, RatioPerBlock,
|
||||
ValuePerBlock, ValuePerBlockWithDeltas, PerBlock, RatioPerBlock, WindowStartVec, Windows,
|
||||
},
|
||||
prices,
|
||||
};
|
||||
@@ -20,7 +20,7 @@ pub struct WithSth<All, Sth = All> {
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct ProfitabilityBucket<M: StorageMode = Rw> {
|
||||
pub supply: WithSth<AmountPerBlockWithDeltas<M>, AmountPerBlock<M>>,
|
||||
pub supply: WithSth<ValuePerBlockWithDeltas<M>, ValuePerBlock<M>>,
|
||||
pub realized_cap: WithSth<PerBlock<Dollars, M>>,
|
||||
pub unrealized_pnl: WithSth<PerBlock<Dollars, M>>,
|
||||
pub nupl: RatioPerBlock<BasisPointsSigned32, M>,
|
||||
@@ -43,18 +43,18 @@ impl ProfitabilityBucket {
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
supply: WithSth {
|
||||
all: AmountPerBlockWithDeltas::forced_import(
|
||||
all: ValuePerBlockWithDeltas::forced_import(
|
||||
db,
|
||||
&format!("{name}_supply"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
sth: AmountPerBlock::forced_import(
|
||||
sth: ValuePerBlock::forced_import(
|
||||
db,
|
||||
&format!("{name}_sth_supply"),
|
||||
version,
|
||||
@@ -267,13 +267,13 @@ impl ProfitabilityMetrics {
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &CachedWindowStarts,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let range = ProfitabilityRange::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
|
||||
})?;
|
||||
|
||||
let aggregate_version = version + Version::ONE;
|
||||
let aggregate_version = version + Version::TWO;
|
||||
|
||||
let profit = Profit::try_new(|name| {
|
||||
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
|
||||
|
||||
@@ -62,10 +62,10 @@ impl RealizedCore {
|
||||
);
|
||||
|
||||
let neg_loss_sum = minimal.loss.sum.0.map_with_suffix(|suffix, slot| {
|
||||
LazyPerBlock::from_height_source::<NegCentsUnsignedToDollars>(
|
||||
LazyPerBlock::from_height_source::<NegCentsUnsignedToDollars, _>(
|
||||
&cfg.name(&format!("realized_loss_neg_sum_{suffix}")),
|
||||
cfg.version + Version::ONE,
|
||||
slot.cents.height.read_only_boxed_clone(),
|
||||
slot.cents.height.clone(),
|
||||
cfg.indexes,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
blocks,
|
||||
distribution::state::{CohortState, CostBasisData, RealizedState, WithCapital},
|
||||
internal::{
|
||||
AmountPerBlockCumulativeRolling, FiatPerBlockCumulativeWithSums, PercentPerBlock,
|
||||
ValuePerBlockCumulativeRolling, FiatPerBlockCumulativeWithSums, PercentPerBlock,
|
||||
PercentRollingWindows, PriceWithRatioExtendedPerBlock, RatioCents64, RatioCentsBp32,
|
||||
RatioCentsSignedCentsBps32, RatioCentsSignedDollarsBps32, RatioDollarsBp32,
|
||||
RatioPerBlockPercentiles, RatioPerBlockStdDevBands, RatioSma, RollingWindows,
|
||||
@@ -45,7 +45,7 @@ pub struct RealizedPeakRegret<M: StorageMode = Rw> {
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct RealizedInvestor<M: StorageMode = Rw> {
|
||||
pub struct RealizedCapitalized<M: StorageMode = Rw> {
|
||||
pub price: PriceWithRatioExtendedPerBlock<M>,
|
||||
#[traversable(hidden)]
|
||||
pub cap_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
|
||||
@@ -63,7 +63,7 @@ pub struct RealizedFull<M: StorageMode = Rw> {
|
||||
pub net_pnl: RealizedNetPnl<M>,
|
||||
pub sopr: RealizedSopr<M>,
|
||||
pub peak_regret: RealizedPeakRegret<M>,
|
||||
pub investor: RealizedInvestor<M>,
|
||||
pub capitalized: RealizedCapitalized<M>,
|
||||
|
||||
pub profit_to_loss_ratio: RollingWindows<StoredF64, M>,
|
||||
|
||||
@@ -108,10 +108,10 @@ impl RealizedFull {
|
||||
value: cfg.import("realized_peak_regret", Version::new(3))?,
|
||||
};
|
||||
|
||||
// Investor
|
||||
let investor = RealizedInvestor {
|
||||
price: cfg.import("investor_price", v0)?,
|
||||
cap_raw: cfg.import("investor_cap_raw", v0)?,
|
||||
// Capitalized
|
||||
let capitalized = RealizedCapitalized {
|
||||
price: cfg.import("capitalized_price", v0)?,
|
||||
cap_raw: cfg.import("capitalized_cap_raw", v0)?,
|
||||
};
|
||||
|
||||
// Price ratio stats
|
||||
@@ -125,7 +125,7 @@ impl RealizedFull {
|
||||
net_pnl,
|
||||
sopr,
|
||||
peak_regret,
|
||||
investor,
|
||||
capitalized,
|
||||
profit_to_loss_ratio: cfg.import("realized_profit_to_loss_ratio", v1)?,
|
||||
cap_raw: cfg.import("cap_raw", v0)?,
|
||||
cap_to_own_mcap: cfg.import("realized_cap_to_own_mcap", v1)?,
|
||||
@@ -151,13 +151,13 @@ impl RealizedFull {
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.investor
|
||||
self.capitalized
|
||||
.price
|
||||
.cents
|
||||
.height
|
||||
.len()
|
||||
.min(self.cap_raw.len())
|
||||
.min(self.investor.cap_raw.len())
|
||||
.min(self.capitalized.cap_raw.len())
|
||||
.min(self.peak_regret.value.block.cents.len())
|
||||
}
|
||||
|
||||
@@ -167,15 +167,15 @@ impl RealizedFull {
|
||||
state: &CohortState<RealizedState, CostBasisData<WithCapital>>,
|
||||
) {
|
||||
self.core.push_state(state);
|
||||
self.investor
|
||||
self.capitalized
|
||||
.price
|
||||
.cents
|
||||
.height
|
||||
.push(state.realized.investor_price());
|
||||
.push(state.realized.capitalized_price());
|
||||
self.cap_raw.push(state.realized.cap_raw());
|
||||
self.investor
|
||||
self.capitalized
|
||||
.cap_raw
|
||||
.push(state.realized.investor_cap_raw());
|
||||
.push(state.realized.capitalized_cap_raw());
|
||||
self.peak_regret
|
||||
.value
|
||||
.block
|
||||
@@ -185,9 +185,9 @@ impl RealizedFull {
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = self.core.collect_vecs_mut();
|
||||
vecs.push(&mut self.investor.price.cents.height);
|
||||
vecs.push(&mut self.capitalized.price.cents.height);
|
||||
vecs.push(&mut self.cap_raw as &mut dyn AnyStoredVec);
|
||||
vecs.push(&mut self.investor.cap_raw as &mut dyn AnyStoredVec);
|
||||
vecs.push(&mut self.capitalized.cap_raw as &mut dyn AnyStoredVec);
|
||||
vecs.push(&mut self.peak_regret.value.block.cents);
|
||||
vecs
|
||||
}
|
||||
@@ -207,17 +207,17 @@ impl RealizedFull {
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_accum(&mut self, accum: &RealizedFullAccum) {
|
||||
self.cap_raw.push(accum.cap_raw);
|
||||
self.investor.cap_raw.push(accum.investor_cap_raw);
|
||||
self.capitalized.cap_raw.push(accum.capitalized_cap_raw);
|
||||
|
||||
let investor_price = {
|
||||
let capitalized_price = {
|
||||
let cap = accum.cap_raw.as_u128();
|
||||
if cap == 0 {
|
||||
Cents::ZERO
|
||||
} else {
|
||||
Cents::new((accum.investor_cap_raw / cap) as u64)
|
||||
Cents::new((accum.capitalized_cap_raw / cap) as u64)
|
||||
}
|
||||
};
|
||||
self.investor.price.cents.height.push(investor_price);
|
||||
self.capitalized.price.cents.height.push(capitalized_price);
|
||||
|
||||
self.peak_regret.value.block.cents.push(accum.peak_regret());
|
||||
}
|
||||
@@ -243,7 +243,7 @@ impl RealizedFull {
|
||||
starting_indexes: &Indexes,
|
||||
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
|
||||
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
|
||||
activity_transfer_volume: &AmountPerBlockCumulativeRolling,
|
||||
activity_transfer_volume: &ValuePerBlockCumulativeRolling,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.core.compute_rest_part2(
|
||||
@@ -298,8 +298,8 @@ impl RealizedFull {
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Investor price ratio, percentiles and bands
|
||||
self.investor
|
||||
// Capitalized price ratio, percentiles and bands
|
||||
self.capitalized
|
||||
.price
|
||||
.compute_rest(prices, starting_indexes, exit)?;
|
||||
|
||||
@@ -374,14 +374,14 @@ impl RealizedFull {
|
||||
#[derive(Default)]
|
||||
pub struct RealizedFullAccum {
|
||||
pub(crate) cap_raw: CentsSats,
|
||||
pub(crate) investor_cap_raw: CentsSquaredSats,
|
||||
pub(crate) capitalized_cap_raw: CentsSquaredSats,
|
||||
peak_regret: CentsSats,
|
||||
}
|
||||
|
||||
impl RealizedFullAccum {
|
||||
pub(crate) fn add(&mut self, state: &RealizedState) {
|
||||
self.cap_raw += state.cap_raw();
|
||||
self.investor_cap_raw += state.investor_cap_raw();
|
||||
self.capitalized_cap_raw += state.capitalized_cap_raw();
|
||||
self.peak_regret += CentsSats::new(state.peak_regret_raw());
|
||||
}
|
||||
|
||||
|
||||
@@ -118,11 +118,11 @@ impl RealizedMinimal {
|
||||
|(i, cap_cents, supply, ..)| {
|
||||
let cap = cap_cents.as_u128();
|
||||
let supply_sats = Sats::from(supply).as_u128();
|
||||
if supply_sats == 0 {
|
||||
(i, Cents::ZERO)
|
||||
} else {
|
||||
(i, Cents::from(cap * Sats::ONE_BTC_U128 / supply_sats))
|
||||
}
|
||||
let cents = (cap * Sats::ONE_BTC_U128)
|
||||
.checked_div(supply_sats)
|
||||
.map(Cents::from)
|
||||
.unwrap_or(Cents::ZERO);
|
||||
(i, cents)
|
||||
},
|
||||
exit,
|
||||
)?)
|
||||
|
||||
@@ -4,9 +4,9 @@ use brk_types::{Dollars, Height};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::distribution::metrics::{ImportConfig, SupplyCore, UnrealizedFull};
|
||||
use crate::distribution::metrics::{ImportConfig, RealizedFull, SupplyCore, UnrealizedFull};
|
||||
|
||||
use super::{RelativeExtendedOwnPnl, RelativeFull};
|
||||
use super::{RelativeExtendedOwnPnl, RelativeFull, RelativeInvestedCapital};
|
||||
|
||||
/// Relative metrics for the "all" cohort (base + own_pnl, NO rel_to_all).
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
@@ -17,6 +17,8 @@ pub struct RelativeForAll<M: StorageMode = Rw> {
|
||||
pub base: RelativeFull<M>,
|
||||
#[traversable(flatten)]
|
||||
pub extended_own_pnl: RelativeExtendedOwnPnl<M>,
|
||||
#[traversable(flatten)]
|
||||
pub invested_capital: RelativeInvestedCapital<M>,
|
||||
}
|
||||
|
||||
impl RelativeForAll {
|
||||
@@ -24,6 +26,7 @@ impl RelativeForAll {
|
||||
Ok(Self {
|
||||
base: RelativeFull::forced_import(cfg)?,
|
||||
extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg)?,
|
||||
invested_capital: RelativeInvestedCapital::forced_import(cfg)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,6 +35,7 @@ impl RelativeForAll {
|
||||
max_from: Height,
|
||||
supply: &SupplyCore,
|
||||
unrealized: &UnrealizedFull,
|
||||
realized: &RealizedFull,
|
||||
market_cap: &impl ReadableVec<Height, Dollars>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
@@ -43,6 +47,8 @@ impl RelativeForAll {
|
||||
&unrealized.gross_pnl.usd.height,
|
||||
exit,
|
||||
)?;
|
||||
self.invested_capital
|
||||
.compute(max_from, unrealized, realized, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ use crate::{
|
||||
/// Full relative metrics (sth/lth/all tier).
|
||||
#[derive(Traversable)]
|
||||
pub struct RelativeFull<M: StorageMode = Rw> {
|
||||
#[traversable(wrap = "supply/in_profit", rename = "to_own")]
|
||||
pub supply_in_profit_to_own: PercentPerBlock<BasisPoints16, M>,
|
||||
#[traversable(wrap = "supply/in_loss", rename = "to_own")]
|
||||
pub supply_in_loss_to_own: PercentPerBlock<BasisPoints16, M>,
|
||||
#[traversable(wrap = "supply/in_profit", rename = "share")]
|
||||
pub supply_in_profit_share: PercentPerBlock<BasisPoints16, M>,
|
||||
#[traversable(wrap = "supply/in_loss", rename = "share")]
|
||||
pub supply_in_loss_share: PercentPerBlock<BasisPoints16, M>,
|
||||
|
||||
#[traversable(wrap = "unrealized/profit", rename = "to_mcap")]
|
||||
pub unrealized_profit_to_mcap: PercentPerBlock<BasisPoints16, M>,
|
||||
@@ -28,8 +28,8 @@ impl RelativeFull {
|
||||
let v2 = Version::new(2);
|
||||
|
||||
Ok(Self {
|
||||
supply_in_profit_to_own: cfg.import("supply_in_profit_to_own", v1)?,
|
||||
supply_in_loss_to_own: cfg.import("supply_in_loss_to_own", v1)?,
|
||||
supply_in_profit_share: cfg.import("supply_in_profit_share", v1)?,
|
||||
supply_in_loss_share: cfg.import("supply_in_loss_share", v1)?,
|
||||
unrealized_profit_to_mcap: cfg.import("unrealized_profit_to_mcap", v2)?,
|
||||
unrealized_loss_to_mcap: cfg.import("unrealized_loss_to_mcap", v2)?,
|
||||
})
|
||||
@@ -43,14 +43,14 @@ impl RelativeFull {
|
||||
market_cap: &impl ReadableVec<Height, Dollars>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.supply_in_profit_to_own
|
||||
self.supply_in_profit_share
|
||||
.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&supply.in_profit.sats.height,
|
||||
&supply.total.sats.height,
|
||||
exit,
|
||||
)?;
|
||||
self.supply_in_loss_to_own
|
||||
self.supply_in_loss_share
|
||||
.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&supply.in_loss.sats.height,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, Cents, Height, Version};
|
||||
use vecdb::{Exit, Rw, StorageMode};
|
||||
|
||||
use crate::internal::{PercentPerBlock, RatioCentsBp16};
|
||||
|
||||
use crate::distribution::metrics::{ImportConfig, RealizedFull, UnrealizedFull};
|
||||
|
||||
/// Shares of invested capital in profit / in loss relative to own realized cap.
|
||||
/// Present for cohorts with `UnrealizedFull` (all, sth, lth).
|
||||
#[derive(Traversable)]
|
||||
pub struct RelativeInvestedCapital<M: StorageMode = Rw> {
|
||||
#[traversable(wrap = "invested_capital/in_profit", rename = "share")]
|
||||
pub in_profit_share: PercentPerBlock<BasisPoints16, M>,
|
||||
#[traversable(wrap = "invested_capital/in_loss", rename = "share")]
|
||||
pub in_loss_share: PercentPerBlock<BasisPoints16, M>,
|
||||
}
|
||||
|
||||
impl RelativeInvestedCapital {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let v0 = Version::ZERO;
|
||||
Ok(Self {
|
||||
in_profit_share: cfg.import("invested_capital_in_profit_share", v0)?,
|
||||
in_loss_share: cfg.import("invested_capital_in_loss_share", v0)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
unrealized: &UnrealizedFull,
|
||||
realized: &RealizedFull,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let realized_cap = &realized.core.minimal.cap.cents.height;
|
||||
self.in_profit_share
|
||||
.compute_binary::<Cents, Cents, RatioCentsBp16>(
|
||||
max_from,
|
||||
&unrealized.invested_capital.in_profit.cents.height,
|
||||
realized_cap,
|
||||
exit,
|
||||
)?;
|
||||
self.in_loss_share
|
||||
.compute_binary::<Cents, Cents, RatioCentsBp16>(
|
||||
max_from,
|
||||
&unrealized.invested_capital.in_loss.cents.height,
|
||||
realized_cap,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ mod extended_own_market_cap;
|
||||
mod extended_own_pnl;
|
||||
mod for_all;
|
||||
mod full;
|
||||
mod to_all;
|
||||
mod invested_capital;
|
||||
mod with_extended;
|
||||
|
||||
pub use extended_own_market_cap::RelativeExtendedOwnMarketCap;
|
||||
pub use extended_own_pnl::RelativeExtendedOwnPnl;
|
||||
pub use for_all::RelativeForAll;
|
||||
pub use full::RelativeFull;
|
||||
pub use to_all::RelativeToAll;
|
||||
pub use invested_capital::RelativeInvestedCapital;
|
||||
pub use with_extended::RelativeWithExtended;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, Height, Sats, Version};
|
||||
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::internal::{PercentPerBlock, RatioSatsBp16};
|
||||
|
||||
use crate::distribution::metrics::{ImportConfig, SupplyCore};
|
||||
|
||||
/// Relative-to-all metrics (not present for the "all" cohort itself).
|
||||
#[derive(Traversable)]
|
||||
pub struct RelativeToAll<M: StorageMode = Rw> {
|
||||
#[traversable(wrap = "supply", rename = "to_circulating")]
|
||||
pub supply_to_circulating: PercentPerBlock<BasisPoints16, M>,
|
||||
#[traversable(wrap = "supply/in_profit", rename = "to_circulating")]
|
||||
pub supply_in_profit_to_circulating: PercentPerBlock<BasisPoints16, M>,
|
||||
#[traversable(wrap = "supply/in_loss", rename = "to_circulating")]
|
||||
pub supply_in_loss_to_circulating: PercentPerBlock<BasisPoints16, M>,
|
||||
}
|
||||
|
||||
impl RelativeToAll {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
Ok(Self {
|
||||
supply_to_circulating: cfg.import("supply_to_circulating", Version::ONE)?,
|
||||
supply_in_profit_to_circulating: cfg
|
||||
.import("supply_in_profit_to_circulating", Version::ONE)?,
|
||||
supply_in_loss_to_circulating: cfg
|
||||
.import("supply_in_loss_to_circulating", Version::ONE)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
supply: &SupplyCore,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.supply_to_circulating
|
||||
.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&supply.total.sats.height,
|
||||
all_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
self.supply_in_profit_to_circulating
|
||||
.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&supply.in_profit.sats.height,
|
||||
all_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
self.supply_in_loss_to_circulating
|
||||
.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&supply.in_loss.sats.height,
|
||||
all_supply_sats,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Dollars, Height, Sats};
|
||||
use brk_types::{Dollars, Height};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::distribution::metrics::{ImportConfig, SupplyCore, UnrealizedFull};
|
||||
use crate::distribution::metrics::{ImportConfig, RealizedFull, SupplyCore, UnrealizedFull};
|
||||
|
||||
use super::{RelativeExtendedOwnMarketCap, RelativeExtendedOwnPnl, RelativeFull, RelativeToAll};
|
||||
use super::{
|
||||
RelativeExtendedOwnMarketCap, RelativeExtendedOwnPnl, RelativeFull, RelativeInvestedCapital,
|
||||
};
|
||||
|
||||
/// Full extended relative metrics (base + rel_to_all + own_market_cap + own_pnl).
|
||||
/// Full extended relative metrics (base + own_market_cap + own_pnl + invested_capital).
|
||||
/// Used by: sth, lth cohorts.
|
||||
#[derive(Deref, DerefMut, Traversable)]
|
||||
pub struct RelativeWithExtended<M: StorageMode = Rw> {
|
||||
@@ -17,20 +19,20 @@ pub struct RelativeWithExtended<M: StorageMode = Rw> {
|
||||
#[traversable(flatten)]
|
||||
pub base: RelativeFull<M>,
|
||||
#[traversable(flatten)]
|
||||
pub rel_to_all: RelativeToAll<M>,
|
||||
#[traversable(flatten)]
|
||||
pub extended_own_market_cap: RelativeExtendedOwnMarketCap<M>,
|
||||
#[traversable(flatten)]
|
||||
pub extended_own_pnl: RelativeExtendedOwnPnl<M>,
|
||||
#[traversable(flatten)]
|
||||
pub invested_capital: RelativeInvestedCapital<M>,
|
||||
}
|
||||
|
||||
impl RelativeWithExtended {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
Ok(Self {
|
||||
base: RelativeFull::forced_import(cfg)?,
|
||||
rel_to_all: RelativeToAll::forced_import(cfg)?,
|
||||
extended_own_market_cap: RelativeExtendedOwnMarketCap::forced_import(cfg)?,
|
||||
extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg)?,
|
||||
invested_capital: RelativeInvestedCapital::forced_import(cfg)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,15 +42,13 @@ impl RelativeWithExtended {
|
||||
max_from: Height,
|
||||
supply: &SupplyCore,
|
||||
unrealized: &UnrealizedFull,
|
||||
realized: &RealizedFull,
|
||||
market_cap: &impl ReadableVec<Height, Dollars>,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
own_market_cap: &impl ReadableVec<Height, Dollars>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.base
|
||||
.compute(max_from, supply, &unrealized.inner.basic, market_cap, exit)?;
|
||||
self.rel_to_all
|
||||
.compute(max_from, supply, all_supply_sats, exit)?;
|
||||
self.extended_own_market_cap
|
||||
.compute(max_from, &unrealized.inner, own_market_cap, exit)?;
|
||||
self.extended_own_pnl.compute(
|
||||
@@ -57,6 +57,8 @@ impl RelativeWithExtended {
|
||||
&unrealized.gross_pnl.usd.height,
|
||||
exit,
|
||||
)?;
|
||||
self.invested_capital
|
||||
.compute(max_from, unrealized, realized, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Sats, StoredU64, Version};
|
||||
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{indexes, internal::ValuePerBlock, prices};
|
||||
|
||||
/// Average amount held per UTXO and per funded address.
|
||||
///
|
||||
/// `utxo = supply / utxo_count`, `addr = supply / funded_addr_count`.
|
||||
#[derive(Traversable)]
|
||||
pub struct AvgAmountMetrics<M: StorageMode = Rw> {
|
||||
pub utxo: ValuePerBlock<M>,
|
||||
pub addr: ValuePerBlock<M>,
|
||||
}
|
||||
|
||||
impl AvgAmountMetrics {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let name = |suffix: &str| {
|
||||
if prefix.is_empty() {
|
||||
suffix.to_string()
|
||||
} else {
|
||||
format!("{prefix}_{suffix}")
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
utxo: ValuePerBlock::forced_import(db, &name("avg_utxo_amount"), version, indexes)?,
|
||||
addr: ValuePerBlock::forced_import(db, &name("avg_addr_amount"), version, indexes)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.utxo.sats.height as &mut dyn AnyStoredVec,
|
||||
&mut self.utxo.cents.height,
|
||||
&mut self.addr.sats.height,
|
||||
&mut self.addr.cents.height,
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.utxo.sats.height.reset()?;
|
||||
self.utxo.cents.height.reset()?;
|
||||
self.addr.sats.height.reset()?;
|
||||
self.addr.cents.height.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
prices: &prices::Vecs,
|
||||
supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
utxo_count: &impl ReadableVec<Height, StoredU64>,
|
||||
funded_addr_count: &impl ReadableVec<Height, StoredU64>,
|
||||
max_from: Height,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.utxo
|
||||
.sats
|
||||
.height
|
||||
.compute_divide(max_from, supply_sats, utxo_count, exit)?;
|
||||
self.utxo.compute(prices, max_from, exit)?;
|
||||
|
||||
self.addr
|
||||
.sats
|
||||
.height
|
||||
.compute_divide(max_from, supply_sats, funded_addr_count, exit)?;
|
||||
self.addr.compute(prices, max_from, exit)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,33 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPointsSigned32, Height, Indexes, Sats, SatsSigned, Version};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
|
||||
use brk_types::{BasisPoints16, BasisPointsSigned32, Height, Indexes, Sats, SatsSigned, Version};
|
||||
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
distribution::state::{CohortState, CostBasisOps, RealizedOps},
|
||||
prices,
|
||||
};
|
||||
|
||||
use crate::internal::{AmountPerBlock, LazyRollingDeltasFromHeight};
|
||||
use crate::internal::{
|
||||
LazyRollingDeltasAmountFromHeight, PercentPerBlock, RatioSatsBp16, ValuePerBlock,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
|
||||
/// Base supply metrics: total supply only (2 stored vecs).
|
||||
/// Base supply metrics: total supply + dominance (share of circulating).
|
||||
#[derive(Traversable)]
|
||||
pub struct SupplyBase<M: StorageMode = Rw> {
|
||||
pub total: AmountPerBlock<M>,
|
||||
pub delta: LazyRollingDeltasFromHeight<Sats, SatsSigned, BasisPointsSigned32>,
|
||||
pub total: ValuePerBlock<M>,
|
||||
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, BasisPointsSigned32>,
|
||||
#[traversable(rename = "dominance")]
|
||||
pub dominance: PercentPerBlock<BasisPoints16, M>,
|
||||
}
|
||||
|
||||
impl SupplyBase {
|
||||
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
|
||||
let supply: AmountPerBlock = cfg.import("supply", Version::ZERO)?;
|
||||
let supply: ValuePerBlock = cfg.import("supply", Version::ZERO)?;
|
||||
|
||||
let delta = LazyRollingDeltasFromHeight::new(
|
||||
let delta = LazyRollingDeltasAmountFromHeight::new(
|
||||
&cfg.name("supply_delta"),
|
||||
cfg.version + Version::ONE,
|
||||
&supply.sats.height,
|
||||
@@ -31,9 +35,12 @@ impl SupplyBase {
|
||||
cfg.indexes,
|
||||
);
|
||||
|
||||
let dominance = cfg.import("supply_dominance", Version::ZERO)?;
|
||||
|
||||
Ok(Self {
|
||||
total: supply,
|
||||
delta,
|
||||
dominance,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,7 +56,8 @@ impl SupplyBase {
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
vec![
|
||||
&mut self.total.sats.height as &mut dyn AnyStoredVec,
|
||||
&mut self.total.cents.height as &mut dyn AnyStoredVec,
|
||||
&mut self.total.cents.height,
|
||||
&mut self.dominance.bps.height,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -62,6 +70,21 @@ impl SupplyBase {
|
||||
self.total.compute(prices, max_from, exit)
|
||||
}
|
||||
|
||||
pub(crate) fn compute_dominance(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
all_supply_sats: &impl ReadableVec<Height, Sats>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.dominance
|
||||
.compute_binary::<Sats, Sats, RatioSatsBp16>(
|
||||
max_from,
|
||||
&self.total.sats.height,
|
||||
all_supply_sats,
|
||||
exit,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn compute_from_stateful(
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
|
||||
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
|
||||
use crate::{distribution::state::UnrealizedState, prices};
|
||||
|
||||
use crate::internal::{
|
||||
AmountPerBlock, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyAmountPerBlock,
|
||||
ValuePerBlock, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock,
|
||||
};
|
||||
|
||||
use crate::distribution::metrics::ImportConfig;
|
||||
@@ -22,9 +22,9 @@ pub struct SupplyCore<M: StorageMode = Rw> {
|
||||
#[traversable(flatten)]
|
||||
pub base: SupplyBase<M>,
|
||||
|
||||
pub half: LazyAmountPerBlock,
|
||||
pub in_profit: AmountPerBlock<M>,
|
||||
pub in_loss: AmountPerBlock<M>,
|
||||
pub half: LazyValuePerBlock,
|
||||
pub in_profit: ValuePerBlock<M>,
|
||||
pub in_loss: ValuePerBlock<M>,
|
||||
}
|
||||
|
||||
impl SupplyCore {
|
||||
@@ -32,7 +32,7 @@ impl SupplyCore {
|
||||
let v0 = Version::ZERO;
|
||||
let base = SupplyBase::forced_import(cfg)?;
|
||||
|
||||
let half = LazyAmountPerBlock::from_block_source::<
|
||||
let half = LazyValuePerBlock::from_block_source::<
|
||||
HalveSats,
|
||||
HalveSatsToBitcoin,
|
||||
HalveCents,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
mod avg_amount;
|
||||
mod base;
|
||||
mod core;
|
||||
|
||||
pub use avg_amount::AvgAmountMetrics;
|
||||
pub use self::core::SupplyCore;
|
||||
pub use base::SupplyBase;
|
||||
|
||||
@@ -33,8 +33,8 @@ pub struct UnrealizedFull<M: StorageMode = Rw> {
|
||||
pub gross_pnl: FiatPerBlock<Cents, M>,
|
||||
pub invested_capital: UnrealizedInvestedCapital<M>,
|
||||
|
||||
pub investor_cap_in_profit_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
|
||||
pub investor_cap_in_loss_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
|
||||
pub capitalized_cap_in_profit_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
|
||||
pub capitalized_cap_in_loss_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
|
||||
|
||||
pub sentiment: UnrealizedSentiment<M>,
|
||||
}
|
||||
@@ -53,8 +53,8 @@ impl UnrealizedFull {
|
||||
in_loss: cfg.import("invested_capital_in_loss", v1)?,
|
||||
};
|
||||
|
||||
let investor_cap_in_profit_raw = cfg.import("investor_cap_in_profit_raw", v0)?;
|
||||
let investor_cap_in_loss_raw = cfg.import("investor_cap_in_loss_raw", v0)?;
|
||||
let capitalized_cap_in_profit_raw = cfg.import("capitalized_cap_in_profit_raw", v0)?;
|
||||
let capitalized_cap_in_loss_raw = cfg.import("capitalized_cap_in_loss_raw", v0)?;
|
||||
|
||||
let sentiment = UnrealizedSentiment {
|
||||
pain_index: cfg.import("pain_index", v1)?,
|
||||
@@ -66,33 +66,33 @@ impl UnrealizedFull {
|
||||
inner,
|
||||
gross_pnl,
|
||||
invested_capital,
|
||||
investor_cap_in_profit_raw,
|
||||
investor_cap_in_loss_raw,
|
||||
capitalized_cap_in_profit_raw,
|
||||
capitalized_cap_in_loss_raw,
|
||||
sentiment,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
// Only check per-block pushed vecs (investor_cap_raw).
|
||||
// Only check per-block pushed vecs (capitalized_cap_raw).
|
||||
// Core-level vecs (profit/loss) are aggregated from age_range, not stateful.
|
||||
self.investor_cap_in_profit_raw
|
||||
self.capitalized_cap_in_profit_raw
|
||||
.len()
|
||||
.min(self.investor_cap_in_loss_raw.len())
|
||||
.min(self.capitalized_cap_in_loss_raw.len())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_state_all(&mut self, state: &UnrealizedState) {
|
||||
self.inner.push_state(state);
|
||||
self.investor_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(state.investor_cap_in_profit_raw));
|
||||
self.investor_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(state.investor_cap_in_loss_raw));
|
||||
self.capitalized_cap_in_profit_raw
|
||||
.push(CentsSquaredSats::new(state.capitalized_cap_in_profit_raw));
|
||||
self.capitalized_cap_in_loss_raw
|
||||
.push(CentsSquaredSats::new(state.capitalized_cap_in_loss_raw));
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
|
||||
let mut vecs = self.inner.collect_vecs_mut();
|
||||
vecs.push(&mut self.investor_cap_in_profit_raw as &mut dyn AnyStoredVec);
|
||||
vecs.push(&mut self.investor_cap_in_loss_raw as &mut dyn AnyStoredVec);
|
||||
vecs.push(&mut self.capitalized_cap_in_profit_raw as &mut dyn AnyStoredVec);
|
||||
vecs.push(&mut self.capitalized_cap_in_loss_raw as &mut dyn AnyStoredVec);
|
||||
vecs
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ impl UnrealizedFull {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute sentiment using investor_price (original formula).
|
||||
/// Compute sentiment using capitalized_price (original formula).
|
||||
/// Called after cost_basis.in_profit/loss are computed at the cohort level.
|
||||
pub(crate) fn compute_sentiment(
|
||||
&mut self,
|
||||
@@ -162,45 +162,45 @@ impl UnrealizedFull {
|
||||
spot: &impl ReadableVec<Height, Cents>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// greed = spot - investor_price_winners
|
||||
// investor_price = investor_cap / invested_cap
|
||||
// greed = spot - capitalized_price_winners
|
||||
// capitalized_price = capitalized_cap / invested_cap
|
||||
// invested_cap is in Cents (already / ONE_BTC), multiply back for CentsSats scale
|
||||
self.sentiment.greed_index.cents.height.compute_transform3(
|
||||
starting_indexes.height,
|
||||
&self.investor_cap_in_profit_raw,
|
||||
&self.capitalized_cap_in_profit_raw,
|
||||
&self.invested_capital.in_profit.cents.height,
|
||||
spot,
|
||||
|(h, investor_cap, invested_cap_cents, spot, ..)| {
|
||||
|(h, capitalized_cap, invested_cap_cents, spot, ..)| {
|
||||
let invested_cap_raw = invested_cap_cents.as_u128() * Sats::ONE_BTC_U128;
|
||||
if invested_cap_raw == 0 {
|
||||
return (h, Cents::ZERO);
|
||||
}
|
||||
let investor_price = investor_cap.inner() / invested_cap_raw;
|
||||
let capitalized_price = capitalized_cap.inner() / invested_cap_raw;
|
||||
let spot_u128 = spot.as_u128();
|
||||
(
|
||||
h,
|
||||
Cents::new(spot_u128.saturating_sub(investor_price) as u64),
|
||||
Cents::new(spot_u128.saturating_sub(capitalized_price) as u64),
|
||||
)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// pain = investor_price_losers - spot
|
||||
// pain = capitalized_price_losers - spot
|
||||
self.sentiment.pain_index.cents.height.compute_transform3(
|
||||
starting_indexes.height,
|
||||
&self.investor_cap_in_loss_raw,
|
||||
&self.capitalized_cap_in_loss_raw,
|
||||
&self.invested_capital.in_loss.cents.height,
|
||||
spot,
|
||||
|(h, investor_cap, invested_cap_cents, spot, ..)| {
|
||||
|(h, capitalized_cap, invested_cap_cents, spot, ..)| {
|
||||
let invested_cap_raw = invested_cap_cents.as_u128() * Sats::ONE_BTC_U128;
|
||||
if invested_cap_raw == 0 {
|
||||
return (h, Cents::ZERO);
|
||||
}
|
||||
let investor_price = investor_cap.inner() / invested_cap_raw;
|
||||
let capitalized_price = capitalized_cap.inner() / invested_cap_raw;
|
||||
let spot_u128 = spot.as_u128();
|
||||
(
|
||||
h,
|
||||
Cents::new(investor_price.saturating_sub(spot_u128) as u64),
|
||||
Cents::new(capitalized_price.saturating_sub(spot_u128) as u64),
|
||||
)
|
||||
},
|
||||
exit,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub struct SendPrecomputed {
|
||||
pub current_ps: CentsSats,
|
||||
pub prev_ps: CentsSats,
|
||||
pub ath_ps: CentsSats,
|
||||
pub prev_investor_cap: CentsSquaredSats,
|
||||
pub prev_capitalized_cap: CentsSquaredSats,
|
||||
}
|
||||
|
||||
impl SendPrecomputed {
|
||||
@@ -42,7 +42,7 @@ impl SendPrecomputed {
|
||||
} else {
|
||||
CentsSats::from_price_sats(ath, sats)
|
||||
};
|
||||
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
|
||||
let prev_capitalized_cap = prev_ps.to_capitalized_cap(prev_price);
|
||||
Some(Self {
|
||||
sats,
|
||||
prev_price,
|
||||
@@ -50,7 +50,7 @@ impl SendPrecomputed {
|
||||
current_ps,
|
||||
prev_ps,
|
||||
ath_ps,
|
||||
prev_investor_cap,
|
||||
prev_capitalized_cap,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
pub(crate) fn restore_realized_cap(&mut self) {
|
||||
self.realized.set_cap_raw(self.cost_basis.cap_raw());
|
||||
self.realized
|
||||
.set_investor_cap_raw(self.cost_basis.investor_cap_raw());
|
||||
.set_capitalized_cap_raw(self.cost_basis.capitalized_cap_raw());
|
||||
}
|
||||
|
||||
pub(crate) fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
|
||||
@@ -117,12 +117,12 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
|
||||
if s.supply_state.value > Sats::ZERO {
|
||||
self.realized
|
||||
.increment_snapshot(s.price_sats, s.investor_cap);
|
||||
.increment_snapshot(s.price_sats, s.capitalized_cap_raw);
|
||||
self.cost_basis.increment(
|
||||
s.realized_price,
|
||||
s.supply_state.value,
|
||||
s.price_sats,
|
||||
s.investor_cap,
|
||||
s.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -132,12 +132,12 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
|
||||
if s.supply_state.value > Sats::ZERO {
|
||||
self.realized
|
||||
.decrement_snapshot(s.price_sats, s.investor_cap);
|
||||
.decrement_snapshot(s.price_sats, s.capitalized_cap_raw);
|
||||
self.cost_basis.decrement(
|
||||
s.realized_price,
|
||||
s.supply_state.value,
|
||||
s.price_sats,
|
||||
s.investor_cap,
|
||||
s.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
snapshot.realized_price,
|
||||
supply.value,
|
||||
snapshot.price_sats,
|
||||
snapshot.investor_cap,
|
||||
snapshot.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
current.realized_price,
|
||||
current.supply_state.value,
|
||||
current.price_sats,
|
||||
current.investor_cap,
|
||||
current.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
prev.realized_price,
|
||||
prev.supply_state.value,
|
||||
prev.price_sats,
|
||||
prev.investor_cap,
|
||||
prev.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -212,11 +212,11 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
pre.current_ps,
|
||||
pre.prev_ps,
|
||||
pre.ath_ps,
|
||||
pre.prev_investor_cap,
|
||||
pre.prev_capitalized_cap,
|
||||
);
|
||||
|
||||
self.cost_basis
|
||||
.decrement(pre.prev_price, pre.sats, pre.prev_ps, pre.prev_investor_cap);
|
||||
.decrement(pre.prev_price, pre.sats, pre.prev_ps, pre.prev_capitalized_cap);
|
||||
}
|
||||
|
||||
pub(crate) fn send_utxo(
|
||||
@@ -265,17 +265,17 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
let current_ps = CentsSats::from_price_sats(current_price, sats);
|
||||
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
|
||||
let ath_ps = CentsSats::from_price_sats(ath, sats);
|
||||
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
|
||||
let prev_capitalized_cap = prev_ps.to_capitalized_cap(prev_price);
|
||||
|
||||
self.realized
|
||||
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
|
||||
.send(sats, current_ps, prev_ps, ath_ps, prev_capitalized_cap);
|
||||
|
||||
if current.supply_state.value.is_not_zero() {
|
||||
self.cost_basis.increment(
|
||||
current.realized_price,
|
||||
current.supply_state.value,
|
||||
current.price_sats,
|
||||
current.investor_cap,
|
||||
current.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
|
||||
prev.realized_price,
|
||||
prev.supply_state.value,
|
||||
prev.price_sats,
|
||||
prev.investor_cap,
|
||||
prev.capitalized_cap_raw,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user